output descriptor support and node fixes

This commit is contained in:
Craig Raw 2020-05-28 11:42:02 +02:00
parent 7871413573
commit 11978e1f48
2 changed files with 219 additions and 26 deletions

View file

@ -7,10 +7,7 @@ import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.policy.PolicyType; import com.sparrowwallet.drongo.policy.PolicyType;
import java.util.ArrayList; import java.util.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static com.sparrowwallet.drongo.policy.PolicyType.*; import static com.sparrowwallet.drongo.policy.PolicyType.*;
@ -58,6 +55,16 @@ public enum ScriptType {
throw new ProtocolException("No script derived output script for non pay to script type"); throw new ProtocolException("No script derived output script for non pay to script type");
} }
@Override
public String getOutputDescriptor(ECKey key) {
return "pk(" + Utils.bytesToHex(key.getPubKey()) + ")";
}
@Override
public String getOutputDescriptor(Script script) {
throw new ProtocolException("No script derived output descriptor for non pay to script type");
}
@Override @Override
public boolean isScriptType(Script script) { public boolean isScriptType(Script script) {
List<ScriptChunk> chunks = script.chunks; List<ScriptChunk> chunks = script.chunks;
@ -128,6 +135,16 @@ public enum ScriptType {
throw new ProtocolException("No script derived output script for non pay to script type"); throw new ProtocolException("No script derived output script for non pay to script type");
} }
@Override
public String getOutputDescriptor(ECKey key) {
return "pkh(" + Utils.bytesToHex(key.getPubKey()) + ")";
}
@Override
public String getOutputDescriptor(Script script) {
throw new ProtocolException("No script derived output descriptor for non pay to script type");
}
@Override @Override
public boolean isScriptType(Script script) { public boolean isScriptType(Script script) {
List<ScriptChunk> chunks = script.chunks; List<ScriptChunk> chunks = script.chunks;
@ -217,6 +234,34 @@ public enum ScriptType {
return new Script(chunks); return new Script(chunks);
} }
@Override
public String getOutputDescriptor(ECKey key) {
throw new ProtocolException("No single key output descriptor for multisig script type");
}
@Override
public String getOutputDescriptor(Script script) {
if(!isScriptType(script)) {
throw new IllegalArgumentException("Can only create output descriptor from multisig script");
}
int threshold = getThreshold(script);
ECKey[] pubKeys = getPublicKeysFromScript(script);
List<byte[]> pubKeyBytes = new ArrayList<>();
for(ECKey key : pubKeys) {
pubKeyBytes.add(key.getPubKey());
}
pubKeyBytes.sort(new Utils.LexicographicByteArrayComparator());
StringJoiner joiner = new StringJoiner(",");
for(byte[] pubKey : pubKeyBytes) {
joiner.add(Utils.bytesToHex(pubKey));
}
return "multi(" + threshold + "," + joiner.toString() + ")";
}
@Override @Override
public boolean isScriptType(Script script) { public boolean isScriptType(Script script) {
List<ScriptChunk> chunks = script.chunks; List<ScriptChunk> chunks = script.chunks;
@ -306,6 +351,20 @@ public enum ScriptType {
return getOutputScript(Utils.sha256hash160(script.getProgram())); return getOutputScript(Utils.sha256hash160(script.getProgram()));
} }
@Override
public String getOutputDescriptor(ECKey key) {
throw new ProtocolException("No single key output descriptor for script hash type");
}
@Override
public String getOutputDescriptor(Script script) {
if(!MULTISIG.isScriptType(script)) {
throw new IllegalArgumentException("Can only create output descriptor from multisig script");
}
return "sh(" + MULTISIG.getOutputDescriptor(script) + ")";
}
@Override @Override
public boolean isScriptType(Script script) { public boolean isScriptType(Script script) {
List<ScriptChunk> chunks = script.chunks; List<ScriptChunk> chunks = script.chunks;
@ -382,6 +441,16 @@ public enum ScriptType {
throw new ProtocolException("Provided script is not a P2WPKH script"); throw new ProtocolException("Provided script is not a P2WPKH script");
} }
@Override
public String getOutputDescriptor(ECKey key) {
return "sh(wpkh(" + Utils.bytesToHex(key.getPubKey()) + "))";
}
@Override
public String getOutputDescriptor(Script script) {
throw new ProtocolException("No script derived output descriptor for non pay to script type");
}
@Override @Override
public boolean isScriptType(Script script) { public boolean isScriptType(Script script) {
return P2SH.isScriptType(script); return P2SH.isScriptType(script);
@ -430,6 +499,20 @@ public enum ScriptType {
return P2SH.getOutputScript(p2wshScript); return P2SH.getOutputScript(p2wshScript);
} }
@Override
public String getOutputDescriptor(ECKey key) {
throw new ProtocolException("No single key output descriptor for script hash type");
}
@Override
public String getOutputDescriptor(Script script) {
if(!MULTISIG.isScriptType(script)) {
throw new IllegalArgumentException("Can only create output descriptor from multisig script");
}
return "sh(wsh(" + MULTISIG.getOutputDescriptor(script) + "))";
}
@Override @Override
public boolean isScriptType(Script script) { public boolean isScriptType(Script script) {
return P2SH.isScriptType(script); return P2SH.isScriptType(script);
@ -480,6 +563,16 @@ public enum ScriptType {
throw new ProtocolException("No script derived output script for non pay to script type"); throw new ProtocolException("No script derived output script for non pay to script type");
} }
@Override
public String getOutputDescriptor(ECKey key) {
return "wpkh(" + Utils.bytesToHex(key.getPubKey()) + ")";
}
@Override
public String getOutputDescriptor(Script script) {
throw new ProtocolException("No script derived output descriptor for non pay to script type");
}
@Override @Override
public boolean isScriptType(Script script) { public boolean isScriptType(Script script) {
List<ScriptChunk> chunks = script.chunks; List<ScriptChunk> chunks = script.chunks;
@ -540,6 +633,20 @@ public enum ScriptType {
return getOutputScript(Sha256Hash.hash(script.getProgram())); return getOutputScript(Sha256Hash.hash(script.getProgram()));
} }
@Override
public String getOutputDescriptor(ECKey key) {
throw new ProtocolException("No single key output descriptor for script hash type");
}
@Override
public String getOutputDescriptor(Script script) {
if(!MULTISIG.isScriptType(script)) {
throw new IllegalArgumentException("Can only create output descriptor from multisig script");
}
return "wsh(" + MULTISIG.getOutputDescriptor(script) + ")";
}
@Override @Override
public boolean isScriptType(Script script) { public boolean isScriptType(Script script) {
List<ScriptChunk> chunks = script.chunks; List<ScriptChunk> chunks = script.chunks;
@ -630,6 +737,10 @@ public enum ScriptType {
throw new UnsupportedOperationException("Only defined for MULTISIG script type"); throw new UnsupportedOperationException("Only defined for MULTISIG script type");
} }
public abstract String getOutputDescriptor(ECKey key);
public abstract String getOutputDescriptor(Script script);
public abstract boolean isScriptType(Script script); public abstract boolean isScriptType(Script script);
public abstract byte[] getHashFromScript(Script script); public abstract byte[] getHashFromScript(Script script);

View file

@ -23,9 +23,7 @@ public class Wallet {
private ScriptType scriptType; private ScriptType scriptType;
private Policy defaultPolicy; private Policy defaultPolicy;
private List<Keystore> keystores = new ArrayList<>(); private List<Keystore> keystores = new ArrayList<>();
private final List<Node> accountNodes = new ArrayList<>(); private final Set<Node> accountNodes = new TreeSet<>();
private transient int lookAhead = DEFAULT_LOOKAHEAD;
public Wallet() { public Wallet() {
} }
@ -82,6 +80,10 @@ public class Wallet {
this.keystores = keystores; this.keystores = keystores;
} }
private Set<Node> getAccountNodes() {
return accountNodes;
}
public Node getNode(KeyPurpose keyPurpose) { public Node getNode(KeyPurpose keyPurpose) {
Node purposeNode; Node purposeNode;
Optional<Node> optionalPurposeNode = accountNodes.stream().filter(node -> node.getKeyPurpose().equals(keyPurpose)).findFirst(); Optional<Node> optionalPurposeNode = accountNodes.stream().filter(node -> node.getKeyPurpose().equals(keyPurpose)).findFirst();
@ -92,12 +94,18 @@ public class Wallet {
purposeNode = optionalPurposeNode.get(); purposeNode = optionalPurposeNode.get();
} }
purposeNode.fillToLookAhead(getLookAhead()); purposeNode.fillToIndex(getLookAhead(purposeNode) - 1);
return purposeNode; return purposeNode;
} }
public int getLookAhead() { public int getLookAhead(Node node) {
//TODO: Calculate using seen transactions //TODO: Calculate using seen transactions
int lookAhead = DEFAULT_LOOKAHEAD;
Integer maxIndex = node.getHighestIndex();
if(maxIndex != null) {
lookAhead = Math.max(maxIndex + lookAhead/2, lookAhead);
}
return lookAhead; return lookAhead;
} }
@ -115,11 +123,16 @@ public class Wallet {
Node node = getNode(keyPurpose); Node node = getNode(keyPurpose);
if(index >= node.getChildren().size()) { if(index >= node.getChildren().size()) {
lookAhead = index; node.fillToIndex(index);
node.fillToLookAhead(lookAhead);
} }
return node.getChildren().get(index); for(Node childNode : node.getChildren()) {
if(childNode.getIndex() == index) {
return childNode;
}
}
throw new IllegalStateException("Could not fill nodes to index " + index);
} }
public Address getAddress(KeyPurpose keyPurpose, int index) { public Address getAddress(KeyPurpose keyPurpose, int index) {
@ -150,6 +163,20 @@ public class Wallet {
} }
} }
public String getOutputDescriptor(KeyPurpose keyPurpose, int index) {
if(policyType == PolicyType.SINGLE) {
Keystore keystore = getKeystores().get(0);
DeterministicKey key = keystore.getKey(keyPurpose, index);
return scriptType.getOutputDescriptor(key);
} else if(policyType == PolicyType.MULTI) {
List<ECKey> pubKeys = getKeystores().stream().map(keystore -> keystore.getKey(keyPurpose, index)).collect(Collectors.toList());
Script script = ScriptType.MULTISIG.getOutputScript(defaultPolicy.getNumSignaturesRequired(), pubKeys);
return scriptType.getOutputDescriptor(script);
} else {
throw new UnsupportedOperationException("Cannot determine output descriptor for custom policies");
}
}
public boolean isValid() { public boolean isValid() {
if(policyType == null || scriptType == null || defaultPolicy == null || keystores.isEmpty()) { if(policyType == null || scriptType == null || defaultPolicy == null || keystores.isEmpty()) {
return false; return false;
@ -245,6 +272,21 @@ public class Wallet {
for(Keystore keystore : keystores) { for(Keystore keystore : keystores) {
copy.getKeystores().add(keystore.copy()); copy.getKeystores().add(keystore.copy());
} }
for(Wallet.Node node : accountNodes) {
Node nodeCopy = copy.copyNode(node);
copy.getAccountNodes().add(nodeCopy);
}
return copy;
}
private Node copyNode(Node node) {
Node copy = new Node(node.derivationPath);
copy.setLabel(node.label);
copy.setAmount(node.amount);
for(Node child : node.getChildren()) {
copy.getChildren().add(copyNode(child));
}
return copy; return copy;
} }
@ -302,21 +344,19 @@ public class Wallet {
} }
} }
public class Node { public class Node implements Comparable<Node> {
private final String derivationPath; private final String derivationPath;
private String label; private String label;
private Long amount; private Long amount;
private final List<Node> children = new ArrayList<>(); private Set<Node> children = new TreeSet<>();
private final transient KeyPurpose keyPurpose; private transient KeyPurpose keyPurpose;
private final transient int index; private transient int index = -1;
private final transient List<ChildNumber> derivation; private transient List<ChildNumber> derivation;
public Node(String derivationPath) { public Node(String derivationPath) {
this.derivationPath = derivationPath; this.derivationPath = derivationPath;
this.derivation = KeyDerivation.parsePath(derivationPath); parseDerivation();
this.keyPurpose = KeyPurpose.fromChildNumber(derivation.get(0));
this.index = derivation.get(derivation.size() - 1).num();
} }
public Node(KeyPurpose keyPurpose) { public Node(KeyPurpose keyPurpose) {
@ -337,14 +377,36 @@ public class Wallet {
return derivationPath; return derivationPath;
} }
private void parseDerivation() {
this.derivation = KeyDerivation.parsePath(derivationPath);
this.keyPurpose = KeyPurpose.fromChildNumber(derivation.get(0));
this.index = derivation.get(derivation.size() - 1).num();
}
public int getIndex() { public int getIndex() {
if(index < 0) {
parseDerivation();
}
return index; return index;
} }
public KeyPurpose getKeyPurpose() { public KeyPurpose getKeyPurpose() {
if(keyPurpose == null) {
parseDerivation();
}
return keyPurpose; return keyPurpose;
} }
public List<ChildNumber> getDerivation() {
if(derivation == null) {
parseDerivation();
}
return derivation;
}
public String getLabel() { public String getLabel() {
return label; return label;
} }
@ -361,10 +423,14 @@ public class Wallet {
this.amount = amount; this.amount = amount;
} }
public List<Node> getChildren() { public Set<Node> getChildren() {
return children; return children;
} }
public void setChildren(Set<Node> children) {
this.children = children;
}
public Address getAddress() { public Address getAddress() {
return Wallet.this.getAddress(keyPurpose, index); return Wallet.this.getAddress(keyPurpose, index);
} }
@ -373,15 +439,26 @@ public class Wallet {
return Wallet.this.getOutputScript(keyPurpose, index); return Wallet.this.getOutputScript(keyPurpose, index);
} }
public void fillToLookAhead(int lookAhead) { public String getOutputDescriptor() {
for(int i = 0; i < lookAhead; i++) { return Wallet.this.getOutputDescriptor(keyPurpose, index);
}
public void fillToIndex(int index) {
for(int i = 0; i <= index; i++) {
Node node = new Node(getKeyPurpose(), i); Node node = new Node(getKeyPurpose(), i);
if(!getChildren().contains(node)) { getChildren().add(node);
getChildren().add(node);
}
} }
} }
public Integer getHighestIndex() {
Node highestNode = null;
for(Node childNode : getChildren()) {
highestNode = childNode;
}
return highestNode == null ? null : highestNode.index;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
@ -394,5 +471,10 @@ public class Wallet {
public int hashCode() { public int hashCode() {
return Objects.hash(derivationPath); return Objects.hash(derivationPath);
} }
@Override
public int compareTo(Node node) {
return getIndex() - node.getIndex();
}
} }
} }