mirror of
https://github.com/sparrowwallet/drongo.git
synced 2024-11-02 18:26:43 +00:00
handle master private keys
This commit is contained in:
parent
27dda91576
commit
c5042cf130
13 changed files with 231 additions and 117 deletions
|
@ -7,27 +7,25 @@ import com.sparrowwallet.drongo.protocol.ScriptType;
|
|||
import java.nio.ByteBuffer;
|
||||
import java.util.*;
|
||||
|
||||
import static com.sparrowwallet.drongo.KeyDerivation.parsePath;
|
||||
|
||||
public class ExtendedPublicKey {
|
||||
public class ExtendedKey {
|
||||
private final byte[] parentFingerprint;
|
||||
private final DeterministicKey pubKey;
|
||||
private final ChildNumber pubKeyChildNumber;
|
||||
private final DeterministicKey key;
|
||||
private final ChildNumber keyChildNumber;
|
||||
private final DeterministicHierarchy hierarchy;
|
||||
|
||||
public ExtendedPublicKey(DeterministicKey pubKey, byte[] parentFingerprint, ChildNumber pubKeyChildNumber) {
|
||||
public ExtendedKey(DeterministicKey key, byte[] parentFingerprint, ChildNumber keyChildNumber) {
|
||||
this.parentFingerprint = parentFingerprint;
|
||||
this.pubKey = pubKey;
|
||||
this.pubKeyChildNumber = pubKeyChildNumber;
|
||||
this.hierarchy = new DeterministicHierarchy(pubKey);
|
||||
this.key = key;
|
||||
this.keyChildNumber = keyChildNumber;
|
||||
this.hierarchy = new DeterministicHierarchy(key);
|
||||
}
|
||||
|
||||
public byte[] getParentFingerprint() {
|
||||
return parentFingerprint;
|
||||
}
|
||||
|
||||
public DeterministicKey getPubKey() {
|
||||
return pubKey;
|
||||
public DeterministicKey getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public DeterministicKey getKey(List<ChildNumber> path) {
|
||||
|
@ -35,46 +33,51 @@ public class ExtendedPublicKey {
|
|||
}
|
||||
|
||||
public String toString() {
|
||||
return getExtendedPublicKey();
|
||||
return getExtendedKey();
|
||||
}
|
||||
|
||||
public String toString(XpubHeader xpubHeader) {
|
||||
return getExtendedPublicKey(xpubHeader);
|
||||
public String toString(Header extendedKeyHeader) {
|
||||
return getExtendedKey(extendedKeyHeader);
|
||||
}
|
||||
|
||||
public String getExtendedPublicKey() {
|
||||
return Base58.encodeChecked(getExtendedPublicKeyBytes());
|
||||
public String getExtendedKey() {
|
||||
return Base58.encodeChecked(getExtendedKeyBytes());
|
||||
}
|
||||
|
||||
public String getExtendedPublicKey(XpubHeader xpubHeader) {
|
||||
return Base58.encodeChecked(getExtendedPublicKeyBytes(xpubHeader));
|
||||
public String getExtendedKey(Header extendedKeyHeader) {
|
||||
return Base58.encodeChecked(getExtendedKeyBytes(extendedKeyHeader));
|
||||
}
|
||||
|
||||
public ChildNumber getPubKeyChildNumber() {
|
||||
return pubKeyChildNumber;
|
||||
public ChildNumber getKeyChildNumber() {
|
||||
return keyChildNumber;
|
||||
}
|
||||
|
||||
public byte[] getExtendedPublicKeyBytes() {
|
||||
return getExtendedPublicKeyBytes(XpubHeader.xpub);
|
||||
public byte[] getExtendedKeyBytes() {
|
||||
return getExtendedKeyBytes(key.isPubKeyOnly() ? Header.xpub : Header.xprv);
|
||||
}
|
||||
|
||||
public byte[] getExtendedPublicKeyBytes(XpubHeader xpubHeader) {
|
||||
public byte[] getExtendedKeyBytes(Header extendedKeyHeader) {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(78);
|
||||
buffer.putInt(xpubHeader.header);
|
||||
buffer.put((byte)pubKey.getDepth());
|
||||
buffer.putInt(extendedKeyHeader.header);
|
||||
buffer.put((byte) key.getDepth());
|
||||
buffer.put(parentFingerprint);
|
||||
buffer.putInt(pubKeyChildNumber.i());
|
||||
buffer.put(pubKey.getChainCode());
|
||||
buffer.put(pubKey.getPubKey());
|
||||
buffer.putInt(keyChildNumber.i());
|
||||
buffer.put(key.getChainCode());
|
||||
if(key.isPubKeyOnly()) {
|
||||
buffer.put(key.getPubKey());
|
||||
} else {
|
||||
buffer.put((byte)0);
|
||||
buffer.put(key.getPrivKeyBytes());
|
||||
}
|
||||
|
||||
return buffer.array();
|
||||
}
|
||||
|
||||
public static ExtendedPublicKey fromDescriptor(String extPubKey) {
|
||||
public static ExtendedKey fromDescriptor(String extPubKey) {
|
||||
byte[] serializedKey = Base58.decodeChecked(extPubKey);
|
||||
ByteBuffer buffer = ByteBuffer.wrap(serializedKey);
|
||||
int header = buffer.getInt();
|
||||
if(!XpubHeader.isValidHeader(header)) {
|
||||
if(!Header.isValidHeader(header)) {
|
||||
throw new IllegalArgumentException("Unknown header bytes: " + DeterministicKey.toBase58(serializedKey).substring(0, 4));
|
||||
}
|
||||
|
||||
|
@ -86,7 +89,7 @@ public class ExtendedPublicKey {
|
|||
List<ChildNumber> path;
|
||||
|
||||
if(depth == 0) {
|
||||
//Poorly formatted extended public key, add first child path element
|
||||
//Poorly formatted extended key, add first child path element
|
||||
childNumber = new ChildNumber(0, false);
|
||||
} else if ((i & ChildNumber.HARDENED_BIT) != 0) {
|
||||
childNumber = new ChildNumber(i ^ ChildNumber.HARDENED_BIT, true); //already hardened
|
||||
|
@ -104,12 +107,12 @@ public class ExtendedPublicKey {
|
|||
}
|
||||
|
||||
DeterministicKey pubKey = new DeterministicKey(path, chainCode, new LazyECPoint(ECKey.CURVE.getCurve(), data), depth, parentFingerprint);
|
||||
return new ExtendedPublicKey(pubKey, parentFingerprint, childNumber);
|
||||
return new ExtendedKey(pubKey, parentFingerprint, childNumber);
|
||||
}
|
||||
|
||||
public static boolean isValid(String extPubKey) {
|
||||
try {
|
||||
ExtendedPublicKey.fromDescriptor(extPubKey);
|
||||
ExtendedKey.fromDescriptor(extPubKey);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
|
@ -117,16 +120,16 @@ public class ExtendedPublicKey {
|
|||
return true;
|
||||
}
|
||||
|
||||
public ExtendedPublicKey copy() {
|
||||
public ExtendedKey copy() {
|
||||
//DeterministicKey is effectively final
|
||||
return new ExtendedPublicKey(pubKey, Arrays.copyOf(parentFingerprint, parentFingerprint.length), pubKeyChildNumber);
|
||||
return new ExtendedKey(key, Arrays.copyOf(parentFingerprint, parentFingerprint.length), keyChildNumber);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
ExtendedPublicKey that = (ExtendedPublicKey) o;
|
||||
ExtendedKey that = (ExtendedKey) o;
|
||||
return that.toString().equals(this.toString());
|
||||
}
|
||||
|
||||
|
@ -135,13 +138,15 @@ public class ExtendedPublicKey {
|
|||
return toString().hashCode();
|
||||
}
|
||||
|
||||
public enum XpubHeader {
|
||||
public enum Header {
|
||||
xprv("xprv", 0x0488ADE4, null),
|
||||
xpub("xpub", 0x0488B21E, ScriptType.P2PKH),
|
||||
ypub("ypub", 0x049D7CB2, ScriptType.P2SH_P2WPKH),
|
||||
zpub("zpub", 0x04B24746, ScriptType.P2WPKH),
|
||||
Ypub("Ypub", 0x0295b43f, ScriptType.P2SH_P2WSH),
|
||||
Zpub("Zpub", 0x02aa7ed3, ScriptType.P2WSH),
|
||||
tpub("tpub", 0x043587cf, ScriptType.P2PKH),
|
||||
tprv("tprv", 0x04358394, null),
|
||||
upub("upub", 0x044a5262, ScriptType.P2SH_P2WPKH),
|
||||
vpub("vpub", 0x045f1cf6, ScriptType.P2WPKH),
|
||||
Upub("Upub", 0x024289ef, ScriptType.P2SH_P2WSH),
|
||||
|
@ -151,7 +156,7 @@ public class ExtendedPublicKey {
|
|||
private final int header;
|
||||
private final ScriptType defaultScriptType;
|
||||
|
||||
XpubHeader(String name, int header, ScriptType defaultScriptType) {
|
||||
Header(String name, int header, ScriptType defaultScriptType) {
|
||||
this.name = name;
|
||||
this.header = header;
|
||||
this.defaultScriptType = defaultScriptType;
|
||||
|
@ -169,29 +174,29 @@ public class ExtendedPublicKey {
|
|||
return defaultScriptType;
|
||||
}
|
||||
|
||||
public static XpubHeader fromXpub(String xpub) {
|
||||
for(XpubHeader xpubHeader : XpubHeader.values()) {
|
||||
if(xpub.startsWith(xpubHeader.name)) {
|
||||
return xpubHeader;
|
||||
public static Header fromExtendedKey(String xkey) {
|
||||
for(Header extendedKeyHeader : Header.values()) {
|
||||
if(xkey.startsWith(extendedKeyHeader.name)) {
|
||||
return extendedKeyHeader;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Unrecognised xpub header for xpub: " + xpub);
|
||||
throw new IllegalArgumentException("Unrecognised extended key header for extended key: " + xpub);
|
||||
}
|
||||
|
||||
public static XpubHeader fromScriptType(ScriptType scriptType) {
|
||||
for(XpubHeader xpubHeader : XpubHeader.values()) {
|
||||
if(xpubHeader.defaultScriptType.equals(scriptType)) {
|
||||
return xpubHeader;
|
||||
public static Header fromScriptType(ScriptType scriptType) {
|
||||
for(Header extendedKeyHeader : Header.values()) {
|
||||
if(extendedKeyHeader.defaultScriptType != null && extendedKeyHeader.defaultScriptType.equals(scriptType)) {
|
||||
return extendedKeyHeader;
|
||||
}
|
||||
}
|
||||
|
||||
return XpubHeader.xpub;
|
||||
return Header.xpub;
|
||||
}
|
||||
|
||||
public static boolean isValidHeader(int header) {
|
||||
for(XpubHeader xpubHeader : XpubHeader.values()) {
|
||||
if(header == xpubHeader.header) {
|
||||
for(Header extendedKeyHeader : Header.values()) {
|
||||
if(header == extendedKeyHeader.header) {
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -21,68 +21,68 @@ public class OutputDescriptor {
|
|||
|
||||
private final String script;
|
||||
private final int multisigThreshold;
|
||||
private final Map<ExtendedPublicKey, KeyDerivation> extendedPublicKeys;
|
||||
private final Map<ExtendedPublicKey, String> mapChildrenDerivations;
|
||||
private final Map<ExtendedKey, KeyDerivation> extendedPublicKeys;
|
||||
private final Map<ExtendedKey, String> mapChildrenDerivations;
|
||||
|
||||
public OutputDescriptor(String script, ExtendedPublicKey extendedPublicKey, KeyDerivation keyDerivation) {
|
||||
public OutputDescriptor(String script, ExtendedKey extendedPublicKey, KeyDerivation keyDerivation) {
|
||||
this(script, Collections.singletonMap(extendedPublicKey, keyDerivation));
|
||||
}
|
||||
|
||||
public OutputDescriptor(String script, Map<ExtendedPublicKey, KeyDerivation> extendedPublicKeys) {
|
||||
public OutputDescriptor(String script, Map<ExtendedKey, KeyDerivation> extendedPublicKeys) {
|
||||
this(script, 0, extendedPublicKeys);
|
||||
}
|
||||
|
||||
public OutputDescriptor(String script, int multisigThreshold, Map<ExtendedPublicKey, KeyDerivation> extendedPublicKeys) {
|
||||
public OutputDescriptor(String script, int multisigThreshold, Map<ExtendedKey, KeyDerivation> extendedPublicKeys) {
|
||||
this(script, multisigThreshold, extendedPublicKeys, new LinkedHashMap<>());
|
||||
}
|
||||
|
||||
public OutputDescriptor(String script, int multisigThreshold, Map<ExtendedPublicKey, KeyDerivation> extendedPublicKeys, Map<ExtendedPublicKey, String> mapChildrenDerivations) {
|
||||
public OutputDescriptor(String script, int multisigThreshold, Map<ExtendedKey, KeyDerivation> extendedPublicKeys, Map<ExtendedKey, String> mapChildrenDerivations) {
|
||||
this.script = script;
|
||||
this.multisigThreshold = multisigThreshold;
|
||||
this.extendedPublicKeys = extendedPublicKeys;
|
||||
this.mapChildrenDerivations = mapChildrenDerivations;
|
||||
}
|
||||
|
||||
public Set<ExtendedPublicKey> getExtendedPublicKeys() {
|
||||
public Set<ExtendedKey> getExtendedPublicKeys() {
|
||||
return Collections.unmodifiableSet(extendedPublicKeys.keySet());
|
||||
}
|
||||
|
||||
public KeyDerivation getKeyDerivation(ExtendedPublicKey extendedPublicKey) {
|
||||
public KeyDerivation getKeyDerivation(ExtendedKey extendedPublicKey) {
|
||||
return extendedPublicKeys.get(extendedPublicKey);
|
||||
}
|
||||
|
||||
public String getChildDerivationPath(ExtendedPublicKey extendedPublicKey) {
|
||||
public String getChildDerivationPath(ExtendedKey extendedPublicKey) {
|
||||
return mapChildrenDerivations.get(extendedPublicKey);
|
||||
}
|
||||
|
||||
public boolean describesMultipleAddresses(ExtendedPublicKey extendedPublicKey) {
|
||||
public boolean describesMultipleAddresses(ExtendedKey extendedPublicKey) {
|
||||
return getChildDerivationPath(extendedPublicKey).endsWith("/*");
|
||||
}
|
||||
|
||||
public List<ChildNumber> getReceivingDerivation(ExtendedPublicKey extendedPublicKey, int wildCardReplacement) {
|
||||
public List<ChildNumber> getReceivingDerivation(ExtendedKey extendedPublicKey, int wildCardReplacement) {
|
||||
String childDerivationPath = getChildDerivationPath(extendedPublicKey);
|
||||
if(describesMultipleAddresses(extendedPublicKey)) {
|
||||
if(childDerivationPath.endsWith("0/*")) {
|
||||
return getChildDerivation(extendedPublicKey.getPubKey().getChildNumber(), childDerivationPath, wildCardReplacement);
|
||||
return getChildDerivation(extendedPublicKey.getKey().getChildNumber(), childDerivationPath, wildCardReplacement);
|
||||
}
|
||||
|
||||
if(extendedPublicKey.getPubKeyChildNumber().num() == 0 && childDerivationPath.endsWith("/*")) {
|
||||
return getChildDerivation(new ChildNumber(0, extendedPublicKey.getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement);
|
||||
if(extendedPublicKey.getKeyChildNumber().num() == 0 && childDerivationPath.endsWith("/*")) {
|
||||
return getChildDerivation(new ChildNumber(0, extendedPublicKey.getKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement);
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalStateException("Cannot derive receiving address from output descriptor " + this.toString());
|
||||
}
|
||||
|
||||
public List<ChildNumber> getChangeDerivation(ExtendedPublicKey extendedPublicKey, int wildCardReplacement) {
|
||||
public List<ChildNumber> getChangeDerivation(ExtendedKey extendedPublicKey, int wildCardReplacement) {
|
||||
String childDerivationPath = getChildDerivationPath(extendedPublicKey);
|
||||
if(describesMultipleAddresses(extendedPublicKey)) {
|
||||
if(childDerivationPath.endsWith("0/*")) {
|
||||
return getChildDerivation(extendedPublicKey.getPubKey().getChildNumber(), childDerivationPath.replace("0/*", "1/*"), wildCardReplacement);
|
||||
return getChildDerivation(extendedPublicKey.getKey().getChildNumber(), childDerivationPath.replace("0/*", "1/*"), wildCardReplacement);
|
||||
}
|
||||
|
||||
if(extendedPublicKey.getPubKeyChildNumber().num() == 1 && childDerivationPath.endsWith("/*")) {
|
||||
return getChildDerivation(new ChildNumber(1, extendedPublicKey.getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement);
|
||||
if(extendedPublicKey.getKeyChildNumber().num() == 1 && childDerivationPath.endsWith("/*")) {
|
||||
return getChildDerivation(new ChildNumber(1, extendedPublicKey.getKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -97,20 +97,20 @@ public class OutputDescriptor {
|
|||
return path;
|
||||
}
|
||||
|
||||
public List<ChildNumber> getChildDerivation(ExtendedPublicKey extendedPublicKey) {
|
||||
public List<ChildNumber> getChildDerivation(ExtendedKey extendedPublicKey) {
|
||||
return getChildDerivation(extendedPublicKey, 0);
|
||||
}
|
||||
|
||||
public List<ChildNumber> getChildDerivation(ExtendedPublicKey extendedPublicKey, int wildCardReplacement) {
|
||||
public List<ChildNumber> getChildDerivation(ExtendedKey extendedPublicKey, int wildCardReplacement) {
|
||||
String childDerivationPath = getChildDerivationPath(extendedPublicKey);
|
||||
return getChildDerivation(extendedPublicKey.getPubKey().getChildNumber(), childDerivationPath, wildCardReplacement);
|
||||
return getChildDerivation(extendedPublicKey.getKey().getChildNumber(), childDerivationPath, wildCardReplacement);
|
||||
}
|
||||
|
||||
public boolean isMultisig() {
|
||||
return extendedPublicKeys.size() > 1;
|
||||
}
|
||||
|
||||
public ExtendedPublicKey getSingletonExtendedPublicKey() {
|
||||
public ExtendedKey getSingletonExtendedPublicKey() {
|
||||
if(isMultisig()) {
|
||||
throw new IllegalStateException("Output descriptor contains multiple public keys but singleton requested");
|
||||
}
|
||||
|
@ -123,7 +123,7 @@ public class OutputDescriptor {
|
|||
}
|
||||
|
||||
public boolean describesMultipleAddresses() {
|
||||
for(ExtendedPublicKey pubKey : extendedPublicKeys.keySet()) {
|
||||
for(ExtendedKey pubKey : extendedPublicKeys.keySet()) {
|
||||
if(describesMultipleAddresses(pubKey)) {
|
||||
return false;
|
||||
}
|
||||
|
@ -134,7 +134,7 @@ public class OutputDescriptor {
|
|||
|
||||
public List<ChildNumber> getChildDerivation() {
|
||||
List<ChildNumber> lastDerivation = null;
|
||||
for(ExtendedPublicKey pubKey : extendedPublicKeys.keySet()) {
|
||||
for(ExtendedKey pubKey : extendedPublicKeys.keySet()) {
|
||||
List<ChildNumber> 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");
|
||||
|
@ -210,7 +210,7 @@ public class OutputDescriptor {
|
|||
List<ScriptChunk> chunks = new ArrayList<>();
|
||||
chunks.add(new ScriptChunk(Script.encodeToOpN(multisigThreshold), null));
|
||||
|
||||
for(ExtendedPublicKey pubKey : extendedPublicKeys.keySet()) {
|
||||
for(ExtendedKey pubKey : extendedPublicKeys.keySet()) {
|
||||
List<ChildNumber> keyPath = null;
|
||||
if(path.get(0).num() == 0) {
|
||||
keyPath = getReceivingDerivation(pubKey, path.get(1).num());
|
||||
|
@ -258,8 +258,8 @@ public class OutputDescriptor {
|
|||
}
|
||||
|
||||
private static OutputDescriptor getOutputDescriptorImpl(String script, int multisigThreshold, String descriptor) {
|
||||
Map<ExtendedPublicKey, KeyDerivation> keyDerivationMap = new LinkedHashMap<>();
|
||||
Map<ExtendedPublicKey, String> keyChildDerivationMap = new LinkedHashMap<>();
|
||||
Map<ExtendedKey, KeyDerivation> keyDerivationMap = new LinkedHashMap<>();
|
||||
Map<ExtendedKey, String> keyChildDerivationMap = new LinkedHashMap<>();
|
||||
Matcher matcher = XPUB_PATTERN.matcher(descriptor);
|
||||
while(matcher.find()) {
|
||||
String masterFingerprint = null;
|
||||
|
@ -282,7 +282,7 @@ public class OutputDescriptor {
|
|||
}
|
||||
|
||||
KeyDerivation keyDerivation = new KeyDerivation(masterFingerprint, keyDerivationPath);
|
||||
ExtendedPublicKey extendedPublicKey = ExtendedPublicKey.fromDescriptor(extPubKey);
|
||||
ExtendedKey extendedPublicKey = ExtendedKey.fromDescriptor(extPubKey);
|
||||
keyDerivationMap.put(extendedPublicKey, keyDerivation);
|
||||
keyChildDerivationMap.put(extendedPublicKey, childDerivationPath);
|
||||
}
|
||||
|
@ -298,13 +298,13 @@ public class OutputDescriptor {
|
|||
if(isMultisig()) {
|
||||
StringJoiner joiner = new StringJoiner(",");
|
||||
joiner.add(Integer.toString(multisigThreshold));
|
||||
for(ExtendedPublicKey pubKey : extendedPublicKeys.keySet()) {
|
||||
for(ExtendedKey pubKey : extendedPublicKeys.keySet()) {
|
||||
joiner.add(pubKey.toString());
|
||||
joiner.add(mapChildrenDerivations.get(pubKey));
|
||||
}
|
||||
builder.append(joiner.toString());
|
||||
} else {
|
||||
ExtendedPublicKey extendedPublicKey = getSingletonExtendedPublicKey();
|
||||
ExtendedKey extendedPublicKey = getSingletonExtendedPublicKey();
|
||||
builder.append(extendedPublicKey);
|
||||
builder.append(mapChildrenDerivations.get(extendedPublicKey));
|
||||
}
|
||||
|
|
|
@ -30,9 +30,9 @@ public class DeterministicHierarchy {
|
|||
*
|
||||
* @param path the path to the key
|
||||
* @return next newly created key using the child derivation function
|
||||
* @throws IllegalArgumentException if create is false and the path was not found.
|
||||
* @throws HDDerivationException if create is false and the path was not found.
|
||||
*/
|
||||
public DeterministicKey get(List<ChildNumber> path) {
|
||||
public DeterministicKey get(List<ChildNumber> path) throws HDDerivationException {
|
||||
if(!keys.containsKey(path)) {
|
||||
if(path.size() == 0) {
|
||||
throw new IllegalArgumentException("Can't derive the master key: nothing to derive from.");
|
||||
|
|
|
@ -5,6 +5,7 @@ import com.sparrowwallet.drongo.Utils;
|
|||
import com.sparrowwallet.drongo.protocol.Base58;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
@ -54,6 +55,18 @@ public class DeterministicKey extends ECKey {
|
|||
this.parentFingerprint = (parent != null) ? parent.getFingerprint() : new byte[4];
|
||||
}
|
||||
|
||||
public DeterministicKey(List<ChildNumber> childNumberPath,
|
||||
byte[] chainCode,
|
||||
BigInteger priv,
|
||||
DeterministicKey parent) {
|
||||
super(priv, ECKey.publicPointFromPrivate(priv), true);
|
||||
this.parent = parent;
|
||||
this.childNumberPath = childNumberPath;
|
||||
this.chainCode = Arrays.copyOf(chainCode, chainCode.length);
|
||||
this.depth = parent == null ? 0 : parent.depth + 1;
|
||||
this.parentFingerprint = (parent != null) ? parent.getFingerprint() : new byte[4];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return this key's depth in the hierarchy, where the root node is at depth zero.
|
||||
* This may be different than the number of segments in the path if this key was
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
package com.sparrowwallet.drongo.crypto;
|
||||
|
||||
public class HDDerivationException extends RuntimeException {
|
||||
public HDDerivationException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public HDDerivationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public HDDerivationException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public HDDerivationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
|
@ -5,22 +5,52 @@ import org.bouncycastle.math.ec.ECPoint;
|
|||
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class HDKeyDerivation {
|
||||
public static DeterministicKey deriveChildKey(DeterministicKey parent, ChildNumber childNumber) {
|
||||
public static final String BITCOIN_SEED_KEY = "Bitcoin seed";
|
||||
|
||||
public static DeterministicKey createMasterPrivateKey(byte[] seed) throws HDDerivationException {
|
||||
byte[] hmacSha512 = Utils.getHmacSha512Hash(BITCOIN_SEED_KEY.getBytes(StandardCharsets.UTF_8), seed);
|
||||
byte[] privKeyBytes = Arrays.copyOfRange(hmacSha512, 0, 32);
|
||||
byte[] chainCode = Arrays.copyOfRange(hmacSha512, 32, 64);
|
||||
Arrays.fill(hmacSha512, (byte)0);
|
||||
DeterministicKey masterPrivKey = createMasterPrivKeyFromBytes(privKeyBytes, chainCode);
|
||||
Arrays.fill(privKeyBytes, (byte)0);
|
||||
Arrays.fill(chainCode, (byte)0);
|
||||
return masterPrivKey;
|
||||
}
|
||||
|
||||
public static DeterministicKey createMasterPrivKeyFromBytes(byte[] privKeyBytes, byte[] chainCode) throws HDDerivationException {
|
||||
// childNumberPath is an empty list because we are creating the root key.
|
||||
return createMasterPrivKeyFromBytes(privKeyBytes, chainCode, Collections.emptyList());
|
||||
}
|
||||
|
||||
public static DeterministicKey createMasterPrivKeyFromBytes(byte[] privKeyBytes, byte[] chainCode, List<ChildNumber> childNumberPath) throws HDDerivationException {
|
||||
BigInteger priv = new BigInteger(1, privKeyBytes);
|
||||
if(priv.equals(BigInteger.ZERO) || priv.compareTo(ECKey.CURVE.getN()) > 0) {
|
||||
throw new HDDerivationException("Private key bytes are not valid");
|
||||
}
|
||||
|
||||
return new DeterministicKey(childNumberPath, chainCode, priv, null);
|
||||
}
|
||||
|
||||
public static DeterministicKey deriveChildKey(DeterministicKey parent, ChildNumber childNumber) throws HDDerivationException {
|
||||
RawKeyBytes rawKey = deriveChildKeyBytesFromPublic(parent, childNumber);
|
||||
return new DeterministicKey(Utils.appendChild(parent.getPath(), childNumber), rawKey.chainCode, new LazyECPoint(ECKey.CURVE.getCurve(), rawKey.keyBytes), parent);
|
||||
}
|
||||
|
||||
public static RawKeyBytes deriveChildKeyBytesFromPublic(DeterministicKey parent, ChildNumber childNumber) {
|
||||
public static RawKeyBytes deriveChildKeyBytesFromPublic(DeterministicKey parent, ChildNumber childNumber) throws HDDerivationException {
|
||||
if(childNumber.isHardened()) {
|
||||
throw new IllegalArgumentException("Can't use private derivation with public keys only.");
|
||||
throw new HDDerivationException("Can't use private derivation with public keys only.");
|
||||
}
|
||||
|
||||
byte[] parentPublicKey = parent.getPubKeyPoint().getEncoded(true);
|
||||
if(parentPublicKey.length != 33) {
|
||||
throw new IllegalArgumentException("Parent pubkey must be 33 bytes, but is " + parentPublicKey.length);
|
||||
throw new HDDerivationException("Parent pubkey must be 33 bytes, but is " + parentPublicKey.length);
|
||||
}
|
||||
|
||||
ByteBuffer data = ByteBuffer.allocate(37);
|
||||
|
@ -28,7 +58,7 @@ public class HDKeyDerivation {
|
|||
data.putInt(childNumber.i());
|
||||
byte[] i = Utils.getHmacSha512Hash(parent.getChainCode(), data.array());
|
||||
if(i.length != 64) {
|
||||
throw new IllegalStateException("HmacSHA512 output must be 64 bytes, is" + i.length);
|
||||
throw new HDDerivationException("HmacSHA512 output must be 64 bytes, is" + i.length);
|
||||
}
|
||||
|
||||
byte[] il = Arrays.copyOfRange(i, 0, 32);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package com.sparrowwallet.drongo.psbt;
|
||||
|
||||
import com.sparrowwallet.drongo.ExtendedPublicKey;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
|
@ -39,7 +39,7 @@ public class PSBT {
|
|||
|
||||
private Transaction transaction = null;
|
||||
private Integer version = null;
|
||||
private Map<ExtendedPublicKey, KeyDerivation> extendedPublicKeys = new LinkedHashMap<>();
|
||||
private Map<ExtendedKey, KeyDerivation> extendedPublicKeys = new LinkedHashMap<>();
|
||||
private Map<String, String> globalProprietary = new LinkedHashMap<>();
|
||||
|
||||
private List<PSBTInput> psbtInputs = new ArrayList<>();
|
||||
|
@ -221,9 +221,9 @@ public class PSBT {
|
|||
case PSBT_GLOBAL_BIP32_PUBKEY:
|
||||
entry.checkOneBytePlusXpubKey();
|
||||
KeyDerivation keyDerivation = parseKeyDerivation(entry.getData());
|
||||
ExtendedPublicKey pubKey = ExtendedPublicKey.fromDescriptor(Base58.encodeChecked(entry.getKeyData()));
|
||||
ExtendedKey pubKey = ExtendedKey.fromDescriptor(Base58.encodeChecked(entry.getKeyData()));
|
||||
this.extendedPublicKeys.put(pubKey, keyDerivation);
|
||||
log.debug("Pubkey with master fingerprint " + keyDerivation.getMasterFingerprint() + " at path " + keyDerivation.getDerivationPath() + ": " + pubKey.getExtendedPublicKey());
|
||||
log.debug("Pubkey with master fingerprint " + keyDerivation.getMasterFingerprint() + " at path " + keyDerivation.getDerivationPath() + ": " + pubKey.getExtendedKey());
|
||||
break;
|
||||
case PSBT_GLOBAL_VERSION:
|
||||
entry.checkOneByteKey();
|
||||
|
@ -382,12 +382,12 @@ public class PSBT {
|
|||
return version;
|
||||
}
|
||||
|
||||
public KeyDerivation getKeyDerivation(ExtendedPublicKey publicKey) {
|
||||
public KeyDerivation getKeyDerivation(ExtendedKey publicKey) {
|
||||
return extendedPublicKeys.get(publicKey);
|
||||
}
|
||||
|
||||
public List<ExtendedPublicKey> getExtendedPublicKeys() {
|
||||
return new ArrayList<ExtendedPublicKey>(extendedPublicKeys.keySet());
|
||||
public List<ExtendedKey> getExtendedPublicKeys() {
|
||||
return new ArrayList<ExtendedKey>(extendedPublicKeys.keySet());
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
|
|
|
@ -10,7 +10,7 @@ import java.nio.charset.StandardCharsets;
|
|||
import java.text.Normalizer;
|
||||
import java.util.*;
|
||||
|
||||
public class Bip39 {
|
||||
public class Bip39Calculator {
|
||||
private Map<String, Integer> wordlistIndex;
|
||||
|
||||
public byte[] getSeed(List<String> mnemonicWords, String passphrase) {
|
|
@ -1,8 +1,11 @@
|
|||
package com.sparrowwallet.drongo.wallet;
|
||||
|
||||
import com.sparrowwallet.drongo.ExtendedPublicKey;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.crypto.DeterministicKey;
|
||||
import com.sparrowwallet.drongo.crypto.HDKeyDerivation;
|
||||
|
||||
public class Keystore {
|
||||
public static final String DEFAULT_LABEL = "Keystore 1";
|
||||
|
@ -11,7 +14,8 @@ public class Keystore {
|
|||
private KeystoreSource source = KeystoreSource.SW_WATCH;
|
||||
private WalletModel walletModel = WalletModel.SPARROW;
|
||||
private KeyDerivation keyDerivation;
|
||||
private ExtendedPublicKey extendedPublicKey;
|
||||
private ExtendedKey extendedPublicKey;
|
||||
private byte[] seed;
|
||||
|
||||
public Keystore() {
|
||||
this(DEFAULT_LABEL);
|
||||
|
@ -57,14 +61,34 @@ public class Keystore {
|
|||
this.keyDerivation = keyDerivation;
|
||||
}
|
||||
|
||||
public ExtendedPublicKey getExtendedPublicKey() {
|
||||
public ExtendedKey getExtendedPublicKey() {
|
||||
return extendedPublicKey;
|
||||
}
|
||||
|
||||
public void setExtendedPublicKey(ExtendedPublicKey extendedPublicKey) {
|
||||
public void setExtendedPublicKey(ExtendedKey extendedPublicKey) {
|
||||
this.extendedPublicKey = extendedPublicKey;
|
||||
}
|
||||
|
||||
public byte[] getSeed() {
|
||||
return seed;
|
||||
}
|
||||
|
||||
public void setSeed(byte[] seed) {
|
||||
this.seed = seed;
|
||||
}
|
||||
|
||||
public DeterministicKey getMasterPrivateKey() {
|
||||
if(seed == null) {
|
||||
throw new IllegalArgumentException("Keystore does not contain a seed");
|
||||
}
|
||||
|
||||
return HDKeyDerivation.createMasterPrivateKey(seed);
|
||||
}
|
||||
|
||||
public ExtendedKey getExtendedPrivateKey() {
|
||||
return new ExtendedKey(getMasterPrivateKey(), new byte[4], ChildNumber.ZERO);
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
if(label == null || source == null || walletModel == null || keyDerivation == null || extendedPublicKey == null) {
|
||||
return false;
|
||||
|
|
|
@ -45,7 +45,7 @@ public class OutputDescriptorTest {
|
|||
public void masterP2PKH() {
|
||||
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("pkh([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)");
|
||||
Assert.assertEquals("pkh(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)", descriptor.toString());
|
||||
ExtendedPublicKey extendedPublicKey = descriptor.getSingletonExtendedPublicKey();
|
||||
ExtendedKey extendedPublicKey = descriptor.getSingletonExtendedPublicKey();
|
||||
KeyDerivation derivation = descriptor.getKeyDerivation(extendedPublicKey);
|
||||
Assert.assertEquals("d34db33f", derivation.getMasterFingerprint());
|
||||
Assert.assertEquals("m/44'/0'/0'", derivation.getDerivationPath());
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package com.sparrowwallet.drongo.psbt;
|
||||
|
||||
import com.sparrowwallet.drongo.ExtendedPublicKey;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.protocol.NonStandardScriptException;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
|
@ -216,7 +216,7 @@ 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);
|
||||
|
||||
ExtendedPublicKey extendedPublicKey = psbt1.getExtendedPublicKeys().get(0);
|
||||
ExtendedKey extendedPublicKey = psbt1.getExtendedPublicKeys().get(0);
|
||||
KeyDerivation keyDerivation = psbt1.getKeyDerivation(extendedPublicKey);
|
||||
Assert.assertEquals("27569c50", keyDerivation.getMasterFingerprint());
|
||||
Assert.assertEquals("m/49'/0'/0'", keyDerivation.getDerivationPath());
|
||||
|
|
|
@ -7,14 +7,14 @@ import org.junit.Test;
|
|||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class Bip39Test {
|
||||
public class Bip39CalculatorTest {
|
||||
@Test
|
||||
public void bip39TwelveWordsTest() {
|
||||
String words = "absent essay fox snake vast pumpkin height crouch silent bulb excuse razor";
|
||||
List<String> wordlist = Arrays.asList(words.split(" "));
|
||||
|
||||
Bip39 bip39 = new Bip39();
|
||||
byte[] seed = bip39.getSeed(wordlist, "");
|
||||
Bip39Calculator bip39Calculator = new Bip39Calculator();
|
||||
byte[] seed = bip39Calculator.getSeed(wordlist, "");
|
||||
|
||||
Assert.assertEquals("727ecfcf0bce9d8ec0ef066f7aeb845c271bdd4ee06a37398cebd40dc810140bb620b6c10a8ad671afdceaf37aa55d92d6478f747e8b92430dd938ab5be961dd", Utils.bytesToHex(seed));
|
||||
}
|
||||
|
@ -24,8 +24,8 @@ public class Bip39Test {
|
|||
String words = "arch easily near social civil image seminar monkey engine party promote turtle";
|
||||
List<String> wordlist = Arrays.asList(words.split(" "));
|
||||
|
||||
Bip39 bip39 = new Bip39();
|
||||
byte[] seed = bip39.getSeed(wordlist, "anotherpass867");
|
||||
Bip39Calculator bip39Calculator = new Bip39Calculator();
|
||||
byte[] seed = bip39Calculator.getSeed(wordlist, "anotherpass867");
|
||||
|
||||
Assert.assertEquals("ca50764cda44a2cf52aef3c677bebf26011f9dc2b9fddfed2a8a5a9ecb8542956990a16e6873b7724044e83708d9d3a662b765e8800e6e79b289f51c2bcad756", Utils.bytesToHex(seed));
|
||||
}
|
||||
|
@ -35,8 +35,8 @@ public class Bip39Test {
|
|||
String words = "open grunt omit snap behave inch engine hamster hope increase exotic segment news choose roast";
|
||||
List<String> wordlist = Arrays.asList(words.split(" "));
|
||||
|
||||
Bip39 bip39 = new Bip39();
|
||||
byte[] seed = bip39.getSeed(wordlist, "");
|
||||
Bip39Calculator bip39Calculator = new Bip39Calculator();
|
||||
byte[] seed = bip39Calculator.getSeed(wordlist, "");
|
||||
|
||||
Assert.assertEquals("2174deae5fd315253dc065db7ef97f46957eb68a12505adccfb7f8aca5b63788c587e73430848f85417d9a7d95e6396d2eb3af73c9fb507ebcb9268a5ad47885", Utils.bytesToHex(seed));
|
||||
}
|
||||
|
@ -46,8 +46,8 @@ public class Bip39Test {
|
|||
String words = "mandate lend daring actual health dilemma throw muffin garden pony inherit volume slim visual police supreme bless crush";
|
||||
List<String> wordlist = Arrays.asList(words.split(" "));
|
||||
|
||||
Bip39 bip39 = new Bip39();
|
||||
byte[] seed = bip39.getSeed(wordlist, "");
|
||||
Bip39Calculator bip39Calculator = new Bip39Calculator();
|
||||
byte[] seed = bip39Calculator.getSeed(wordlist, "");
|
||||
|
||||
Assert.assertEquals("04bd65f582e288bbf595213048b06e1552017776d20ca290ac06d840e197bcaaccd4a85a45a41219be4183dd2e521e7a7a2d6aea3069f04e503ef6d9c8dfa651", Utils.bytesToHex(seed));
|
||||
}
|
||||
|
@ -57,8 +57,8 @@ public class Bip39Test {
|
|||
String words = "mirror milk file hope drill conduct empty mutual physical easily sell patient green final release excuse name asset update advance resource";
|
||||
List<String> wordlist = Arrays.asList(words.split(" "));
|
||||
|
||||
Bip39 bip39 = new Bip39();
|
||||
byte[] seed = bip39.getSeed(wordlist, "");
|
||||
Bip39Calculator bip39Calculator = new Bip39Calculator();
|
||||
byte[] seed = bip39Calculator.getSeed(wordlist, "");
|
||||
|
||||
Assert.assertEquals("f3a88a437153333f9759f323dfe7910e6a649c34da5800e6c978d77baad54b67b06eab17c0107243f3e8b395a2de98c910e9528127539efda2eea5ae50e94019", Utils.bytesToHex(seed));
|
||||
}
|
||||
|
@ -68,8 +68,8 @@ public class Bip39Test {
|
|||
String words = "earth easily dwarf dance forum muscle brick often huge base long steel silk frost quiz liquid echo adapt annual expand slim rookie venture oval";
|
||||
List<String> wordlist = Arrays.asList(words.split(" "));
|
||||
|
||||
Bip39 bip39 = new Bip39();
|
||||
byte[] seed = bip39.getSeed(wordlist, "");
|
||||
Bip39Calculator bip39Calculator = new Bip39Calculator();
|
||||
byte[] seed = bip39Calculator.getSeed(wordlist, "");
|
||||
|
||||
Assert.assertEquals("60f825219a1fcfa479de28435e9bf2aa5734e212982daee582ca0427ad6141c65be9863c3ce0f18e2b173083ea49dcf47d07148734a5f748ac60d470cee6a2bc", Utils.bytesToHex(seed));
|
||||
}
|
||||
|
@ -79,8 +79,8 @@ public class Bip39Test {
|
|||
String words = "earth easily dwarf dance forum muscle brick often huge base long steel silk frost quiz liquid echo adapt annual expand slim rookie venture oval";
|
||||
List<String> wordlist = Arrays.asList(words.split(" "));
|
||||
|
||||
Bip39 bip39 = new Bip39();
|
||||
byte[] seed = bip39.getSeed(wordlist, "thispass");
|
||||
Bip39Calculator bip39Calculator = new Bip39Calculator();
|
||||
byte[] seed = bip39Calculator.getSeed(wordlist, "thispass");
|
||||
|
||||
Assert.assertEquals("a652d123f421f56257391af26063e900619678b552dafd3850e699f6da0667269bbcaebb0509557481db29607caac0294b3cd337d740174cfa05f552fe9e0272", Utils.bytesToHex(seed));
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package com.sparrowwallet.drongo.wallet;
|
||||
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class KeystoreTest {
|
||||
@Test
|
||||
public void testExtendedPrivateKey() {
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setSeed(Utils.hexToBytes("727ecfcf0bce9d8ec0ef066f7aeb845c271bdd4ee06a37398cebd40dc810140bb620b6c10a8ad671afdceaf37aa55d92d6478f747e8b92430dd938ab5be961dd"));
|
||||
|
||||
Assert.assertEquals("xprv9s21ZrQH143K3rN5vhm4bKDKsk1PmUK1mzxSMwkVSp2GbomwGmjLaGqrs8Nn9r14jCsfCNWfTR6pAtCsJutUH6QSHX65CePNW3YVyGxqvJa", keystore.getExtendedPrivateKey().toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExtendedPrivateKeyTwo() {
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setSeed(Utils.hexToBytes("4d8c47d0d6241169d0b17994219211c4a980f7146bb70dbc897416790e9de23a6265c708b88176e24f6eb7378a7c55cd4bdc067cafe574eaf3480f9a41c3c43c"));
|
||||
|
||||
Assert.assertEquals("xprv9s21ZrQH143K4AkrxAyivDeTCWhZV6fdLfBRR8QerWe9hHiRqMjBMj9MFNef7oFufgcDcW54LhguPNm6MVLEMWPX5qxKhmNjCzi9Zy6yhkc", keystore.getExtendedPrivateKey().toString());
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue