mirror of
https://github.com/sparrowwallet/drongo.git
synced 2024-12-26 18:16:45 +00:00
output descriptor support and node fixes
This commit is contained in:
parent
7871413573
commit
11978e1f48
2 changed files with 219 additions and 26 deletions
|
@ -7,10 +7,7 @@ import com.sparrowwallet.drongo.crypto.ChildNumber;
|
|||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
@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
|
||||
public boolean isScriptType(Script script) {
|
||||
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");
|
||||
}
|
||||
|
||||
@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
|
||||
public boolean isScriptType(Script script) {
|
||||
List<ScriptChunk> chunks = script.chunks;
|
||||
|
@ -217,6 +234,34 @@ public enum ScriptType {
|
|||
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
|
||||
public boolean isScriptType(Script script) {
|
||||
List<ScriptChunk> chunks = script.chunks;
|
||||
|
@ -306,6 +351,20 @@ public enum ScriptType {
|
|||
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
|
||||
public boolean isScriptType(Script script) {
|
||||
List<ScriptChunk> chunks = script.chunks;
|
||||
|
@ -382,6 +441,16 @@ public enum ScriptType {
|
|||
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
|
||||
public boolean isScriptType(Script script) {
|
||||
return P2SH.isScriptType(script);
|
||||
|
@ -430,6 +499,20 @@ public enum ScriptType {
|
|||
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
|
||||
public boolean isScriptType(Script 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");
|
||||
}
|
||||
|
||||
@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
|
||||
public boolean isScriptType(Script script) {
|
||||
List<ScriptChunk> chunks = script.chunks;
|
||||
|
@ -540,6 +633,20 @@ public enum ScriptType {
|
|||
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
|
||||
public boolean isScriptType(Script script) {
|
||||
List<ScriptChunk> chunks = script.chunks;
|
||||
|
@ -630,6 +737,10 @@ public enum ScriptType {
|
|||
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 byte[] getHashFromScript(Script script);
|
||||
|
|
|
@ -23,9 +23,7 @@ public class Wallet {
|
|||
private ScriptType scriptType;
|
||||
private Policy defaultPolicy;
|
||||
private List<Keystore> keystores = new ArrayList<>();
|
||||
private final List<Node> accountNodes = new ArrayList<>();
|
||||
|
||||
private transient int lookAhead = DEFAULT_LOOKAHEAD;
|
||||
private final Set<Node> accountNodes = new TreeSet<>();
|
||||
|
||||
public Wallet() {
|
||||
}
|
||||
|
@ -82,6 +80,10 @@ public class Wallet {
|
|||
this.keystores = keystores;
|
||||
}
|
||||
|
||||
private Set<Node> getAccountNodes() {
|
||||
return accountNodes;
|
||||
}
|
||||
|
||||
public Node getNode(KeyPurpose keyPurpose) {
|
||||
Node purposeNode;
|
||||
Optional<Node> optionalPurposeNode = accountNodes.stream().filter(node -> node.getKeyPurpose().equals(keyPurpose)).findFirst();
|
||||
|
@ -92,12 +94,18 @@ public class Wallet {
|
|||
purposeNode = optionalPurposeNode.get();
|
||||
}
|
||||
|
||||
purposeNode.fillToLookAhead(getLookAhead());
|
||||
purposeNode.fillToIndex(getLookAhead(purposeNode) - 1);
|
||||
return purposeNode;
|
||||
}
|
||||
|
||||
public int getLookAhead() {
|
||||
public int getLookAhead(Node node) {
|
||||
//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;
|
||||
}
|
||||
|
||||
|
@ -115,11 +123,16 @@ public class Wallet {
|
|||
|
||||
Node node = getNode(keyPurpose);
|
||||
if(index >= node.getChildren().size()) {
|
||||
lookAhead = index;
|
||||
node.fillToLookAhead(lookAhead);
|
||||
node.fillToIndex(index);
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -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() {
|
||||
if(policyType == null || scriptType == null || defaultPolicy == null || keystores.isEmpty()) {
|
||||
return false;
|
||||
|
@ -245,6 +272,21 @@ public class Wallet {
|
|||
for(Keystore keystore : keystores) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -302,21 +344,19 @@ public class Wallet {
|
|||
}
|
||||
}
|
||||
|
||||
public class Node {
|
||||
public class Node implements Comparable<Node> {
|
||||
private final String derivationPath;
|
||||
private String label;
|
||||
private Long amount;
|
||||
private final List<Node> children = new ArrayList<>();
|
||||
private Set<Node> children = new TreeSet<>();
|
||||
|
||||
private final transient KeyPurpose keyPurpose;
|
||||
private final transient int index;
|
||||
private final transient List<ChildNumber> derivation;
|
||||
private transient KeyPurpose keyPurpose;
|
||||
private transient int index = -1;
|
||||
private transient List<ChildNumber> derivation;
|
||||
|
||||
public Node(String derivationPath) {
|
||||
this.derivationPath = derivationPath;
|
||||
this.derivation = KeyDerivation.parsePath(derivationPath);
|
||||
this.keyPurpose = KeyPurpose.fromChildNumber(derivation.get(0));
|
||||
this.index = derivation.get(derivation.size() - 1).num();
|
||||
parseDerivation();
|
||||
}
|
||||
|
||||
public Node(KeyPurpose keyPurpose) {
|
||||
|
@ -337,14 +377,36 @@ public class Wallet {
|
|||
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() {
|
||||
if(index < 0) {
|
||||
parseDerivation();
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
public KeyPurpose getKeyPurpose() {
|
||||
if(keyPurpose == null) {
|
||||
parseDerivation();
|
||||
}
|
||||
|
||||
return keyPurpose;
|
||||
}
|
||||
|
||||
public List<ChildNumber> getDerivation() {
|
||||
if(derivation == null) {
|
||||
parseDerivation();
|
||||
}
|
||||
|
||||
return derivation;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
@ -361,10 +423,14 @@ public class Wallet {
|
|||
this.amount = amount;
|
||||
}
|
||||
|
||||
public List<Node> getChildren() {
|
||||
public Set<Node> getChildren() {
|
||||
return children;
|
||||
}
|
||||
|
||||
public void setChildren(Set<Node> children) {
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
public Address getAddress() {
|
||||
return Wallet.this.getAddress(keyPurpose, index);
|
||||
}
|
||||
|
@ -373,15 +439,26 @@ public class Wallet {
|
|||
return Wallet.this.getOutputScript(keyPurpose, index);
|
||||
}
|
||||
|
||||
public void fillToLookAhead(int lookAhead) {
|
||||
for(int i = 0; i < lookAhead; i++) {
|
||||
public String getOutputDescriptor() {
|
||||
return Wallet.this.getOutputDescriptor(keyPurpose, index);
|
||||
}
|
||||
|
||||
public void fillToIndex(int index) {
|
||||
for(int i = 0; i <= index; 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
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
@ -394,5 +471,10 @@ public class Wallet {
|
|||
public int hashCode() {
|
||||
return Objects.hash(derivationPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Node node) {
|
||||
return getIndex() - node.getIndex();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue