support creating wallets from descriptors containing master xprvs

This commit is contained in:
Craig Raw 2024-01-09 11:37:54 +02:00
parent 579c86b1a7
commit 6f90d0fa82
2 changed files with 71 additions and 14 deletions

View file

@ -34,6 +34,7 @@ public class OutputDescriptor {
private final Map<ExtendedKey, KeyDerivation> extendedPublicKeys; private final Map<ExtendedKey, KeyDerivation> extendedPublicKeys;
private final Map<ExtendedKey, String> mapChildrenDerivations; private final Map<ExtendedKey, String> mapChildrenDerivations;
private final Map<ExtendedKey, String> mapExtendedPublicKeyLabels; private final Map<ExtendedKey, String> mapExtendedPublicKeyLabels;
private final Map<ExtendedKey, ExtendedKey> extendedMasterPrivateKeys;
public OutputDescriptor(ScriptType scriptType, ExtendedKey extendedPublicKey, KeyDerivation keyDerivation) { public OutputDescriptor(ScriptType scriptType, ExtendedKey extendedPublicKey, KeyDerivation keyDerivation) {
this(scriptType, Collections.singletonMap(extendedPublicKey, keyDerivation)); this(scriptType, Collections.singletonMap(extendedPublicKey, keyDerivation));
@ -56,11 +57,16 @@ public class OutputDescriptor {
} }
public OutputDescriptor(ScriptType scriptType, int multisigThreshold, Map<ExtendedKey, KeyDerivation> extendedPublicKeys, Map<ExtendedKey, String> mapChildrenDerivations, Map<ExtendedKey, String> mapExtendedPublicKeyLabels) { public OutputDescriptor(ScriptType scriptType, int multisigThreshold, Map<ExtendedKey, KeyDerivation> extendedPublicKeys, Map<ExtendedKey, String> mapChildrenDerivations, Map<ExtendedKey, String> mapExtendedPublicKeyLabels) {
this(scriptType, multisigThreshold, extendedPublicKeys, mapChildrenDerivations, mapExtendedPublicKeyLabels, new LinkedHashMap<>());
}
public OutputDescriptor(ScriptType scriptType, int multisigThreshold, Map<ExtendedKey, KeyDerivation> extendedPublicKeys, Map<ExtendedKey, String> mapChildrenDerivations, Map<ExtendedKey, String> mapExtendedPublicKeyLabels, Map<ExtendedKey, ExtendedKey> extendedMasterPrivateKeys) {
this.scriptType = scriptType; this.scriptType = scriptType;
this.multisigThreshold = multisigThreshold; this.multisigThreshold = multisigThreshold;
this.extendedPublicKeys = extendedPublicKeys; this.extendedPublicKeys = extendedPublicKeys;
this.mapChildrenDerivations = mapChildrenDerivations; this.mapChildrenDerivations = mapChildrenDerivations;
this.mapExtendedPublicKeyLabels = mapExtendedPublicKeyLabels; this.mapExtendedPublicKeyLabels = mapExtendedPublicKeyLabels;
this.extendedMasterPrivateKeys = extendedMasterPrivateKeys;
} }
public Set<ExtendedKey> getExtendedPublicKeys() { public Set<ExtendedKey> getExtendedPublicKeys() {
@ -255,11 +261,26 @@ public class OutputDescriptor {
wallet.setScriptType(scriptType); wallet.setScriptType(scriptType);
for(Map.Entry<ExtendedKey,KeyDerivation> extKeyEntry : extendedPublicKeys.entrySet()) { for(Map.Entry<ExtendedKey,KeyDerivation> extKeyEntry : extendedPublicKeys.entrySet()) {
ExtendedKey xpub = extKeyEntry.getKey();
Keystore keystore = new Keystore(); Keystore keystore = new Keystore();
keystore.setSource(KeystoreSource.SW_WATCH); if(extendedMasterPrivateKeys.containsKey(xpub)) {
keystore.setWalletModel(WalletModel.SPARROW); ExtendedKey xprv = extendedMasterPrivateKeys.get(xpub);
keystore.setKeyDerivation(extKeyEntry.getValue()); MasterPrivateExtendedKey masterPrivateExtendedKey = new MasterPrivateExtendedKey(xprv.getKey().getPrivKeyBytes(), xprv.getKey().getChainCode());
keystore.setExtendedPublicKey(extKeyEntry.getKey()); 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); setKeystoreLabel(keystore);
wallet.makeLabelsUnique(keystore); wallet.makeLabelsUnique(keystore);
wallet.getKeystores().add(keystore); wallet.getKeystores().add(keystore);
@ -377,11 +398,12 @@ public class OutputDescriptor {
Map<ExtendedKey, KeyDerivation> keyDerivationMap = new LinkedHashMap<>(); Map<ExtendedKey, KeyDerivation> keyDerivationMap = new LinkedHashMap<>();
Map<ExtendedKey, String> keyChildDerivationMap = new LinkedHashMap<>(); Map<ExtendedKey, String> keyChildDerivationMap = new LinkedHashMap<>();
Map<ExtendedKey, ExtendedKey> masterPrivateKeyMap = new LinkedHashMap<>();
Matcher matcher = XPUB_PATTERN.matcher(descriptor); Matcher matcher = XPUB_PATTERN.matcher(descriptor);
while(matcher.find()) { while(matcher.find()) {
String masterFingerprint = null; String masterFingerprint = null;
String keyDerivationPath = null; String keyDerivationPath = null;
String extPubKey; String extKey;
String childDerivationPath = null; String childDerivationPath = null;
if(matcher.group(1) != 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) { if(matcher.group(3) != null) {
childDerivationPath = matcher.group(3); childDerivationPath = matcher.group(3);
} }
KeyDerivation keyDerivation = new KeyDerivation(masterFingerprint, keyDerivationPath); KeyDerivation keyDerivation = new KeyDerivation(masterFingerprint, keyDerivationPath);
try { try {
ExtendedKey extendedPublicKey = ExtendedKey.fromDescriptor(extPubKey); ExtendedKey extendedKey = ExtendedKey.fromDescriptor(extKey);
if(extendedPublicKey.getKey().hasPrivKey()) { if(extendedKey.getKey().hasPrivKey()) {
ExtendedKey privateExtendedKey = extendedKey;
List<ChildNumber> derivation = keyDerivation.getDerivation(); List<ChildNumber> derivation = keyDerivation.getDerivation();
int depth = derivation.size() == 0 ? scriptType.getDefaultDerivation().size() : derivation.size(); int depth = derivation.size() == 0 ? scriptType.getDefaultDerivation().size() : derivation.size();
DeterministicKey prvKey = extendedPublicKey.getKey(); DeterministicKey prvKey = extendedKey.getKey();
DeterministicKey pubKey = new DeterministicKey(prvKey.getPath(), prvKey.getChainCode(), prvKey.getPubKey(), depth, extendedPublicKey.getParentFingerprint()); DeterministicKey pubKey = new DeterministicKey(prvKey.getPath(), prvKey.getChainCode(), prvKey.getPubKey(), depth, extendedKey.getParentFingerprint());
extendedPublicKey = new ExtendedKey(pubKey, pubKey.getParentFingerprint(), extendedPublicKey.getKeyChildNumber()); extendedKey = new ExtendedKey(pubKey, pubKey.getParentFingerprint(), extendedKey.getKeyChildNumber());
if(derivation.size() == 0) {
masterPrivateKeyMap.put(extendedKey, privateExtendedKey);
}
} }
keyDerivationMap.put(extendedPublicKey, keyDerivation); keyDerivationMap.put(extendedKey, keyDerivation);
keyChildDerivationMap.put(extendedPublicKey, childDerivationPath); keyChildDerivationMap.put(extendedKey, childDerivationPath);
} catch(ProtocolException e) { } catch(ProtocolException e) {
throw new ProtocolException("Invalid xpub: " + e.getMessage()); 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) { public static String normalize(String descriptor) {

View file

@ -134,4 +134,34 @@ public class OutputDescriptorTest {
Assert.assertEquals("Unique 1", wallet.getKeystores().get(0).getLabel()); Assert.assertEquals("Unique 1", wallet.getKeystores().get(0).getLabel());
Assert.assertEquals("Unique 2", wallet.getKeystores().get(1).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());
}
} }