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.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);
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue