refactor extendedpublickey and keyderivation

This commit is contained in:
Craig Raw 2020-04-18 10:42:51 +02:00
parent 764841635c
commit 075707f1ad
11 changed files with 243 additions and 85 deletions

View file

@ -16,16 +16,13 @@ public class ExtendedPublicKey {
private static final int bip32HeaderP2WHSHPub = 0x2AA7ED3; // The 4 byte header that serializes in base58 to "Zpub"
private static final int bip32HeaderTestnetPub = 0x43587CF; // The 4 byte header that serializes in base58 to "tpub"
private KeyDerivation keyDerivation;
private byte[] parentFingerprint;
private DeterministicKey pubKey;
private String childDerivationPath;
private ChildNumber pubKeyChildNumber;
private final byte[] parentFingerprint;
private final DeterministicKey pubKey;
private final String childDerivationPath;
private final ChildNumber pubKeyChildNumber;
private final DeterministicHierarchy hierarchy;
private DeterministicHierarchy hierarchy;
public ExtendedPublicKey(String masterFingerprint, byte[] parentFingerprint, String keyDerivationPath, DeterministicKey pubKey, String childDerivationPath, ChildNumber pubKeyChildNumber) {
this.keyDerivation = new KeyDerivation(masterFingerprint, keyDerivationPath);
public ExtendedPublicKey(DeterministicKey pubKey, byte[] parentFingerprint, String childDerivationPath, ChildNumber pubKeyChildNumber) {
this.parentFingerprint = parentFingerprint;
this.pubKey = pubKey;
this.childDerivationPath = childDerivationPath;
@ -34,10 +31,6 @@ public class ExtendedPublicKey {
this.hierarchy = new DeterministicHierarchy(pubKey);
}
public String getMasterFingerprint() {
return keyDerivation.getMasterFingerprint();
}
public byte[] getParentFingerprint() {
return parentFingerprint;
}
@ -46,14 +39,6 @@ public class ExtendedPublicKey {
return pubKey.getFingerprint();
}
public String getKeyDerivationPath() {
return keyDerivation.getDerivationPath();
}
public List<ChildNumber> getKeyDerivation() {
return keyDerivation.getParsedDerivationPath();
}
public DeterministicKey getPubKey() {
return pubKey;
}
@ -139,7 +124,7 @@ public class ExtendedPublicKey {
return buffer.array();
}
public static ExtendedPublicKey fromDescriptor(String masterFingerprint, String keyDerivationPath, String extPubKey, String childDerivationPath) {
public static ExtendedPublicKey fromDescriptor(String extPubKey, String childDerivationPath) {
byte[] serializedKey = Base58.decodeChecked(extPubKey);
ByteBuffer buffer = ByteBuffer.wrap(serializedKey);
int header = buffer.getInt();
@ -177,7 +162,17 @@ public class ExtendedPublicKey {
}
DeterministicKey pubKey = new DeterministicKey(path, chainCode, new LazyECPoint(ECKey.CURVE.getCurve(), data), depth, parentFingerprint);
return new ExtendedPublicKey(masterFingerprint, parentFingerprint, keyDerivationPath, pubKey, childDerivationPath, childNumber);
return new ExtendedPublicKey(pubKey, parentFingerprint, childDerivationPath, childNumber);
}
public static boolean isValid(String extPubKey) {
try {
ExtendedPublicKey.fromDescriptor(extPubKey, null);
} catch (Exception e) {
return false;
}
return true;
}
@Override

View file

@ -3,15 +3,18 @@ package com.sparrowwallet.drongo;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class KeyDerivation {
private String masterFingerprint;
private String derivationPath;
private final String masterFingerprint;
private final String derivationPath;
private final List<ChildNumber> derivation;
public KeyDerivation(String masterFingerprint, String derivationPath) {
this.masterFingerprint = masterFingerprint;
this.derivationPath = derivationPath;
this.derivation = parsePath(derivationPath);
}
public String getMasterFingerprint() {
@ -22,8 +25,8 @@ public class KeyDerivation {
return derivationPath;
}
public List<ChildNumber> getParsedDerivationPath() {
return parsePath(derivationPath);
public List<ChildNumber> getDerivation() {
return Collections.unmodifiableList(derivation);
}
public static List<ChildNumber> parsePath(String path) {
@ -31,9 +34,12 @@ public class KeyDerivation {
}
public static List<ChildNumber> parsePath(String path, int wildcardReplacement) {
String[] parsedNodes = path.replace("M", "").replace("m", "").split("/");
List<ChildNumber> nodes = new ArrayList<>();
if(path == null) {
return nodes;
}
String[] parsedNodes = path.replace("M", "").replace("m", "").split("/");
for (String n : parsedNodes) {
n = n.replaceAll(" ", "");
if (n.length() == 0) continue;
@ -57,6 +63,16 @@ public class KeyDerivation {
return path;
}
public static boolean isValid(String derivationPath) {
try {
parsePath(derivationPath);
} catch (Exception e) {
return false;
}
return true;
}
public String toString() {
return masterFingerprint + (derivationPath != null ? derivationPath.replace("m", "") : "");
}

View file

@ -8,10 +8,7 @@ import com.sparrowwallet.drongo.protocol.ScriptChunk;
import com.sparrowwallet.drongo.protocol.ScriptOpCodes;
import com.sparrowwallet.drongo.protocol.ScriptType;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.StringJoiner;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -20,26 +17,30 @@ public class OutputDescriptor {
private static final Pattern MULTI_PATTERN = Pattern.compile("multi\\(([\\d+])");
private static final Pattern KEY_ORIGIN_PATTERN = Pattern.compile("\\[([a-f0-9]+)([/\\d']+)\\]");
private String script;
private int multisigThreshold;
private List<ExtendedPublicKey> extendedPublicKeys;
private final String script;
private final int multisigThreshold;
private final Map<ExtendedPublicKey, KeyDerivation> extendedPublicKeys;
public OutputDescriptor(String script, ExtendedPublicKey extendedPublicKey) {
this(script, Collections.singletonList(extendedPublicKey));
public OutputDescriptor(String script, ExtendedPublicKey extendedPublicKey, KeyDerivation keyDerivation) {
this(script, Collections.singletonMap(extendedPublicKey, keyDerivation));
}
public OutputDescriptor(String script, List<ExtendedPublicKey> extendedPublicKeys) {
public OutputDescriptor(String script, Map<ExtendedPublicKey, KeyDerivation> extendedPublicKeys) {
this(script, 0, extendedPublicKeys);
}
public OutputDescriptor(String script, int multisigThreshold, List<ExtendedPublicKey> extendedPublicKeys) {
public OutputDescriptor(String script, int multisigThreshold, Map<ExtendedPublicKey, KeyDerivation> extendedPublicKeys) {
this.script = script;
this.multisigThreshold = multisigThreshold;
this.extendedPublicKeys = extendedPublicKeys;
}
public List<ExtendedPublicKey> getExtendedPublicKeys() {
return extendedPublicKeys;
public Set<ExtendedPublicKey> getExtendedPublicKeys() {
return Collections.unmodifiableSet(extendedPublicKeys.keySet());
}
public KeyDerivation getKeyDerivation(ExtendedPublicKey extendedPublicKey) {
return extendedPublicKeys.get(extendedPublicKey);
}
public boolean isMultisig() {
@ -51,7 +52,7 @@ public class OutputDescriptor {
throw new IllegalStateException("Output descriptor contains multiple public keys but singleton requested");
}
return extendedPublicKeys.get(0);
return extendedPublicKeys.keySet().iterator().next();
}
public String getScript() {
@ -59,7 +60,7 @@ public class OutputDescriptor {
}
public boolean describesMultipleAddresses() {
for(ExtendedPublicKey pubKey : extendedPublicKeys) {
for(ExtendedPublicKey pubKey : extendedPublicKeys.keySet()) {
if(!pubKey.describesMultipleAddresses()) {
return false;
}
@ -70,7 +71,7 @@ public class OutputDescriptor {
public List<ChildNumber> getChildDerivation() {
List<ChildNumber> lastDerivation = null;
for(ExtendedPublicKey pubKey : extendedPublicKeys) {
for(ExtendedPublicKey pubKey : extendedPublicKeys.keySet()) {
List<ChildNumber> 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");
@ -146,7 +147,7 @@ public class OutputDescriptor {
List<ScriptChunk> chunks = new ArrayList<>();
chunks.add(new ScriptChunk(Script.encodeToOpN(multisigThreshold), null));
for(ExtendedPublicKey pubKey : extendedPublicKeys) {
for(ExtendedPublicKey pubKey : extendedPublicKeys.keySet()) {
List<ChildNumber> keyPath = null;
if(path.get(0).num() == 0) {
keyPath = pubKey.getReceivingDerivation(path.get(1).num());
@ -193,8 +194,8 @@ public class OutputDescriptor {
}
}
private static List<ExtendedPublicKey> getExtendedPublicKeys(String descriptor) {
List<ExtendedPublicKey> keys = new ArrayList<>();
private static Map<ExtendedPublicKey, KeyDerivation> getExtendedPublicKeys(String descriptor) {
Map<ExtendedPublicKey, KeyDerivation> keys = new LinkedHashMap<>();
Matcher matcher = XPUB_PATTERN.matcher(descriptor);
while(matcher.find()) {
String masterFingerprint = null;
@ -216,8 +217,9 @@ public class OutputDescriptor {
childDerivationPath = matcher.group(3);
}
ExtendedPublicKey extendedPublicKey = ExtendedPublicKey.fromDescriptor(masterFingerprint, keyDerivationPath, extPubKey, childDerivationPath);
keys.add(extendedPublicKey);
KeyDerivation keyDerivation = new KeyDerivation(masterFingerprint, keyDerivationPath);
ExtendedPublicKey extendedPublicKey = ExtendedPublicKey.fromDescriptor(extPubKey, childDerivationPath);
keys.put(extendedPublicKey, keyDerivation);
}
return keys;
@ -231,7 +233,7 @@ public class OutputDescriptor {
if(isMultisig()) {
StringJoiner joiner = new StringJoiner(",");
joiner.add(Integer.toString(multisigThreshold));
for(ExtendedPublicKey pubKey : extendedPublicKeys) {
for(ExtendedPublicKey pubKey : extendedPublicKeys.keySet()) {
joiner.add(pubKey.toString());
}
builder.append(joiner.toString());

View file

@ -0,0 +1,38 @@
package com.sparrowwallet.drongo.policy;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Miniscript {
private static final Pattern SINGLE_PATTERN = Pattern.compile("pkh?\\(");
private static final Pattern MULTI_PATTERN = Pattern.compile("multi\\(([\\d+])");
private String script;
public Miniscript(String script) {
this.script = script;
}
public String getScript() {
return script;
}
public void setScript(String script) {
this.script = script;
}
public int getNumSignaturesRequired() {
Matcher singleMatcher = SINGLE_PATTERN.matcher(script);
if(singleMatcher.find()) {
return 1;
}
Matcher multiMatcher = MULTI_PATTERN.matcher(script);
if(multiMatcher.find()) {
String threshold = multiMatcher.group(1);
return Integer.parseInt(threshold);
} else {
throw new IllegalArgumentException("Could not find multisig threshold in " + this);
}
}
}

View file

@ -1,35 +1,56 @@
package com.sparrowwallet.drongo.policy;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import java.util.List;
import static com.sparrowwallet.drongo.protocol.ScriptType.*;
import static com.sparrowwallet.drongo.policy.PolicyType.*;
public class Policy {
private String policy;
private static final String DEFAULT_NAME = "Default";
public Policy(String policy) {
this.policy = policy;
private String name;
private Miniscript miniscript;
public Policy(Miniscript miniscript) {
this(DEFAULT_NAME, miniscript);
}
public String getPolicy() {
return policy;
public Policy(String name, Miniscript miniscript) {
this.name = name;
this.miniscript = miniscript;
}
public void setPolicy(String policy) {
this.policy = policy;
public Miniscript getMiniscript() {
return miniscript;
}
public static Policy getPolicy(PolicyType policyType, ScriptType scriptType, Integer threshold, Integer numCosigners) {
public void setMiniscript(Miniscript miniscript) {
this.miniscript = miniscript;
}
public int getNumSignaturesRequired() {
return getMiniscript().getNumSignaturesRequired();
}
public static Policy getPolicy(PolicyType policyType, ScriptType scriptType, List<Keystore> keystores, Integer threshold) {
if(SINGLE.equals(policyType)) {
if(P2PK.equals(scriptType)) {
return new Policy("pk(<key1>)");
return new Policy(new Miniscript("pk(" + keystores.get(0).getScriptName() + ")"));
}
return new Policy("pkh(<key1>)");
return new Policy(new Miniscript("pkh(" + keystores.get(0).getScriptName() + ")"));
}
if(MULTI.equals(policyType)) {
return new Policy("multi(<threshold>,<key1>,<key2>)");
StringBuilder builder = new StringBuilder("multi(");
builder.append(threshold);
for(Keystore keystore : keystores) {
builder.append(",").append(keystore.getScriptName());
}
builder.append(")");
return new Policy(new Miniscript(builder.toString()));
}
throw new PolicyException("No standard policy for " + policyType + " policy with script type " + scriptType);

View file

@ -6,14 +6,16 @@ import com.sparrowwallet.drongo.policy.PolicyType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import static com.sparrowwallet.drongo.policy.PolicyType.*;
import static com.sparrowwallet.drongo.protocol.Script.decodeFromOpN;
import static com.sparrowwallet.drongo.protocol.ScriptOpCodes.*;
public enum ScriptType {
P2PK("P2PK", new PolicyType[]{SINGLE}) {
P2PK("P2PK") {
@Override
public Address getAddress(byte[] pubKey) {
return new P2PKAddress(pubKey);
@ -59,8 +61,13 @@ public enum ScriptType {
public ECKey getPublicKeyFromScript(Script script) {
return ECKey.fromPublicOnly(script.chunks.get(0).data);
}
@Override
public List<PolicyType> getAllowedPolicyTypes() {
return List.of(SINGLE);
}
},
P2PKH("P2PKH", new PolicyType[]{SINGLE}) {
P2PKH("P2PKH") {
@Override
public Address getAddress(byte[] pubKeyHash) {
return new P2PKHAddress(pubKeyHash);
@ -103,8 +110,13 @@ public enum ScriptType {
public byte[] getHashFromScript(Script script) {
return script.chunks.get(2).data;
}
@Override
public List<PolicyType> getAllowedPolicyTypes() {
return List.of(SINGLE);
}
},
MULTISIG("Bare Multisig", new PolicyType[]{MULTI}) {
MULTISIG("Bare Multisig") {
@Override
public Address getAddress(byte[] bytes) {
throw new ProtocolException("No single address for multisig script type");
@ -178,8 +190,13 @@ public enum ScriptType {
public int getThreshold(Script script) {
return decodeFromOpN(script.chunks.get(0).opcode);
}
@Override
public List<PolicyType> getAllowedPolicyTypes() {
return List.of(MULTI);
}
},
P2SH("P2SH", new PolicyType[]{MULTI}) {
P2SH("P2SH") {
@Override
public Address getAddress(byte[] bytes) {
return new P2SHAddress(bytes);
@ -224,8 +241,13 @@ public enum ScriptType {
public byte[] getHashFromScript(Script script) {
return script.chunks.get(1).data;
}
@Override
public List<PolicyType> getAllowedPolicyTypes() {
return List.of(MULTI);
}
},
P2SH_P2WPKH("P2SH-P2WPKH", new PolicyType[]{SINGLE}) {
P2SH_P2WPKH("P2SH-P2WPKH") {
@Override
public Address getAddress(byte[] bytes) {
return P2SH.getAddress(bytes);
@ -245,8 +267,13 @@ public enum ScriptType {
public byte[] getHashFromScript(Script script) {
return P2SH.getHashFromScript(script);
}
@Override
public List<PolicyType> getAllowedPolicyTypes() {
return List.of(SINGLE);
}
},
P2SH_P2WSH("P2SH-P2WSH", new PolicyType[]{MULTI, CUSTOM}) {
P2SH_P2WSH("P2SH-P2WSH") {
@Override
public Address getAddress(byte[] bytes) {
return P2SH.getAddress(bytes);
@ -266,8 +293,13 @@ public enum ScriptType {
public byte[] getHashFromScript(Script script) {
return P2SH.getHashFromScript(script);
}
@Override
public List<PolicyType> getAllowedPolicyTypes() {
return List.of(MULTI, CUSTOM);
}
},
P2WPKH("P2WPKH", new PolicyType[]{SINGLE}) {
P2WPKH("P2WPKH") {
@Override
public Address getAddress(byte[] bytes) {
return new P2WPKHAddress(bytes);
@ -301,8 +333,13 @@ public enum ScriptType {
public byte[] getHashFromScript(Script script) {
return script.chunks.get(1).data;
}
@Override
public List<PolicyType> getAllowedPolicyTypes() {
return List.of(SINGLE);
}
},
P2WSH("P2WSH", new PolicyType[]{MULTI, CUSTOM}) {
P2WSH("P2WSH") {
@Override
public Address getAddress(byte[] bytes) {
return new P2WSHAddress(bytes);
@ -336,22 +373,27 @@ public enum ScriptType {
public byte[] getHashFromScript(Script script) {
return script.chunks.get(1).data;
}
@Override
public List<PolicyType> getAllowedPolicyTypes() {
return List.of(MULTI, CUSTOM);
}
};
private final String name;
private final PolicyType[] allowedPolicyTypes;
ScriptType(String name, PolicyType[] allowedPolicyTypes) {
ScriptType(String name) {
this.name = name;
this.allowedPolicyTypes = allowedPolicyTypes;
}
public String getName() {
return name;
}
public PolicyType[] getAllowedPolicyTypes() {
return allowedPolicyTypes;
public abstract List<PolicyType> getAllowedPolicyTypes();
public boolean isAllowed(PolicyType policyType) {
return getAllowedPolicyTypes().contains(policyType);
}
public abstract Address getAddress(byte[] bytes);
@ -380,6 +422,10 @@ public enum ScriptType {
public static final ScriptType[] SINGLE_HASH_TYPES = {P2PKH, P2SH, P2SH_P2WPKH, P2SH_P2WSH, P2WPKH, P2WSH};
public static List<ScriptType> getScriptTypesForPolicyType(PolicyType policyType) {
return Arrays.stream(values()).filter(scriptType -> scriptType.isAllowed(policyType)).collect(Collectors.toList());
}
@Override
public String toString() {
return name;

View file

@ -221,9 +221,9 @@ public class PSBT {
case PSBT_GLOBAL_BIP32_PUBKEY:
entry.checkOneBytePlusXpubKey();
KeyDerivation keyDerivation = parseKeyDerivation(entry.getData());
ExtendedPublicKey pubKey = ExtendedPublicKey.fromDescriptor(keyDerivation.getMasterFingerprint(), keyDerivation.getDerivationPath(), Base58.encodeChecked(entry.getKeyData()), null);
ExtendedPublicKey pubKey = ExtendedPublicKey.fromDescriptor(Base58.encodeChecked(entry.getKeyData()), null);
this.extendedPublicKeys.put(pubKey, keyDerivation);
log.debug("Pubkey with master fingerprint " + pubKey.getMasterFingerprint() + " at path " + pubKey.getKeyDerivationPath() + ": " + pubKey.getExtendedPublicKey());
log.debug("Pubkey with master fingerprint " + keyDerivation.getMasterFingerprint() + " at path " + keyDerivation.getDerivationPath() + ": " + pubKey.getExtendedPublicKey());
break;
case PSBT_GLOBAL_VERSION:
entry.checkOneByteKey();

View file

@ -4,6 +4,39 @@ import com.sparrowwallet.drongo.ExtendedPublicKey;
import com.sparrowwallet.drongo.KeyDerivation;
public class Keystore {
private String label;
private KeyDerivation keyDerivation;
private ExtendedPublicKey extendedPublicKey;
public Keystore(String label) {
this.label = label;
}
public String getLabel() {
return label;
}
public String getScriptName() {
return label.replace(" ", "").toLowerCase();
}
public void setLabel(String label) {
this.label = label;
}
public KeyDerivation getKeyDerivation() {
return keyDerivation;
}
public void setKeyDerivation(KeyDerivation keyDerivation) {
this.keyDerivation = keyDerivation;
}
public ExtendedPublicKey getExtendedPublicKey() {
return extendedPublicKey;
}
public void setExtendedPublicKey(ExtendedPublicKey extendedPublicKey) {
this.extendedPublicKey = extendedPublicKey;
}
}

View file

@ -1,5 +1,6 @@
package com.sparrowwallet.drongo.wallet;
import com.sparrowwallet.drongo.policy.Policy;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
@ -9,7 +10,7 @@ import java.util.List;
public class Wallet {
private PolicyType policyType;
private ScriptType scriptType;
private String policy;
private Policy defaultPolicy;
private List<Keystore> keystores = new ArrayList<>();
public PolicyType getPolicyType() {
@ -28,12 +29,12 @@ public class Wallet {
this.scriptType = scriptType;
}
public String getPolicy() {
return policy;
public Policy getDefaultPolicy() {
return defaultPolicy;
}
public void setPolicy(String policy) {
this.policy = policy;
public void setDefaultPolicy(Policy defaultPolicy) {
this.defaultPolicy = defaultPolicy;
}
public List<Keystore> getKeystores() {

View file

@ -45,7 +45,9 @@ public class OutputDescriptorTest {
public void masterP2PKH() {
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("pkh([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)");
Assert.assertEquals("pkh(xpub6CY2xt3vG5BhUS7krcphJprmHCh3jHYB1A8bxtJocU8NyQttKUCLp5izorV1wdXbp7XSSEcaFiKzUroEAL5tD1de8iAUeHP76byTWZu79SD/1/*)", descriptor.toString());
Assert.assertEquals("d34db33f", descriptor.getSingletonExtendedPublicKey().getMasterFingerprint());
Assert.assertEquals("m/44'/0'/0'", descriptor.getSingletonExtendedPublicKey().getKeyDerivationPath());
ExtendedPublicKey extendedPublicKey = descriptor.getSingletonExtendedPublicKey();
KeyDerivation derivation = descriptor.getKeyDerivation(extendedPublicKey);
Assert.assertEquals("d34db33f", derivation.getMasterFingerprint());
Assert.assertEquals("m/44'/0'/0'", derivation.getDerivationPath());
}
}

View file

@ -1,5 +1,7 @@
package com.sparrowwallet.drongo.psbt;
import com.sparrowwallet.drongo.ExtendedPublicKey;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.protocol.NonStandardScriptException;
import com.sparrowwallet.drongo.protocol.Transaction;
import org.bouncycastle.util.encoders.Hex;
@ -214,8 +216,10 @@ public class PSBTTest {
String psbt = "cHNidP8BAJ0BAAAAAnEOp2q0XFy2Q45gflnMA3YmmBgFrp4N/ZCJASq7C+U1AQAAAAD/////GQmU1qizyMgsy8+y+6QQaqBmObhyqNRHRlwNQliNbWcAAAAAAP////8CAOH1BQAAAAAZdqkUtrwsDuVlWoQ9ea/t0MzD991kNAmIrGBa9AUAAAAAFgAUEYjvjkzgRJ6qyPsUHL9aEXbmoIgAAAAATwEEiLIeA55TDKyAAAAAPbyKXJdp8DGxfnf+oVGGAyIaGP0Y8rmlTGyMGsdcvDUC8jBYSxVdHH8c1FEgplPEjWULQxtnxbLBPyfXFCA3wWkQJ1acUDEAAIAAAACAAAAAgAABAR8A4fUFAAAAABYAFDO5gvkbKPFgySC0q5XljOUN2jpKIgIDMJaA8zx9446mpHzU7NZvH1pJdHxv+4gI7QkDkkPjrVxHMEQCIC1wTO2DDFapCTRL10K2hS3M0QPpY7rpLTjnUlTSu0JFAiAthsQ3GV30bAztoITyopHD2i1kBw92v5uQsZXn7yj3cgEiBgMwloDzPH3jjqakfNTs1m8fWkl0fG/7iAjtCQOSQ+OtXBgnVpxQMQAAgAAAAIAAAACAAAAAAAEAAAAAAQEfAOH1BQAAAAAWABQ4j7lEMH63fvRRl9CwskXgefAR3iICAsd3Fh9z0LfHK57nveZQKT0T8JW8dlatH1Jdpf0uELEQRzBEAiBMsftfhpyULg4mEAV2ElQ5F5rojcqKncO6CPeVOYj6pgIgUh9JynkcJ9cOJzybFGFphZCTYeJb4nTqIA1+CIJ+UU0BIgYCx3cWH3PQt8crnue95lApPRPwlbx2Vq0fUl2l/S4QsRAYJ1acUDEAAIAAAACAAAAAgAAAAAAAAAAAAAAiAgLSDKUC7iiWhtIYFb1DqAY3sGmOH7zb5MrtRF9sGgqQ7xgnVpxQMQAAgAAAAIAAAACAAAAAAAQAAAAA";
PSBT psbt1 = PSBT.fromString(psbt);
Assert.assertEquals("27569c50", psbt1.getExtendedPublicKeys().get(0).getMasterFingerprint());
Assert.assertEquals("m/49'/0'/0'", psbt1.getExtendedPublicKeys().get(0).getKeyDerivationPath());
ExtendedPublicKey extendedPublicKey = psbt1.getExtendedPublicKeys().get(0);
KeyDerivation keyDerivation = psbt1.getKeyDerivation(extendedPublicKey);
Assert.assertEquals("27569c50", keyDerivation.getMasterFingerprint());
Assert.assertEquals("m/49'/0'/0'", keyDerivation.getDerivationPath());
Assert.assertEquals(2, psbt1.getPsbtInputs().size());
}