add support for alternative (non-mainnet) networks

This commit is contained in:
Craig Raw 2020-09-28 14:31:25 +02:00
parent e8d8fa6126
commit 747bfa915f
10 changed files with 252 additions and 37 deletions

View file

@ -6,6 +6,7 @@ import com.sparrowwallet.drongo.protocol.ScriptType;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.stream.Collectors;
public class ExtendedKey {
private final byte[] parentFingerprint;
@ -53,7 +54,7 @@ public class ExtendedKey {
}
public byte[] getExtendedKeyBytes() {
return getExtendedKeyBytes(key.isPubKeyOnly() ? Header.xpub : Header.xprv);
return getExtendedKeyBytes(key.isPubKeyOnly() ? Network.get().getXpubHeader() : Network.get().getXprvHeader());
}
public byte[] getExtendedKeyBytes(Header extendedKeyHeader) {
@ -79,7 +80,7 @@ public class ExtendedKey {
int headerInt = buffer.getInt();
Header header = Header.getHeader(headerInt);
if(header == null) {
throw new IllegalArgumentException("Unknown header bytes: " + DeterministicKey.toBase58(serializedKey).substring(0, 4));
throw new IllegalArgumentException("Unknown header bytes for extended key on " + Network.get().getName() + ": " + DeterministicKey.toBase58(serializedKey).substring(0, 4));
}
int depth = buffer.get() & 0xFF; // convert signed byte to positive int since depth cannot be negative
@ -196,38 +197,62 @@ public class ExtendedKey {
return privateKey;
}
public boolean isMainnet() {
return mainnet;
public Network getNetwork() {
return mainnet ? Network.MAINNET : Network.TESTNET;
}
public static List<Header> getHeaders(Network network) {
return Arrays.stream(Header.values()).filter(header -> header.getNetwork() == network || (header.getNetwork() == Network.TESTNET && network == Network.REGTEST)).collect(Collectors.toList());
}
public static Header fromExtendedKey(String xkey) {
for(Header extendedKeyHeader : Header.values()) {
for(Header extendedKeyHeader : getHeaders(Network.get())) {
if(xkey.startsWith(extendedKeyHeader.name)) {
return extendedKeyHeader;
}
}
throw new IllegalArgumentException("Unrecognised extended key header for extended key: " + xkey);
for(Network network : getOtherNetworks(Network.get())) {
for(Header otherNetworkKeyHeader : getHeaders(network)) {
if(xkey.startsWith(otherNetworkKeyHeader.name)) {
throw new IllegalArgumentException("Provided " + otherNetworkKeyHeader.name + " extended key invalid on configured " + Network.get().getName() + " network. Use a " + network.getName() + " configuration to use this extended key.");
}
}
}
throw new IllegalArgumentException("Unrecognised extended key header for " + Network.get().getName() + ": " + xkey);
}
public static Header fromScriptType(ScriptType scriptType, boolean privateKey) {
for(Header header : Header.values()) {
for(Header header : getHeaders(Network.get())) {
if(header.defaultScriptType != null && header.defaultScriptType.equals(scriptType) && header.isPrivateKey() == privateKey) {
return header;
}
}
return Header.xpub;
return Network.get().getXpubHeader();
}
public static Header getHeader(int header) {
for(Header extendedKeyHeader : Header.values()) {
private static Header getHeader(int header) {
for(Header extendedKeyHeader : getHeaders(Network.get())) {
if(header == extendedKeyHeader.header) {
return extendedKeyHeader;
}
}
for(Network otherNetwork : getOtherNetworks(Network.get())) {
for(Header otherNetworkKeyHeader : getHeaders(otherNetwork)) {
if(header == otherNetworkKeyHeader.header) {
throw new IllegalArgumentException("Provided " + otherNetworkKeyHeader.name + " extended key invalid on configured " + Network.get().getName() + " network. Use a " + otherNetwork.getName() + " configuration to use this extended key.");
}
}
}
return null;
}
private static List<Network> getOtherNetworks(Network providedNetwork) {
return Arrays.stream(Network.values()).filter(network -> network != providedNetwork).collect(Collectors.toList());
}
}
}

View file

@ -0,0 +1,87 @@
package com.sparrowwallet.drongo;
public enum Network {
MAINNET("mainnet", 0, "1", 5, "3", "bc", ExtendedKey.Header.xprv, ExtendedKey.Header.xpub),
TESTNET("testnet", 111, "mn", 196, "2", "tb", ExtendedKey.Header.tprv, ExtendedKey.Header.tpub),
REGTEST("regtest", 111, "mn", 196, "2", "bcrt", ExtendedKey.Header.tprv, ExtendedKey.Header.tpub);
Network(String name, int p2pkhAddressHeader, String p2pkhAddressPrefix, int p2shAddressHeader, String p2shAddressPrefix, String bech32AddressHrp, ExtendedKey.Header xprvHeader, ExtendedKey.Header xpubHeader) {
this.name = name;
this.p2pkhAddressHeader = p2pkhAddressHeader;
this.p2pkhAddressPrefix = p2pkhAddressPrefix;
this.p2shAddressHeader = p2shAddressHeader;
this.p2shAddressPrefix = p2shAddressPrefix;
this.bech32AddressHrp = bech32AddressHrp;
this.xprvHeader = xprvHeader;
this.xpubHeader = xpubHeader;
}
private final String name;
private final int p2pkhAddressHeader;
private final String p2pkhAddressPrefix;
private final int p2shAddressHeader;
private final String p2shAddressPrefix;
private final String bech32AddressHrp;
private final ExtendedKey.Header xprvHeader;
private final ExtendedKey.Header xpubHeader;
private static Network currentNetwork;
public String getName() {
return name;
}
public int getP2PKHAddressHeader() {
return p2pkhAddressHeader;
}
public int getP2SHAddressHeader() {
return p2shAddressHeader;
}
public String getBech32AddressHRP() {
return bech32AddressHrp;
}
public ExtendedKey.Header getXprvHeader() {
return xprvHeader;
}
public ExtendedKey.Header getXpubHeader() {
return xpubHeader;
}
public boolean hasP2PKHAddressPrefix(String address) {
for(String prefix : p2pkhAddressPrefix.split("")) {
if(address.startsWith(prefix)) {
return true;
}
}
return false;
}
public boolean hasP2SHAddressPrefix(String address) {
return address.startsWith(p2shAddressPrefix);
}
public static Network get() {
if(currentNetwork == null) {
currentNetwork = MAINNET;
}
return currentNetwork;
}
public static void set(Network network) {
if(currentNetwork != null && network != currentNetwork && !isTest()) {
throw new IllegalStateException("Network already set to " + currentNetwork.getName());
}
currentNetwork = network;
}
private static boolean isTest() {
return System.getProperty("org.gradle.test.worker") != null;
}
}

View file

@ -1,5 +1,6 @@
package com.sparrowwallet.drongo.address;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.protocol.Base58;
import com.sparrowwallet.drongo.protocol.Bech32;
import com.sparrowwallet.drongo.protocol.Script;
@ -7,8 +8,6 @@ import com.sparrowwallet.drongo.protocol.ScriptType;
import java.util.Arrays;
import static com.sparrowwallet.drongo.address.P2WPKHAddress.HRP;
public abstract class Address {
protected final byte[] hash;
@ -21,14 +20,26 @@ public abstract class Address {
}
public String getAddress() {
return Base58.encodeChecked(getVersion(), hash);
return getAddress(Network.get());
}
public String getAddress(Network network) {
return Base58.encodeChecked(getVersion(network), hash);
}
public String toString() {
return getAddress();
return getAddress(Network.get());
}
public abstract int getVersion();
public String toString(Network network) {
return getAddress(network);
}
public int getVersion() {
return getVersion(Network.get());
}
public abstract int getVersion(Network network);
public abstract ScriptType getScriptType();
@ -52,18 +63,37 @@ public abstract class Address {
}
public static Address fromString(String address) throws InvalidAddressException {
try {
return fromString(Network.get(), address);
} catch(InvalidAddressException e) {
for(Network network : Network.values()) {
try {
fromString(network, address);
if(network != Network.get()) {
throw new InvalidAddressException("Provided " + network.getName() + " address invalid on configured " + Network.get().getName() + " network: " + address + ". Use a " + network.getName() + " configuration to use this address.");
}
} catch(InvalidAddressException i) {
//ignore
}
}
throw e;
}
}
public static Address fromString(Network network, String address) throws InvalidAddressException {
Exception nested = null;
if(address != null && (address.startsWith("1") || address.startsWith("3"))) {
if(address != null && (network.hasP2PKHAddressPrefix(address) || network.hasP2SHAddressPrefix(address))) {
try {
byte[] decodedBytes = Base58.decodeChecked(address);
if(decodedBytes.length == 21) {
int version = decodedBytes[0];
int version = Byte.toUnsignedInt(decodedBytes[0]);
byte[] hash = Arrays.copyOfRange(decodedBytes, 1, 21);
if(version == 0) {
if(version == network.getP2PKHAddressHeader()) {
return new P2PKHAddress(hash);
}
if(version == 5) {
if(version == network.getP2SHAddressHeader()) {
return new P2SHAddress(hash);
}
}
@ -72,10 +102,10 @@ public abstract class Address {
}
}
if(address != null && address.startsWith(HRP)) {
if(address != null && address.startsWith(network.getBech32AddressHRP())) {
try {
Bech32.Bech32Data data = Bech32.decode(address);
if (data.hrp.equals(HRP)) {
if(data.hrp.equals(network.getBech32AddressHRP())) {
int witnessVersion = data.data[0];
if (witnessVersion == 0) {
byte[] convertedProgram = Arrays.copyOfRange(data.data, 1, data.data.length);

View file

@ -1,5 +1,6 @@
package com.sparrowwallet.drongo.address;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.Script;
import com.sparrowwallet.drongo.protocol.ScriptType;
@ -12,8 +13,9 @@ public class P2PKAddress extends Address {
this.pubKey = pubKey;
}
public int getVersion() {
return 0;
@Override
public int getVersion(Network network) {
return network.getP2PKHAddressHeader();
}
public ScriptType getScriptType() {

View file

@ -1,5 +1,6 @@
package com.sparrowwallet.drongo.address;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.protocol.Script;
import com.sparrowwallet.drongo.protocol.ScriptType;
@ -8,14 +9,17 @@ public class P2PKHAddress extends Address {
super(pubKeyHash);
}
public int getVersion() {
return 0;
@Override
public int getVersion(Network network) {
return network.getP2PKHAddressHeader();
}
@Override
public ScriptType getScriptType() {
return ScriptType.P2PKH;
}
@Override
public Script getOutputScript() {
return getScriptType().getOutputScript(hash);
}

View file

@ -1,5 +1,6 @@
package com.sparrowwallet.drongo.address;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.Script;
import com.sparrowwallet.drongo.protocol.ScriptType;
@ -9,8 +10,9 @@ public class P2SHAddress extends Address {
super(scriptHash);
}
public int getVersion() {
return 5;
@Override
public int getVersion(Network network) {
return network.getP2SHAddressHeader();
}
@Override

View file

@ -1,24 +1,26 @@
package com.sparrowwallet.drongo.address;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.protocol.Bech32;
import com.sparrowwallet.drongo.protocol.Script;
import com.sparrowwallet.drongo.protocol.ScriptType;
public class P2WPKHAddress extends Address {
public static final String HRP = "bc";
public P2WPKHAddress(byte[] pubKeyHash) {
super(pubKeyHash);
}
public int getVersion() {
@Override
public int getVersion(Network network) {
return 0;
}
public String getAddress() {
return Bech32.encode(HRP, getVersion(), hash);
@Override
public String getAddress(Network network) {
return Bech32.encode(network.getBech32AddressHRP(), getVersion(), hash);
}
@Override
public ScriptType getScriptType() {
return ScriptType.P2WPKH;
}

View file

@ -1,22 +1,24 @@
package com.sparrowwallet.drongo.address;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.protocol.*;
import static com.sparrowwallet.drongo.address.P2WPKHAddress.HRP;
public class P2WSHAddress extends Address {
public P2WSHAddress(byte[] scriptHash) {
super(scriptHash);
}
public int getVersion() {
@Override
public int getVersion(Network network) {
return 0;
}
public String getAddress() {
return Bech32.encode(HRP, getVersion(), hash);
@Override
public String getAddress(Network network) {
return Bech32.encode(network.getBech32AddressHRP(), getVersion(), hash);
}
@Override
public ScriptType getScriptType() {
return ScriptType.P2WSH;
}

View file

@ -1,5 +1,7 @@
package com.sparrowwallet.drongo.address;
import com.sparrowwallet.drongo.Network;
import org.junit.After;
import org.junit.Assert;
import org.junit.Test;
@ -23,6 +25,51 @@ public class AddressTest {
Address address4 = Address.fromString("34jnjFM4SbaB7Q8aMtNDG849RQ1gUYgpgo");
Assert.assertTrue(address4 instanceof P2SHAddress);
Assert.assertEquals("34jnjFM4SbaB7Q8aMtNDG849RQ1gUYgpgo", address4.toString());
Address address5 = Address.fromString(Network.TESTNET, "tb1qawkzyj2l5yck5jq4wyhkc4837x088580y9uyk8");
Assert.assertTrue(address5 instanceof P2WPKHAddress);
Assert.assertEquals("tb1qawkzyj2l5yck5jq4wyhkc4837x088580y9uyk8", address5.toString(Network.TESTNET));
Address address6 = Address.fromString(Network.TESTNET, "tb1q8kdkthp5a6vfrdas84efkpv25ul3s9wpzc755cra8av48xq4a7wsjcsdma");
Assert.assertTrue(address6 instanceof P2WSHAddress);
Assert.assertEquals("tb1q8kdkthp5a6vfrdas84efkpv25ul3s9wpzc755cra8av48xq4a7wsjcsdma", address6.toString(Network.TESTNET));
Address address7 = Address.fromString(Network.TESTNET, "mng6R5oLWBBo8iFWU9Mx4zFy5pWhrWMeW2");
Assert.assertTrue(address7 instanceof P2PKHAddress);
Assert.assertEquals("mng6R5oLWBBo8iFWU9Mx4zFy5pWhrWMeW2", address7.toString(Network.TESTNET));
Address address8 = Address.fromString(Network.TESTNET, "n1S1rnnZm3RdW9iuAF6Hjk3gLZWGc59zDi");
Assert.assertTrue(address8 instanceof P2PKHAddress);
Assert.assertEquals("n1S1rnnZm3RdW9iuAF6Hjk3gLZWGc59zDi", address8.toString(Network.TESTNET));
Address address9 = Address.fromString(Network.TESTNET, "2NCZUtUt6gzXyBiPEQi5yQyrgR6f6F6Ki6A");
Assert.assertTrue(address9 instanceof P2SHAddress);
Assert.assertEquals("2NCZUtUt6gzXyBiPEQi5yQyrgR6f6F6Ki6A", address9.toString(Network.TESTNET));
}
@Test
public void testnetValidAddressTest() throws InvalidAddressException {
Network.set(Network.TESTNET);
Address address5 = Address.fromString("tb1qawkzyj2l5yck5jq4wyhkc4837x088580y9uyk8");
Assert.assertTrue(address5 instanceof P2WPKHAddress);
Assert.assertEquals("tb1qawkzyj2l5yck5jq4wyhkc4837x088580y9uyk8", address5.toString());
Address address6 = Address.fromString("tb1q8kdkthp5a6vfrdas84efkpv25ul3s9wpzc755cra8av48xq4a7wsjcsdma");
Assert.assertTrue(address6 instanceof P2WSHAddress);
Assert.assertEquals("tb1q8kdkthp5a6vfrdas84efkpv25ul3s9wpzc755cra8av48xq4a7wsjcsdma", address6.toString());
Address address7 = Address.fromString("mng6R5oLWBBo8iFWU9Mx4zFy5pWhrWMeW2");
Assert.assertTrue(address7 instanceof P2PKHAddress);
Assert.assertEquals("mng6R5oLWBBo8iFWU9Mx4zFy5pWhrWMeW2", address7.toString());
Address address8 = Address.fromString("n1S1rnnZm3RdW9iuAF6Hjk3gLZWGc59zDi");
Assert.assertTrue(address8 instanceof P2PKHAddress);
Assert.assertEquals("n1S1rnnZm3RdW9iuAF6Hjk3gLZWGc59zDi", address8.toString());
Address address9 = Address.fromString("2NCZUtUt6gzXyBiPEQi5yQyrgR6f6F6Ki6A");
Assert.assertTrue(address9 instanceof P2SHAddress);
Assert.assertEquals("2NCZUtUt6gzXyBiPEQi5yQyrgR6f6F6Ki6A", address9.toString());
}
@Test
@ -67,4 +114,9 @@ public class AddressTest {
public void invalidChecksumAddressTest2() throws InvalidAddressException {
Address address1 = Address.fromString("bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmb3");
}
@After
public void tearDown() throws Exception {
Network.set(null);
}
}

View file

@ -2,10 +2,12 @@ package com.sparrowwallet.drongo.psbt;
import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.*;
import org.bouncycastle.util.encoders.Hex;
import org.junit.After;
import org.junit.Assert;
import org.junit.Test;
@ -197,6 +199,8 @@ public class PSBTTest {
@Test
public void validP2wshMultisigWithXpubs() throws PSBTParseException {
Network.set(Network.TESTNET);
String psbt = "cHNidP8BAFICAAAAAZ38ZijCbFiZ/hvT3DOGZb/VXXraEPYiCXPfLTht7BJ2AQAAAAD/////AfA9zR0AAAAAFgAUezoAv9wU0neVwrdJAdCdpu8TNXkAAAAATwEENYfPAto/0AiAAAAAlwSLGtBEWx7IJ1UXcnyHtOTrwYogP/oPlMAVZr046QADUbdDiH7h1A3DKmBDck8tZFmztaTXPa7I+64EcvO8Q+IM2QxqT64AAIAAAACATwEENYfPAto/0AiAAAABuQRSQnE5zXjCz/JES+NTzVhgXj5RMoXlKLQH+uP2FzUD0wpel8itvFV9rCrZp+OcFyLrrGnmaLbyZnzB1nHIPKsM2QxqT64AAIABAACAAAEBKwBlzR0AAAAAIgAgLFSGEmxJeAeagU4TcV1l82RZ5NbMre0mbQUIZFuvpjIBBUdSIQKdoSzbWyNWkrkVNq/v5ckcOrlHPY5DtTODarRWKZyIcSEDNys0I07Xz5wf6l0F1EFVeSe+lUKxYusC4ass6AIkwAtSriIGAp2hLNtbI1aSuRU2r+/lyRw6uUc9jkO1M4NqtFYpnIhxENkMak+uAACAAAAAgAAAAAAiBgM3KzQjTtfPnB/qXQXUQVV5J76VQrFi6wLhqyzoAiTACxDZDGpPrgAAgAEAAIAAAAAAACICA57/H1R6HV+S36K6evaslxpL0DukpzSwMVaiVritOh75EO3kXMUAAACAAAAAgAEAAIAA";
PSBT psbt1 = PSBT.fromString(psbt);
@ -371,4 +375,9 @@ public class PSBTTest {
Assert.assertEquals("0200000000010258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7500000000da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752aeffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d01000000232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00000000", Utils.bytesToHex(transaction.bitcoinSerialize()));
}
@After
public void tearDown() throws Exception {
Network.set(null);
}
}