From 6f90d0fa82d1f64444a9c16b068fd76bb19633eb Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Tue, 9 Jan 2024 11:37:54 +0200 Subject: [PATCH] support creating wallets from descriptors containing master xprvs --- .../drongo/OutputDescriptor.java | 55 ++++++++++++++----- .../drongo/OutputDescriptorTest.java | 30 ++++++++++ 2 files changed, 71 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/sparrowwallet/drongo/OutputDescriptor.java b/src/main/java/com/sparrowwallet/drongo/OutputDescriptor.java index 4c02f5f..304a7c9 100644 --- a/src/main/java/com/sparrowwallet/drongo/OutputDescriptor.java +++ b/src/main/java/com/sparrowwallet/drongo/OutputDescriptor.java @@ -34,6 +34,7 @@ public class OutputDescriptor { private final Map extendedPublicKeys; private final Map mapChildrenDerivations; private final Map mapExtendedPublicKeyLabels; + private final Map extendedMasterPrivateKeys; public OutputDescriptor(ScriptType scriptType, ExtendedKey extendedPublicKey, KeyDerivation keyDerivation) { this(scriptType, Collections.singletonMap(extendedPublicKey, keyDerivation)); @@ -56,11 +57,16 @@ public class OutputDescriptor { } public OutputDescriptor(ScriptType scriptType, int multisigThreshold, Map extendedPublicKeys, Map mapChildrenDerivations, Map mapExtendedPublicKeyLabels) { + this(scriptType, multisigThreshold, extendedPublicKeys, mapChildrenDerivations, mapExtendedPublicKeyLabels, new LinkedHashMap<>()); + } + + public OutputDescriptor(ScriptType scriptType, int multisigThreshold, Map extendedPublicKeys, Map mapChildrenDerivations, Map mapExtendedPublicKeyLabels, Map extendedMasterPrivateKeys) { this.scriptType = scriptType; this.multisigThreshold = multisigThreshold; this.extendedPublicKeys = extendedPublicKeys; this.mapChildrenDerivations = mapChildrenDerivations; this.mapExtendedPublicKeyLabels = mapExtendedPublicKeyLabels; + this.extendedMasterPrivateKeys = extendedMasterPrivateKeys; } public Set getExtendedPublicKeys() { @@ -255,11 +261,26 @@ public class OutputDescriptor { wallet.setScriptType(scriptType); for(Map.Entry extKeyEntry : extendedPublicKeys.entrySet()) { + ExtendedKey xpub = extKeyEntry.getKey(); Keystore keystore = new Keystore(); - keystore.setSource(KeystoreSource.SW_WATCH); - keystore.setWalletModel(WalletModel.SPARROW); - keystore.setKeyDerivation(extKeyEntry.getValue()); - keystore.setExtendedPublicKey(extKeyEntry.getKey()); + if(extendedMasterPrivateKeys.containsKey(xpub)) { + ExtendedKey xprv = extendedMasterPrivateKeys.get(xpub); + MasterPrivateExtendedKey masterPrivateExtendedKey = new MasterPrivateExtendedKey(xprv.getKey().getPrivKeyBytes(), xprv.getKey().getChainCode()); + String childDerivation = mapChildrenDerivations.get(xpub) == null ? scriptType.getDefaultDerivationPath() : mapChildrenDerivations.get(xpub); + if(childDerivation.endsWith("/0/*") || childDerivation.endsWith("/1/*")) { + childDerivation = childDerivation.substring(0, childDerivation.length() - 4); + } + try { + keystore = Keystore.fromMasterPrivateExtendedKey(masterPrivateExtendedKey, KeyDerivation.parsePath(childDerivation)); + } catch(MnemonicException e) { + throw new RuntimeException(e); + } + } else { + keystore.setSource(KeystoreSource.SW_WATCH); + keystore.setWalletModel(WalletModel.SPARROW); + keystore.setKeyDerivation(extKeyEntry.getValue()); + keystore.setExtendedPublicKey(xpub); + } setKeystoreLabel(keystore); wallet.makeLabelsUnique(keystore); wallet.getKeystores().add(keystore); @@ -377,11 +398,12 @@ public class OutputDescriptor { Map keyDerivationMap = new LinkedHashMap<>(); Map keyChildDerivationMap = new LinkedHashMap<>(); + Map masterPrivateKeyMap = new LinkedHashMap<>(); Matcher matcher = XPUB_PATTERN.matcher(descriptor); while(matcher.find()) { String masterFingerprint = null; String keyDerivationPath = null; - String extPubKey; + String extKey; String childDerivationPath = null; if(matcher.group(1) != null) { @@ -397,23 +419,28 @@ public class OutputDescriptor { } } - extPubKey = matcher.group(2); + extKey = matcher.group(2); if(matcher.group(3) != null) { childDerivationPath = matcher.group(3); } KeyDerivation keyDerivation = new KeyDerivation(masterFingerprint, keyDerivationPath); try { - ExtendedKey extendedPublicKey = ExtendedKey.fromDescriptor(extPubKey); - if(extendedPublicKey.getKey().hasPrivKey()) { + ExtendedKey extendedKey = ExtendedKey.fromDescriptor(extKey); + if(extendedKey.getKey().hasPrivKey()) { + ExtendedKey privateExtendedKey = extendedKey; List derivation = keyDerivation.getDerivation(); int depth = derivation.size() == 0 ? scriptType.getDefaultDerivation().size() : derivation.size(); - DeterministicKey prvKey = extendedPublicKey.getKey(); - DeterministicKey pubKey = new DeterministicKey(prvKey.getPath(), prvKey.getChainCode(), prvKey.getPubKey(), depth, extendedPublicKey.getParentFingerprint()); - extendedPublicKey = new ExtendedKey(pubKey, pubKey.getParentFingerprint(), extendedPublicKey.getKeyChildNumber()); + DeterministicKey prvKey = extendedKey.getKey(); + DeterministicKey pubKey = new DeterministicKey(prvKey.getPath(), prvKey.getChainCode(), prvKey.getPubKey(), depth, extendedKey.getParentFingerprint()); + extendedKey = new ExtendedKey(pubKey, pubKey.getParentFingerprint(), extendedKey.getKeyChildNumber()); + + if(derivation.size() == 0) { + masterPrivateKeyMap.put(extendedKey, privateExtendedKey); + } } - keyDerivationMap.put(extendedPublicKey, keyDerivation); - keyChildDerivationMap.put(extendedPublicKey, childDerivationPath); + keyDerivationMap.put(extendedKey, keyDerivation); + keyChildDerivationMap.put(extendedKey, childDerivationPath); } catch(ProtocolException e) { throw new ProtocolException("Invalid xpub: " + e.getMessage()); } @@ -426,7 +453,7 @@ public class OutputDescriptor { } } - return new OutputDescriptor(scriptType, multisigThreshold, keyDerivationMap, keyChildDerivationMap); + return new OutputDescriptor(scriptType, multisigThreshold, keyDerivationMap, keyChildDerivationMap, new LinkedHashMap<>(), masterPrivateKeyMap); } public static String normalize(String descriptor) { diff --git a/src/test/java/com/sparrowwallet/drongo/OutputDescriptorTest.java b/src/test/java/com/sparrowwallet/drongo/OutputDescriptorTest.java index 6aad892..88f33fb 100644 --- a/src/test/java/com/sparrowwallet/drongo/OutputDescriptorTest.java +++ b/src/test/java/com/sparrowwallet/drongo/OutputDescriptorTest.java @@ -134,4 +134,34 @@ public class OutputDescriptorTest { Assert.assertEquals("Unique 1", wallet.getKeystores().get(0).getLabel()); Assert.assertEquals("Unique 2", wallet.getKeystores().get(1).getLabel()); } + + @Test + public void testMasterPrivateKey() { + String desc = "wpkh(xprv9s21ZrQH143K2x63uS9B5XiQqBKDs5ke5jF7dH7cwKaAycKs72VyR7zfBAqQFAnWMwpW6w2eJKc4pKfkMebXv1qi5cs5eQ1N9n2rwbsp94g)"; + OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(desc); + Wallet wallet = outputDescriptor.toWallet(); + Assert.assertEquals("fe05631b", wallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint()); + Assert.assertEquals("m/84'/0'/0'", wallet.getKeystores().get(0).getKeyDerivation().getDerivationPath()); + Assert.assertEquals("xpub6DTvSp2zaQ3DHrB19BnXTPEsMhnsVPKFgb47x8tkg1VjuwkKvyEeL3Jc4ojgiVUit2ron1SqkQph1hVPtGfREGkiZ8KCbN2TGXnoXHnQ12E", wallet.getKeystores().get(0).getExtendedPublicKey().toString()); + } + + @Test + public void testMasterPrivateKeyWithChildDerivation() { + String desc = "wpkh(xprv9s21ZrQH143K2x63uS9B5XiQqBKDs5ke5jF7dH7cwKaAycKs72VyR7zfBAqQFAnWMwpW6w2eJKc4pKfkMebXv1qi5cs5eQ1N9n2rwbsp94g/84'/1'/0'/0/*)"; + OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(desc); + Wallet wallet = outputDescriptor.toWallet(); + Assert.assertEquals("fe05631b", wallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint()); + Assert.assertEquals("m/84'/1'/0'", wallet.getKeystores().get(0).getKeyDerivation().getDerivationPath()); + Assert.assertEquals("xpub6BwAZuXFhV4oufDPGLi89BXMWkFSWDY8EGjLN7GReoKcBQC2MV9A6siCKefwMitca3YnvRCWKWp2RJoDeG9djtucWkH2EibPEvpm2fyNLK3", wallet.getKeystores().get(0).getExtendedPublicKey().toString()); + } + + @Test + public void testMasterPrivateKeyWithNonBip32ChildDerivation() { + String desc = "wpkh(xprv9s21ZrQH143K2x63uS9B5XiQqBKDs5ke5jF7dH7cwKaAycKs72VyR7zfBAqQFAnWMwpW6w2eJKc4pKfkMebXv1qi5cs5eQ1N9n2rwbsp94g/84'/1'/0'/3/*)"; + OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(desc); + Wallet wallet = outputDescriptor.toWallet(); + Assert.assertEquals("fe05631b", wallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint()); + Assert.assertEquals("m/84'/1'/0'/3/0", wallet.getKeystores().get(0).getKeyDerivation().getDerivationPath()); + Assert.assertEquals("xpub6FmRnopYz7J3zbEmKVnrxkuqQUqoL6wbAffNQJrDeXF29nJaTzUruDWbwG4Q3UR7MWpw3GfbqVnt65GbHsYJitzQpTCLkv8oh8dtcW9bNmr", wallet.getKeystores().get(0).getExtendedPublicKey().toString()); + } }