add multisig output descriptors

This commit is contained in:
Craig Raw 2019-09-06 14:47:38 +02:00
parent e8f9e329a7
commit 45154359e9
6 changed files with 408 additions and 182 deletions

View file

@ -0,0 +1,185 @@
package com.craigraw.drongo;
import com.craigraw.drongo.crypto.*;
import com.craigraw.drongo.protocol.Base58;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class ExtendedPublicKey {
private static final int bip32HeaderP2PKHXPub = 0x0488B21E; //The 4 byte header that serializes in base58 to "xpub".
private static final int bip32HeaderP2PKHYPub = 0x049D7CB2; //The 4 byte header that serializes in base58 to "ypub".
private static final int bip32HeaderP2WPKHZPub = 0x04B24746; // The 4 byte header that serializes in base58 to "zpub"
private static final int bip32HeaderP2WHSHPub = 0x2AA7ED3; // The 4 byte header that serializes in base58 to "Zpub"
private int parentFingerprint;
private String keyDerivationPath;
private DeterministicKey pubKey;
private String childDerivationPath;
private ChildNumber pubKeyChildNumber;
private DeterministicHierarchy hierarchy;
public ExtendedPublicKey(int parentFingerprint, String keyDerivationPath, DeterministicKey pubKey, String childDerivationPath, ChildNumber pubKeyChildNumber) {
this.parentFingerprint = parentFingerprint;
this.keyDerivationPath = keyDerivationPath;
this.pubKey = pubKey;
this.childDerivationPath = childDerivationPath;
this.pubKeyChildNumber = pubKeyChildNumber;
this.hierarchy = new DeterministicHierarchy(pubKey);
}
public int getParentFingerprint() {
return parentFingerprint;
}
public List<ChildNumber> getKeyDerivation() {
return parsePath(keyDerivationPath);
}
public DeterministicKey getPubKey() {
return pubKey;
}
public List<ChildNumber> getChildDerivation() {
return getChildDerivation(0);
}
public List<ChildNumber> getChildDerivation(int wildCardReplacement) {
return getChildDerivation(getPubKey().getChildNumber(), childDerivationPath, wildCardReplacement);
}
public boolean describesMultipleAddresses() {
return childDerivationPath.endsWith("/*");
}
public List<ChildNumber> getReceivingDerivation(int wildCardReplacement) {
if(describesMultipleAddresses()) {
if(childDerivationPath.endsWith("0/*")) {
return getChildDerivation(getPubKey().getChildNumber(), childDerivationPath, wildCardReplacement);
}
if(pubKeyChildNumber.num() == 0 && childDerivationPath.endsWith("/*")) {
return getChildDerivation(new ChildNumber(0, getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement);
}
}
throw new IllegalStateException("Cannot derive receiving address from output descriptor " + this.toString());
}
public List<ChildNumber> getChangeDerivation(int wildCardReplacement) {
if(describesMultipleAddresses()) {
if(childDerivationPath.endsWith("0/*")) {
return getChildDerivation(getPubKey().getChildNumber(), childDerivationPath.replace("0/*", "1/*"), wildCardReplacement);
}
if(pubKeyChildNumber.num() == 1 && childDerivationPath.endsWith("/*")) {
return getChildDerivation(new ChildNumber(1, getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement);
}
}
throw new IllegalStateException("Cannot derive change address from output descriptor " + this.toString());
}
private List<ChildNumber> getChildDerivation(ChildNumber firstChild, String derivationPath, int wildCardReplacement) {
List<ChildNumber> path = new ArrayList<>();
path.add(firstChild);
path.addAll(parsePath(derivationPath, wildCardReplacement));
return path;
}
public DeterministicKey getKey(List<ChildNumber> path) {
return hierarchy.get(path);
}
public static List<ChildNumber> parsePath(String path) {
return parsePath(path, 0);
}
public static List<ChildNumber> parsePath(String path, int wildcardReplacement) {
String[] parsedNodes = path.replace("M", "").split("/");
List<ChildNumber> nodes = new ArrayList<>();
for (String n : parsedNodes) {
n = n.replaceAll(" ", "");
if (n.length() == 0) continue;
boolean isHard = n.endsWith("H") || n.endsWith("h") || n.endsWith("'");
if (isHard) n = n.substring(0, n.length() - 1);
if (n.equals("*")) n = Integer.toString(wildcardReplacement);
int nodeNumber = Integer.parseInt(n);
nodes.add(new ChildNumber(nodeNumber, isHard));
}
return nodes;
}
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append(getExtendedPublicKey());
builder.append(childDerivationPath);
return builder.toString();
}
public String getExtendedPublicKey() {
return Base58.encodeChecked(getExtendedPublicKeyBytes());
}
public byte[] getExtendedPublicKeyBytes() {
ByteBuffer buffer = ByteBuffer.allocate(78);
buffer.putInt(bip32HeaderP2PKHXPub);
List<ChildNumber> childPath = parsePath(childDerivationPath);
int depth = 5 - childPath.size();
buffer.put((byte)depth);
buffer.putInt(parentFingerprint);
buffer.putInt(pubKeyChildNumber.i());
buffer.put(pubKey.getChainCode());
buffer.put(pubKey.getPubKey());
return buffer.array();
}
static ExtendedPublicKey fromDescriptor(String keyDerivationPath, String extPubKey, String childDerivationPath) {
byte[] serializedKey = Base58.decodeChecked(extPubKey);
ByteBuffer buffer = ByteBuffer.wrap(serializedKey);
int header = buffer.getInt();
if(!(header == bip32HeaderP2PKHXPub || header == bip32HeaderP2PKHYPub || header == bip32HeaderP2WPKHZPub || header == bip32HeaderP2WHSHPub)) {
throw new IllegalArgumentException("Unknown header bytes: " + DeterministicKey.toBase58(serializedKey).substring(0, 4));
}
int depth = buffer.get() & 0xFF; // convert signed byte to positive int since depth cannot be negative
final int parentFingerprint = buffer.getInt();
final int i = buffer.getInt();
ChildNumber childNumber;
List<ChildNumber> path;
if(depth == 0) {
//Poorly formatted extended public 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
} else {
childNumber = new ChildNumber(i, false);
}
path = Collections.unmodifiableList(new ArrayList<>(Arrays.asList(childNumber)));
byte[] chainCode = new byte[32];
buffer.get(chainCode);
byte[] data = new byte[33];
buffer.get(data);
if(buffer.hasRemaining()) {
throw new IllegalArgumentException("Found unexpected data in key");
}
DeterministicKey pubKey = new DeterministicKey(path, chainCode, new LazyECPoint(ECKey.CURVE.getCurve(), data), depth, parentFingerprint);
return new ExtendedPublicKey(parentFingerprint, keyDerivationPath, pubKey, childDerivationPath, childNumber);
}
}

View file

@ -1,115 +1,116 @@
package com.craigraw.drongo; package com.craigraw.drongo;
import com.craigraw.drongo.address.Address; import com.craigraw.drongo.address.*;
import com.craigraw.drongo.address.P2PKHAddress;
import com.craigraw.drongo.address.P2SHAddress;
import com.craigraw.drongo.address.P2WPKHAddress;
import com.craigraw.drongo.crypto.ChildNumber; import com.craigraw.drongo.crypto.ChildNumber;
import com.craigraw.drongo.crypto.DeterministicKey; import com.craigraw.drongo.crypto.DeterministicKey;
import com.craigraw.drongo.crypto.ECKey;
import com.craigraw.drongo.crypto.LazyECPoint;
import com.craigraw.drongo.protocol.Base58;
import com.craigraw.drongo.protocol.Script; import com.craigraw.drongo.protocol.Script;
import org.slf4j.Logger; import com.craigraw.drongo.protocol.ScriptChunk;
import org.slf4j.LoggerFactory; import com.craigraw.drongo.protocol.ScriptOpCodes;
import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.StringJoiner;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
public class OutputDescriptor { public class OutputDescriptor {
private static final Logger log = LoggerFactory.getLogger(OutputDescriptor.class); private static final Pattern XPUB_PATTERN = Pattern.compile("(\\[[^\\]]+\\])?(.pub[^/\\)]+)(/[/\\d*']+)?");
private static final Pattern MULTI_PATTERN = Pattern.compile("multi\\(([\\d+])");
private static final int bip32HeaderP2PKHXPub = 0x0488B21E; //The 4 byte header that serializes in base58 to "xpub".
private static final int bip32HeaderP2PKHYPub = 0x049D7CB2; //The 4 byte header that serializes in base58 to "ypub".
private static final int bip32HeaderP2WPKHZPub = 0x04B24746; // The 4 byte header that serializes in base58 to "zpub"
private static final Pattern DESCRIPTOR_PATTERN = Pattern.compile("(.+)\\((\\[[^\\]]+\\])?(xpub[^/\\)]+)(/[/\\d*']+)?\\)\\)?");
private String script; private String script;
private int parentFingerprint; private int multisigThreshold;
private String keyDerivationPath; private List<ExtendedPublicKey> extendedPublicKeys;
private DeterministicKey pubKey;
private String childDerivationPath;
private ChildNumber pubKeyChildNumber;
public OutputDescriptor(String script, int parentFingerprint, String keyDerivationPath, DeterministicKey pubKey, String childDerivationPath, ChildNumber pubKeyChildNumber) { public OutputDescriptor(String script, ExtendedPublicKey extendedPublicKey) {
this(script, Collections.singletonList(extendedPublicKey));
}
public OutputDescriptor(String script, List<ExtendedPublicKey> extendedPublicKeys) {
this(script, 0, extendedPublicKeys);
}
public OutputDescriptor(String script, int multisigThreshold, List<ExtendedPublicKey> extendedPublicKeys) {
this.script = script; this.script = script;
this.parentFingerprint = parentFingerprint; this.multisigThreshold = multisigThreshold;
this.keyDerivationPath = keyDerivationPath; this.extendedPublicKeys = extendedPublicKeys;
this.pubKey = pubKey; }
this.childDerivationPath = childDerivationPath;
this.pubKeyChildNumber = pubKeyChildNumber; public List<ExtendedPublicKey> getExtendedPublicKeys() {
return extendedPublicKeys;
}
public boolean isMultisig() {
return extendedPublicKeys.size() > 1;
}
public ExtendedPublicKey getSingletonExtendedPublicKey() {
if(isMultisig()) {
throw new IllegalStateException("Output descriptor contains multiple public keys but singleton requested");
}
return extendedPublicKeys.get(0);
} }
public String getScript() { public String getScript() {
return script; return script;
} }
public int getParentFingerprint() { public boolean describesMultipleAddresses() {
return parentFingerprint; for(ExtendedPublicKey pubKey : extendedPublicKeys) {
if(!pubKey.describesMultipleAddresses()) {
return false;
}
} }
public List<ChildNumber> getKeyDerivation() { return true;
return parsePath(keyDerivationPath);
}
public DeterministicKey getPubKey() {
return pubKey;
} }
public List<ChildNumber> getChildDerivation() { public List<ChildNumber> getChildDerivation() {
return getChildDerivation(0); List<ChildNumber> lastDerivation = null;
for(ExtendedPublicKey pubKey : extendedPublicKeys) {
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");
}
lastDerivation = derivation;
} }
public List<ChildNumber> getChildDerivation(int wildCardReplacement) { return lastDerivation;
return getChildDerivation(new ChildNumber(0, getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement);
}
public boolean describesMultipleAddresses() {
return childDerivationPath.endsWith("/*");
} }
public List<ChildNumber> getReceivingDerivation(int wildCardReplacement) { public List<ChildNumber> getReceivingDerivation(int wildCardReplacement) {
if(describesMultipleAddresses()) { if(isMultisig()) {
if(childDerivationPath.endsWith("0/*")) { List<ChildNumber> path = new ArrayList<>();
return getChildDerivation(new ChildNumber(0, getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement); path.add(new ChildNumber(0));
path.add(new ChildNumber(wildCardReplacement));
return path;
} }
if(pubKeyChildNumber.num() == 0 && childDerivationPath.endsWith("/*")) { return getSingletonExtendedPublicKey().getReceivingDerivation(wildCardReplacement);
return getChildDerivation(new ChildNumber(0, getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement);
}
}
throw new IllegalStateException("Cannot derive receiving address from output descriptor " + this.toString());
} }
public List<ChildNumber> getChangeDerivation(int wildCardReplacement) { public List<ChildNumber> getChangeDerivation(int wildCardReplacement) {
if(describesMultipleAddresses()) { if(isMultisig()) {
if(childDerivationPath.endsWith("0/*")) {
return getChildDerivation(new ChildNumber(0, getPubKey().getChildNumber().isHardened()), childDerivationPath.replace("0/*", "1/*"), wildCardReplacement);
}
if(pubKeyChildNumber.num() == 1 && childDerivationPath.endsWith("/*")) {
return getChildDerivation(new ChildNumber(1, getPubKey().getChildNumber().isHardened()), childDerivationPath, wildCardReplacement);
}
}
throw new IllegalStateException("Cannot derive change address from output descriptor " + this.toString());
}
private List<ChildNumber> getChildDerivation(ChildNumber firstChild, String derivationPath, int wildCardReplacement) {
List<ChildNumber> path = new ArrayList<>(); List<ChildNumber> path = new ArrayList<>();
path.add(firstChild); path.add(new ChildNumber(1));
path.addAll(parsePath(derivationPath, wildCardReplacement)); path.add(new ChildNumber(wildCardReplacement));
return path; return path;
} }
return getSingletonExtendedPublicKey().getChangeDerivation(wildCardReplacement);
}
public Address getAddress(List<ChildNumber> path) {
if(isMultisig()) {
Script script = getMultisigScript(path);
return getAddress(script);
}
DeterministicKey childKey = getSingletonExtendedPublicKey().getKey(path);
return getAddress(childKey);
}
public Address getAddress(DeterministicKey childKey) { public Address getAddress(DeterministicKey childKey) {
Address address = null; Address address = null;
if(script.equals("pkh")) { if(script.equals("pkh")) {
@ -127,105 +128,110 @@ public class OutputDescriptor {
return address; return address;
} }
private Address getAddress(Script multisigScript) {
Address address = null;
if(script.equals("sh(multi")) {
address = P2SHAddress.fromProgram(multisigScript.getProgram());
} else if(script.equals("wsh(multi")) {
address = P2WSHAddress.fromProgram(multisigScript.getProgram());
} else {
throw new IllegalStateException("Cannot determine address for multisig script " + script);
}
return address;
}
private Script getMultisigScript(List<ChildNumber> path) {
List<ScriptChunk> chunks = new ArrayList<>();
chunks.add(new ScriptChunk(Script.encodeToOpN(multisigThreshold), null));
for(ExtendedPublicKey pubKey : extendedPublicKeys) {
List<ChildNumber> keyPath = null;
if(path.get(0).num() == 0) {
keyPath = pubKey.getReceivingDerivation(path.get(1).num());
} else if(path.get(0).num() == 1) {
keyPath = pubKey.getChangeDerivation(path.get(1).num());
} else {
keyPath = pubKey.getChildDerivation(path.get(1).num());
}
byte[] pubKeyBytes = pubKey.getKey(keyPath).getPubKey();
chunks.add(new ScriptChunk(pubKeyBytes.length, pubKeyBytes));
}
chunks.add(new ScriptChunk(Script.encodeToOpN(extendedPublicKeys.size()), null));
chunks.add(new ScriptChunk(ScriptOpCodes.OP_CHECKMULTISIG, null));
return new Script(chunks);
}
// See https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md // See https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md
public static OutputDescriptor getOutputDescriptor(String descriptor) { public static OutputDescriptor getOutputDescriptor(String descriptor) {
String script; if(descriptor.startsWith("pkh") || descriptor.startsWith("xpub")) {
return new OutputDescriptor("pkh", getExtendedPublicKeys(descriptor));
} else if(descriptor.startsWith("wpkh") || descriptor.startsWith("zpub")) {
return new OutputDescriptor("wpkh", getExtendedPublicKeys(descriptor));
} else if(descriptor.startsWith("sh(wpkh") || descriptor.startsWith("ypub")) {
return new OutputDescriptor("sh(wpkh", getExtendedPublicKeys(descriptor));
} else if(descriptor.startsWith("sh(multi") || descriptor.startsWith("Ypub")) {
return new OutputDescriptor("sh(multi", getMultsigThreshold(descriptor), getExtendedPublicKeys(descriptor));
} else if(descriptor.startsWith("wsh(multi") || descriptor.startsWith("Zpub")) {
return new OutputDescriptor("wsh(multi", getMultsigThreshold(descriptor), getExtendedPublicKeys(descriptor));
} else {
throw new IllegalArgumentException("Could not parse output descriptor:" + descriptor);
}
}
private static int getMultsigThreshold(String descriptor) {
Matcher matcher = MULTI_PATTERN.matcher(descriptor);
if(matcher.find()) {
String threshold = matcher.group(1);
return Integer.parseInt(threshold);
} else {
throw new IllegalArgumentException("Could not find multisig threshold in output descriptor:" + descriptor);
}
}
private static List<ExtendedPublicKey> getExtendedPublicKeys(String descriptor) {
List<ExtendedPublicKey> keys = new ArrayList<>();
Matcher matcher = XPUB_PATTERN.matcher(descriptor);
while(matcher.find()) {
String keyDerivationPath =""; String keyDerivationPath ="";
String extPubKey = null; String extPubKey = null;
String childDerivationPath = "/0/*"; String childDerivationPath = "/0/*";
Matcher matcher = DESCRIPTOR_PATTERN.matcher(descriptor); if(matcher.group(1) != null) {
if(matcher.matches()) { keyDerivationPath = matcher.group(1);
script = matcher.group(1);
if(matcher.group(2) != null) {
keyDerivationPath = matcher.group(2);
} }
extPubKey = matcher.group(3); extPubKey = matcher.group(2);
if(matcher.group(4) != null) { if(matcher.group(3) != null) {
childDerivationPath = matcher.group(4); childDerivationPath = matcher.group(3);
}
} else if (descriptor.startsWith("xpub")) {
extPubKey = descriptor;
script = "pkh";
} else if(descriptor.startsWith("ypub")) {
extPubKey = descriptor;
script = "sh(wpkh";
} else if(descriptor.startsWith("zpub")) {
extPubKey = descriptor;
script = "wpkh";
} else {
throw new IllegalArgumentException("Could not parse output descriptor:" + descriptor);
} }
byte[] serializedKey = Base58.decodeChecked(extPubKey); ExtendedPublicKey extendedPublicKey = ExtendedPublicKey.fromDescriptor(keyDerivationPath, extPubKey, childDerivationPath);
ByteBuffer buffer = ByteBuffer.wrap(serializedKey); keys.add(extendedPublicKey);
int header = buffer.getInt();
if(!(header == bip32HeaderP2PKHXPub || header == bip32HeaderP2PKHYPub || header == bip32HeaderP2WPKHZPub)) {
throw new IllegalArgumentException("Unknown header bytes: " + DeterministicKey.toBase58(serializedKey).substring(0, 4));
} }
int depth = buffer.get() & 0xFF; // convert signed byte to positive int since depth cannot be negative return keys;
final int parentFingerprint = buffer.getInt();
final int i = buffer.getInt();
ChildNumber childNumber;
List<ChildNumber> path;
if(depth == 0) {
//Poorly formatted extended public 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
} else {
childNumber = new ChildNumber(i, false);
}
path = Collections.unmodifiableList(new ArrayList<>(Arrays.asList(childNumber)));
//Remove account level for depth 4 keys
if(depth == 4 && (descriptor.startsWith("xpub") || descriptor.startsWith("ypub") || descriptor.startsWith("zpub"))) {
log.warn("Output descriptor describes a public key derived at depth 4; change addresses not available");
childDerivationPath = "/*";
}
byte[] chainCode = new byte[32];
buffer.get(chainCode);
byte[] data = new byte[33];
buffer.get(data);
if(buffer.hasRemaining()) {
throw new IllegalArgumentException("Found unexpected data in key");
}
DeterministicKey pubKey = new DeterministicKey(path, chainCode, new LazyECPoint(ECKey.CURVE.getCurve(), data), depth, parentFingerprint);
return new OutputDescriptor(script, parentFingerprint, keyDerivationPath, pubKey, childDerivationPath, childNumber);
}
public static List<ChildNumber> parsePath(String path) {
return parsePath(path, 0);
}
public static List<ChildNumber> parsePath(String path, int wildcardReplacement) {
String[] parsedNodes = path.replace("M", "").split("/");
List<ChildNumber> nodes = new ArrayList<>();
for (String n : parsedNodes) {
n = n.replaceAll(" ", "");
if (n.length() == 0) continue;
boolean isHard = n.endsWith("H") || n.endsWith("h") || n.endsWith("'");
if (isHard) n = n.substring(0, n.length() - 1);
if (n.equals("*")) n = Integer.toString(wildcardReplacement);
int nodeNumber = Integer.parseInt(n);
nodes.add(new ChildNumber(nodeNumber, isHard));
}
return nodes;
} }
public String toString() { public String toString() {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
builder.append(script); builder.append(script);
builder.append("("); builder.append("(");
builder.append(getExtendedPublicKey());
builder.append(childDerivationPath); if(isMultisig()) {
StringJoiner joiner = new StringJoiner(",");
joiner.add(Integer.toString(multisigThreshold));
for(ExtendedPublicKey pubKey : extendedPublicKeys) {
joiner.add(pubKey.toString());
}
builder.append(joiner.toString());
} else {
builder.append(getSingletonExtendedPublicKey());
}
builder.append(")"); builder.append(")");
if(script.contains("(")){ if(script.contains("(")){
@ -234,26 +240,4 @@ public class OutputDescriptor {
return builder.toString(); return builder.toString();
} }
public String getExtendedPublicKey() {
return Base58.encodeChecked(getExtendedPublicKeyBytes());
}
public byte[] getExtendedPublicKeyBytes() {
ByteBuffer buffer = ByteBuffer.allocate(78);
buffer.putInt(bip32HeaderP2PKHXPub);
List<ChildNumber> childPath = parsePath(childDerivationPath);
int depth = 5 - childPath.size();
buffer.put((byte)depth);
buffer.putInt(parentFingerprint);
buffer.putInt(pubKeyChildNumber.i());
buffer.put(pubKey.getChainCode());
buffer.put(pubKey.getPubKey());
return buffer.array();
}
} }

View file

@ -11,32 +11,30 @@ public class WatchWallet {
private String name; private String name;
private OutputDescriptor outputDescriptor; private OutputDescriptor outputDescriptor;
private DeterministicHierarchy hierarchy;
private HashMap<Address,List<ChildNumber>> addresses = new HashMap<>(LOOK_AHEAD_LIMIT*2); private HashMap<Address,List<ChildNumber>> addresses = new HashMap<>(LOOK_AHEAD_LIMIT*2);
public WatchWallet(String name, String descriptor) { public WatchWallet(String name, String descriptor) {
this.name = name; this.name = name;
this.outputDescriptor = OutputDescriptor.getOutputDescriptor(descriptor); this.outputDescriptor = OutputDescriptor.getOutputDescriptor(descriptor);
this.hierarchy = new DeterministicHierarchy(outputDescriptor.getPubKey());
} }
public void initialiseAddresses() { public void initialiseAddresses() {
if(outputDescriptor.describesMultipleAddresses()) { if(outputDescriptor.describesMultipleAddresses()) {
for(int index = 0; index <= LOOK_AHEAD_LIMIT; index++) { for(int index = 0; index <= LOOK_AHEAD_LIMIT; index++) {
List<ChildNumber> receivingDerivation = outputDescriptor.getReceivingDerivation(index); List<ChildNumber> receivingDerivation = outputDescriptor.getReceivingDerivation(index);
Address address = getAddress(receivingDerivation); Address address = getReceivingAddress(index);
addresses.put(address, receivingDerivation); addresses.put(address, receivingDerivation);
} }
for(int index = 0; index <= LOOK_AHEAD_LIMIT; index++) { for(int index = 0; index <= LOOK_AHEAD_LIMIT; index++) {
List<ChildNumber> changeDerivation = outputDescriptor.getChangeDerivation(index); List<ChildNumber> changeDerivation = outputDescriptor.getChangeDerivation(index);
Address address = getAddress(changeDerivation); Address address = getChangeAddress(index);
addresses.put(address, changeDerivation); addresses.put(address, changeDerivation);
} }
} else { } else {
List<ChildNumber> derivation = outputDescriptor.getChildDerivation(); List<ChildNumber> derivation = outputDescriptor.getChildDerivation();
Address address = getAddress(derivation); Address address = outputDescriptor.getAddress(derivation);
addresses.put(address, derivation); addresses.put(address, derivation);
} }
} }
@ -61,8 +59,11 @@ public class WatchWallet {
return getAddress(outputDescriptor.getChangeDerivation(index)); return getAddress(outputDescriptor.getChangeDerivation(index));
} }
private Address getAddress(List<ChildNumber> path) { public OutputDescriptor getOutputDescriptor() {
DeterministicKey childKey = hierarchy.get(path); return outputDescriptor;
return outputDescriptor.getAddress(childKey); }
public Address getAddress(List<ChildNumber> path) {
return outputDescriptor.getAddress(path);
} }
} }

View file

@ -0,0 +1,34 @@
package com.craigraw.drongo.address;
import com.craigraw.drongo.protocol.*;
import java.util.ArrayList;
import java.util.List;
import static com.craigraw.drongo.address.P2WPKHAddress.HRP;
public class P2WSHAddress extends Address {
public P2WSHAddress(byte[] pubKeyHash) {
super(pubKeyHash);
}
public int getVersion() {
return 0;
}
public String getAddress() {
return Bech32.encode(HRP, getVersion(), pubKeyHash);
}
public Script getOutputScript() {
List<ScriptChunk> chunks = new ArrayList<>();
chunks.add(new ScriptChunk(Script.encodeToOpN(getVersion()), null));
chunks.add(new ScriptChunk(pubKeyHash.length, pubKeyHash));
return new Script(chunks);
}
public static P2WSHAddress fromProgram(byte[] program) {
return new P2WSHAddress(Sha256Hash.hash(program));
}
}

View file

@ -13,7 +13,7 @@ public class OutputDescriptorTest {
@Test @Test
public void iancolemanP2PKH() { public void iancolemanP2PKH() {
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("xpub6EEznxrqoN5HUXfD3QC3B8Vjw8Lj9UnRj17uTzNaBnEYN5xgwe6Un46Z443sSTBP2bzLZuDzygkdD1FtVWSexFmg4yAuCTxE2HxXFtz541z"); OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("xpub6EEznxrqoN5HUXfD3QC3B8Vjw8Lj9UnRj17uTzNaBnEYN5xgwe6Un46Z443sSTBP2bzLZuDzygkdD1FtVWSexFmg4yAuCTxE2HxXFtz541z/*");
Assert.assertEquals("pkh(xpub6EEznxrqoN5HUXfD3QC3B8Vjw8Lj9UnRj17uTzNaBnEYN5xgwe6Un46Z443sSTBP2bzLZuDzygkdD1FtVWSexFmg4yAuCTxE2HxXFtz541z/*)", descriptor.toString()); Assert.assertEquals("pkh(xpub6EEznxrqoN5HUXfD3QC3B8Vjw8Lj9UnRj17uTzNaBnEYN5xgwe6Un46Z443sSTBP2bzLZuDzygkdD1FtVWSexFmg4yAuCTxE2HxXFtz541z/*)", descriptor.toString());
} }
@ -25,7 +25,7 @@ public class OutputDescriptorTest {
@Test @Test
public void iancolemanP2SHP2WPKH() { public void iancolemanP2SHP2WPKH() {
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("ypub6Zken22QbjfomRUXki5v4ndP6T1DEtaBhGGZBvR4ocoooM44dFmnF8DyFmvcK76TKnuvdFfaPnicVvTAPdqEcbuEfKEqfnRoUjSkTB4u1os"); OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor("ypub6Zken22QbjfomRUXki5v4ndP6T1DEtaBhGGZBvR4ocoooM44dFmnF8DyFmvcK76TKnuvdFfaPnicVvTAPdqEcbuEfKEqfnRoUjSkTB4u1os/*");
Assert.assertEquals("sh(wpkh(xpub6EvPUMMVT48Kv8HQvMJHrhXsvUrmJGagn9kLQXXBRcRvkFEqNbcDd4ZqEZy2KCSXv9o7sn51w8N4cdqbfwRDpNDdnyYR5scKD1P74ZAKbGm/*))", descriptor.toString()); Assert.assertEquals("sh(wpkh(xpub6EvPUMMVT48Kv8HQvMJHrhXsvUrmJGagn9kLQXXBRcRvkFEqNbcDd4ZqEZy2KCSXv9o7sn51w8N4cdqbfwRDpNDdnyYR5scKD1P74ZAKbGm/*))", descriptor.toString());
} }

View file

@ -16,7 +16,7 @@ public class WatchWalletTest {
@Test @Test
public void iancolemanP2PKH() { public void iancolemanP2PKH() {
WatchWallet wallet = new WatchWallet("", "xpub6EEznxrqoN5HUXfD3QC3B8Vjw8Lj9UnRj17uTzNaBnEYN5xgwe6Un46Z443sSTBP2bzLZuDzygkdD1FtVWSexFmg4yAuCTxE2HxXFtz541z"); WatchWallet wallet = new WatchWallet("", "xpub6EEznxrqoN5HUXfD3QC3B8Vjw8Lj9UnRj17uTzNaBnEYN5xgwe6Un46Z443sSTBP2bzLZuDzygkdD1FtVWSexFmg4yAuCTxE2HxXFtz541z/*");
Assert.assertEquals("179cMrkiyx6zD2E1sqBAQLg1SQPAS5vjQW", wallet.getReceivingAddress(0).toString()); Assert.assertEquals("179cMrkiyx6zD2E1sqBAQLg1SQPAS5vjQW", wallet.getReceivingAddress(0).toString());
Assert.assertEquals("1GdWCzdt5oDYh5n1qeZQCxg5rQKVTuTMJg", wallet.getReceivingAddress(1).toString()); Assert.assertEquals("1GdWCzdt5oDYh5n1qeZQCxg5rQKVTuTMJg", wallet.getReceivingAddress(1).toString());
@ -33,7 +33,7 @@ public class WatchWalletTest {
@Test @Test
public void iancolemanP2SHP2WPKH() { public void iancolemanP2SHP2WPKH() {
WatchWallet wallet = new WatchWallet("", "ypub6Zken22QbjfomRUXki5v4ndP6T1DEtaBhGGZBvR4ocoooM44dFmnF8DyFmvcK76TKnuvdFfaPnicVvTAPdqEcbuEfKEqfnRoUjSkTB4u1os"); WatchWallet wallet = new WatchWallet("", "ypub6Zken22QbjfomRUXki5v4ndP6T1DEtaBhGGZBvR4ocoooM44dFmnF8DyFmvcK76TKnuvdFfaPnicVvTAPdqEcbuEfKEqfnRoUjSkTB4u1os/*");
Assert.assertEquals("34SgiHwNwJt3nYCVUQcgJWhefVRBZ4aSHf", wallet.getReceivingAddress(0).toString()); Assert.assertEquals("34SgiHwNwJt3nYCVUQcgJWhefVRBZ4aSHf", wallet.getReceivingAddress(0).toString());
Assert.assertEquals("3MgPnbF6UYM3FBhZWXoL2ebLPEa3zCCXLh", wallet.getReceivingAddress(1).toString()); Assert.assertEquals("3MgPnbF6UYM3FBhZWXoL2ebLPEa3zCCXLh", wallet.getReceivingAddress(1).toString());
@ -55,4 +55,26 @@ public class WatchWalletTest {
Assert.assertEquals("34TBBnwqv338BT6BVnTKqziFq8HWY6BNbw", wallet.getReceivingAddress(0).toString()); Assert.assertEquals("34TBBnwqv338BT6BVnTKqziFq8HWY6BNbw", wallet.getReceivingAddress(0).toString());
Assert.assertEquals("35Jhf9LGCpb1ihJjWH7uLZ8othr1diuspS", wallet.getChangeAddress(0).toString()); Assert.assertEquals("35Jhf9LGCpb1ihJjWH7uLZ8othr1diuspS", wallet.getChangeAddress(0).toString());
} }
@Test
public void electrumP2WSHMulti() {
WatchWallet wallet = new WatchWallet("", "wsh(multi(2,xpub699B7APGMoPLUrvPsXiBFrJRV8sTHDBHptpHSH36aESP5SLYs4VcEotnX1EvvA5ZoKF2rZ24Wh4U5ALxM21CfL5Kcj6Tu41PjRr2KKMkJTJ/0/*,xpub6Ds1jx5qxAtdczVBnJfHeGgpspzYuxnXHXLCoPZFFyyMoKJ7zzLgcERB1t7eDV1UuBQL1UKNxHFvcMJ7Zj6D2amdaA8gb21cZSXPrpG1bZr/0/*))");
Assert.assertEquals("bc1q2jxsrw70ug8jgskmhynvs49h3q5h8fglkdl3trvrc6wsde07wuzqfz98z0", wallet.getReceivingAddress(0).toString());
Assert.assertEquals("bc1qzw9j02k6l7z598edcgjh5mks507xevhk34rmnerxv45ptsluf0pqyxmyve", wallet.getChangeAddress(0).toString());
}
@Test
public void electrumP2WSHMulti2() {
WatchWallet wallet = new WatchWallet("", "wsh(multi(2,Zpub6yhnqjTYE82fc2U1UukQW6qEYsCcNoqsyPWPvL6Qi22YopXv8nD1a44zN87aUQcJr4YdE6DJKEA4xuBr5dzBQHZCBsbiUH7NAcFBgPyx3LB/0/*,Zpub74eXjdXzGCRsixpRAZT3U8ssQ15uhUa1dCFdRJvY3L2qo18He71qWUfpxfbL9e2EYuWKe1tH7qzgUSRVTAektLDVRKwCbAtyRW5j2yhqLiD/0/*))");
Assert.assertEquals("bc1qa842ug2njv36ycnhq8wjcg6wxjv7p7h4v0tnl40u6nfxxxffyjnq409pr9", wallet.getReceivingAddress(0).toString());
Assert.assertEquals("bc1q3auk6c8f77dda0w8y9dz4yd3wqhkf4eufzk8x2quszvzzcyjk6rqgz70pd", wallet.getChangeAddress(0).toString());
}
@Test
public void electrumP2WSHMultiSingle() {
WatchWallet wallet = new WatchWallet("", "wsh(multi(2,xpub699B7APGMoPLUrvPsXiBFrJRV8sTHDBHptpHSH36aESP5SLYs4VcEotnX1EvvA5ZoKF2rZ24Wh4U5ALxM21CfL5Kcj6Tu41PjRr2KKMkJTJ/0/0,xpub6Ds1jx5qxAtdczVBnJfHeGgpspzYuxnXHXLCoPZFFyyMoKJ7zzLgcERB1t7eDV1UuBQL1UKNxHFvcMJ7Zj6D2amdaA8gb21cZSXPrpG1bZr/0/0))");
Assert.assertEquals("bc1q2jxsrw70ug8jgskmhynvs49h3q5h8fglkdl3trvrc6wsde07wuzqfz98z0", wallet.getAddress(wallet.getOutputDescriptor().getChildDerivation()).toString());
}
} }