mirror of
https://github.com/sparrowwallet/drongo.git
synced 2024-11-04 19:16:44 +00:00
support extended master private key keystores
This commit is contained in:
parent
1aeaacaf59
commit
85e8b97a8c
4 changed files with 210 additions and 25 deletions
|
@ -17,6 +17,7 @@ public class Keystore {
|
||||||
private WalletModel walletModel = WalletModel.SPARROW;
|
private WalletModel walletModel = WalletModel.SPARROW;
|
||||||
private KeyDerivation keyDerivation;
|
private KeyDerivation keyDerivation;
|
||||||
private ExtendedKey extendedPublicKey;
|
private ExtendedKey extendedPublicKey;
|
||||||
|
private MasterPrivateExtendedKey masterPrivateExtendedKey;
|
||||||
private DeterministicSeed seed;
|
private DeterministicSeed seed;
|
||||||
|
|
||||||
public Keystore() {
|
public Keystore() {
|
||||||
|
@ -71,6 +72,22 @@ public class Keystore {
|
||||||
this.extendedPublicKey = extendedPublicKey;
|
this.extendedPublicKey = extendedPublicKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasMasterPrivateExtendedKey() {
|
||||||
|
return masterPrivateExtendedKey != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MasterPrivateExtendedKey getMasterPrivateExtendedKey() {
|
||||||
|
return masterPrivateExtendedKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMasterPrivateExtendedKey(MasterPrivateExtendedKey masterPrivateExtendedKey) {
|
||||||
|
this.masterPrivateExtendedKey = masterPrivateExtendedKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasSeed() {
|
||||||
|
return seed != null;
|
||||||
|
}
|
||||||
|
|
||||||
public DeterministicSeed getSeed() {
|
public DeterministicSeed getSeed() {
|
||||||
return seed;
|
return seed;
|
||||||
}
|
}
|
||||||
|
@ -79,16 +96,28 @@ public class Keystore {
|
||||||
this.seed = seed;
|
this.seed = seed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasPrivateKey() {
|
||||||
|
return hasSeed() || hasMasterPrivateExtendedKey();
|
||||||
|
}
|
||||||
|
|
||||||
public DeterministicKey getMasterPrivateKey() throws MnemonicException {
|
public DeterministicKey getMasterPrivateKey() throws MnemonicException {
|
||||||
if(seed == null) {
|
if(seed == null && masterPrivateExtendedKey == null) {
|
||||||
throw new IllegalArgumentException("Keystore does not contain a seed");
|
throw new IllegalArgumentException("Keystore does not contain a master private key, or seed to derive one from");
|
||||||
}
|
}
|
||||||
|
|
||||||
if(seed.isEncrypted()) {
|
if(seed != null) {
|
||||||
throw new IllegalArgumentException("Seed is encrypted");
|
if(seed.isEncrypted()) {
|
||||||
|
throw new IllegalArgumentException("Seed is encrypted");
|
||||||
|
}
|
||||||
|
|
||||||
|
return HDKeyDerivation.createMasterPrivateKey(seed.getSeedBytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
return HDKeyDerivation.createMasterPrivateKey(seed.getSeedBytes());
|
if(masterPrivateExtendedKey.isEncrypted()) {
|
||||||
|
throw new IllegalArgumentException("Master private key is encrypted");
|
||||||
|
}
|
||||||
|
|
||||||
|
return masterPrivateExtendedKey.getPrivateKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ExtendedKey getExtendedMasterPrivateKey() throws MnemonicException {
|
public ExtendedKey getExtendedMasterPrivateKey() throws MnemonicException {
|
||||||
|
@ -178,11 +207,11 @@ public class Keystore {
|
||||||
}
|
}
|
||||||
|
|
||||||
if(source == KeystoreSource.SW_SEED) {
|
if(source == KeystoreSource.SW_SEED) {
|
||||||
if(seed == null) {
|
if(seed == null && masterPrivateExtendedKey == null) {
|
||||||
throw new InvalidKeystoreException("Source of " + source + " but no seed is present");
|
throw new InvalidKeystoreException("Source of " + source + " but no seed or master private key is present");
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!seed.isEncrypted()) {
|
if((seed != null && !seed.isEncrypted()) || (masterPrivateExtendedKey != null && !masterPrivateExtendedKey.isEncrypted())) {
|
||||||
try {
|
try {
|
||||||
List<ChildNumber> derivation = getKeyDerivation().getDerivation();
|
List<ChildNumber> derivation = getKeyDerivation().getDerivation();
|
||||||
DeterministicKey derivedKey = getExtendedMasterPrivateKey().getKey(derivation);
|
DeterministicKey derivedKey = getExtendedMasterPrivateKey().getKey(derivation);
|
||||||
|
@ -208,6 +237,9 @@ public class Keystore {
|
||||||
if(extendedPublicKey != null) {
|
if(extendedPublicKey != null) {
|
||||||
copy.setExtendedPublicKey(extendedPublicKey.copy());
|
copy.setExtendedPublicKey(extendedPublicKey.copy());
|
||||||
}
|
}
|
||||||
|
if(masterPrivateExtendedKey != null) {
|
||||||
|
copy.setMasterPrivateExtendedKey(masterPrivateExtendedKey.copy());
|
||||||
|
}
|
||||||
if(seed != null) {
|
if(seed != null) {
|
||||||
copy.setSeed(seed.copy());
|
copy.setSeed(seed.copy());
|
||||||
}
|
}
|
||||||
|
@ -217,50 +249,69 @@ public class Keystore {
|
||||||
public static Keystore fromSeed(DeterministicSeed seed, List<ChildNumber> derivation) throws MnemonicException {
|
public static Keystore fromSeed(DeterministicSeed seed, List<ChildNumber> derivation) throws MnemonicException {
|
||||||
Keystore keystore = new Keystore();
|
Keystore keystore = new Keystore();
|
||||||
keystore.setSeed(seed);
|
keystore.setSeed(seed);
|
||||||
|
keystore.setLabel(seed.getType().name());
|
||||||
|
rederiveKeystoreFromMaster(keystore, derivation);
|
||||||
|
return keystore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Keystore fromMasterPrivateExtendedKey(MasterPrivateExtendedKey masterPrivateExtendedKey, List<ChildNumber> derivation) throws MnemonicException {
|
||||||
|
Keystore keystore = new Keystore();
|
||||||
|
keystore.setMasterPrivateExtendedKey(masterPrivateExtendedKey);
|
||||||
|
keystore.setLabel("Master Key");
|
||||||
|
rederiveKeystoreFromMaster(keystore, derivation);
|
||||||
|
return keystore;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void rederiveKeystoreFromMaster(Keystore keystore, List<ChildNumber> derivation) throws MnemonicException {
|
||||||
ExtendedKey xprv = keystore.getExtendedMasterPrivateKey();
|
ExtendedKey xprv = keystore.getExtendedMasterPrivateKey();
|
||||||
String masterFingerprint = Utils.bytesToHex(xprv.getKey().getFingerprint());
|
String masterFingerprint = Utils.bytesToHex(xprv.getKey().getFingerprint());
|
||||||
DeterministicKey derivedKey = xprv.getKey(derivation);
|
DeterministicKey derivedKey = xprv.getKey(derivation);
|
||||||
DeterministicKey derivedKeyPublicOnly = derivedKey.dropPrivateBytes().dropParent();
|
DeterministicKey derivedKeyPublicOnly = derivedKey.dropPrivateBytes().dropParent();
|
||||||
ExtendedKey xpub = new ExtendedKey(derivedKeyPublicOnly, derivedKey.getParentFingerprint(), derivation.isEmpty() ? ChildNumber.ZERO : derivation.get(derivation.size() - 1));
|
ExtendedKey xpub = new ExtendedKey(derivedKeyPublicOnly, derivedKey.getParentFingerprint(), derivation.isEmpty() ? ChildNumber.ZERO : derivation.get(derivation.size() - 1));
|
||||||
|
|
||||||
keystore.setLabel(seed.getType().name());
|
|
||||||
keystore.setSource(KeystoreSource.SW_SEED);
|
keystore.setSource(KeystoreSource.SW_SEED);
|
||||||
keystore.setWalletModel(WalletModel.SPARROW);
|
keystore.setWalletModel(WalletModel.SPARROW);
|
||||||
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, KeyDerivation.writePath(derivation)));
|
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, KeyDerivation.writePath(derivation)));
|
||||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub.toString()));
|
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub.toString()));
|
||||||
|
|
||||||
return keystore;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean hasSeed() {
|
|
||||||
return seed != null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isEncrypted() {
|
public boolean isEncrypted() {
|
||||||
return seed != null && seed.isEncrypted();
|
return (seed != null && seed.isEncrypted()) || (masterPrivateExtendedKey != null && masterPrivateExtendedKey.isEncrypted());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void encrypt(Key key) {
|
public void encrypt(Key key) {
|
||||||
if(hasSeed() && !seed.isEncrypted()) {
|
if(hasSeed() && !seed.isEncrypted()) {
|
||||||
seed = seed.encrypt(key);
|
seed = seed.encrypt(key);
|
||||||
}
|
}
|
||||||
|
if(hasMasterPrivateExtendedKey() && !masterPrivateExtendedKey.isEncrypted()) {
|
||||||
|
masterPrivateExtendedKey = masterPrivateExtendedKey.encrypt(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void decrypt(CharSequence password) {
|
public void decrypt(CharSequence password) {
|
||||||
if(hasSeed() && seed.isEncrypted()) {
|
if(hasSeed() && seed.isEncrypted()) {
|
||||||
seed = seed.decrypt(password);
|
seed = seed.decrypt(password);
|
||||||
}
|
}
|
||||||
|
if(hasMasterPrivateExtendedKey() && masterPrivateExtendedKey.isEncrypted()) {
|
||||||
|
masterPrivateExtendedKey = masterPrivateExtendedKey.decrypt(password);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void decrypt(Key key) {
|
public void decrypt(Key key) {
|
||||||
if(hasSeed() && seed.isEncrypted()) {
|
if(hasSeed() && seed.isEncrypted()) {
|
||||||
seed = seed.decrypt(key);
|
seed = seed.decrypt(key);
|
||||||
}
|
}
|
||||||
|
if(hasMasterPrivateExtendedKey() && masterPrivateExtendedKey.isEncrypted()) {
|
||||||
|
masterPrivateExtendedKey = masterPrivateExtendedKey.decrypt(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clearPrivate() {
|
public void clearPrivate() {
|
||||||
if(hasSeed()) {
|
if(hasSeed()) {
|
||||||
seed.clear();
|
seed.clear();
|
||||||
}
|
}
|
||||||
|
if(hasMasterPrivateExtendedKey()) {
|
||||||
|
masterPrivateExtendedKey.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,134 @@
|
||||||
|
package com.sparrowwallet.drongo.wallet;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.ExtendedKey;
|
||||||
|
import com.sparrowwallet.drongo.crypto.*;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class MasterPrivateExtendedKey implements EncryptableItem {
|
||||||
|
private final byte[] privateKey;
|
||||||
|
private final byte[] chainCode;
|
||||||
|
|
||||||
|
private final EncryptedData encryptedKey;
|
||||||
|
|
||||||
|
public MasterPrivateExtendedKey(byte[] privateKey, byte[] chainCode) {
|
||||||
|
this.privateKey = privateKey;
|
||||||
|
this.chainCode = chainCode;
|
||||||
|
this.encryptedKey = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MasterPrivateExtendedKey(EncryptedData encryptedKey) {
|
||||||
|
this.privateKey = null;
|
||||||
|
this.chainCode = null;
|
||||||
|
this.encryptedKey = encryptedKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DeterministicKey getPrivateKey() {
|
||||||
|
return HDKeyDerivation.createMasterPrivKeyFromBytes(privateKey, chainCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExtendedKey getExtendedPrivateKey() {
|
||||||
|
return new ExtendedKey(getPrivateKey(), new byte[4], ChildNumber.ZERO);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEncrypted() {
|
||||||
|
if((privateKey != null || chainCode != null) && encryptedKey != null) {
|
||||||
|
throw new IllegalStateException("Cannot be in a encrypted and unencrypted state");
|
||||||
|
}
|
||||||
|
|
||||||
|
return encryptedKey != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getSecretBytes() {
|
||||||
|
if(privateKey == null || chainCode == null) {
|
||||||
|
throw new IllegalStateException("Cannot get secret bytes for null or encrypted key");
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteBuffer byteBuffer = ByteBuffer.allocate(64);
|
||||||
|
byteBuffer.put(privateKey);
|
||||||
|
byteBuffer.put(chainCode);
|
||||||
|
return byteBuffer.array();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public EncryptedData getEncryptedData() {
|
||||||
|
return encryptedKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public EncryptionType getEncryptionType() {
|
||||||
|
return new EncryptionType(EncryptionType.Deriver.ARGON2, EncryptionType.Crypter.AES_CBC_PKCS7);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getCreationTimeSeconds() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MasterPrivateExtendedKey encrypt(Key key) {
|
||||||
|
if(encryptedKey != null) {
|
||||||
|
throw new IllegalArgumentException("Trying to encrypt twice");
|
||||||
|
}
|
||||||
|
if(privateKey == null || chainCode == null) {
|
||||||
|
throw new IllegalArgumentException("Private key data missing so cannot encrypt");
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyCrypter keyCrypter = getEncryptionType().getCrypter().getKeyCrypter();
|
||||||
|
byte[] secretBytes = getSecretBytes();
|
||||||
|
EncryptedData encryptedKeyData = keyCrypter.encrypt(secretBytes, null, key);
|
||||||
|
Arrays.fill(secretBytes != null ? secretBytes : new byte[0], (byte)0);
|
||||||
|
|
||||||
|
return new MasterPrivateExtendedKey(encryptedKeyData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MasterPrivateExtendedKey decrypt(CharSequence password) {
|
||||||
|
if(!isEncrypted()) {
|
||||||
|
throw new IllegalStateException("Cannot decrypt unencrypted master private key");
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyDeriver keyDeriver = getEncryptionType().getDeriver().getKeyDeriver(encryptedKey.getKeySalt());
|
||||||
|
Key key = keyDeriver.deriveKey(password);
|
||||||
|
MasterPrivateExtendedKey seed = decrypt(key);
|
||||||
|
key.clear();
|
||||||
|
|
||||||
|
return seed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MasterPrivateExtendedKey decrypt(Key key) {
|
||||||
|
if(!isEncrypted()) {
|
||||||
|
throw new IllegalStateException("Cannot decrypt unencrypted master private key");
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyCrypter keyCrypter = getEncryptionType().getCrypter().getKeyCrypter();
|
||||||
|
byte[] decrypted = keyCrypter.decrypt(encryptedKey, key);
|
||||||
|
try {
|
||||||
|
return new MasterPrivateExtendedKey(Arrays.copyOfRange(decrypted, 0, 32), Arrays.copyOfRange(decrypted, 32, 64));
|
||||||
|
} finally {
|
||||||
|
Arrays.fill(decrypted, (byte)0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public MasterPrivateExtendedKey copy() {
|
||||||
|
if(isEncrypted()) {
|
||||||
|
return new MasterPrivateExtendedKey(encryptedKey.copy());
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MasterPrivateExtendedKey(Arrays.copyOf(privateKey, 32), Arrays.copyOf(chainCode, 32));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
if(privateKey != null) {
|
||||||
|
Arrays.fill(privateKey, (byte)0);
|
||||||
|
}
|
||||||
|
if(chainCode != null) {
|
||||||
|
Arrays.fill(chainCode, (byte)0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public MasterPrivateExtendedKey fromXprv(ExtendedKey xprv) {
|
||||||
|
return new MasterPrivateExtendedKey(xprv.getKey().getPrivKeyBytes(), xprv.getKey().getChainCode());
|
||||||
|
}
|
||||||
|
}
|
|
@ -815,7 +815,7 @@ public class Wallet {
|
||||||
public void sign(PSBT psbt) throws MnemonicException {
|
public void sign(PSBT psbt) throws MnemonicException {
|
||||||
Map<PSBTInput, WalletNode> signingNodes = getSigningNodes(psbt);
|
Map<PSBTInput, WalletNode> signingNodes = getSigningNodes(psbt);
|
||||||
for(Keystore keystore : getKeystores()) {
|
for(Keystore keystore : getKeystores()) {
|
||||||
if(keystore.hasSeed()) {
|
if(keystore.hasPrivateKey()) {
|
||||||
for(Map.Entry<PSBTInput, WalletNode> signingEntry : signingNodes.entrySet()) {
|
for(Map.Entry<PSBTInput, WalletNode> signingEntry : signingNodes.entrySet()) {
|
||||||
ECKey privKey = keystore.getKey(signingEntry.getValue());
|
ECKey privKey = keystore.getKey(signingEntry.getValue());
|
||||||
PSBTInput psbtInput = signingEntry.getKey();
|
PSBTInput psbtInput = signingEntry.getKey();
|
||||||
|
@ -1061,9 +1061,9 @@ public class Wallet {
|
||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean containsSeeds() {
|
public boolean containsPrivateKeys() {
|
||||||
for(Keystore keystore : keystores) {
|
for(Keystore keystore : keystores) {
|
||||||
if(keystore.hasSeed()) {
|
if(keystore.hasPrivateKey()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,27 +16,27 @@ public class PolicyTest {
|
||||||
Keystore keystore3 = new Keystore("Keystore 3");
|
Keystore keystore3 = new Keystore("Keystore 3");
|
||||||
|
|
||||||
Policy policy = Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2PKH, List.of(keystore1), 1);
|
Policy policy = Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2PKH, List.of(keystore1), 1);
|
||||||
Assert.assertEquals("pkh(keystore1)", policy.getMiniscript().toString());
|
Assert.assertEquals("pkh(keystore1)", policy.getMiniscript().toString().toLowerCase());
|
||||||
Assert.assertEquals(1, policy.getNumSignaturesRequired());
|
Assert.assertEquals(1, policy.getNumSignaturesRequired());
|
||||||
|
|
||||||
Policy policy2 = Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2SH_P2WPKH, List.of(keystore1), 1);
|
Policy policy2 = Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2SH_P2WPKH, List.of(keystore1), 1);
|
||||||
Assert.assertEquals("sh(wpkh(keystore1))", policy2.getMiniscript().toString());
|
Assert.assertEquals("sh(wpkh(keystore1))", policy2.getMiniscript().toString().toLowerCase());
|
||||||
Assert.assertEquals(1, policy2.getNumSignaturesRequired());
|
Assert.assertEquals(1, policy2.getNumSignaturesRequired());
|
||||||
|
|
||||||
Policy policy3 = Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2WPKH, List.of(keystore1), 1);
|
Policy policy3 = Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2WPKH, List.of(keystore1), 1);
|
||||||
Assert.assertEquals("wpkh(keystore1)", policy3.getMiniscript().toString());
|
Assert.assertEquals("wpkh(keystore1)", policy3.getMiniscript().toString().toLowerCase());
|
||||||
Assert.assertEquals(1, policy3.getNumSignaturesRequired());
|
Assert.assertEquals(1, policy3.getNumSignaturesRequired());
|
||||||
|
|
||||||
Policy policy4 = Policy.getPolicy(PolicyType.MULTI, ScriptType.P2SH, List.of(keystore1, keystore2, keystore3), 2);
|
Policy policy4 = Policy.getPolicy(PolicyType.MULTI, ScriptType.P2SH, List.of(keystore1, keystore2, keystore3), 2);
|
||||||
Assert.assertEquals("sh(sortedmulti(2,keystore1,keystore2,keystore3))", policy4.getMiniscript().toString());
|
Assert.assertEquals("sh(sortedmulti(2,keystore1,keystore2,keystore3))", policy4.getMiniscript().toString().toLowerCase());
|
||||||
Assert.assertEquals(2, policy4.getNumSignaturesRequired());
|
Assert.assertEquals(2, policy4.getNumSignaturesRequired());
|
||||||
|
|
||||||
Policy policy5 = Policy.getPolicy(PolicyType.MULTI, ScriptType.P2SH_P2WSH, List.of(keystore1, keystore2, keystore3), 2);
|
Policy policy5 = Policy.getPolicy(PolicyType.MULTI, ScriptType.P2SH_P2WSH, List.of(keystore1, keystore2, keystore3), 2);
|
||||||
Assert.assertEquals("sh(wsh(sortedmulti(2,keystore1,keystore2,keystore3)))", policy5.getMiniscript().toString());
|
Assert.assertEquals("sh(wsh(sortedmulti(2,keystore1,keystore2,keystore3)))", policy5.getMiniscript().toString().toLowerCase());
|
||||||
Assert.assertEquals(2, policy5.getNumSignaturesRequired());
|
Assert.assertEquals(2, policy5.getNumSignaturesRequired());
|
||||||
|
|
||||||
Policy policy6 = Policy.getPolicy(PolicyType.MULTI, ScriptType.P2WSH, List.of(keystore1, keystore2, keystore3), 2);
|
Policy policy6 = Policy.getPolicy(PolicyType.MULTI, ScriptType.P2WSH, List.of(keystore1, keystore2, keystore3), 2);
|
||||||
Assert.assertEquals("wsh(sortedmulti(2,keystore1,keystore2,keystore3))", policy6.getMiniscript().toString());
|
Assert.assertEquals("wsh(sortedmulti(2,keystore1,keystore2,keystore3))", policy6.getMiniscript().toString().toLowerCase());
|
||||||
Assert.assertEquals(2, policy6.getNumSignaturesRequired());
|
Assert.assertEquals(2, policy6.getNumSignaturesRequired());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue