From c8226ea947d910186d3a2e37ba9c5e85c2ec09ef Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 9 Apr 2020 14:35:50 +0200 Subject: [PATCH] psbt and scripting improvments --- .../sparrowwallet/drongo/protocol/Script.java | 15 ++++- .../drongo/protocol/ScriptChunk.java | 16 +++++ .../drongo/protocol/ScriptPattern.java | 21 +++---- .../drongo/protocol/Transaction.java | 17 +++--- .../drongo/protocol/TransactionInput.java | 2 +- .../drongo/protocol/TransactionWitness.java | 59 ++++++++++++++++--- .../sparrowwallet/drongo/psbt/PSBTInput.java | 46 ++++++++++----- src/main/java/module-info.java | 1 + .../sparrowwallet/drongo/psbt/PSBTTest.java | 2 +- 9 files changed, 135 insertions(+), 44 deletions(-) diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/Script.java b/src/main/java/com/sparrowwallet/drongo/protocol/Script.java index f03e053..a522de8 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/Script.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/Script.java @@ -142,7 +142,7 @@ public class Script { */ public Address[] getToAddresses() throws NonStandardScriptException { if (ScriptPattern.isP2PK(this)) - return new Address[] { new P2PKAddress( ScriptPattern.extractPKFromP2PK(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)) @@ -179,6 +179,17 @@ public class Script { return null; } + public List getSignatures() { + List signatures = new ArrayList<>(); + for(ScriptChunk chunk : chunks) { + if(chunk.isSignature()) { + signatures.add(chunk.getSignature()); + } + } + + return signatures; + } + public static int decodeFromOpN(int opcode) { if((opcode != OP_0 && opcode != OP_1NEGATE) && (opcode < OP_1 || opcode > OP_16)) { throw new ProtocolException("decodeFromOpN called on non OP_N opcode: " + opcode); @@ -271,7 +282,7 @@ public class Script { if(chunk.isSignature()) { builder.append(""); } else if(chunk.isScript()) { - Script nestedScript = new Script(chunk.getData()); + Script nestedScript = chunk.getScript(); if(ScriptPattern.isP2WPKH(nestedScript)) { builder.append("(OP_0 )"); } else if(ScriptPattern.isP2WSH(nestedScript)) { diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/ScriptChunk.java b/src/main/java/com/sparrowwallet/drongo/protocol/ScriptChunk.java index 09fb56a..1e43cf8 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/ScriptChunk.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/ScriptChunk.java @@ -96,6 +96,14 @@ public class ScriptChunk { return true; } + public TransactionSignature getSignature() { + try { + return TransactionSignature.decodeFromBitcoin(data, false, false); + } catch(SignatureDecodeException e) { + throw new ProtocolException("Could not decode signature", e); + } + } + public boolean isScript() { if(data == null || data.length == 0) { return false; @@ -110,6 +118,10 @@ public class ScriptChunk { return true; } + public Script getScript() { + return new Script(data); + } + public boolean isPubKey() { if(data == null || data.length == 0) { return false; @@ -118,6 +130,10 @@ public class ScriptChunk { return ECKey.isPubKey(data); } + public ECKey getPubKey() { + return ECKey.fromPublicOnly(data); + } + public byte[] toByteArray() { ByteArrayOutputStream stream = new ByteArrayOutputStream(); try { diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/ScriptPattern.java b/src/main/java/com/sparrowwallet/drongo/protocol/ScriptPattern.java index 75270af..8c688a0 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/ScriptPattern.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/ScriptPattern.java @@ -2,6 +2,7 @@ package com.sparrowwallet.drongo.protocol; import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.P2PKAddress; +import com.sparrowwallet.drongo.crypto.ECKey; import java.util.ArrayList; import java.util.List; @@ -34,8 +35,8 @@ public class ScriptPattern { * Extract the pubkey from a P2PK scriptPubKey. It's important that the script is in the correct form, so you * will want to guard calls to this method with {@link #isP2PK(Script)}. */ - public static byte[] extractPKFromP2PK(Script script) { - return script.chunks.get(0).data; + public static ECKey extractPKFromP2PK(Script script) { + return ECKey.fromPublicOnly(script.chunks.get(0).data); } /** @@ -106,6 +107,14 @@ public class ScriptPattern { return true; } + /** + * Extract the script hash from a P2SH scriptPubKey. It's important that the script is in the correct form, so you + * will want to guard calls to this method with {@link #isP2SH(Script)}. + */ + public static byte[] extractHashFromP2SH(Script script) { + return script.chunks.get(1).data; + } + /** * Returns whether this script matches the format used for multisig outputs: * {@code [n] [keys...] [m] CHECKMULTISIG} @@ -150,14 +159,6 @@ public class ScriptPattern { return addresses.toArray(new Address[addresses.size()]); } - /** - * Extract the script hash from a P2SH scriptPubKey. It's important that the script is in the correct form, so you - * will want to guard calls to this method with {@link #isP2SH(Script)}. - */ - public static byte[] extractHashFromP2SH(Script script) { - return script.chunks.get(1).data; - } - /** * Returns true if this script is of the form {@code OP_0 }. This is a P2WPKH scriptPubKey. */ diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/Transaction.java b/src/main/java/com/sparrowwallet/drongo/protocol/Transaction.java index 4ebebc8..0738bf5 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/Transaction.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/Transaction.java @@ -165,7 +165,9 @@ public class Transaction extends TransactionPart { // script_witnesses if (useSegwit) { for (TransactionInput in : inputs) { - in.getWitness().bitcoinSerializeToStream(stream); + if (in.hasWitness()) { + in.getWitness().bitcoinSerializeToStream(stream); + } } } // lock_time @@ -226,14 +228,9 @@ public class Transaction extends TransactionPart { private void parseWitnesses() { int numWitnesses = inputs.size(); for (int i = 0; i < numWitnesses; i++) { - long pushCount = readVarInt(); - TransactionWitness witness = new TransactionWitness((int) pushCount); + TransactionWitness witness = new TransactionWitness(this, rawtx, cursor); inputs.get(i).setWitness(witness); - for (int y = 0; y < pushCount; y++) { - long pushSize = readVarInt(); - byte[] push = readBytes((int) pushSize); - witness.setPush(y, push); - } + cursor += witness.getLength(); } } @@ -261,7 +258,9 @@ public class Transaction extends TransactionPart { // script_witnesses if(isSegwit()) { for (TransactionInput in : inputs) { - wu += in.getWitness().getLength(); + if (in.hasWitness()) { + wu += in.getWitness().getLength(); + } } } // lock_time diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/TransactionInput.java b/src/main/java/com/sparrowwallet/drongo/protocol/TransactionInput.java index 5f0ace3..d9e3372 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/TransactionInput.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/TransactionInput.java @@ -66,7 +66,7 @@ public class TransactionInput extends TransactionPart { } public TransactionWitness getWitness() { - return witness != null ? witness : TransactionWitness.EMPTY; + return witness; } public void setWitness(TransactionWitness witness) { diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/TransactionWitness.java b/src/main/java/com/sparrowwallet/drongo/protocol/TransactionWitness.java index b3f8a7e..f2632e6 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/TransactionWitness.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/TransactionWitness.java @@ -1,8 +1,8 @@ package com.sparrowwallet.drongo.protocol; -import com.sparrowwallet.drongo.Utils; import org.bouncycastle.util.encoders.Hex; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; @@ -10,20 +10,35 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -public class TransactionWitness { - public static final TransactionWitness EMPTY = new TransactionWitness(0); +public class TransactionWitness extends TransactionPart { + private List pushes; - private final List pushes; + public TransactionWitness(Transaction parent, byte[] rawtx, int offset) { + super(rawtx, offset); + setParent(parent); + if(pushes == null) { + pushes = new ArrayList<>(); + } + } - public TransactionWitness(int pushCount) { - pushes = new ArrayList<>(Math.min(pushCount, Utils.MAX_INITIAL_ARRAY_LENGTH)); + protected void parse() throws ProtocolException { + long pushCount = readVarInt(); + for (int y = 0; y < pushCount; y++) { + long pushSize = readVarInt(); + byte[] push = readBytes((int)pushSize); + setPush(y, push); + } } public List getPushes() { return Collections.unmodifiableList(pushes); } - public void setPush(int i, byte[] value) { + protected void setPush(int i, byte[] value) { + if(pushes == null) { + pushes = new ArrayList<>(); + } + while (i >= pushes.size()) { pushes.add(new byte[]{}); } @@ -54,6 +69,15 @@ public class TransactionWitness { } } + public byte[] toByteArray() { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + bitcoinSerializeToStream(baos); + } catch(IOException e) { } + + return baos.toByteArray(); + } + @Override public String toString() { StringBuilder builder = new StringBuilder(); @@ -80,6 +104,27 @@ public class TransactionWitness { return scriptChunks; } + public List getSignatures() { + List signatures = new ArrayList<>(); + List scriptChunks = this.asScriptChunks(); + for(ScriptChunk chunk : scriptChunks) { + if(chunk.isSignature()) { + signatures.add(chunk.getSignature()); + } + } + + return signatures; + } + + public Script getWitnessScript() { + List scriptChunks = this.asScriptChunks(); + if(scriptChunks.get(scriptChunks.size() - 1).isScript()) { + return scriptChunks.get(scriptChunks.size() - 1).getScript(); + } + + return null; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java index e450df3..054d0df 100644 --- a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java +++ b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java @@ -11,10 +11,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import static com.sparrowwallet.drongo.psbt.PSBTEntry.parseKeyDerivation; @@ -39,7 +36,7 @@ public class PSBTInput { private Script witnessScript; private Map derivedPublicKeys = new LinkedHashMap<>(); private Script finalScriptSig; - private Script finalScriptWitness; + private TransactionWitness finalScriptWitness; private String porCommitment; private Map proprietary = new LinkedHashMap<>(); @@ -162,9 +159,9 @@ public class PSBTInput { break; case PSBT_IN_FINAL_SCRIPTWITNESS: entry.checkOneByteKey(); - Script finalScriptWitness = new Script(entry.getData()); + TransactionWitness finalScriptWitness = new TransactionWitness(null, entry.getData(), 0); this.finalScriptWitness = finalScriptWitness; - log.debug("Found input final scriptWitness script hex " + Hex.toHexString(finalScriptWitness.getProgram()) + " script " + finalScriptWitness.toString()); + log.debug("Found input final scriptWitness " + finalScriptWitness.toString()); break; case PSBT_IN_POR_COMMITMENT: entry.checkOneByteKey(); @@ -217,7 +214,7 @@ public class PSBTInput { return finalScriptSig; } - public Script getFinalScriptWitness() { + public TransactionWitness getFinalScriptWitness() { return finalScriptWitness; } @@ -229,6 +226,18 @@ public class PSBTInput { return partialSignatures; } + public ECKey getKeyForSignature(TransactionSignature signature) { + if(partialSignatures != null) { + for(Map.Entry entry : partialSignatures.entrySet()) { + if(entry.getValue().equals(signature)) { + return entry.getKey(); + } + } + } + + return null; + } + public Map getDerivedPublicKeys() { return derivedPublicKeys; } @@ -263,6 +272,8 @@ public class PSBTInput { } } + //TODO: Implement Bitcoin Script engine to verify finalScriptSig and finalScriptWitness + return true; } } @@ -275,10 +286,12 @@ public class PSBTInput { Script signingScript = getNonWitnessUtxo() != null ? getNonWitnessUtxo().getOutputs().get(vout).getScript() : getWitnessUtxo().getScript(); if(ScriptPattern.isP2SH(signingScript)) { - if(getRedeemScript() == null) { - return null; - } else { + if(getRedeemScript() != null) { signingScript = getRedeemScript(); + } else if(getFinalScriptSig() != null) { + signingScript = getFinalScriptSig().getFirstNestedScript(); + } else { + return null; } } @@ -286,10 +299,15 @@ public class PSBTInput { Address address = new P2PKHAddress(signingScript.getPubKeyHash()); signingScript = address.getOutputScript(); } else if(ScriptPattern.isP2WSH(signingScript)) { - if(getWitnessScript() == null) { - return null; - } else { + if(getWitnessScript() != null) { signingScript = getWitnessScript(); + } else if(getFinalScriptWitness() != null) { + List witnessChunks = getFinalScriptWitness().asScriptChunks(); + if(witnessChunks.get(witnessChunks.size() - 1).isScript()) { + return witnessChunks.get(witnessChunks.size() - 1).getScript(); + } + } else { + return null; } } diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 5126e34..8ca205e 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -5,4 +5,5 @@ module com.sparrowwallet.drongo { exports com.sparrowwallet.drongo.psbt; exports com.sparrowwallet.drongo.protocol; exports com.sparrowwallet.drongo.address; + exports com.sparrowwallet.drongo.crypto; } \ No newline at end of file diff --git a/src/test/java/com/sparrowwallet/drongo/psbt/PSBTTest.java b/src/test/java/com/sparrowwallet/drongo/psbt/PSBTTest.java index bd65492..a6851ce 100644 --- a/src/test/java/com/sparrowwallet/drongo/psbt/PSBTTest.java +++ b/src/test/java/com/sparrowwallet/drongo/psbt/PSBTTest.java @@ -284,6 +284,6 @@ public class PSBTTest { Assert.assertEquals("00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae", psbt1.getPsbtInputs().get(0).getFinalScriptSig().getProgramAsHex()); Assert.assertEquals("2200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903", psbt1.getPsbtInputs().get(1).getFinalScriptSig().getProgramAsHex()); - Assert.assertEquals("0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae", psbt1.getPsbtInputs().get(1).getFinalScriptWitness().getProgramAsHex()); + Assert.assertEquals("0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae", psbt1.getPsbtInputs().get(1).getFinalScriptWitness().toByteArray()); } }