diff --git a/src/main/java/com/sparrowwallet/drongo/ExtendedPublicKey.java b/src/main/java/com/sparrowwallet/drongo/ExtendedPublicKey.java index 3f20950..417a2ff 100644 --- a/src/main/java/com/sparrowwallet/drongo/ExtendedPublicKey.java +++ b/src/main/java/com/sparrowwallet/drongo/ExtendedPublicKey.java @@ -18,16 +18,13 @@ public class ExtendedPublicKey { private final byte[] parentFingerprint; private final DeterministicKey pubKey; - private final String childDerivationPath; private final ChildNumber pubKeyChildNumber; private final DeterministicHierarchy hierarchy; - public ExtendedPublicKey(DeterministicKey pubKey, byte[] parentFingerprint, String childDerivationPath, ChildNumber pubKeyChildNumber) { + public ExtendedPublicKey(DeterministicKey pubKey, byte[] parentFingerprint, ChildNumber pubKeyChildNumber) { this.parentFingerprint = parentFingerprint; this.pubKey = pubKey; - this.childDerivationPath = childDerivationPath; this.pubKeyChildNumber = pubKeyChildNumber; - this.hierarchy = new DeterministicHierarchy(pubKey); } @@ -35,62 +32,10 @@ public class ExtendedPublicKey { return parentFingerprint; } - public byte[] getFingerprint() { - return pubKey.getFingerprint(); - } - 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); } @@ -98,7 +43,6 @@ public class ExtendedPublicKey { public String toString() { StringBuilder builder = new StringBuilder(); builder.append(getExtendedPublicKey()); - builder.append(childDerivationPath); return builder.toString(); } @@ -106,25 +50,23 @@ public class ExtendedPublicKey { return Base58.encodeChecked(getExtendedPublicKeyBytes()); } + public ChildNumber getPubKeyChildNumber() { + return pubKeyChildNumber; + } + 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.put((byte)pubKey.getDepth()); buffer.put(parentFingerprint); - buffer.putInt(pubKeyChildNumber.i()); - buffer.put(pubKey.getChainCode()); buffer.put(pubKey.getPubKey()); return buffer.array(); } - public static ExtendedPublicKey fromDescriptor(String extPubKey, String childDerivationPath) { + public static ExtendedPublicKey fromDescriptor(String extPubKey) { byte[] serializedKey = Base58.decodeChecked(extPubKey); ByteBuffer buffer = ByteBuffer.wrap(serializedKey); int header = buffer.getInt(); @@ -147,7 +89,7 @@ public class ExtendedPublicKey { } else { childNumber = new ChildNumber(i, false); } - path = Collections.unmodifiableList(new ArrayList<>(Arrays.asList(childNumber))); + path = List.of(childNumber); byte[] chainCode = new byte[32]; buffer.get(chainCode); @@ -157,17 +99,13 @@ public class ExtendedPublicKey { throw new IllegalArgumentException("Found unexpected data in key"); } - if(childDerivationPath == null) { - childDerivationPath = writePath(Collections.singletonList(childNumber)); - } - DeterministicKey pubKey = new DeterministicKey(path, chainCode, new LazyECPoint(ECKey.CURVE.getCurve(), data), depth, parentFingerprint); - return new ExtendedPublicKey(pubKey, parentFingerprint, childDerivationPath, childNumber); + return new ExtendedPublicKey(pubKey, parentFingerprint, childNumber); } public static boolean isValid(String extPubKey) { try { - ExtendedPublicKey.fromDescriptor(extPubKey, null); + ExtendedPublicKey.fromDescriptor(extPubKey); } catch (Exception e) { return false; } diff --git a/src/main/java/com/sparrowwallet/drongo/OutputDescriptor.java b/src/main/java/com/sparrowwallet/drongo/OutputDescriptor.java index 01007ad..30a0f86 100644 --- a/src/main/java/com/sparrowwallet/drongo/OutputDescriptor.java +++ b/src/main/java/com/sparrowwallet/drongo/OutputDescriptor.java @@ -8,10 +8,13 @@ import com.sparrowwallet.drongo.protocol.ScriptChunk; import com.sparrowwallet.drongo.protocol.ScriptOpCodes; import com.sparrowwallet.drongo.protocol.ScriptType; +import java.security.KeyPair; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; +import static com.sparrowwallet.drongo.KeyDerivation.parsePath; + public class OutputDescriptor { private static final Pattern XPUB_PATTERN = Pattern.compile("(\\[[^\\]]+\\])?(.pub[^/\\)]+)(/[/\\d*']+)?"); private static final Pattern MULTI_PATTERN = Pattern.compile("multi\\(([\\d+])"); @@ -20,6 +23,7 @@ public class OutputDescriptor { private final String script; private final int multisigThreshold; private final Map extendedPublicKeys; + private final Map mapChildrenDerivations; public OutputDescriptor(String script, ExtendedPublicKey extendedPublicKey, KeyDerivation keyDerivation) { this(script, Collections.singletonMap(extendedPublicKey, keyDerivation)); @@ -30,9 +34,14 @@ public class OutputDescriptor { } public OutputDescriptor(String script, int multisigThreshold, Map extendedPublicKeys) { + this(script, multisigThreshold, extendedPublicKeys, new LinkedHashMap<>()); + } + + public OutputDescriptor(String script, int multisigThreshold, Map extendedPublicKeys, Map mapChildrenDerivations) { this.script = script; this.multisigThreshold = multisigThreshold; this.extendedPublicKeys = extendedPublicKeys; + this.mapChildrenDerivations = mapChildrenDerivations; } public Set getExtendedPublicKeys() { @@ -43,6 +52,61 @@ public class OutputDescriptor { return extendedPublicKeys.get(extendedPublicKey); } + public String getChildDerivationPath(ExtendedPublicKey extendedPublicKey) { + return mapChildrenDerivations.get(extendedPublicKey); + } + + public boolean describesMultipleAddresses(ExtendedPublicKey extendedPublicKey) { + return getChildDerivationPath(extendedPublicKey).endsWith("/*"); + } + + public List getReceivingDerivation(ExtendedPublicKey extendedPublicKey, int wildCardReplacement) { + String childDerivationPath = getChildDerivationPath(extendedPublicKey); + if(describesMultipleAddresses(extendedPublicKey)) { + if(childDerivationPath.endsWith("0/*")) { + return getChildDerivation(extendedPublicKey.getPubKey().getChildNumber(), childDerivationPath, wildCardReplacement); + } + + if(extendedPublicKey.getPubKeyChildNumber().num() == 0 && childDerivationPath.endsWith("/*")) { + return getChildDerivation(new ChildNumber(0, extendedPublicKey.getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement); + } + } + + throw new IllegalStateException("Cannot derive receiving address from output descriptor " + this.toString()); + } + + public List getChangeDerivation(ExtendedPublicKey extendedPublicKey, int wildCardReplacement) { + String childDerivationPath = getChildDerivationPath(extendedPublicKey); + if(describesMultipleAddresses(extendedPublicKey)) { + if(childDerivationPath.endsWith("0/*")) { + return getChildDerivation(extendedPublicKey.getPubKey().getChildNumber(), childDerivationPath.replace("0/*", "1/*"), wildCardReplacement); + } + + if(extendedPublicKey.getPubKeyChildNumber().num() == 1 && childDerivationPath.endsWith("/*")) { + return getChildDerivation(new ChildNumber(1, extendedPublicKey.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 List getChildDerivation(ExtendedPublicKey extendedPublicKey) { + return getChildDerivation(extendedPublicKey, 0); + } + + public List getChildDerivation(ExtendedPublicKey extendedPublicKey, int wildCardReplacement) { + String childDerivationPath = getChildDerivationPath(extendedPublicKey); + return getChildDerivation(extendedPublicKey.getPubKey().getChildNumber(), childDerivationPath, wildCardReplacement); + } + public boolean isMultisig() { return extendedPublicKeys.size() > 1; } @@ -61,7 +125,7 @@ public class OutputDescriptor { public boolean describesMultipleAddresses() { for(ExtendedPublicKey pubKey : extendedPublicKeys.keySet()) { - if(!pubKey.describesMultipleAddresses()) { + if(describesMultipleAddresses(pubKey)) { return false; } } @@ -72,7 +136,7 @@ public class OutputDescriptor { public List getChildDerivation() { List lastDerivation = null; for(ExtendedPublicKey pubKey : extendedPublicKeys.keySet()) { - List derivation = pubKey.getChildDerivation(); + List derivation = getChildDerivation(pubKey); 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"); } @@ -90,7 +154,7 @@ public class OutputDescriptor { return path; } - return getSingletonExtendedPublicKey().getReceivingDerivation(wildCardReplacement); + return getReceivingDerivation(getSingletonExtendedPublicKey(), wildCardReplacement); } public List getChangeDerivation(int wildCardReplacement) { @@ -101,7 +165,7 @@ public class OutputDescriptor { return path; } - return getSingletonExtendedPublicKey().getChangeDerivation(wildCardReplacement); + return getChangeDerivation(getSingletonExtendedPublicKey(), wildCardReplacement); } public Address getAddress(List path) { @@ -150,11 +214,11 @@ public class OutputDescriptor { for(ExtendedPublicKey pubKey : extendedPublicKeys.keySet()) { List keyPath = null; if(path.get(0).num() == 0) { - keyPath = pubKey.getReceivingDerivation(path.get(1).num()); + keyPath = getReceivingDerivation(pubKey, path.get(1).num()); } else if(path.get(0).num() == 1) { - keyPath = pubKey.getChangeDerivation(path.get(1).num()); + keyPath = getChangeDerivation(pubKey, path.get(1).num()); } else { - keyPath = pubKey.getChildDerivation(path.get(1).num()); + keyPath = getChildDerivation(pubKey, path.get(1).num()); } byte[] pubKeyBytes = pubKey.getKey(keyPath).getPubKey(); @@ -170,15 +234,15 @@ public class OutputDescriptor { // See https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md public static OutputDescriptor getOutputDescriptor(String descriptor) { if(descriptor.startsWith("pkh") || descriptor.startsWith("xpub")) { - return new OutputDescriptor("pkh", getExtendedPublicKeys(descriptor)); + return getOutputDescriptorImpl("pkh", 0, descriptor); } else if(descriptor.startsWith("wpkh") || descriptor.startsWith("zpub")) { - return new OutputDescriptor("wpkh", getExtendedPublicKeys(descriptor)); + return getOutputDescriptorImpl("wpkh", 0, descriptor); } else if(descriptor.startsWith("sh(wpkh") || descriptor.startsWith("ypub")) { - return new OutputDescriptor("sh(wpkh", getExtendedPublicKeys(descriptor)); + return getOutputDescriptorImpl("sh(wpkh", 0, descriptor); } else if(descriptor.startsWith("sh(multi") || descriptor.startsWith("Ypub")) { - return new OutputDescriptor("sh(multi", getMultsigThreshold(descriptor), getExtendedPublicKeys(descriptor)); + return getOutputDescriptorImpl("sh(multi", getMultsigThreshold(descriptor), descriptor); } else if(descriptor.startsWith("wsh(multi") || descriptor.startsWith("Zpub")) { - return new OutputDescriptor("wsh(multi", getMultsigThreshold(descriptor), getExtendedPublicKeys(descriptor)); + return getOutputDescriptorImpl("wsh(multi", getMultsigThreshold(descriptor), descriptor); } else { throw new IllegalArgumentException("Could not parse output descriptor:" + descriptor); } @@ -194,8 +258,9 @@ public class OutputDescriptor { } } - private static Map getExtendedPublicKeys(String descriptor) { - Map keys = new LinkedHashMap<>(); + private static OutputDescriptor getOutputDescriptorImpl(String script, int multisigThreshold, String descriptor) { + Map keyDerivationMap = new LinkedHashMap<>(); + Map keyChildDerivationMap = new LinkedHashMap<>(); Matcher matcher = XPUB_PATTERN.matcher(descriptor); while(matcher.find()) { String masterFingerprint = null; @@ -218,11 +283,12 @@ public class OutputDescriptor { } KeyDerivation keyDerivation = new KeyDerivation(masterFingerprint, keyDerivationPath); - ExtendedPublicKey extendedPublicKey = ExtendedPublicKey.fromDescriptor(extPubKey, childDerivationPath); - keys.put(extendedPublicKey, keyDerivation); + ExtendedPublicKey extendedPublicKey = ExtendedPublicKey.fromDescriptor(extPubKey); + keyDerivationMap.put(extendedPublicKey, keyDerivation); + keyChildDerivationMap.put(extendedPublicKey, childDerivationPath); } - return keys; + return new OutputDescriptor(script, multisigThreshold, keyDerivationMap, keyChildDerivationMap); } public String toString() { @@ -235,10 +301,13 @@ public class OutputDescriptor { joiner.add(Integer.toString(multisigThreshold)); for(ExtendedPublicKey pubKey : extendedPublicKeys.keySet()) { joiner.add(pubKey.toString()); + joiner.add(mapChildrenDerivations.get(pubKey)); } builder.append(joiner.toString()); } else { - builder.append(getSingletonExtendedPublicKey()); + ExtendedPublicKey extendedPublicKey = getSingletonExtendedPublicKey(); + builder.append(extendedPublicKey); + builder.append(mapChildrenDerivations.get(extendedPublicKey)); } builder.append(")"); diff --git a/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java b/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java index ac302b2..e893de9 100644 --- a/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java +++ b/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java @@ -221,7 +221,7 @@ public class PSBT { case PSBT_GLOBAL_BIP32_PUBKEY: entry.checkOneBytePlusXpubKey(); KeyDerivation keyDerivation = parseKeyDerivation(entry.getData()); - ExtendedPublicKey pubKey = ExtendedPublicKey.fromDescriptor(Base58.encodeChecked(entry.getKeyData()), null); + ExtendedPublicKey pubKey = ExtendedPublicKey.fromDescriptor(Base58.encodeChecked(entry.getKeyData())); this.extendedPublicKeys.put(pubKey, keyDerivation); log.debug("Pubkey with master fingerprint " + keyDerivation.getMasterFingerprint() + " at path " + keyDerivation.getDerivationPath() + ": " + pubKey.getExtendedPublicKey()); break; diff --git a/src/test/java/com/sparrowwallet/drongo/OutputDescriptorTest.java b/src/test/java/com/sparrowwallet/drongo/OutputDescriptorTest.java index a0da296..dad08f1 100644 --- a/src/test/java/com/sparrowwallet/drongo/OutputDescriptorTest.java +++ b/src/test/java/com/sparrowwallet/drongo/OutputDescriptorTest.java @@ -8,7 +8,7 @@ public class OutputDescriptorTest { @Test public void electrumP2PKH() { OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("xpub661MyMwAqRbcFT5HwyRoP5hebbeRDvy2RGDTH2uxFyDPaf5FLtu4njuishddViQxTABZKzoWKuwpy6MsgfPvTw9pKnRGDL5eBxDej9kF54Z"); - Assert.assertEquals("pkh(xpub6BemYiVEULcbpkxh3wp6KUzfzGPFL7JNcxbfQcXxGnJ6sPugTkR69neX8RT9iXdMHFV1FCge72a21WpoHjgoeBTcZju3JKyFf9DztGT2FhE/0/*)", descriptor.toString()); + Assert.assertEquals("pkh(xpub661MyMwAqRbcFT5HwyRoP5hebbeRDvy2RGDTH2uxFyDPaf5FLtu4njuishddViQxTABZKzoWKuwpy6MsgfPvTw9pKnRGDL5eBxDej9kF54Z/0/*)", descriptor.toString()); } @Test @@ -20,7 +20,7 @@ public class OutputDescriptorTest { @Test public void electrumP2WPKH() { OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("zpub6njbcfTHEfK4U96Z8dBaTULdb1LGWMtj73yYZ76kfmE9nuf3KhNSsXfzDefz5KV6TreWjnQbgvnSmSttudzTugesV2HFunYu7gWYJUD4eoR"); - Assert.assertEquals("wpkh(xpub6CqLiu9VMua6V5yFXtXrfZgJqWsG2a8dQdBuk34KFdCCYXvCtx41CmWugPJVZNzBXyHCWy8uHgVUMpePCxh2S3VXueYG8dWLDh49dQ9MJGu/0/*)", descriptor.toString()); + Assert.assertEquals("wpkh(xpub69551L7SwJE6mYiKTucL3J9dF53Nd7ujGpw6zKJyukUPgi2apP3KdQMiBEkp5WBFeaQuEqDUmc5LzsfmUFASKDHfkLtQjxuvaEPFXNDF4Kg/0/*)", descriptor.toString()); } @Test @@ -44,7 +44,7 @@ public class OutputDescriptorTest { @Test public void masterP2PKH() { OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("pkh([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)"); - Assert.assertEquals("pkh(xpub6CY2xt3vG5BhUS7krcphJprmHCh3jHYB1A8bxtJocU8NyQttKUCLp5izorV1wdXbp7XSSEcaFiKzUroEAL5tD1de8iAUeHP76byTWZu79SD/1/*)", descriptor.toString()); + Assert.assertEquals("pkh(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)", descriptor.toString()); ExtendedPublicKey extendedPublicKey = descriptor.getSingletonExtendedPublicKey(); KeyDerivation derivation = descriptor.getKeyDerivation(extendedPublicKey); Assert.assertEquals("d34db33f", derivation.getMasterFingerprint());