address and output script derivation support

This commit is contained in:
Craig Raw 2020-05-23 16:52:54 +02:00
parent 785040898b
commit de70f44535
6 changed files with 416 additions and 24 deletions

View file

@ -29,6 +29,12 @@ public class KeyDerivation {
return Collections.unmodifiableList(derivation);
}
public KeyDerivation extend(ChildNumber childNumber) {
List<ChildNumber> extendedDerivation = new ArrayList<>(derivation);
extendedDerivation.add(childNumber);
return new KeyDerivation(masterFingerprint, writePath(extendedDerivation));
}
public static List<ChildNumber> parsePath(String path) {
return parsePath(path, 0);
}

View file

@ -284,4 +284,23 @@ public class Utils {
hmacSha512.doFinal(out, 0);
return out;
}
public static class LexicographicByteArrayComparator implements Comparator<byte[]> {
@Override
public int compare(byte[] left, byte[] right) {
int minLength = Math.min(left.length, right.length);
for (int i = 0; i < minLength; i++) {
int result = compare(left[i], right[i]);
if (result != 0) {
return result;
}
}
return left.length - right.length;
}
public static int compare(byte a, byte b) {
return Byte.toUnsignedInt(a) - Byte.toUnsignedInt(b);
}
}
}

View file

@ -1,6 +1,7 @@
package com.sparrowwallet.drongo.protocol;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.*;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.ECKey;
@ -23,6 +24,16 @@ public enum ScriptType {
return new P2PKAddress(pubKey);
}
@Override
public Address getAddress(ECKey key) {
return getAddress(key.getPubKey());
}
@Override
public Address getAddress(Script script) {
throw new ProtocolException("No script derived address for non pay to script type");
}
@Override
public Address[] getAddresses(Script script) {
return new Address[] { getAddress(getPublicKeyFromScript(script).getPubKey()) };
@ -37,6 +48,16 @@ public enum ScriptType {
return new Script(chunks);
}
@Override
public Script getOutputScript(ECKey key) {
return getOutputScript(key.getPubKey());
}
@Override
public Script getOutputScript(Script script) {
throw new ProtocolException("No script derived output script for non pay to script type");
}
@Override
public boolean isScriptType(Script script) {
List<ScriptChunk> chunks = script.chunks;
@ -75,6 +96,16 @@ public enum ScriptType {
return new P2PKHAddress(pubKeyHash);
}
@Override
public Address getAddress(ECKey key) {
return getAddress(key.getPubKeyHash());
}
@Override
public Address getAddress(Script script) {
throw new ProtocolException("No script derived address for non pay to script type");
}
@Override
public Script getOutputScript(byte[] pubKeyHash) {
List<ScriptChunk> chunks = new ArrayList<>();
@ -87,6 +118,16 @@ public enum ScriptType {
return new Script(chunks);
}
@Override
public Script getOutputScript(ECKey key) {
return getOutputScript(key.getPubKeyHash());
}
@Override
public Script getOutputScript(Script script) {
throw new ProtocolException("No script derived output script for non pay to script type");
}
@Override
public boolean isScriptType(Script script) {
List<ScriptChunk> chunks = script.chunks;
@ -124,6 +165,16 @@ public enum ScriptType {
throw new ProtocolException("No single address for multisig script type");
}
@Override
public Address getAddress(Script script) {
throw new ProtocolException("No single address for multisig script type");
}
@Override
public Address getAddress(ECKey key) {
throw new ProtocolException("No single key address for multisig script type");
}
@Override
public Address[] getAddresses(Script script) {
return Arrays.stream(getPublicKeysFromScript(script)).map(pubKey -> new P2PKAddress(pubKey.getPubKey())).toArray(Address[]::new);
@ -131,16 +182,37 @@ public enum ScriptType {
@Override
public Script getOutputScript(byte[] bytes) {
throw new ProtocolException("Output script for multisig script type must be constructed with method getOutputScript(int threshold, byte[] pubKey1, byte[] pubKey2, ...)");
throw new ProtocolException("Output script for multisig script type must be constructed with method getOutputScript(int threshold, List<ECKey> pubKeys)");
}
public Script getOutputScript(int threshold, byte[] ...pubKeys) {
@Override
public Script getOutputScript(ECKey key) {
throw new ProtocolException("Output script for multisig script type must be constructed with method getOutputScript(int threshold, List<ECKey> pubKeys)");
}
@Override
public Script getOutputScript(Script script) {
if(isScriptType(script)) {
return script;
}
throw new ProtocolException("No script derived output script for non pay to script type");
}
@Override
public Script getOutputScript(int threshold, List<ECKey> pubKeys) {
List<byte[]> pubKeyBytes = new ArrayList<>();
for(ECKey key : pubKeys) {
pubKeyBytes.add(key.getPubKey());
}
pubKeyBytes.sort(new Utils.LexicographicByteArrayComparator());
List<ScriptChunk> chunks = new ArrayList<>();
chunks.add(new ScriptChunk(Script.encodeToOpN(threshold), null));
for(byte[] pubKey : pubKeys) {
for(byte[] pubKey : pubKeyBytes) {
chunks.add(new ScriptChunk(pubKey.length, pubKey));
}
chunks.add(new ScriptChunk(Script.encodeToOpN(pubKeys.length), null));
chunks.add(new ScriptChunk(Script.encodeToOpN(pubKeys.size()), null));
chunks.add(new ScriptChunk(ScriptOpCodes.OP_CHECKMULTISIG, null));
return new Script(chunks);
}
@ -200,20 +272,40 @@ public enum ScriptType {
},
P2SH("P2SH", "m/45'/0'/0'") {
@Override
public Address getAddress(byte[] bytes) {
return new P2SHAddress(bytes);
public Address getAddress(byte[] scriptHash) {
return new P2SHAddress(scriptHash);
}
@Override
public Script getOutputScript(byte[] bytes) {
public Address getAddress(ECKey key) {
throw new ProtocolException("No single key address for script hash type");
}
@Override
public Address getAddress(Script script) {
return getAddress(Utils.sha256hash160(script.getProgram()));
}
@Override
public Script getOutputScript(byte[] scriptHash) {
List<ScriptChunk> chunks = new ArrayList<>();
chunks.add(new ScriptChunk(ScriptOpCodes.OP_HASH160, null));
chunks.add(new ScriptChunk(bytes.length, bytes));
chunks.add(new ScriptChunk(scriptHash.length, scriptHash));
chunks.add(new ScriptChunk(ScriptOpCodes.OP_EQUAL, null));
return new Script(chunks);
}
@Override
public Script getOutputScript(ECKey key) {
throw new ProtocolException("No single key output script for script hash type");
}
@Override
public Script getOutputScript(Script script) {
return getOutputScript(Utils.sha256hash160(script.getProgram()));
}
@Override
public boolean isScriptType(Script script) {
List<ScriptChunk> chunks = script.chunks;
@ -251,13 +343,43 @@ public enum ScriptType {
},
P2SH_P2WPKH("P2SH-P2WPKH", "m/49'/0'/0'") {
@Override
public Address getAddress(byte[] bytes) {
return P2SH.getAddress(bytes);
public Address getAddress(byte[] scriptHash) {
return P2SH.getAddress(scriptHash);
}
@Override
public Script getOutputScript(byte[] bytes) {
return P2SH.getOutputScript(bytes);
public Address getAddress(ECKey key) {
Script p2wpkhScript = P2WPKH.getOutputScript(key.getPubKeyHash());
return P2SH.getAddress(p2wpkhScript);
}
@Override
public Address getAddress(Script script) {
if(P2WPKH.isScriptType(script)) {
return P2SH.getAddress(script);
}
throw new ProtocolException("Provided script is not a P2WPKH script");
}
@Override
public Script getOutputScript(byte[] scriptHash) {
return P2SH.getOutputScript(scriptHash);
}
@Override
public Script getOutputScript(ECKey key) {
Script p2wpkhScript = P2WPKH.getOutputScript(key.getPubKeyHash());
return P2SH.getOutputScript(p2wpkhScript);
}
@Override
public Script getOutputScript(Script script) {
if(P2WPKH.isScriptType(script)) {
return P2SH.getOutputScript(script);
}
throw new ProtocolException("Provided script is not a P2WPKH script");
}
@Override
@ -277,13 +399,35 @@ public enum ScriptType {
},
P2SH_P2WSH("P2SH-P2WSH", "m/48'/0'/0'/1'") {
@Override
public Address getAddress(byte[] bytes) {
return P2SH.getAddress(bytes);
public Address getAddress(byte[] scriptHash) {
return P2SH.getAddress(scriptHash);
}
@Override
public Script getOutputScript(byte[] bytes) {
return P2SH.getOutputScript(bytes);
public Address getAddress(ECKey key) {
throw new ProtocolException("No single key address for wrapped witness script hash type");
}
@Override
public Address getAddress(Script script) {
Script p2wshScript = P2WSH.getOutputScript(script);
return P2SH.getAddress(p2wshScript);
}
@Override
public Script getOutputScript(byte[] scriptHash) {
return P2SH.getOutputScript(scriptHash);
}
@Override
public Script getOutputScript(ECKey key) {
throw new ProtocolException("No single key output script for wrapped witness script hash type");
}
@Override
public Script getOutputScript(Script script) {
Script p2wshScript = P2WSH.getOutputScript(script);
return P2SH.getOutputScript(p2wshScript);
}
@Override
@ -303,19 +447,39 @@ public enum ScriptType {
},
P2WPKH("P2WPKH", "m/84'/0'/0'") {
@Override
public Address getAddress(byte[] bytes) {
return new P2WPKHAddress(bytes);
public Address getAddress(byte[] pubKeyHash) {
return new P2WPKHAddress(pubKeyHash);
}
@Override
public Script getOutputScript(byte[] bytes) {
public Address getAddress(ECKey key) {
return getAddress(key.getPubKeyHash());
}
@Override
public Address getAddress(Script script) {
throw new ProtocolException("No script derived address for non pay to script type");
}
@Override
public Script getOutputScript(byte[] pubKeyHash) {
List<ScriptChunk> chunks = new ArrayList<>();
chunks.add(new ScriptChunk(OP_0, null));
chunks.add(new ScriptChunk(bytes.length, bytes));
chunks.add(new ScriptChunk(pubKeyHash.length, pubKeyHash));
return new Script(chunks);
}
@Override
public Script getOutputScript(ECKey key) {
return getOutputScript(key.getPubKeyHash());
}
@Override
public Script getOutputScript(Script script) {
throw new ProtocolException("No script derived output script for non pay to script type");
}
@Override
public boolean isScriptType(Script script) {
List<ScriptChunk> chunks = script.chunks;
@ -343,19 +507,39 @@ public enum ScriptType {
},
P2WSH("P2WSH", "m/48'/0'/0'/2'") {
@Override
public Address getAddress(byte[] bytes) {
return new P2WSHAddress(bytes);
public Address getAddress(byte[] scriptHash) {
return new P2WSHAddress(scriptHash);
}
@Override
public Script getOutputScript(byte[] bytes) {
public Address getAddress(ECKey key) {
throw new ProtocolException("No single key address for witness script hash type");
}
@Override
public Address getAddress(Script script) {
return getAddress(Sha256Hash.hash(script.getProgram()));
}
@Override
public Script getOutputScript(byte[] scriptHash) {
List<ScriptChunk> chunks = new ArrayList<>();
chunks.add(new ScriptChunk(OP_0, null));
chunks.add(new ScriptChunk(bytes.length, bytes));
chunks.add(new ScriptChunk(scriptHash.length, scriptHash));
return new Script(chunks);
}
@Override
public Script getOutputScript(ECKey key) {
throw new ProtocolException("No single key output script for witness script hash type");
}
@Override
public Script getOutputScript(Script script) {
return getOutputScript(Sha256Hash.hash(script.getProgram()));
}
@Override
public boolean isScriptType(Script script) {
List<ScriptChunk> chunks = script.chunks;
@ -432,8 +616,20 @@ public enum ScriptType {
public abstract Address getAddress(byte[] bytes);
public abstract Address getAddress(ECKey key);
public abstract Address getAddress(Script script);
public abstract Script getOutputScript(byte[] bytes);
public abstract Script getOutputScript(ECKey key);
public abstract Script getOutputScript(Script script);
public Script getOutputScript(int threshold, List<ECKey> pubKeys) {
throw new UnsupportedOperationException("Only defined for MULTISIG script type");
}
public abstract boolean isScriptType(Script script);
public abstract byte[] getHashFromScript(Script script);

View file

@ -103,6 +103,24 @@ public class Keystore {
return new ExtendedKey(derivedKey, derivedKey.getParentFingerprint(), derivation.get(derivation.size() - 1));
}
public DeterministicKey getReceivingKey(int keyIndex) {
List<ChildNumber> receivingDerivation = List.of(extendedPublicKey.getKeyChildNumber(), new ChildNumber(0), new ChildNumber(keyIndex));
return extendedPublicKey.getKey(receivingDerivation);
}
public KeyDerivation getReceivingDerivation(int keyIndex) {
return getKeyDerivation().extend(new ChildNumber(0)).extend(new ChildNumber(keyIndex));
}
public DeterministicKey getChangeKey(int keyIndex) {
List<ChildNumber> receivingDerivation = List.of(extendedPublicKey.getKeyChildNumber(), new ChildNumber(1), new ChildNumber(keyIndex));
return extendedPublicKey.getKey(receivingDerivation);
}
public KeyDerivation getChangeDerivation(int keyIndex) {
return getKeyDerivation().extend(new ChildNumber(1)).extend(new ChildNumber(keyIndex));
}
public boolean isValid() {
if(label == null || source == null || walletModel == null || keyDerivation == null || extendedPublicKey == null) {
return false;

View file

@ -1,11 +1,16 @@
package com.sparrowwallet.drongo.wallet;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.crypto.DeterministicKey;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.crypto.Key;
import com.sparrowwallet.drongo.policy.Policy;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.Script;
import com.sparrowwallet.drongo.protocol.ScriptType;
import java.util.*;
import java.util.stream.Collectors;
public class Wallet {
@ -70,6 +75,34 @@ public class Wallet {
this.keystores = keystores;
}
public Address getReceivingAddress(int index) {
if(policyType == PolicyType.SINGLE) {
Keystore keystore = getKeystores().get(0);
DeterministicKey key = keystore.getReceivingKey(index);
return scriptType.getAddress(key);
} else if(policyType == PolicyType.MULTI) {
List<ECKey> pubKeys = getKeystores().stream().map(keystore -> keystore.getReceivingKey(index)).collect(Collectors.toList());
Script script = ScriptType.MULTISIG.getOutputScript(defaultPolicy.getNumSignaturesRequired(), pubKeys);
return scriptType.getAddress(script);
} else {
throw new UnsupportedOperationException("Cannot determine receiving addresses for custom policies");
}
}
public Address getChangeAddress(int index) {
if(policyType == PolicyType.SINGLE) {
Keystore keystore = getKeystores().get(0);
DeterministicKey key = keystore.getChangeKey(index);
return scriptType.getAddress(key);
} else if(policyType == PolicyType.MULTI) {
List<ECKey> pubKeys = getKeystores().stream().map(keystore -> keystore.getChangeKey(index)).collect(Collectors.toList());
Script script = ScriptType.MULTISIG.getOutputScript(defaultPolicy.getNumSignaturesRequired(), pubKeys);
return scriptType.getAddress(script);
} else {
throw new UnsupportedOperationException("Cannot determine change addresses for custom policies");
}
}
public boolean isValid() {
if(policyType == null || scriptType == null || defaultPolicy == null || keystores.isEmpty()) {
return false;

View file

@ -66,4 +66,124 @@ public class WalletTest {
Assert.assertEquals("Electrum 1", eekeystore.getLabel());
Assert.assertEquals("Electrum 2", eekeystore2.getLabel());
}
@Test
public void p2pkhDerivationTest() throws MnemonicException {
String words = "absent essay fox snake vast pumpkin height crouch silent bulb excuse razor";
DeterministicSeed seed = new DeterministicSeed(words, "pp", 0, DeterministicSeed.Type.BIP39);
Wallet wallet = new Wallet();
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setScriptType(ScriptType.P2PKH);
Keystore keystore = Keystore.fromSeed(seed, wallet.getScriptType().getDefaultDerivation());
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2PKH, wallet.getKeystores(), 1));
Assert.assertEquals("12kTQjuWDp7Uu6PwY6CsS1KLTt3d1DBHZa", wallet.getReceivingAddress(0).toString());
Assert.assertEquals("1HbQwQCitHQxVtP39isXmUdHx7hQCZovrK", wallet.getReceivingAddress(1).toString());
}
@Test
public void p2shP2wpkhDerivationTest() throws MnemonicException {
String words = "absent essay fox snake vast pumpkin height crouch silent bulb excuse razor";
DeterministicSeed seed = new DeterministicSeed(words, "pp", 0, DeterministicSeed.Type.BIP39);
Wallet wallet = new Wallet();
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setScriptType(ScriptType.P2SH_P2WPKH);
Keystore keystore = Keystore.fromSeed(seed, wallet.getScriptType().getDefaultDerivation());
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2SH_P2WPKH, wallet.getKeystores(), 1));
Assert.assertEquals("3NZLE4TntsjtcZ5MbrfxwtYo9meBVybVQj", wallet.getReceivingAddress(0).toString());
Assert.assertEquals("32YBBuRsp8XTeLx4T6BmD2L4nANGaNDkSg", wallet.getReceivingAddress(1).toString());
}
@Test
public void p2wpkhDerivationTest() throws MnemonicException {
String words = "absent essay fox snake vast pumpkin height crouch silent bulb excuse razor";
DeterministicSeed seed = new DeterministicSeed(words, "pp", 0, DeterministicSeed.Type.BIP39);
Wallet wallet = new Wallet();
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setScriptType(ScriptType.P2WPKH);
Keystore keystore = Keystore.fromSeed(seed, wallet.getScriptType().getDefaultDerivation());
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2WPKH, wallet.getKeystores(), 1));
Assert.assertEquals("bc1quvxdut936uswuxwxrk6nvjmgwxh463r0fjwn55", wallet.getReceivingAddress(0).toString());
Assert.assertEquals("bc1q95j2862dz7mqpraw6qdjc70gumyu5z7adgq9x9", wallet.getReceivingAddress(1).toString());
}
@Test
public void p2shDerivationTest() throws MnemonicException {
String words = "absent essay fox snake vast pumpkin height crouch silent bulb excuse razor";
DeterministicSeed seed = new DeterministicSeed(words, "pp", 0, DeterministicSeed.Type.BIP39);
String words2 = "chef huge whisper year move obscure post pepper play minute foster lawn";
DeterministicSeed seed2 = new DeterministicSeed(words2, "", 0, DeterministicSeed.Type.BIP39);
Wallet wallet = new Wallet();
wallet.setPolicyType(PolicyType.MULTI);
wallet.setScriptType(ScriptType.P2SH);
Keystore keystore = Keystore.fromSeed(seed, ScriptType.P2PKH.getDefaultDerivation());
Assert.assertEquals("xprv9s21ZrQH143K4G3jeUxf7h93qLeinXNULjjaef1yZFXpoc5D16iHEFkgJ7ThkWzAEBwNNwyJFtrVhJVJRjCc9ew76JrgsVoXT4VYHJBbbSV", keystore.getExtendedMasterPrivateKey().toString());
Assert.assertEquals("xpub6DLZWwJhGmq2SwdAytDWhCUrM4MojYSLHhHMZ1sob9UGXnSvgczEL7zV1wtcy9qcH6yduKMp1bPWcSxxSmz6LEpw4xTABLL3XwX5KGzkNqZ", keystore.getExtendedPublicKey().toString());
wallet.getKeystores().add(keystore);
Keystore keystore2 = Keystore.fromSeed(seed2, ScriptType.P2PKH.getDefaultDerivation());
Assert.assertEquals("xprv9s21ZrQH143K4FNcBwXNXfzVNskpoRS7cf4jQTLrhbPkhhXp8hz4QRXT62HziiHziM3Pxyd2Qx3UQkoRpcDu2BauuJJRdyrduXBJGgjAgFx", keystore2.getExtendedMasterPrivateKey().toString());
Assert.assertEquals("xpub6ChqMsFBYpJiJYzcJgEvddHtbZr1mTaE1o4RbhFRBAYVxN8SScGb9kjwkXtM33JKejR16gBZhNbkV14AccetR5u2McnCgTCpDBfa8hee9v8", keystore2.getExtendedPublicKey().toString());
wallet.getKeystores().add(keystore2);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI, ScriptType.P2SH, wallet.getKeystores(), 2));
Assert.assertEquals("38kq6yz4VcYymTExQPY3gppbz38mtPLveK", wallet.getReceivingAddress(0).toString());
Assert.assertEquals("3EdKaNsnjBTBggWcSMRyVju6GbHWy68mAH", wallet.getChangeAddress(1).toString());
}
@Test
public void p2shP2wshDerivationTest() throws MnemonicException {
String words = "absent essay fox snake vast pumpkin height crouch silent bulb excuse razor";
DeterministicSeed seed = new DeterministicSeed(words, "pp", 0, DeterministicSeed.Type.BIP39);
String words2 = "chef huge whisper year move obscure post pepper play minute foster lawn";
DeterministicSeed seed2 = new DeterministicSeed(words2, "", 0, DeterministicSeed.Type.BIP39);
Wallet wallet = new Wallet();
wallet.setPolicyType(PolicyType.MULTI);
wallet.setScriptType(ScriptType.P2SH_P2WSH);
Keystore keystore = Keystore.fromSeed(seed, ScriptType.P2PKH.getDefaultDerivation());
Assert.assertEquals("xprv9s21ZrQH143K4G3jeUxf7h93qLeinXNULjjaef1yZFXpoc5D16iHEFkgJ7ThkWzAEBwNNwyJFtrVhJVJRjCc9ew76JrgsVoXT4VYHJBbbSV", keystore.getExtendedMasterPrivateKey().toString());
Assert.assertEquals("xpub6DLZWwJhGmq2SwdAytDWhCUrM4MojYSLHhHMZ1sob9UGXnSvgczEL7zV1wtcy9qcH6yduKMp1bPWcSxxSmz6LEpw4xTABLL3XwX5KGzkNqZ", keystore.getExtendedPublicKey().toString());
wallet.getKeystores().add(keystore);
Keystore keystore2 = Keystore.fromSeed(seed2, ScriptType.P2PKH.getDefaultDerivation());
Assert.assertEquals("xprv9s21ZrQH143K4FNcBwXNXfzVNskpoRS7cf4jQTLrhbPkhhXp8hz4QRXT62HziiHziM3Pxyd2Qx3UQkoRpcDu2BauuJJRdyrduXBJGgjAgFx", keystore2.getExtendedMasterPrivateKey().toString());
Assert.assertEquals("xpub6ChqMsFBYpJiJYzcJgEvddHtbZr1mTaE1o4RbhFRBAYVxN8SScGb9kjwkXtM33JKejR16gBZhNbkV14AccetR5u2McnCgTCpDBfa8hee9v8", keystore2.getExtendedPublicKey().toString());
wallet.getKeystores().add(keystore2);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI, ScriptType.P2SH_P2WSH, wallet.getKeystores(), 2));
Assert.assertEquals("3Mw8xqAHh8g3eBvh7q1UEUmoexqdXDK9Tf", wallet.getReceivingAddress(0).toString());
Assert.assertEquals("35dFo1ivJ8jyHpyf42MWvnYf5LBU8Siren", wallet.getChangeAddress(1).toString());
}
@Test
public void p2wshDerivationTest() throws MnemonicException {
String words = "absent essay fox snake vast pumpkin height crouch silent bulb excuse razor";
DeterministicSeed seed = new DeterministicSeed(words, "pp", 0, DeterministicSeed.Type.BIP39);
String words2 = "chef huge whisper year move obscure post pepper play minute foster lawn";
DeterministicSeed seed2 = new DeterministicSeed(words2, "", 0, DeterministicSeed.Type.BIP39);
Wallet wallet = new Wallet();
wallet.setPolicyType(PolicyType.MULTI);
wallet.setScriptType(ScriptType.P2WSH);
Keystore keystore = Keystore.fromSeed(seed, ScriptType.P2PKH.getDefaultDerivation());
Assert.assertEquals("xprv9s21ZrQH143K4G3jeUxf7h93qLeinXNULjjaef1yZFXpoc5D16iHEFkgJ7ThkWzAEBwNNwyJFtrVhJVJRjCc9ew76JrgsVoXT4VYHJBbbSV", keystore.getExtendedMasterPrivateKey().toString());
Assert.assertEquals("xpub6DLZWwJhGmq2SwdAytDWhCUrM4MojYSLHhHMZ1sob9UGXnSvgczEL7zV1wtcy9qcH6yduKMp1bPWcSxxSmz6LEpw4xTABLL3XwX5KGzkNqZ", keystore.getExtendedPublicKey().toString());
wallet.getKeystores().add(keystore);
Keystore keystore2 = Keystore.fromSeed(seed2, ScriptType.P2PKH.getDefaultDerivation());
Assert.assertEquals("xprv9s21ZrQH143K4FNcBwXNXfzVNskpoRS7cf4jQTLrhbPkhhXp8hz4QRXT62HziiHziM3Pxyd2Qx3UQkoRpcDu2BauuJJRdyrduXBJGgjAgFx", keystore2.getExtendedMasterPrivateKey().toString());
Assert.assertEquals("xpub6ChqMsFBYpJiJYzcJgEvddHtbZr1mTaE1o4RbhFRBAYVxN8SScGb9kjwkXtM33JKejR16gBZhNbkV14AccetR5u2McnCgTCpDBfa8hee9v8", keystore2.getExtendedPublicKey().toString());
wallet.getKeystores().add(keystore2);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI, ScriptType.P2WSH, wallet.getKeystores(), 2));
Assert.assertEquals("bc1q20e4vm656h5lvmngz9ztz6hjzftvh39yzngqhuqzk8qzj7tqnzaqgclrwc", wallet.getReceivingAddress(0).toString());
Assert.assertEquals("bc1q2epdx7dplwaas2jucfrzmxm8350rqh68hs6vqreysku80ye44mfqla85f2", wallet.getChangeAddress(1).toString());
}
}