From 08e8df0807a01b2716ac80e9d6d9e2c113025848 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Fri, 17 Apr 2020 10:02:30 +0200 Subject: [PATCH] wallet implementation first commit --- .../drongo/OutputDescriptor.java | 4 +- .../sparrowwallet/drongo/policy/Policy.java | 37 ++ .../drongo/policy/PolicyException.java | 22 + .../drongo/policy/PolicyType.java | 30 ++ .../sparrowwallet/drongo/protocol/Script.java | 73 ++-- .../drongo/protocol/ScriptType.java | 387 ++++++++++++++++++ .../drongo/protocol/Transaction.java | 3 +- .../sparrowwallet/drongo/psbt/PSBTInput.java | 22 +- .../sparrowwallet/drongo/wallet/Keystore.java | 9 + .../sparrowwallet/drongo/wallet/Wallet.java | 46 +++ src/main/java/module-info.java | 2 + .../drongo/protocol/TransactionTest.java | 6 +- 12 files changed, 592 insertions(+), 49 deletions(-) create mode 100644 src/main/java/com/sparrowwallet/drongo/policy/Policy.java create mode 100644 src/main/java/com/sparrowwallet/drongo/policy/PolicyException.java create mode 100644 src/main/java/com/sparrowwallet/drongo/policy/PolicyType.java create mode 100644 src/main/java/com/sparrowwallet/drongo/protocol/ScriptType.java create mode 100644 src/main/java/com/sparrowwallet/drongo/wallet/Keystore.java create mode 100644 src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java diff --git a/src/main/java/com/sparrowwallet/drongo/OutputDescriptor.java b/src/main/java/com/sparrowwallet/drongo/OutputDescriptor.java index 463e5f8..c949262 100644 --- a/src/main/java/com/sparrowwallet/drongo/OutputDescriptor.java +++ b/src/main/java/com/sparrowwallet/drongo/OutputDescriptor.java @@ -6,6 +6,7 @@ import com.sparrowwallet.drongo.crypto.DeterministicKey; import com.sparrowwallet.drongo.protocol.Script; import com.sparrowwallet.drongo.protocol.ScriptChunk; import com.sparrowwallet.drongo.protocol.ScriptOpCodes; +import com.sparrowwallet.drongo.protocol.ScriptType; import java.util.ArrayList; import java.util.Collections; @@ -117,8 +118,7 @@ public class OutputDescriptor { if(script.equals("pkh")) { address = new P2PKHAddress(childKey.getPubKeyHash()); } else if(script.equals("sh(wpkh")) { - Address p2wpkhAddress = new P2WPKHAddress(childKey.getPubKeyHash()); - Script receivingP2wpkhScript = p2wpkhAddress.getOutputScript(); + Script receivingP2wpkhScript = ScriptType.P2WPKH.getOutputScript(childKey.getPubKeyHash()); address = P2SHAddress.fromProgram(receivingP2wpkhScript.getProgram()); } else if(script.equals("wpkh")) { address = new P2WPKHAddress(childKey.getPubKeyHash()); diff --git a/src/main/java/com/sparrowwallet/drongo/policy/Policy.java b/src/main/java/com/sparrowwallet/drongo/policy/Policy.java new file mode 100644 index 0000000..a60007d --- /dev/null +++ b/src/main/java/com/sparrowwallet/drongo/policy/Policy.java @@ -0,0 +1,37 @@ +package com.sparrowwallet.drongo.policy; + +import com.sparrowwallet.drongo.protocol.ScriptType; + +import static com.sparrowwallet.drongo.protocol.ScriptType.*; +import static com.sparrowwallet.drongo.policy.PolicyType.*; + +public class Policy { + private String policy; + + public Policy(String policy) { + this.policy = policy; + } + + public String getPolicy() { + return policy; + } + + public void setPolicy(String policy) { + this.policy = policy; + } + + public static Policy getPolicy(PolicyType policyType, ScriptType scriptType, Integer threshold, Integer numCosigners) { + if(SINGLE.equals(policyType)) { + if(P2PK.equals(scriptType)) { + return new Policy("pk()"); + } + return new Policy("pkh()"); + } + + if(MULTI.equals(policyType)) { + return new Policy("multi(,,)"); + } + + throw new PolicyException("No standard policy for " + policyType + " policy with script type " + scriptType); + } +} diff --git a/src/main/java/com/sparrowwallet/drongo/policy/PolicyException.java b/src/main/java/com/sparrowwallet/drongo/policy/PolicyException.java new file mode 100644 index 0000000..0f9e579 --- /dev/null +++ b/src/main/java/com/sparrowwallet/drongo/policy/PolicyException.java @@ -0,0 +1,22 @@ +package com.sparrowwallet.drongo.policy; + +public class PolicyException extends RuntimeException { + public PolicyException() { + } + + public PolicyException(String message) { + super(message); + } + + public PolicyException(String message, Throwable cause) { + super(message, cause); + } + + public PolicyException(Throwable cause) { + super(cause); + } + + public PolicyException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/main/java/com/sparrowwallet/drongo/policy/PolicyType.java b/src/main/java/com/sparrowwallet/drongo/policy/PolicyType.java new file mode 100644 index 0000000..f3ea5db --- /dev/null +++ b/src/main/java/com/sparrowwallet/drongo/policy/PolicyType.java @@ -0,0 +1,30 @@ +package com.sparrowwallet.drongo.policy; + +import com.sparrowwallet.drongo.protocol.ScriptType; + +import static com.sparrowwallet.drongo.protocol.ScriptType.*; + +public enum PolicyType { + SINGLE("Single Signature", P2WPKH), MULTI("Multi Signature", P2WSH), CUSTOM("Custom", P2WSH); + + private String name; + private ScriptType defaultScriptType; + + PolicyType(String name, ScriptType defaultScriptType) { + this.name = name; + this.defaultScriptType = defaultScriptType; + } + + public String getName() { + return name; + } + + public ScriptType getDefaultScriptType() { + return defaultScriptType; + } + + @Override + public String toString() { + return name; + } +} diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/Script.java b/src/main/java/com/sparrowwallet/drongo/protocol/Script.java index ee2e0a6..f04331b 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/Script.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/Script.java @@ -2,6 +2,7 @@ package com.sparrowwallet.drongo.protocol; import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.address.*; +import com.sparrowwallet.drongo.crypto.ECKey; import org.bouncycastle.util.encoders.Hex; import java.io.ByteArrayInputStream; @@ -12,6 +13,8 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import static com.sparrowwallet.drongo.protocol.ScriptType.*; + import static com.sparrowwallet.drongo.protocol.ScriptOpCodes.*; public class Script { @@ -124,7 +127,13 @@ public class Script { * Returns true if this script has the required form to contain a destination address */ public boolean containsToAddress() { - return ScriptPattern.isP2PK(this) || ScriptPattern.isP2PKH(this) || ScriptPattern.isP2SH(this) || ScriptPattern.isP2WPKH(this) || ScriptPattern.isP2WSH(this) || ScriptPattern.isMultisig(this); + for(ScriptType scriptType : ScriptType.values()) { + if(scriptType.isScriptType(this)) { + return true; + } + } + + return false; } /** @@ -133,46 +142,52 @@ public class Script { *

Otherwise this method throws a ScriptException.

*/ public byte[] getPubKeyHash() throws ProtocolException { - if (ScriptPattern.isP2PKH(this)) - return ScriptPattern.extractHashFromP2PKH(this); - else if (ScriptPattern.isP2SH(this)) - return ScriptPattern.extractHashFromP2SH(this); - else if (ScriptPattern.isP2WPKH(this) || ScriptPattern.isP2WSH(this)) - return ScriptPattern.extractHashFromP2WH(this); - else - throw new ProtocolException("Script not in the standard scriptPubKey form"); + for(ScriptType scriptType : SINGLE_HASH_TYPES) { + if(scriptType.isScriptType(this)) { + return scriptType.getHashFromScript(this); + } + } + + throw new ProtocolException("Script not a standard form that contains a single hash"); } /** * Gets the destination address from this script, if it's in the required form. */ public Address[] getToAddresses() throws NonStandardScriptException { - if (ScriptPattern.isP2PK(this)) - return new Address[] { new P2PKAddress( ScriptPattern.extractPKFromP2PK(this).getPubKey()) }; - else if (ScriptPattern.isP2PKH(this)) - return new Address[] { new P2PKHAddress( ScriptPattern.extractHashFromP2PKH(this)) }; - else if (ScriptPattern.isP2SH(this)) - return new Address[] { new P2SHAddress(ScriptPattern.extractHashFromP2SH(this)) }; - else if (ScriptPattern.isP2WPKH(this)) - return new Address[] { new P2WPKHAddress(ScriptPattern.extractHashFromP2WH(this)) }; - else if (ScriptPattern.isP2WSH(this)) - return new Address[] { new P2WSHAddress(ScriptPattern.extractHashFromP2WH(this)) }; - else if (ScriptPattern.isMultisig(this)) - return ScriptPattern.extractMultisigAddresses(this); - else - throw new NonStandardScriptException("Cannot find addresses in non standard script: " + toString()); + for(ScriptType scriptType : SINGLE_HASH_TYPES) { + if(scriptType.isScriptType(this)) { + return new Address[] { scriptType.getAddress(scriptType.getHashFromScript(this)) }; + } + } + + if(P2PK.isScriptType(this)) { + return new Address[] { P2PK.getAddress(P2PK.getPublicKeyFromScript(this).getPubKey()) }; + } + + if(MULTISIG.isScriptType(this)) { + List
addresses = new ArrayList<>(); + ECKey[] pubKeys = MULTISIG.getPublicKeysFromScript(this); + for(ECKey pubKey : pubKeys) { + addresses.add(new P2PKAddress(pubKey.getPubKey())); + } + + return addresses.toArray(new Address[addresses.size()]); + } + + throw new NonStandardScriptException("Cannot find addresses in non standard script: " + toString()); } public int getNumRequiredSignatures() throws NonStandardScriptException { - if(ScriptPattern.isP2PK(this) || ScriptPattern.isP2PKH(this) || ScriptPattern.isP2WPKH(this)) { + if(P2PK.isScriptType(this) || P2PKH.isScriptType(this) || P2WPKH.isScriptType(this)) { return 1; } - if(ScriptPattern.isMultisig(this)) { - return ScriptPattern.extractMultisigThreshold(this); + if(MULTISIG.isScriptType(this)) { + return MULTISIG.getThreshold(this); } - throw new NonStandardScriptException("Cannot find number of required signatures for non standard script: " + toString()); + throw new NonStandardScriptException("Cannot find number of required signatures for script: " + toString()); } public Script getFirstNestedScript() { @@ -289,9 +304,9 @@ public class Script { builder.append(""); } else if(chunk.isScript()) { Script nestedScript = chunk.getScript(); - if(ScriptPattern.isP2WPKH(nestedScript)) { + if(P2WPKH.isScriptType(nestedScript)) { builder.append("(OP_0 )"); - } else if(ScriptPattern.isP2WSH(nestedScript)) { + } else if(P2WSH.isScriptType(nestedScript)) { builder.append("(OP_0 )"); } else { builder.append("(").append(nestedScript.toDisplayString()).append(")"); diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/ScriptType.java b/src/main/java/com/sparrowwallet/drongo/protocol/ScriptType.java new file mode 100644 index 0000000..99bcf9b --- /dev/null +++ b/src/main/java/com/sparrowwallet/drongo/protocol/ScriptType.java @@ -0,0 +1,387 @@ +package com.sparrowwallet.drongo.protocol; + +import com.sparrowwallet.drongo.address.*; +import com.sparrowwallet.drongo.crypto.ECKey; +import com.sparrowwallet.drongo.policy.PolicyType; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static com.sparrowwallet.drongo.policy.PolicyType.*; +import static com.sparrowwallet.drongo.protocol.Script.decodeFromOpN; +import static com.sparrowwallet.drongo.protocol.ScriptOpCodes.*; + +public enum ScriptType { + P2PK("P2PK", new PolicyType[]{SINGLE}) { + @Override + public Address getAddress(byte[] pubKey) { + return new P2PKAddress(pubKey); + } + + @Override + public Address[] getAddresses(Script script) { + return new Address[] { getAddress(getPublicKeyFromScript(script).getPubKey()) }; + } + + @Override + public Script getOutputScript(byte[] pubKey) { + List chunks = new ArrayList<>(); + chunks.add(new ScriptChunk(pubKey.length, pubKey)); + chunks.add(new ScriptChunk(ScriptOpCodes.OP_CHECKSIG, null)); + + return new Script(chunks); + } + + @Override + public boolean isScriptType(Script script) { + List chunks = script.chunks; + if (chunks.size() != 2) + return false; + if (!chunks.get(0).equalsOpCode(0x21) && !chunks.get(0).equalsOpCode(0x41)) + return false; + byte[] chunk2data = chunks.get(0).data; + if (chunk2data == null) + return false; + if (chunk2data.length != 33 && chunk2data.length != 65) + return false; + if (!chunks.get(1).equalsOpCode(OP_CHECKSIG)) + return false; + return true; + } + + @Override + public byte[] getHashFromScript(Script script) { + throw new ProtocolException("P2PK script does contain hash, use getPublicKeyFromScript(script) to retreive public key"); + } + + @Override + public ECKey getPublicKeyFromScript(Script script) { + return ECKey.fromPublicOnly(script.chunks.get(0).data); + } + }, + P2PKH("P2PKH", new PolicyType[]{SINGLE}) { + @Override + public Address getAddress(byte[] pubKeyHash) { + return new P2PKHAddress(pubKeyHash); + } + + @Override + public Script getOutputScript(byte[] pubKeyHash) { + List chunks = new ArrayList<>(); + chunks.add(new ScriptChunk(ScriptOpCodes.OP_DUP, null)); + chunks.add(new ScriptChunk(ScriptOpCodes.OP_HASH160, null)); + chunks.add(new ScriptChunk(pubKeyHash.length, pubKeyHash)); + chunks.add(new ScriptChunk(ScriptOpCodes.OP_EQUALVERIFY, null)); + chunks.add(new ScriptChunk(ScriptOpCodes.OP_CHECKSIG, null)); + + return new Script(chunks); + } + + @Override + public boolean isScriptType(Script script) { + List chunks = script.chunks; + if (chunks.size() != 5) + return false; + if (!chunks.get(0).equalsOpCode(OP_DUP)) + return false; + if (!chunks.get(1).equalsOpCode(OP_HASH160)) + return false; + byte[] chunk2data = chunks.get(2).data; + if (chunk2data == null) + return false; + if (chunk2data.length != 20) + return false; + if (!chunks.get(3).equalsOpCode(OP_EQUALVERIFY)) + return false; + if (!chunks.get(4).equalsOpCode(OP_CHECKSIG)) + return false; + return true; + } + + @Override + public byte[] getHashFromScript(Script script) { + return script.chunks.get(2).data; + } + }, + MULTISIG("Bare Multisig", new PolicyType[]{MULTI}) { + @Override + public Address getAddress(byte[] bytes) { + throw new ProtocolException("No single 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); + } + + @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, ...)"); + } + + public Script getOutputScript(int threshold, byte[] ...pubKeys) { + List chunks = new ArrayList<>(); + chunks.add(new ScriptChunk(Script.encodeToOpN(threshold), null)); + for(byte[] pubKey : pubKeys) { + chunks.add(new ScriptChunk(pubKey.length, pubKey)); + } + chunks.add(new ScriptChunk(Script.encodeToOpN(pubKeys.length), null)); + chunks.add(new ScriptChunk(ScriptOpCodes.OP_CHECKMULTISIG, null)); + return new Script(chunks); + } + + @Override + public boolean isScriptType(Script script) { + List chunks = script.chunks; + if (chunks.size() < 4) return false; + ScriptChunk chunk = chunks.get(chunks.size() - 1); + // Must end in OP_CHECKMULTISIG[VERIFY]. + if (!chunk.isOpCode()) return false; + if (!(chunk.equalsOpCode(OP_CHECKMULTISIG) || chunk.equalsOpCode(OP_CHECKMULTISIGVERIFY))) return false; + try { + // Second to last chunk must be an OP_N opcode and there should be that many data chunks (keys). + ScriptChunk m = chunks.get(chunks.size() - 2); + if (!m.isOpCode()) return false; + int numKeys = Script.decodeFromOpN(m.opcode); + if (numKeys < 1 || chunks.size() != 3 + numKeys) return false; + for (int i = 1; i < chunks.size() - 2; i++) { + if (chunks.get(i).isOpCode()) return false; + } + // First chunk must be an OP_N opcode too. + if (Script.decodeFromOpN(chunks.get(0).opcode) < 1) return false; + } catch (IllegalStateException e) { + return false; // Not an OP_N opcode. + } + return true; + } + + @Override + public byte[] getHashFromScript(Script script) { + throw new ProtocolException("Public keys for bare multisig script type must be retrieved with method getPublicKeysFromScript(Script script)"); + } + + @Override + public ECKey[] getPublicKeysFromScript(Script script) { + List pubKeys = new ArrayList<>(); + + List chunks = script.chunks; + for (int i = 1; i < chunks.size() - 2; i++) { + byte[] pubKey = chunks.get(i).data; + pubKeys.add(ECKey.fromPublicOnly(pubKey)); + } + + return pubKeys.toArray(new ECKey[pubKeys.size()]); + } + + @Override + public int getThreshold(Script script) { + return decodeFromOpN(script.chunks.get(0).opcode); + } + }, + P2SH("P2SH", new PolicyType[]{MULTI}) { + @Override + public Address getAddress(byte[] bytes) { + return new P2SHAddress(bytes); + } + + @Override + public Script getOutputScript(byte[] bytes) { + List chunks = new ArrayList<>(); + chunks.add(new ScriptChunk(ScriptOpCodes.OP_HASH160, null)); + chunks.add(new ScriptChunk(bytes.length, bytes)); + chunks.add(new ScriptChunk(ScriptOpCodes.OP_EQUAL, null)); + + return new Script(chunks); + } + + @Override + public boolean isScriptType(Script script) { + List chunks = script.chunks; + // We check for the effective serialized form because BIP16 defines a P2SH output using an exact byte + // template, not the logical program structure. Thus you can have two programs that look identical when + // printed out but one is a P2SH script and the other isn't! :( + // We explicitly test that the op code used to load the 20 bytes is 0x14 and not something logically + // equivalent like {@code OP_HASH160 OP_PUSHDATA1 0x14 <20 bytes of script hash> OP_EQUAL} + if (chunks.size() != 3) + return false; + if (!chunks.get(0).equalsOpCode(OP_HASH160)) + return false; + ScriptChunk chunk1 = chunks.get(1); + if (chunk1.opcode != 0x14) + return false; + byte[] chunk1data = chunk1.data; + if (chunk1data == null) + return false; + if (chunk1data.length != 20) + return false; + if (!chunks.get(2).equalsOpCode(OP_EQUAL)) + return false; + return true; + } + + @Override + public byte[] getHashFromScript(Script script) { + return script.chunks.get(1).data; + } + }, + P2SH_P2WPKH("P2SH-P2WPKH", new PolicyType[]{SINGLE}) { + @Override + public Address getAddress(byte[] bytes) { + return P2SH.getAddress(bytes); + } + + @Override + public Script getOutputScript(byte[] bytes) { + return P2SH.getOutputScript(bytes); + } + + @Override + public boolean isScriptType(Script script) { + return P2SH.isScriptType(script); + } + + @Override + public byte[] getHashFromScript(Script script) { + return P2SH.getHashFromScript(script); + } + }, + P2SH_P2WSH("P2SH-P2WSH", new PolicyType[]{MULTI, CUSTOM}) { + @Override + public Address getAddress(byte[] bytes) { + return P2SH.getAddress(bytes); + } + + @Override + public Script getOutputScript(byte[] bytes) { + return P2SH.getOutputScript(bytes); + } + + @Override + public boolean isScriptType(Script script) { + return P2SH.isScriptType(script); + } + + @Override + public byte[] getHashFromScript(Script script) { + return P2SH.getHashFromScript(script); + } + }, + P2WPKH("P2WPKH", new PolicyType[]{SINGLE}) { + @Override + public Address getAddress(byte[] bytes) { + return new P2WPKHAddress(bytes); + } + + @Override + public Script getOutputScript(byte[] bytes) { + List chunks = new ArrayList<>(); + chunks.add(new ScriptChunk(OP_0, null)); + chunks.add(new ScriptChunk(bytes.length, bytes)); + + return new Script(chunks); + } + + @Override + public boolean isScriptType(Script script) { + List chunks = script.chunks; + if (chunks.size() != 2) + return false; + if (!chunks.get(0).equalsOpCode(OP_0)) + return false; + byte[] chunk1data = chunks.get(1).data; + if (chunk1data == null) + return false; + if (chunk1data.length != 20) + return false; + return true; + } + + @Override + public byte[] getHashFromScript(Script script) { + return script.chunks.get(1).data; + } + }, + P2WSH("P2WSH", new PolicyType[]{MULTI, CUSTOM}) { + @Override + public Address getAddress(byte[] bytes) { + return new P2WSHAddress(bytes); + } + + @Override + public Script getOutputScript(byte[] bytes) { + List chunks = new ArrayList<>(); + chunks.add(new ScriptChunk(OP_0, null)); + chunks.add(new ScriptChunk(bytes.length, bytes)); + + return new Script(chunks); + } + + @Override + public boolean isScriptType(Script script) { + List chunks = script.chunks; + if (chunks.size() != 2) + return false; + if (!chunks.get(0).equalsOpCode(OP_0)) + return false; + byte[] chunk1data = chunks.get(1).data; + if (chunk1data == null) + return false; + if (chunk1data.length != 32) + return false; + return true; + } + + @Override + public byte[] getHashFromScript(Script script) { + return script.chunks.get(1).data; + } + }; + + private final String name; + private final PolicyType[] allowedPolicyTypes; + + ScriptType(String name, PolicyType[] allowedPolicyTypes) { + this.name = name; + this.allowedPolicyTypes = allowedPolicyTypes; + } + + public String getName() { + return name; + } + + public PolicyType[] getAllowedPolicyTypes() { + return allowedPolicyTypes; + } + + public abstract Address getAddress(byte[] bytes); + + public abstract Script getOutputScript(byte[] bytes); + + public abstract boolean isScriptType(Script script); + + public abstract byte[] getHashFromScript(Script script); + + public Address[] getAddresses(Script script) { + return new Address[] { getAddress(getHashFromScript(script)) }; + } + + public ECKey getPublicKeyFromScript(Script script) { + throw new ProtocolException("Script type " + this + " does not contain a public key"); + } + + public ECKey[] getPublicKeysFromScript(Script script) { + throw new ProtocolException("Script type " + this + " does not contain public keys"); + } + + public int getThreshold(Script script) { + throw new ProtocolException("Script type " + this + " is not a multisig script"); + } + + public static final ScriptType[] SINGLE_HASH_TYPES = {P2PKH, P2SH, P2SH_P2WPKH, P2SH_P2WSH, P2WPKH, P2WSH}; + + @Override + public String toString() { + return name; + } +} diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/Transaction.java b/src/main/java/com/sparrowwallet/drongo/protocol/Transaction.java index 48e6ce3..42e3a67 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/Transaction.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/Transaction.java @@ -544,8 +544,7 @@ public class Transaction extends TransactionPart { Transaction transaction = new Transaction(transactionBytes); ECKey pubKey = ECKey.fromPublicOnly(Utils.hexToBytes("026dccc749adc2a9d0d89497ac511f760f45c47dc5ed9cf352a58ac706453880ae")); - P2PKHAddress address = new P2PKHAddress(pubKey.getPubKeyHash()); - System.out.println(address.getOutputScript().getProgram().length); + System.out.println(ScriptType.P2PKH.getOutputScript(pubKey.getPubKeyHash()).getProgram().length); Script script = new Script(Utils.hexToBytes("21026dccc749adc2a9d0d89497ac511f760f45c47dc5ed9cf352a58ac706453880aeadab210255a9626aebf5e29c0e6538428ba0d1dcf6ca98ffdf086aa8ced5e0d0215ea465ac")); Sha256Hash hash = transaction.hashForWitnessSignature(1, script,4900000000L, SigHash.SINGLE, false); System.out.println("Sighash: " + hash.toString()); diff --git a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java index 3020fd8..8f166d6 100644 --- a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java +++ b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java @@ -2,8 +2,6 @@ package com.sparrowwallet.drongo.psbt; import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.Utils; -import com.sparrowwallet.drongo.address.Address; -import com.sparrowwallet.drongo.address.P2PKHAddress; import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.protocol.*; import org.bouncycastle.util.encoders.Hex; @@ -13,6 +11,7 @@ import org.slf4j.LoggerFactory; import java.nio.charset.StandardCharsets; import java.util.*; +import static com.sparrowwallet.drongo.protocol.ScriptType.*; import static com.sparrowwallet.drongo.psbt.PSBTEntry.parseKeyDerivation; public class PSBTInput { @@ -80,7 +79,7 @@ public class PSBTInput { throw new PSBTParseException("Cannot have both witness and non-witness utxos in PSBT input"); } TransactionOutput witnessTxOutput = new TransactionOutput(null, entry.getData(), 0); - if(!ScriptPattern.isP2SH(witnessTxOutput.getScript()) && !ScriptPattern.isP2WPKH(witnessTxOutput.getScript()) && !ScriptPattern.isP2WSH(witnessTxOutput.getScript())) { + if(!P2SH.isScriptType(witnessTxOutput.getScript()) && !P2WPKH.isScriptType(witnessTxOutput.getScript()) && !P2WSH.isScriptType(witnessTxOutput.getScript())) { throw new PSBTParseException("Witness UTXO provided for non-witness or unknown input"); } this.witnessUtxo = witnessTxOutput; @@ -113,11 +112,11 @@ public class PSBTInput { scriptPubKey = this.nonWitnessUtxo.getOutputs().get((int)transaction.getInputs().get(index).getOutpoint().getIndex()).getScript(); } else if(this.witnessUtxo != null) { scriptPubKey = this.witnessUtxo.getScript(); - if(!ScriptPattern.isP2WPKH(redeemScript) && !ScriptPattern.isP2WSH(redeemScript)) { //Witness UTXO should only be provided for P2SH-P2WPKH or P2SH-P2WSH + if(!P2WPKH.isScriptType(redeemScript) && !P2WSH.isScriptType(redeemScript)) { //Witness UTXO should only be provided for P2SH-P2WPKH or P2SH-P2WSH throw new PSBTParseException("Witness UTXO provided but redeem script is not P2WPKH or P2WSH"); } } - if(scriptPubKey == null || !ScriptPattern.isP2SH(scriptPubKey)) { + if(scriptPubKey == null || !P2SH.isScriptType(scriptPubKey)) { throw new PSBTParseException("PSBT provided a redeem script for a transaction output that does not need one"); } if(!Arrays.equals(Utils.sha256hash160(redeemScript.getProgram()), scriptPubKey.getPubKeyHash())) { @@ -131,9 +130,9 @@ public class PSBTInput { entry.checkOneByteKey(); Script witnessScript = new Script(entry.getData()); byte[] pubKeyHash = null; - if(this.redeemScript != null && ScriptPattern.isP2WSH(this.redeemScript)) { //P2SH-P2WSH + if(this.redeemScript != null && P2WSH.isScriptType(this.redeemScript)) { //P2SH-P2WSH pubKeyHash = this.redeemScript.getPubKeyHash(); - } else if(this.witnessUtxo != null && ScriptPattern.isP2WSH(this.witnessUtxo.getScript())) { //P2WSH + } else if(this.witnessUtxo != null && P2WSH.isScriptType(this.witnessUtxo.getScript())) { //P2WSH pubKeyHash = this.witnessUtxo.getScript().getPubKeyHash(); } if(pubKeyHash == null) { @@ -285,7 +284,7 @@ public class PSBTInput { int vout = (int)transaction.getInputs().get(index).getOutpoint().getIndex(); Script signingScript = getNonWitnessUtxo() != null ? getNonWitnessUtxo().getOutputs().get(vout).getScript() : getWitnessUtxo().getScript(); - if(ScriptPattern.isP2SH(signingScript)) { + if(P2SH.isScriptType(signingScript)) { if(getRedeemScript() != null) { signingScript = getRedeemScript(); } else if(getFinalScriptSig() != null) { @@ -295,10 +294,9 @@ public class PSBTInput { } } - if(ScriptPattern.isP2WPKH(signingScript)) { - Address address = new P2PKHAddress(signingScript.getPubKeyHash()); - signingScript = address.getOutputScript(); - } else if(ScriptPattern.isP2WSH(signingScript)) { + if(P2WPKH.isScriptType(signingScript)) { + signingScript = ScriptType.P2PKH.getOutputScript(signingScript.getPubKeyHash()); + } else if(P2WSH.isScriptType(signingScript)) { if(getWitnessScript() != null) { signingScript = getWitnessScript(); } else if(getFinalScriptWitness() != null && getFinalScriptWitness().getWitnessScript() != null) { diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/Keystore.java b/src/main/java/com/sparrowwallet/drongo/wallet/Keystore.java new file mode 100644 index 0000000..c73ea35 --- /dev/null +++ b/src/main/java/com/sparrowwallet/drongo/wallet/Keystore.java @@ -0,0 +1,9 @@ +package com.sparrowwallet.drongo.wallet; + +import com.sparrowwallet.drongo.ExtendedPublicKey; +import com.sparrowwallet.drongo.KeyDerivation; + +public class Keystore { + private KeyDerivation keyDerivation; + private ExtendedPublicKey extendedPublicKey; +} diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java b/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java new file mode 100644 index 0000000..27e7908 --- /dev/null +++ b/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java @@ -0,0 +1,46 @@ +package com.sparrowwallet.drongo.wallet; + +import com.sparrowwallet.drongo.policy.PolicyType; +import com.sparrowwallet.drongo.protocol.ScriptType; + +import java.util.ArrayList; +import java.util.List; + +public class Wallet { + private PolicyType policyType; + private ScriptType scriptType; + private String policy; + private List keystores = new ArrayList<>(); + + public PolicyType getPolicyType() { + return policyType; + } + + public void setPolicyType(PolicyType policyType) { + this.policyType = policyType; + } + + public ScriptType getScriptType() { + return scriptType; + } + + public void setScriptType(ScriptType scriptType) { + this.scriptType = scriptType; + } + + public String getPolicy() { + return policy; + } + + public void setPolicy(String policy) { + this.policy = policy; + } + + public List getKeystores() { + return keystores; + } + + public void setKeystores(List keystores) { + this.keystores = keystores; + } +} diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 8ca205e..bb71417 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -6,4 +6,6 @@ module com.sparrowwallet.drongo { exports com.sparrowwallet.drongo.protocol; exports com.sparrowwallet.drongo.address; exports com.sparrowwallet.drongo.crypto; + exports com.sparrowwallet.drongo.wallet; + exports com.sparrowwallet.drongo.policy; } \ No newline at end of file diff --git a/src/test/java/com/sparrowwallet/drongo/protocol/TransactionTest.java b/src/test/java/com/sparrowwallet/drongo/protocol/TransactionTest.java index d837ad8..7a08ecf 100644 --- a/src/test/java/com/sparrowwallet/drongo/protocol/TransactionTest.java +++ b/src/test/java/com/sparrowwallet/drongo/protocol/TransactionTest.java @@ -14,8 +14,7 @@ public class TransactionTest { Transaction transaction = new Transaction(Utils.hexToBytes(hex)); ECKey pubKey = ECKey.fromPublicOnly(Utils.hexToBytes("025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee6357")); - P2PKHAddress address = new P2PKHAddress(pubKey.getPubKeyHash()); - Sha256Hash hash = transaction.hashForWitnessSignature(1, address.getOutputScript(),600000000L, Transaction.SigHash.ALL, false); + Sha256Hash hash = transaction.hashForWitnessSignature(1, ScriptType.P2PKH.getOutputScript(pubKey.getPubKeyHash()),600000000L, Transaction.SigHash.ALL, false); TransactionSignature signature = TransactionSignature.decodeFromBitcoin(Utils.hexToBytes("304402203609e17b84f6a7d30c80bfa610b5b4542f32a8a0d5447a12fb1366d7f01cc44a0220573a954c4518331561406f90300e8f3358f51928d43c212a8caed02de67eebee"), false, true); Assert.assertTrue(pubKey.verify(hash, signature)); } @@ -26,8 +25,7 @@ public class TransactionTest { Transaction transaction = new Transaction(Utils.hexToBytes(hex)); ECKey pubKey = ECKey.fromPublicOnly(Utils.hexToBytes("03ad1d8e89212f0b92c74d23bb710c00662ad1470198ac48c43f7d6f93a2a26873")); - Address address = new P2PKHAddress(pubKey.getPubKeyHash()); - Sha256Hash hash = transaction.hashForWitnessSignature(0, address.getOutputScript(),1000000000L, Transaction.SigHash.ALL, false); + Sha256Hash hash = transaction.hashForWitnessSignature(0, ScriptType.P2PKH.getOutputScript(pubKey.getPubKeyHash()),1000000000L, Transaction.SigHash.ALL, false); TransactionSignature signature = TransactionSignature.decodeFromBitcoin(Utils.hexToBytes("3044022047ac8e878352d3ebbde1c94ce3a10d057c24175747116f8288e5d794d12d482f0220217f36a485cae903c713331d877c1f64677e3622ad4010726870540656fe9dcb01"), true, true); Assert.assertTrue(pubKey.verify(hash, signature)); }