refactor and update outputdescriptor to use script type

This commit is contained in:
Craig Raw 2020-09-07 15:07:29 +02:00
parent 3115669c46
commit 0516426dc0
4 changed files with 143 additions and 93 deletions

View file

@ -207,7 +207,7 @@ public class ExtendedKey {
} }
} }
throw new IllegalArgumentException("Unrecognised extended key header for extended key: " + xpub); throw new IllegalArgumentException("Unrecognised extended key header for extended key: " + xkey);
} }
public static Header fromScriptType(ScriptType scriptType, boolean privateKey) { public static Header fromScriptType(ScriptType scriptType, boolean privateKey) {

View file

@ -3,9 +3,8 @@ package com.sparrowwallet.drongo;
import com.sparrowwallet.drongo.address.*; import com.sparrowwallet.drongo.address.*;
import com.sparrowwallet.drongo.crypto.ChildNumber; import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.DeterministicKey; import com.sparrowwallet.drongo.crypto.DeterministicKey;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.Script; import com.sparrowwallet.drongo.protocol.Script;
import com.sparrowwallet.drongo.protocol.ScriptChunk;
import com.sparrowwallet.drongo.protocol.ScriptOpCodes;
import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.ScriptType;
import java.util.*; import java.util.*;
@ -15,29 +14,29 @@ import java.util.regex.Pattern;
import static com.sparrowwallet.drongo.KeyDerivation.parsePath; import static com.sparrowwallet.drongo.KeyDerivation.parsePath;
public class OutputDescriptor { public class OutputDescriptor {
private static final Pattern XPUB_PATTERN = Pattern.compile("(\\[[^\\]]+\\])?(.pub[^/\\)]+)(/[/\\d*']+)?"); private static final Pattern XPUB_PATTERN = Pattern.compile("(\\[[^\\]]+\\])?(.pub[^/\\)]{100,112})(/[/\\d*'hH]+)?");
private static final Pattern MULTI_PATTERN = Pattern.compile("multi\\(([\\d+])"); private static final Pattern MULTI_PATTERN = Pattern.compile("multi\\(([\\d+])");
private static final Pattern KEY_ORIGIN_PATTERN = Pattern.compile("\\[([a-f0-9]+)([/\\d']+)\\]"); private static final Pattern KEY_ORIGIN_PATTERN = Pattern.compile("\\[([A-Fa-f0-9]{8})([/\\d'hH]+)\\]");
private final String script; private final ScriptType scriptType;
private final int multisigThreshold; private final int multisigThreshold;
private final Map<ExtendedKey, KeyDerivation> extendedPublicKeys; private final Map<ExtendedKey, KeyDerivation> extendedPublicKeys;
private final Map<ExtendedKey, String> mapChildrenDerivations; private final Map<ExtendedKey, String> mapChildrenDerivations;
public OutputDescriptor(String script, ExtendedKey extendedPublicKey, KeyDerivation keyDerivation) { public OutputDescriptor(ScriptType scriptType, ExtendedKey extendedPublicKey, KeyDerivation keyDerivation) {
this(script, Collections.singletonMap(extendedPublicKey, keyDerivation)); this(scriptType, Collections.singletonMap(extendedPublicKey, keyDerivation));
} }
public OutputDescriptor(String script, Map<ExtendedKey, KeyDerivation> extendedPublicKeys) { public OutputDescriptor(ScriptType scriptType, Map<ExtendedKey, KeyDerivation> extendedPublicKeys) {
this(script, 0, extendedPublicKeys); this(scriptType, 0, extendedPublicKeys);
} }
public OutputDescriptor(String script, int multisigThreshold, Map<ExtendedKey, KeyDerivation> extendedPublicKeys) { public OutputDescriptor(ScriptType scriptType, int multisigThreshold, Map<ExtendedKey, KeyDerivation> extendedPublicKeys) {
this(script, multisigThreshold, extendedPublicKeys, new LinkedHashMap<>()); this(scriptType, multisigThreshold, extendedPublicKeys, new LinkedHashMap<>());
} }
public OutputDescriptor(String script, int multisigThreshold, Map<ExtendedKey, KeyDerivation> extendedPublicKeys, Map<ExtendedKey, String> mapChildrenDerivations) { public OutputDescriptor(ScriptType scriptType, int multisigThreshold, Map<ExtendedKey, KeyDerivation> extendedPublicKeys, Map<ExtendedKey, String> mapChildrenDerivations) {
this.script = script; this.scriptType = scriptType;
this.multisigThreshold = multisigThreshold; this.multisigThreshold = multisigThreshold;
this.extendedPublicKeys = extendedPublicKeys; this.extendedPublicKeys = extendedPublicKeys;
this.mapChildrenDerivations = mapChildrenDerivations; this.mapChildrenDerivations = mapChildrenDerivations;
@ -51,6 +50,10 @@ public class OutputDescriptor {
return extendedPublicKeys.get(extendedPublicKey); return extendedPublicKeys.get(extendedPublicKey);
} }
public int getMultisigThreshold() {
return multisigThreshold;
}
public String getChildDerivationPath(ExtendedKey extendedPublicKey) { public String getChildDerivationPath(ExtendedKey extendedPublicKey) {
return mapChildrenDerivations.get(extendedPublicKey); return mapChildrenDerivations.get(extendedPublicKey);
} }
@ -81,8 +84,8 @@ public class OutputDescriptor {
return getChildDerivation(extendedPublicKey.getKey().getChildNumber(), childDerivationPath.replace("0/*", "1/*"), wildCardReplacement); return getChildDerivation(extendedPublicKey.getKey().getChildNumber(), childDerivationPath.replace("0/*", "1/*"), wildCardReplacement);
} }
if(extendedPublicKey.getKeyChildNumber().num() == 1 && childDerivationPath.endsWith("/*")) { if(childDerivationPath.endsWith("/*")) {
return getChildDerivation(new ChildNumber(1, extendedPublicKey.getKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement); return getChildDerivation(extendedPublicKey.getKey().getChildNumber(), childDerivationPath, wildCardReplacement);
} }
} }
@ -118,8 +121,8 @@ public class OutputDescriptor {
return extendedPublicKeys.keySet().iterator().next(); return extendedPublicKeys.keySet().iterator().next();
} }
public String getScript() { public ScriptType getScriptType() {
return script; return scriptType;
} }
public boolean describesMultipleAddresses() { public boolean describesMultipleAddresses() {
@ -178,101 +181,83 @@ public class OutputDescriptor {
} }
public Address getAddress(DeterministicKey childKey) { public Address getAddress(DeterministicKey childKey) {
Address address = null; return scriptType.getAddress(childKey);
if(script.equals("pkh")) {
address = new P2PKHAddress(childKey.getPubKeyHash());
} else if(script.equals("sh(wpkh")) {
Script receivingP2wpkhScript = ScriptType.P2WPKH.getOutputScript(childKey.getPubKeyHash());
address = P2SHAddress.fromProgram(receivingP2wpkhScript.getProgram());
} else if(script.equals("wpkh")) {
address = new P2WPKHAddress(childKey.getPubKeyHash());
} else {
throw new IllegalStateException("Cannot determine address for script " + script);
}
return address;
} }
private Address getAddress(Script multisigScript) { private Address getAddress(Script multisigScript) {
Address address = null; return scriptType.getAddress(multisigScript);
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<ChildNumber> path) { private Script getMultisigScript(List<ChildNumber> childPath) {
List<ScriptChunk> chunks = new ArrayList<>(); List<ECKey> keys = new ArrayList<>();
chunks.add(new ScriptChunk(Script.encodeToOpN(multisigThreshold), null));
for(ExtendedKey pubKey : extendedPublicKeys.keySet()) { for(ExtendedKey pubKey : extendedPublicKeys.keySet()) {
List<ChildNumber> keyPath = null; List<ChildNumber> keyPath = getKeyPath(pubKey, childPath);
if(path.get(0).num() == 0) {
keyPath = getReceivingDerivation(pubKey, path.get(1).num());
} else if(path.get(0).num() == 1) {
keyPath = getChangeDerivation(pubKey, path.get(1).num());
} else {
keyPath = getChildDerivation(pubKey, path.get(1).num());
}
byte[] pubKeyBytes = pubKey.getKey(keyPath).getPubKey(); keys.add(pubKey.getKey(keyPath));
chunks.add(new ScriptChunk(pubKeyBytes.length, pubKeyBytes));
} }
chunks.add(new ScriptChunk(Script.encodeToOpN(extendedPublicKeys.size()), null)); return ScriptType.MULTISIG.getOutputScript(multisigThreshold, keys);
chunks.add(new ScriptChunk(ScriptOpCodes.OP_CHECKMULTISIG, null)); }
return new Script(chunks); private List<ChildNumber> getKeyPath(ExtendedKey pubKey, List<ChildNumber> childPath) {
List<ChildNumber> keyPath;
if(childPath.get(0).num() == 0) {
keyPath = getReceivingDerivation(pubKey, childPath.get(1).num());
} else if(childPath.get(0).num() == 1) {
keyPath = getChangeDerivation(pubKey, childPath.get(1).num());
} else {
keyPath = getChildDerivation(pubKey, childPath.get(1).num());
}
return keyPath;
} }
// See https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md // See https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md
public static OutputDescriptor getOutputDescriptor(String descriptor) { public static OutputDescriptor getOutputDescriptor(String descriptor) {
if(descriptor.startsWith("pkh") || descriptor.startsWith("xpub")) { ScriptType scriptType = ScriptType.fromDescriptor(descriptor);
return getOutputDescriptorImpl("pkh", 0, descriptor); if(scriptType == null) {
} else if(descriptor.startsWith("wpkh") || descriptor.startsWith("zpub")) { ExtendedKey.Header header = ExtendedKey.Header.fromExtendedKey(descriptor);
return getOutputDescriptorImpl("wpkh", 0, descriptor); scriptType = header.getDefaultScriptType();
} else if(descriptor.startsWith("sh(wpkh") || descriptor.startsWith("ypub")) {
return getOutputDescriptorImpl("sh(wpkh", 0, descriptor);
} else if(descriptor.startsWith("sh(multi") || descriptor.startsWith("Ypub")) {
return getOutputDescriptorImpl("sh(multi", getMultsigThreshold(descriptor), descriptor);
} else if(descriptor.startsWith("wsh(multi") || descriptor.startsWith("Zpub")) {
return getOutputDescriptorImpl("wsh(multi", getMultsigThreshold(descriptor), descriptor);
} else {
throw new IllegalArgumentException("Could not parse output descriptor:" + descriptor);
} }
if(scriptType == null) {
throw new IllegalArgumentException("Cannot determine script type from descriptor: " + descriptor);
}
int threshold = getMultisigThreshold(descriptor);
return getOutputDescriptorImpl(scriptType, threshold, descriptor);
} }
private static int getMultsigThreshold(String descriptor) { private static int getMultisigThreshold(String descriptor) {
Matcher matcher = MULTI_PATTERN.matcher(descriptor); Matcher matcher = MULTI_PATTERN.matcher(descriptor);
if(matcher.find()) { if(matcher.find()) {
String threshold = matcher.group(1); String threshold = matcher.group(1);
return Integer.parseInt(threshold); return Integer.parseInt(threshold);
} else { } else {
throw new IllegalArgumentException("Could not find multisig threshold in output descriptor:" + descriptor); return 1;
} }
} }
private static OutputDescriptor getOutputDescriptorImpl(String script, int multisigThreshold, String descriptor) { private static OutputDescriptor getOutputDescriptorImpl(ScriptType scriptType, int multisigThreshold, String descriptor) {
Map<ExtendedKey, KeyDerivation> keyDerivationMap = new LinkedHashMap<>(); Map<ExtendedKey, KeyDerivation> keyDerivationMap = new LinkedHashMap<>();
Map<ExtendedKey, String> keyChildDerivationMap = new LinkedHashMap<>(); Map<ExtendedKey, String> keyChildDerivationMap = 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 = null; String extPubKey;
String childDerivationPath = "/0/*"; String childDerivationPath = "/0/*";
if(matcher.group(1) != null) { if(matcher.group(1) != null) {
String keyOrigin = matcher.group(1); String keyOrigin = matcher.group(1);
Matcher keyOriginMatcher = KEY_ORIGIN_PATTERN.matcher(keyOrigin); Matcher keyOriginMatcher = KEY_ORIGIN_PATTERN.matcher(keyOrigin);
if(keyOriginMatcher.matches()) { if(keyOriginMatcher.matches()) {
masterFingerprint = keyOriginMatcher.group(1); byte[] masterFingerprintBytes = Utils.hexToBytes(keyOriginMatcher.group(1));
keyDerivationPath = "m" + keyOriginMatcher.group(2); if(masterFingerprintBytes.length != 4) {
throw new IllegalArgumentException("Master fingerprint must be 4 bytes: " + Utils.bytesToHex(masterFingerprintBytes));
}
masterFingerprint = Utils.bytesToHex(masterFingerprintBytes);
keyDerivationPath = KeyDerivation.writePath(KeyDerivation.parsePath(keyOriginMatcher.group(2)));
} }
} }
@ -287,34 +272,55 @@ public class OutputDescriptor {
keyChildDerivationMap.put(extendedPublicKey, childDerivationPath); keyChildDerivationMap.put(extendedPublicKey, childDerivationPath);
} }
return new OutputDescriptor(script, multisigThreshold, keyDerivationMap, keyChildDerivationMap); return new OutputDescriptor(scriptType, multisigThreshold, keyDerivationMap, keyChildDerivationMap);
} }
public String toString() { public String toString() {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
builder.append(script); builder.append(scriptType.getDescriptor());
builder.append("(");
if(isMultisig()) { if(isMultisig()) {
builder.append(ScriptType.MULTISIG.getDescriptor());
StringJoiner joiner = new StringJoiner(","); StringJoiner joiner = new StringJoiner(",");
joiner.add(Integer.toString(multisigThreshold)); joiner.add(Integer.toString(multisigThreshold));
for(ExtendedKey pubKey : extendedPublicKeys.keySet()) { for(ExtendedKey pubKey : extendedPublicKeys.keySet()) {
joiner.add(pubKey.toString()); String extKeyString = toString(pubKey);
joiner.add(mapChildrenDerivations.get(pubKey)); joiner.add(extKeyString);
} }
builder.append(joiner.toString()); builder.append(joiner.toString());
builder.append(ScriptType.MULTISIG.getCloseDescriptor());
} else { } else {
ExtendedKey extendedPublicKey = getSingletonExtendedPublicKey(); ExtendedKey extendedPublicKey = getSingletonExtendedPublicKey();
builder.append(extendedPublicKey); builder.append(toString(extendedPublicKey));
builder.append(mapChildrenDerivations.get(extendedPublicKey));
}
builder.append(")");
if(script.contains("(")){
builder.append(")");
} }
builder.append(scriptType.getCloseDescriptor());
return builder.toString(); return builder.toString();
} }
private String toString(ExtendedKey pubKey) {
StringBuilder keyBuilder = new StringBuilder();
KeyDerivation keyDerivation = extendedPublicKeys.get(pubKey);
if(keyDerivation != null && keyDerivation.getDerivationPath() != null) {
keyBuilder.append("[");
if(keyDerivation.getMasterFingerprint() != null) {
keyBuilder.append(keyDerivation.getMasterFingerprint());
keyBuilder.append("/");
}
keyBuilder.append(keyDerivation.getDerivationPath().replaceFirst("^m?/", ""));
keyBuilder.append("]");
}
keyBuilder.append(pubKey.toString());
String childDerivation = mapChildrenDerivations.get(pubKey);
if(childDerivation != null) {
if(!childDerivation.startsWith("/")) {
keyBuilder.append("/");
}
keyBuilder.append(childDerivation);
}
return keyBuilder.toString();
}
} }

View file

@ -1102,9 +1102,12 @@ public enum ScriptType {
} }
public static ScriptType fromDescriptor(String descriptor) { public static ScriptType fromDescriptor(String descriptor) {
for(ScriptType type : values()) { List<ScriptType> scriptTypes = Arrays.asList(values());
if(type.getDescriptor().equals(descriptor.toLowerCase())) { scriptTypes.sort((o1, o2) -> o2.getDescriptor().length() - o1.getDescriptor().length());
return type;
for(ScriptType scriptType : scriptTypes) {
if(descriptor.toLowerCase().startsWith(scriptType.getDescriptor())) {
return scriptType;
} }
} }

View file

@ -3,6 +3,9 @@ package com.sparrowwallet.drongo;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import java.util.Iterator;
import java.util.Set;
public class OutputDescriptorTest { public class OutputDescriptorTest {
@Test @Test
@ -44,10 +47,48 @@ public class OutputDescriptorTest {
@Test @Test
public void masterP2PKH() { public void masterP2PKH() {
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("pkh([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)"); OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("pkh([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)");
Assert.assertEquals("pkh(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)", descriptor.toString()); Assert.assertEquals("pkh([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)", descriptor.toString());
ExtendedKey extendedPublicKey = descriptor.getSingletonExtendedPublicKey(); ExtendedKey extendedPublicKey = descriptor.getSingletonExtendedPublicKey();
KeyDerivation derivation = descriptor.getKeyDerivation(extendedPublicKey); KeyDerivation derivation = descriptor.getKeyDerivation(extendedPublicKey);
Assert.assertEquals("d34db33f", derivation.getMasterFingerprint()); Assert.assertEquals("d34db33f", derivation.getMasterFingerprint());
Assert.assertEquals("m/44'/0'/0'", derivation.getDerivationPath()); Assert.assertEquals("m/44'/0'/0'", derivation.getDerivationPath());
Assert.assertEquals("14qCH92HCyDDBFFZdhDt1WMfrMDYnBFYMF", descriptor.getAddress(descriptor.getChangeDerivation(0)).toString());
}
@Test
public void singleP2SH_P2WPKH() {
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("sh(wpkh([f09a3b29/49h/0h/0h]xpub6CjUWYtkq9KT1zkM5NPMxoJTCMm8JSFw7JPyMG6YLBzv5AsCTkASnsVyJhqL1aaqF5XSsFinHK3FDi8RoeEWcTG3DQA2TjqrZ6HJtatYbsU/0/*))");
Assert.assertEquals("sh(wpkh([f09a3b29/49'/0'/0']xpub6CjUWYtkq9KT1zkM5NPMxoJTCMm8JSFw7JPyMG6YLBzv5AsCTkASnsVyJhqL1aaqF5XSsFinHK3FDi8RoeEWcTG3DQA2TjqrZ6HJtatYbsU/0/*))", descriptor.toString());
ExtendedKey extendedPublicKey = descriptor.getSingletonExtendedPublicKey();
KeyDerivation derivation = descriptor.getKeyDerivation(extendedPublicKey);
Assert.assertEquals("f09a3b29", derivation.getMasterFingerprint());
Assert.assertEquals("m/49'/0'/0'", derivation.getDerivationPath());
Assert.assertEquals("31sNBFoYAaFggvNBAnnnLAc5ygfjZRCK6s", descriptor.getAddress(descriptor.getChangeDerivation(0)).toString());
}
@Test
public void multisigP2WSH() {
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("wsh(multi(2,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/0/*,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/*))");
Assert.assertEquals("wsh(multi(2,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/0/*,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/*))", descriptor.toString());
Assert.assertEquals(2, descriptor.getMultisigThreshold());
Assert.assertEquals("bc1qf5l7g5t5v2tp2wnwfeqlktkds7zvprmm7afjn6f85fdesc2pwedsh42kcl", descriptor.getAddress(KeyDerivation.parsePath("0/0")).toString());
}
@Test
public void multisigP2WSH2() {
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("wsh(sortedmulti(2,[04fefef0/48h/0h/0h/2h]xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/0/*,[04ba1ef0/48h/0h/0h/2h]xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/*))");
Set<ExtendedKey> extendedPublicKeys = descriptor.getExtendedPublicKeys();
Iterator<ExtendedKey> iter = extendedPublicKeys.iterator();
ExtendedKey extendedPublicKey1 = iter.next();
Assert.assertEquals("xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB", extendedPublicKey1.toString());
KeyDerivation derivation = descriptor.getKeyDerivation(extendedPublicKey1);
Assert.assertEquals("04fefef0", derivation.getMasterFingerprint());
Assert.assertEquals("m/48'/0'/0'/2'", derivation.getDerivationPath());
ExtendedKey extendedPublicKey2 = iter.next();
Assert.assertEquals("xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH", extendedPublicKey2.toString());
KeyDerivation derivation2 = descriptor.getKeyDerivation(extendedPublicKey2);
Assert.assertEquals("04ba1ef0", derivation2.getMasterFingerprint());
Assert.assertEquals("m/48'/0'/0'/2'", derivation2.getDerivationPath());
} }
} }