From 45154359e9e08069299abff83aa257dba9316447 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Fri, 6 Sep 2019 14:47:38 +0200 Subject: [PATCH] add multisig output descriptors --- .../craigraw/drongo/ExtendedPublicKey.java | 185 ++++++++++ .../com/craigraw/drongo/OutputDescriptor.java | 324 +++++++++--------- .../java/com/craigraw/drongo/WatchWallet.java | 17 +- .../craigraw/drongo/address/P2WSHAddress.java | 34 ++ .../craigraw/drongo/OutputDescriptorTest.java | 4 +- .../com/craigraw/drongo/WatchWalletTest.java | 26 +- 6 files changed, 408 insertions(+), 182 deletions(-) create mode 100644 src/main/java/com/craigraw/drongo/ExtendedPublicKey.java create mode 100644 src/main/java/com/craigraw/drongo/address/P2WSHAddress.java diff --git a/src/main/java/com/craigraw/drongo/ExtendedPublicKey.java b/src/main/java/com/craigraw/drongo/ExtendedPublicKey.java new file mode 100644 index 0000000..48888a8 --- /dev/null +++ b/src/main/java/com/craigraw/drongo/ExtendedPublicKey.java @@ -0,0 +1,185 @@ +package com.craigraw.drongo; + +import com.craigraw.drongo.crypto.*; +import com.craigraw.drongo.protocol.Base58; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class ExtendedPublicKey { + private static final int bip32HeaderP2PKHXPub = 0x0488B21E; //The 4 byte header that serializes in base58 to "xpub". + private static final int bip32HeaderP2PKHYPub = 0x049D7CB2; //The 4 byte header that serializes in base58 to "ypub". + private static final int bip32HeaderP2WPKHZPub = 0x04B24746; // The 4 byte header that serializes in base58 to "zpub" + private static final int bip32HeaderP2WHSHPub = 0x2AA7ED3; // The 4 byte header that serializes in base58 to "Zpub" + + private int parentFingerprint; + private String keyDerivationPath; + private DeterministicKey pubKey; + private String childDerivationPath; + private ChildNumber pubKeyChildNumber; + + private DeterministicHierarchy hierarchy; + + public ExtendedPublicKey(int parentFingerprint, String keyDerivationPath, DeterministicKey pubKey, String childDerivationPath, ChildNumber pubKeyChildNumber) { + this.parentFingerprint = parentFingerprint; + this.keyDerivationPath = keyDerivationPath; + this.pubKey = pubKey; + this.childDerivationPath = childDerivationPath; + this.pubKeyChildNumber = pubKeyChildNumber; + + this.hierarchy = new DeterministicHierarchy(pubKey); + } + + public int getParentFingerprint() { + return parentFingerprint; + } + + public List getKeyDerivation() { + return parsePath(keyDerivationPath); + } + + public DeterministicKey getPubKey() { + return pubKey; + } + + public List getChildDerivation() { + return getChildDerivation(0); + } + + public List getChildDerivation(int wildCardReplacement) { + return getChildDerivation(getPubKey().getChildNumber(), childDerivationPath, wildCardReplacement); + } + + public boolean describesMultipleAddresses() { + return childDerivationPath.endsWith("/*"); + } + + public List getReceivingDerivation(int wildCardReplacement) { + if(describesMultipleAddresses()) { + if(childDerivationPath.endsWith("0/*")) { + return getChildDerivation(getPubKey().getChildNumber(), childDerivationPath, wildCardReplacement); + } + + if(pubKeyChildNumber.num() == 0 && childDerivationPath.endsWith("/*")) { + return getChildDerivation(new ChildNumber(0, getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement); + } + } + + throw new IllegalStateException("Cannot derive receiving address from output descriptor " + this.toString()); + } + + public List getChangeDerivation(int wildCardReplacement) { + if(describesMultipleAddresses()) { + if(childDerivationPath.endsWith("0/*")) { + return getChildDerivation(getPubKey().getChildNumber(), childDerivationPath.replace("0/*", "1/*"), wildCardReplacement); + } + + if(pubKeyChildNumber.num() == 1 && childDerivationPath.endsWith("/*")) { + return getChildDerivation(new ChildNumber(1, getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement); + } + } + + throw new IllegalStateException("Cannot derive change address from output descriptor " + this.toString()); + } + + private List getChildDerivation(ChildNumber firstChild, String derivationPath, int wildCardReplacement) { + List path = new ArrayList<>(); + path.add(firstChild); + path.addAll(parsePath(derivationPath, wildCardReplacement)); + + return path; + } + + public DeterministicKey getKey(List path) { + return hierarchy.get(path); + } + + public static List parsePath(String path) { + return parsePath(path, 0); + } + + public static List parsePath(String path, int wildcardReplacement) { + String[] parsedNodes = path.replace("M", "").split("/"); + List nodes = new ArrayList<>(); + + for (String n : parsedNodes) { + n = n.replaceAll(" ", ""); + if (n.length() == 0) continue; + boolean isHard = n.endsWith("H") || n.endsWith("h") || n.endsWith("'"); + if (isHard) n = n.substring(0, n.length() - 1); + if (n.equals("*")) n = Integer.toString(wildcardReplacement); + int nodeNumber = Integer.parseInt(n); + nodes.add(new ChildNumber(nodeNumber, isHard)); + } + + return nodes; + } + + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(getExtendedPublicKey()); + builder.append(childDerivationPath); + return builder.toString(); + } + + public String getExtendedPublicKey() { + return Base58.encodeChecked(getExtendedPublicKeyBytes()); + } + + public byte[] getExtendedPublicKeyBytes() { + ByteBuffer buffer = ByteBuffer.allocate(78); + buffer.putInt(bip32HeaderP2PKHXPub); + + List childPath = parsePath(childDerivationPath); + int depth = 5 - childPath.size(); + buffer.put((byte)depth); + + buffer.putInt(parentFingerprint); + + buffer.putInt(pubKeyChildNumber.i()); + + buffer.put(pubKey.getChainCode()); + buffer.put(pubKey.getPubKey()); + + return buffer.array(); + } + + static ExtendedPublicKey fromDescriptor(String keyDerivationPath, String extPubKey, String childDerivationPath) { + byte[] serializedKey = Base58.decodeChecked(extPubKey); + ByteBuffer buffer = ByteBuffer.wrap(serializedKey); + int header = buffer.getInt(); + if(!(header == bip32HeaderP2PKHXPub || header == bip32HeaderP2PKHYPub || header == bip32HeaderP2WPKHZPub || header == bip32HeaderP2WHSHPub)) { + throw new IllegalArgumentException("Unknown header bytes: " + DeterministicKey.toBase58(serializedKey).substring(0, 4)); + } + + int depth = buffer.get() & 0xFF; // convert signed byte to positive int since depth cannot be negative + final int parentFingerprint = buffer.getInt(); + final int i = buffer.getInt(); + ChildNumber childNumber; + List path; + + if(depth == 0) { + //Poorly formatted extended public key, add first child path element + childNumber = new ChildNumber(0, false); + } else if ((i & ChildNumber.HARDENED_BIT) != 0) { + childNumber = new ChildNumber(i ^ ChildNumber.HARDENED_BIT, true); //already hardened + } else { + childNumber = new ChildNumber(i, false); + } + path = Collections.unmodifiableList(new ArrayList<>(Arrays.asList(childNumber))); + + byte[] chainCode = new byte[32]; + buffer.get(chainCode); + byte[] data = new byte[33]; + buffer.get(data); + if(buffer.hasRemaining()) { + throw new IllegalArgumentException("Found unexpected data in key"); + } + + DeterministicKey pubKey = new DeterministicKey(path, chainCode, new LazyECPoint(ECKey.CURVE.getCurve(), data), depth, parentFingerprint); + return new ExtendedPublicKey(parentFingerprint, keyDerivationPath, pubKey, childDerivationPath, childNumber); + } +} diff --git a/src/main/java/com/craigraw/drongo/OutputDescriptor.java b/src/main/java/com/craigraw/drongo/OutputDescriptor.java index 11c756d..1600c19 100644 --- a/src/main/java/com/craigraw/drongo/OutputDescriptor.java +++ b/src/main/java/com/craigraw/drongo/OutputDescriptor.java @@ -1,113 +1,114 @@ package com.craigraw.drongo; -import com.craigraw.drongo.address.Address; -import com.craigraw.drongo.address.P2PKHAddress; -import com.craigraw.drongo.address.P2SHAddress; -import com.craigraw.drongo.address.P2WPKHAddress; +import com.craigraw.drongo.address.*; import com.craigraw.drongo.crypto.ChildNumber; import com.craigraw.drongo.crypto.DeterministicKey; -import com.craigraw.drongo.crypto.ECKey; -import com.craigraw.drongo.crypto.LazyECPoint; -import com.craigraw.drongo.protocol.Base58; import com.craigraw.drongo.protocol.Script; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.craigraw.drongo.protocol.ScriptChunk; +import com.craigraw.drongo.protocol.ScriptOpCodes; -import java.nio.ByteBuffer; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.StringJoiner; import java.util.regex.Matcher; import java.util.regex.Pattern; public class OutputDescriptor { - private static final Logger log = LoggerFactory.getLogger(OutputDescriptor.class); - - private static final int bip32HeaderP2PKHXPub = 0x0488B21E; //The 4 byte header that serializes in base58 to "xpub". - private static final int bip32HeaderP2PKHYPub = 0x049D7CB2; //The 4 byte header that serializes in base58 to "ypub". - private static final int bip32HeaderP2WPKHZPub = 0x04B24746; // The 4 byte header that serializes in base58 to "zpub" - - private static final Pattern DESCRIPTOR_PATTERN = Pattern.compile("(.+)\\((\\[[^\\]]+\\])?(xpub[^/\\)]+)(/[/\\d*']+)?\\)\\)?"); + private static final Pattern XPUB_PATTERN = Pattern.compile("(\\[[^\\]]+\\])?(.pub[^/\\)]+)(/[/\\d*']+)?"); + private static final Pattern MULTI_PATTERN = Pattern.compile("multi\\(([\\d+])"); private String script; - private int parentFingerprint; - private String keyDerivationPath; - private DeterministicKey pubKey; - private String childDerivationPath; - private ChildNumber pubKeyChildNumber; + private int multisigThreshold; + private List extendedPublicKeys; - public OutputDescriptor(String script, int parentFingerprint, String keyDerivationPath, DeterministicKey pubKey, String childDerivationPath, ChildNumber pubKeyChildNumber) { + public OutputDescriptor(String script, ExtendedPublicKey extendedPublicKey) { + this(script, Collections.singletonList(extendedPublicKey)); + } + + public OutputDescriptor(String script, List extendedPublicKeys) { + this(script, 0, extendedPublicKeys); + } + + public OutputDescriptor(String script, int multisigThreshold, List extendedPublicKeys) { this.script = script; - this.parentFingerprint = parentFingerprint; - this.keyDerivationPath = keyDerivationPath; - this.pubKey = pubKey; - this.childDerivationPath = childDerivationPath; - this.pubKeyChildNumber = pubKeyChildNumber; + this.multisigThreshold = multisigThreshold; + this.extendedPublicKeys = extendedPublicKeys; + } + + public List getExtendedPublicKeys() { + return extendedPublicKeys; + } + + public boolean isMultisig() { + return extendedPublicKeys.size() > 1; + } + + public ExtendedPublicKey getSingletonExtendedPublicKey() { + if(isMultisig()) { + throw new IllegalStateException("Output descriptor contains multiple public keys but singleton requested"); + } + + return extendedPublicKeys.get(0); } public String getScript() { return script; } - public int getParentFingerprint() { - return parentFingerprint; - } + public boolean describesMultipleAddresses() { + for(ExtendedPublicKey pubKey : extendedPublicKeys) { + if(!pubKey.describesMultipleAddresses()) { + return false; + } + } - public List getKeyDerivation() { - return parsePath(keyDerivationPath); - } - - public DeterministicKey getPubKey() { - return pubKey; + return true; } public List getChildDerivation() { - return getChildDerivation(0); - } + List lastDerivation = null; + for(ExtendedPublicKey pubKey : extendedPublicKeys) { + List derivation = pubKey.getChildDerivation(); + if(lastDerivation != null && !lastDerivation.subList(1, lastDerivation.size()).equals(derivation.subList(1, derivation.size()))) { + throw new IllegalStateException("Cannot determine multisig derivation: constituent derivations do not match"); + } + lastDerivation = derivation; + } - public List getChildDerivation(int wildCardReplacement) { - return getChildDerivation(new ChildNumber(0, getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement); - } - - public boolean describesMultipleAddresses() { - return childDerivationPath.endsWith("/*"); + return lastDerivation; } public List getReceivingDerivation(int wildCardReplacement) { - if(describesMultipleAddresses()) { - if(childDerivationPath.endsWith("0/*")) { - return getChildDerivation(new ChildNumber(0, getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement); - } - - if(pubKeyChildNumber.num() == 0 && childDerivationPath.endsWith("/*")) { - return getChildDerivation(new ChildNumber(0, getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement); - } + if(isMultisig()) { + List path = new ArrayList<>(); + path.add(new ChildNumber(0)); + path.add(new ChildNumber(wildCardReplacement)); + return path; } - throw new IllegalStateException("Cannot derive receiving address from output descriptor " + this.toString()); + return getSingletonExtendedPublicKey().getReceivingDerivation(wildCardReplacement); } public List getChangeDerivation(int wildCardReplacement) { - if(describesMultipleAddresses()) { - if(childDerivationPath.endsWith("0/*")) { - return getChildDerivation(new ChildNumber(0, getPubKey().getChildNumber().isHardened()), childDerivationPath.replace("0/*", "1/*"), wildCardReplacement); - } - - if(pubKeyChildNumber.num() == 1 && childDerivationPath.endsWith("/*")) { - return getChildDerivation(new ChildNumber(1, getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement); - } + if(isMultisig()) { + List path = new ArrayList<>(); + path.add(new ChildNumber(1)); + path.add(new ChildNumber(wildCardReplacement)); + return path; } - throw new IllegalStateException("Cannot derive change address from output descriptor " + this.toString()); + return getSingletonExtendedPublicKey().getChangeDerivation(wildCardReplacement); } - private List getChildDerivation(ChildNumber firstChild, String derivationPath, int wildCardReplacement) { - List path = new ArrayList<>(); - path.add(firstChild); - path.addAll(parsePath(derivationPath, wildCardReplacement)); + public Address getAddress(List path) { + if(isMultisig()) { + Script script = getMultisigScript(path); + return getAddress(script); + } - return path; + DeterministicKey childKey = getSingletonExtendedPublicKey().getKey(path); + return getAddress(childKey); } public Address getAddress(DeterministicKey childKey) { @@ -127,105 +128,110 @@ public class OutputDescriptor { return address; } + private Address getAddress(Script multisigScript) { + Address address = null; + if(script.equals("sh(multi")) { + address = P2SHAddress.fromProgram(multisigScript.getProgram()); + } else if(script.equals("wsh(multi")) { + address = P2WSHAddress.fromProgram(multisigScript.getProgram()); + } else { + throw new IllegalStateException("Cannot determine address for multisig script " + script); + } + + return address; + } + + private Script getMultisigScript(List path) { + List chunks = new ArrayList<>(); + chunks.add(new ScriptChunk(Script.encodeToOpN(multisigThreshold), null)); + + for(ExtendedPublicKey pubKey : extendedPublicKeys) { + List keyPath = null; + if(path.get(0).num() == 0) { + keyPath = pubKey.getReceivingDerivation(path.get(1).num()); + } else if(path.get(0).num() == 1) { + keyPath = pubKey.getChangeDerivation(path.get(1).num()); + } else { + keyPath = pubKey.getChildDerivation(path.get(1).num()); + } + + byte[] pubKeyBytes = pubKey.getKey(keyPath).getPubKey(); + chunks.add(new ScriptChunk(pubKeyBytes.length, pubKeyBytes)); + } + + chunks.add(new ScriptChunk(Script.encodeToOpN(extendedPublicKeys.size()), null)); + chunks.add(new ScriptChunk(ScriptOpCodes.OP_CHECKMULTISIG, null)); + + return new Script(chunks); + } + // See https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md public static OutputDescriptor getOutputDescriptor(String descriptor) { - String script; - String keyDerivationPath =""; - String extPubKey = null; - String childDerivationPath = "/0/*"; - - Matcher matcher = DESCRIPTOR_PATTERN.matcher(descriptor); - if(matcher.matches()) { - script = matcher.group(1); - if(matcher.group(2) != null) { - keyDerivationPath = matcher.group(2); - } - - extPubKey = matcher.group(3); - if(matcher.group(4) != null) { - childDerivationPath = matcher.group(4); - } - } else if (descriptor.startsWith("xpub")) { - extPubKey = descriptor; - script = "pkh"; - } else if(descriptor.startsWith("ypub")) { - extPubKey = descriptor; - script = "sh(wpkh"; - } else if(descriptor.startsWith("zpub")) { - extPubKey = descriptor; - script = "wpkh"; + if(descriptor.startsWith("pkh") || descriptor.startsWith("xpub")) { + return new OutputDescriptor("pkh", getExtendedPublicKeys(descriptor)); + } else if(descriptor.startsWith("wpkh") || descriptor.startsWith("zpub")) { + return new OutputDescriptor("wpkh", getExtendedPublicKeys(descriptor)); + } else if(descriptor.startsWith("sh(wpkh") || descriptor.startsWith("ypub")) { + return new OutputDescriptor("sh(wpkh", getExtendedPublicKeys(descriptor)); + } else if(descriptor.startsWith("sh(multi") || descriptor.startsWith("Ypub")) { + return new OutputDescriptor("sh(multi", getMultsigThreshold(descriptor), getExtendedPublicKeys(descriptor)); + } else if(descriptor.startsWith("wsh(multi") || descriptor.startsWith("Zpub")) { + return new OutputDescriptor("wsh(multi", getMultsigThreshold(descriptor), getExtendedPublicKeys(descriptor)); } else { throw new IllegalArgumentException("Could not parse output descriptor:" + descriptor); } + } - byte[] serializedKey = Base58.decodeChecked(extPubKey); - ByteBuffer buffer = ByteBuffer.wrap(serializedKey); - int header = buffer.getInt(); - if(!(header == bip32HeaderP2PKHXPub || header == bip32HeaderP2PKHYPub || header == bip32HeaderP2WPKHZPub)) { - throw new IllegalArgumentException("Unknown header bytes: " + DeterministicKey.toBase58(serializedKey).substring(0, 4)); - } - - int depth = buffer.get() & 0xFF; // convert signed byte to positive int since depth cannot be negative - final int parentFingerprint = buffer.getInt(); - final int i = buffer.getInt(); - ChildNumber childNumber; - List path; - - if(depth == 0) { - //Poorly formatted extended public key, add first child path element - childNumber = new ChildNumber(0, false); - } else if ((i & ChildNumber.HARDENED_BIT) != 0) { - childNumber = new ChildNumber(i ^ ChildNumber.HARDENED_BIT, true); //already hardened + private static int getMultsigThreshold(String descriptor) { + Matcher matcher = MULTI_PATTERN.matcher(descriptor); + if(matcher.find()) { + String threshold = matcher.group(1); + return Integer.parseInt(threshold); } else { - childNumber = new ChildNumber(i, false); + throw new IllegalArgumentException("Could not find multisig threshold in output descriptor:" + descriptor); } - path = Collections.unmodifiableList(new ArrayList<>(Arrays.asList(childNumber))); - - //Remove account level for depth 4 keys - if(depth == 4 && (descriptor.startsWith("xpub") || descriptor.startsWith("ypub") || descriptor.startsWith("zpub"))) { - log.warn("Output descriptor describes a public key derived at depth 4; change addresses not available"); - childDerivationPath = "/*"; - } - - byte[] chainCode = new byte[32]; - buffer.get(chainCode); - byte[] data = new byte[33]; - buffer.get(data); - if(buffer.hasRemaining()) { - throw new IllegalArgumentException("Found unexpected data in key"); - } - - DeterministicKey pubKey = new DeterministicKey(path, chainCode, new LazyECPoint(ECKey.CURVE.getCurve(), data), depth, parentFingerprint); - return new OutputDescriptor(script, parentFingerprint, keyDerivationPath, pubKey, childDerivationPath, childNumber); } - public static List parsePath(String path) { - return parsePath(path, 0); - } + private static List getExtendedPublicKeys(String descriptor) { + List keys = new ArrayList<>(); + Matcher matcher = XPUB_PATTERN.matcher(descriptor); + while(matcher.find()) { + String keyDerivationPath =""; + String extPubKey = null; + String childDerivationPath = "/0/*"; - public static List parsePath(String path, int wildcardReplacement) { - String[] parsedNodes = path.replace("M", "").split("/"); - List nodes = new ArrayList<>(); + if(matcher.group(1) != null) { + keyDerivationPath = matcher.group(1); + } - for (String n : parsedNodes) { - n = n.replaceAll(" ", ""); - if (n.length() == 0) continue; - boolean isHard = n.endsWith("H") || n.endsWith("h") || n.endsWith("'"); - if (isHard) n = n.substring(0, n.length() - 1); - if (n.equals("*")) n = Integer.toString(wildcardReplacement); - int nodeNumber = Integer.parseInt(n); - nodes.add(new ChildNumber(nodeNumber, isHard)); + extPubKey = matcher.group(2); + if(matcher.group(3) != null) { + childDerivationPath = matcher.group(3); + } + + ExtendedPublicKey extendedPublicKey = ExtendedPublicKey.fromDescriptor(keyDerivationPath, extPubKey, childDerivationPath); + keys.add(extendedPublicKey); } - return nodes; + return keys; } public String toString() { StringBuilder builder = new StringBuilder(); builder.append(script); builder.append("("); - builder.append(getExtendedPublicKey()); - builder.append(childDerivationPath); + + if(isMultisig()) { + StringJoiner joiner = new StringJoiner(","); + joiner.add(Integer.toString(multisigThreshold)); + for(ExtendedPublicKey pubKey : extendedPublicKeys) { + joiner.add(pubKey.toString()); + } + builder.append(joiner.toString()); + } else { + builder.append(getSingletonExtendedPublicKey()); + } + builder.append(")"); if(script.contains("(")){ @@ -234,26 +240,4 @@ public class OutputDescriptor { return builder.toString(); } - - public String getExtendedPublicKey() { - return Base58.encodeChecked(getExtendedPublicKeyBytes()); - } - - public byte[] getExtendedPublicKeyBytes() { - ByteBuffer buffer = ByteBuffer.allocate(78); - buffer.putInt(bip32HeaderP2PKHXPub); - - List childPath = parsePath(childDerivationPath); - int depth = 5 - childPath.size(); - buffer.put((byte)depth); - - buffer.putInt(parentFingerprint); - - buffer.putInt(pubKeyChildNumber.i()); - - buffer.put(pubKey.getChainCode()); - buffer.put(pubKey.getPubKey()); - - return buffer.array(); - } } diff --git a/src/main/java/com/craigraw/drongo/WatchWallet.java b/src/main/java/com/craigraw/drongo/WatchWallet.java index 661ee16..20366d7 100644 --- a/src/main/java/com/craigraw/drongo/WatchWallet.java +++ b/src/main/java/com/craigraw/drongo/WatchWallet.java @@ -11,32 +11,30 @@ public class WatchWallet { private String name; private OutputDescriptor outputDescriptor; - private DeterministicHierarchy hierarchy; private HashMap> addresses = new HashMap<>(LOOK_AHEAD_LIMIT*2); public WatchWallet(String name, String descriptor) { this.name = name; this.outputDescriptor = OutputDescriptor.getOutputDescriptor(descriptor); - this.hierarchy = new DeterministicHierarchy(outputDescriptor.getPubKey()); } public void initialiseAddresses() { if(outputDescriptor.describesMultipleAddresses()) { for(int index = 0; index <= LOOK_AHEAD_LIMIT; index++) { List receivingDerivation = outputDescriptor.getReceivingDerivation(index); - Address address = getAddress(receivingDerivation); + Address address = getReceivingAddress(index); addresses.put(address, receivingDerivation); } for(int index = 0; index <= LOOK_AHEAD_LIMIT; index++) { List changeDerivation = outputDescriptor.getChangeDerivation(index); - Address address = getAddress(changeDerivation); + Address address = getChangeAddress(index); addresses.put(address, changeDerivation); } } else { List derivation = outputDescriptor.getChildDerivation(); - Address address = getAddress(derivation); + Address address = outputDescriptor.getAddress(derivation); addresses.put(address, derivation); } } @@ -61,8 +59,11 @@ public class WatchWallet { return getAddress(outputDescriptor.getChangeDerivation(index)); } - private Address getAddress(List path) { - DeterministicKey childKey = hierarchy.get(path); - return outputDescriptor.getAddress(childKey); + public OutputDescriptor getOutputDescriptor() { + return outputDescriptor; + } + + public Address getAddress(List path) { + return outputDescriptor.getAddress(path); } } diff --git a/src/main/java/com/craigraw/drongo/address/P2WSHAddress.java b/src/main/java/com/craigraw/drongo/address/P2WSHAddress.java new file mode 100644 index 0000000..26b175f --- /dev/null +++ b/src/main/java/com/craigraw/drongo/address/P2WSHAddress.java @@ -0,0 +1,34 @@ +package com.craigraw.drongo.address; + +import com.craigraw.drongo.protocol.*; + +import java.util.ArrayList; +import java.util.List; + +import static com.craigraw.drongo.address.P2WPKHAddress.HRP; + +public class P2WSHAddress extends Address { + public P2WSHAddress(byte[] pubKeyHash) { + super(pubKeyHash); + } + + public int getVersion() { + return 0; + } + + public String getAddress() { + return Bech32.encode(HRP, getVersion(), pubKeyHash); + } + + public Script getOutputScript() { + List chunks = new ArrayList<>(); + chunks.add(new ScriptChunk(Script.encodeToOpN(getVersion()), null)); + chunks.add(new ScriptChunk(pubKeyHash.length, pubKeyHash)); + + return new Script(chunks); + } + + public static P2WSHAddress fromProgram(byte[] program) { + return new P2WSHAddress(Sha256Hash.hash(program)); + } +} diff --git a/src/test/java/com/craigraw/drongo/OutputDescriptorTest.java b/src/test/java/com/craigraw/drongo/OutputDescriptorTest.java index d27da9a..7f744c3 100644 --- a/src/test/java/com/craigraw/drongo/OutputDescriptorTest.java +++ b/src/test/java/com/craigraw/drongo/OutputDescriptorTest.java @@ -13,7 +13,7 @@ public class OutputDescriptorTest { @Test public void iancolemanP2PKH() { - OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("xpub6EEznxrqoN5HUXfD3QC3B8Vjw8Lj9UnRj17uTzNaBnEYN5xgwe6Un46Z443sSTBP2bzLZuDzygkdD1FtVWSexFmg4yAuCTxE2HxXFtz541z"); + OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("xpub6EEznxrqoN5HUXfD3QC3B8Vjw8Lj9UnRj17uTzNaBnEYN5xgwe6Un46Z443sSTBP2bzLZuDzygkdD1FtVWSexFmg4yAuCTxE2HxXFtz541z/*"); Assert.assertEquals("pkh(xpub6EEznxrqoN5HUXfD3QC3B8Vjw8Lj9UnRj17uTzNaBnEYN5xgwe6Un46Z443sSTBP2bzLZuDzygkdD1FtVWSexFmg4yAuCTxE2HxXFtz541z/*)", descriptor.toString()); } @@ -25,7 +25,7 @@ public class OutputDescriptorTest { @Test public void iancolemanP2SHP2WPKH() { - OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("ypub6Zken22QbjfomRUXki5v4ndP6T1DEtaBhGGZBvR4ocoooM44dFmnF8DyFmvcK76TKnuvdFfaPnicVvTAPdqEcbuEfKEqfnRoUjSkTB4u1os"); + OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("ypub6Zken22QbjfomRUXki5v4ndP6T1DEtaBhGGZBvR4ocoooM44dFmnF8DyFmvcK76TKnuvdFfaPnicVvTAPdqEcbuEfKEqfnRoUjSkTB4u1os/*"); Assert.assertEquals("sh(wpkh(xpub6EvPUMMVT48Kv8HQvMJHrhXsvUrmJGagn9kLQXXBRcRvkFEqNbcDd4ZqEZy2KCSXv9o7sn51w8N4cdqbfwRDpNDdnyYR5scKD1P74ZAKbGm/*))", descriptor.toString()); } diff --git a/src/test/java/com/craigraw/drongo/WatchWalletTest.java b/src/test/java/com/craigraw/drongo/WatchWalletTest.java index b7ae1fc..712ba9a 100644 --- a/src/test/java/com/craigraw/drongo/WatchWalletTest.java +++ b/src/test/java/com/craigraw/drongo/WatchWalletTest.java @@ -16,7 +16,7 @@ public class WatchWalletTest { @Test public void iancolemanP2PKH() { - WatchWallet wallet = new WatchWallet("", "xpub6EEznxrqoN5HUXfD3QC3B8Vjw8Lj9UnRj17uTzNaBnEYN5xgwe6Un46Z443sSTBP2bzLZuDzygkdD1FtVWSexFmg4yAuCTxE2HxXFtz541z"); + WatchWallet wallet = new WatchWallet("", "xpub6EEznxrqoN5HUXfD3QC3B8Vjw8Lj9UnRj17uTzNaBnEYN5xgwe6Un46Z443sSTBP2bzLZuDzygkdD1FtVWSexFmg4yAuCTxE2HxXFtz541z/*"); Assert.assertEquals("179cMrkiyx6zD2E1sqBAQLg1SQPAS5vjQW", wallet.getReceivingAddress(0).toString()); Assert.assertEquals("1GdWCzdt5oDYh5n1qeZQCxg5rQKVTuTMJg", wallet.getReceivingAddress(1).toString()); @@ -33,7 +33,7 @@ public class WatchWalletTest { @Test public void iancolemanP2SHP2WPKH() { - WatchWallet wallet = new WatchWallet("", "ypub6Zken22QbjfomRUXki5v4ndP6T1DEtaBhGGZBvR4ocoooM44dFmnF8DyFmvcK76TKnuvdFfaPnicVvTAPdqEcbuEfKEqfnRoUjSkTB4u1os"); + WatchWallet wallet = new WatchWallet("", "ypub6Zken22QbjfomRUXki5v4ndP6T1DEtaBhGGZBvR4ocoooM44dFmnF8DyFmvcK76TKnuvdFfaPnicVvTAPdqEcbuEfKEqfnRoUjSkTB4u1os/*"); Assert.assertEquals("34SgiHwNwJt3nYCVUQcgJWhefVRBZ4aSHf", wallet.getReceivingAddress(0).toString()); Assert.assertEquals("3MgPnbF6UYM3FBhZWXoL2ebLPEa3zCCXLh", wallet.getReceivingAddress(1).toString()); @@ -55,4 +55,26 @@ public class WatchWalletTest { Assert.assertEquals("34TBBnwqv338BT6BVnTKqziFq8HWY6BNbw", wallet.getReceivingAddress(0).toString()); Assert.assertEquals("35Jhf9LGCpb1ihJjWH7uLZ8othr1diuspS", wallet.getChangeAddress(0).toString()); } + + @Test + public void electrumP2WSHMulti() { + WatchWallet wallet = new WatchWallet("", "wsh(multi(2,xpub699B7APGMoPLUrvPsXiBFrJRV8sTHDBHptpHSH36aESP5SLYs4VcEotnX1EvvA5ZoKF2rZ24Wh4U5ALxM21CfL5Kcj6Tu41PjRr2KKMkJTJ/0/*,xpub6Ds1jx5qxAtdczVBnJfHeGgpspzYuxnXHXLCoPZFFyyMoKJ7zzLgcERB1t7eDV1UuBQL1UKNxHFvcMJ7Zj6D2amdaA8gb21cZSXPrpG1bZr/0/*))"); + + Assert.assertEquals("bc1q2jxsrw70ug8jgskmhynvs49h3q5h8fglkdl3trvrc6wsde07wuzqfz98z0", wallet.getReceivingAddress(0).toString()); + Assert.assertEquals("bc1qzw9j02k6l7z598edcgjh5mks507xevhk34rmnerxv45ptsluf0pqyxmyve", wallet.getChangeAddress(0).toString()); + } + + @Test + public void electrumP2WSHMulti2() { + WatchWallet wallet = new WatchWallet("", "wsh(multi(2,Zpub6yhnqjTYE82fc2U1UukQW6qEYsCcNoqsyPWPvL6Qi22YopXv8nD1a44zN87aUQcJr4YdE6DJKEA4xuBr5dzBQHZCBsbiUH7NAcFBgPyx3LB/0/*,Zpub74eXjdXzGCRsixpRAZT3U8ssQ15uhUa1dCFdRJvY3L2qo18He71qWUfpxfbL9e2EYuWKe1tH7qzgUSRVTAektLDVRKwCbAtyRW5j2yhqLiD/0/*))"); + + Assert.assertEquals("bc1qa842ug2njv36ycnhq8wjcg6wxjv7p7h4v0tnl40u6nfxxxffyjnq409pr9", wallet.getReceivingAddress(0).toString()); + Assert.assertEquals("bc1q3auk6c8f77dda0w8y9dz4yd3wqhkf4eufzk8x2quszvzzcyjk6rqgz70pd", wallet.getChangeAddress(0).toString()); + } + + @Test + public void electrumP2WSHMultiSingle() { + WatchWallet wallet = new WatchWallet("", "wsh(multi(2,xpub699B7APGMoPLUrvPsXiBFrJRV8sTHDBHptpHSH36aESP5SLYs4VcEotnX1EvvA5ZoKF2rZ24Wh4U5ALxM21CfL5Kcj6Tu41PjRr2KKMkJTJ/0/0,xpub6Ds1jx5qxAtdczVBnJfHeGgpspzYuxnXHXLCoPZFFyyMoKJ7zzLgcERB1t7eDV1UuBQL1UKNxHFvcMJ7Zj6D2amdaA8gb21cZSXPrpG1bZr/0/0))"); + Assert.assertEquals("bc1q2jxsrw70ug8jgskmhynvs49h3q5h8fglkdl3trvrc6wsde07wuzqfz98z0", wallet.getAddress(wallet.getOutputDescriptor().getChildDerivation()).toString()); + } }