psbt and scripting improvments

This commit is contained in:
Craig Raw 2020-04-09 14:35:50 +02:00
parent 9d15c27bfd
commit c8226ea947
9 changed files with 135 additions and 44 deletions

View file

@ -142,7 +142,7 @@ public class Script {
*/ */
public Address[] getToAddresses() throws NonStandardScriptException { public Address[] getToAddresses() throws NonStandardScriptException {
if (ScriptPattern.isP2PK(this)) 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)) else if (ScriptPattern.isP2PKH(this))
return new Address[] { new P2PKHAddress( ScriptPattern.extractHashFromP2PKH(this)) }; return new Address[] { new P2PKHAddress( ScriptPattern.extractHashFromP2PKH(this)) };
else if (ScriptPattern.isP2SH(this)) else if (ScriptPattern.isP2SH(this))
@ -179,6 +179,17 @@ public class Script {
return null; return null;
} }
public List<TransactionSignature> getSignatures() {
List<TransactionSignature> signatures = new ArrayList<>();
for(ScriptChunk chunk : chunks) {
if(chunk.isSignature()) {
signatures.add(chunk.getSignature());
}
}
return signatures;
}
public static int decodeFromOpN(int opcode) { public static int decodeFromOpN(int opcode) {
if((opcode != OP_0 && opcode != OP_1NEGATE) && (opcode < OP_1 || opcode > OP_16)) { if((opcode != OP_0 && opcode != OP_1NEGATE) && (opcode < OP_1 || opcode > OP_16)) {
throw new ProtocolException("decodeFromOpN called on non OP_N opcode: " + opcode); throw new ProtocolException("decodeFromOpN called on non OP_N opcode: " + opcode);
@ -271,7 +282,7 @@ public class Script {
if(chunk.isSignature()) { if(chunk.isSignature()) {
builder.append("<signature").append(signatureCount++).append(">"); builder.append("<signature").append(signatureCount++).append(">");
} else if(chunk.isScript()) { } else if(chunk.isScript()) {
Script nestedScript = new Script(chunk.getData()); Script nestedScript = chunk.getScript();
if(ScriptPattern.isP2WPKH(nestedScript)) { if(ScriptPattern.isP2WPKH(nestedScript)) {
builder.append("(OP_0 <wpkh>)"); builder.append("(OP_0 <wpkh>)");
} else if(ScriptPattern.isP2WSH(nestedScript)) { } else if(ScriptPattern.isP2WSH(nestedScript)) {

View file

@ -96,6 +96,14 @@ public class ScriptChunk {
return true; 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() { public boolean isScript() {
if(data == null || data.length == 0) { if(data == null || data.length == 0) {
return false; return false;
@ -110,6 +118,10 @@ public class ScriptChunk {
return true; return true;
} }
public Script getScript() {
return new Script(data);
}
public boolean isPubKey() { public boolean isPubKey() {
if(data == null || data.length == 0) { if(data == null || data.length == 0) {
return false; return false;
@ -118,6 +130,10 @@ public class ScriptChunk {
return ECKey.isPubKey(data); return ECKey.isPubKey(data);
} }
public ECKey getPubKey() {
return ECKey.fromPublicOnly(data);
}
public byte[] toByteArray() { public byte[] toByteArray() {
ByteArrayOutputStream stream = new ByteArrayOutputStream(); ByteArrayOutputStream stream = new ByteArrayOutputStream();
try { try {

View file

@ -2,6 +2,7 @@ package com.sparrowwallet.drongo.protocol;
import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.P2PKAddress; import com.sparrowwallet.drongo.address.P2PKAddress;
import com.sparrowwallet.drongo.crypto.ECKey;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; 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 * 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)}. * will want to guard calls to this method with {@link #isP2PK(Script)}.
*/ */
public static byte[] extractPKFromP2PK(Script script) { public static ECKey extractPKFromP2PK(Script script) {
return script.chunks.get(0).data; return ECKey.fromPublicOnly(script.chunks.get(0).data);
} }
/** /**
@ -106,6 +107,14 @@ public class ScriptPattern {
return true; 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: * Returns whether this script matches the format used for multisig outputs:
* {@code [n] [keys...] [m] CHECKMULTISIG} * {@code [n] [keys...] [m] CHECKMULTISIG}
@ -150,14 +159,6 @@ public class ScriptPattern {
return addresses.toArray(new Address[addresses.size()]); 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 <hash[20]>}. This is a P2WPKH scriptPubKey. * Returns true if this script is of the form {@code OP_0 <hash[20]>}. This is a P2WPKH scriptPubKey.
*/ */

View file

@ -165,7 +165,9 @@ public class Transaction extends TransactionPart {
// script_witnesses // script_witnesses
if (useSegwit) { if (useSegwit) {
for (TransactionInput in : inputs) { for (TransactionInput in : inputs) {
in.getWitness().bitcoinSerializeToStream(stream); if (in.hasWitness()) {
in.getWitness().bitcoinSerializeToStream(stream);
}
} }
} }
// lock_time // lock_time
@ -226,14 +228,9 @@ public class Transaction extends TransactionPart {
private void parseWitnesses() { private void parseWitnesses() {
int numWitnesses = inputs.size(); int numWitnesses = inputs.size();
for (int i = 0; i < numWitnesses; i++) { for (int i = 0; i < numWitnesses; i++) {
long pushCount = readVarInt(); TransactionWitness witness = new TransactionWitness(this, rawtx, cursor);
TransactionWitness witness = new TransactionWitness((int) pushCount);
inputs.get(i).setWitness(witness); inputs.get(i).setWitness(witness);
for (int y = 0; y < pushCount; y++) { cursor += witness.getLength();
long pushSize = readVarInt();
byte[] push = readBytes((int) pushSize);
witness.setPush(y, push);
}
} }
} }
@ -261,7 +258,9 @@ public class Transaction extends TransactionPart {
// script_witnesses // script_witnesses
if(isSegwit()) { if(isSegwit()) {
for (TransactionInput in : inputs) { for (TransactionInput in : inputs) {
wu += in.getWitness().getLength(); if (in.hasWitness()) {
wu += in.getWitness().getLength();
}
} }
} }
// lock_time // lock_time

View file

@ -66,7 +66,7 @@ public class TransactionInput extends TransactionPart {
} }
public TransactionWitness getWitness() { public TransactionWitness getWitness() {
return witness != null ? witness : TransactionWitness.EMPTY; return witness;
} }
public void setWitness(TransactionWitness witness) { public void setWitness(TransactionWitness witness) {

View file

@ -1,8 +1,8 @@
package com.sparrowwallet.drongo.protocol; package com.sparrowwallet.drongo.protocol;
import com.sparrowwallet.drongo.Utils;
import org.bouncycastle.util.encoders.Hex; import org.bouncycastle.util.encoders.Hex;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.ArrayList; import java.util.ArrayList;
@ -10,20 +10,35 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
public class TransactionWitness { public class TransactionWitness extends TransactionPart {
public static final TransactionWitness EMPTY = new TransactionWitness(0); private List<byte[]> pushes;
private final List<byte[]> pushes; public TransactionWitness(Transaction parent, byte[] rawtx, int offset) {
super(rawtx, offset);
setParent(parent);
if(pushes == null) {
pushes = new ArrayList<>();
}
}
public TransactionWitness(int pushCount) { protected void parse() throws ProtocolException {
pushes = new ArrayList<>(Math.min(pushCount, Utils.MAX_INITIAL_ARRAY_LENGTH)); long pushCount = readVarInt();
for (int y = 0; y < pushCount; y++) {
long pushSize = readVarInt();
byte[] push = readBytes((int)pushSize);
setPush(y, push);
}
} }
public List<byte[]> getPushes() { public List<byte[]> getPushes() {
return Collections.unmodifiableList(pushes); 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()) { while (i >= pushes.size()) {
pushes.add(new byte[]{}); 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 @Override
public String toString() { public String toString() {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
@ -80,6 +104,27 @@ public class TransactionWitness {
return scriptChunks; return scriptChunks;
} }
public List<TransactionSignature> getSignatures() {
List<TransactionSignature> signatures = new ArrayList<>();
List<ScriptChunk> scriptChunks = this.asScriptChunks();
for(ScriptChunk chunk : scriptChunks) {
if(chunk.isSignature()) {
signatures.add(chunk.getSignature());
}
}
return signatures;
}
public Script getWitnessScript() {
List<ScriptChunk> scriptChunks = this.asScriptChunks();
if(scriptChunks.get(scriptChunks.size() - 1).isScript()) {
return scriptChunks.get(scriptChunks.size() - 1).getScript();
}
return null;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View file

@ -11,10 +11,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Arrays; import java.util.*;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static com.sparrowwallet.drongo.psbt.PSBTEntry.parseKeyDerivation; import static com.sparrowwallet.drongo.psbt.PSBTEntry.parseKeyDerivation;
@ -39,7 +36,7 @@ public class PSBTInput {
private Script witnessScript; private Script witnessScript;
private Map<ECKey, KeyDerivation> derivedPublicKeys = new LinkedHashMap<>(); private Map<ECKey, KeyDerivation> derivedPublicKeys = new LinkedHashMap<>();
private Script finalScriptSig; private Script finalScriptSig;
private Script finalScriptWitness; private TransactionWitness finalScriptWitness;
private String porCommitment; private String porCommitment;
private Map<String, String> proprietary = new LinkedHashMap<>(); private Map<String, String> proprietary = new LinkedHashMap<>();
@ -162,9 +159,9 @@ public class PSBTInput {
break; break;
case PSBT_IN_FINAL_SCRIPTWITNESS: case PSBT_IN_FINAL_SCRIPTWITNESS:
entry.checkOneByteKey(); entry.checkOneByteKey();
Script finalScriptWitness = new Script(entry.getData()); TransactionWitness finalScriptWitness = new TransactionWitness(null, entry.getData(), 0);
this.finalScriptWitness = finalScriptWitness; 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; break;
case PSBT_IN_POR_COMMITMENT: case PSBT_IN_POR_COMMITMENT:
entry.checkOneByteKey(); entry.checkOneByteKey();
@ -217,7 +214,7 @@ public class PSBTInput {
return finalScriptSig; return finalScriptSig;
} }
public Script getFinalScriptWitness() { public TransactionWitness getFinalScriptWitness() {
return finalScriptWitness; return finalScriptWitness;
} }
@ -229,6 +226,18 @@ public class PSBTInput {
return partialSignatures; return partialSignatures;
} }
public ECKey getKeyForSignature(TransactionSignature signature) {
if(partialSignatures != null) {
for(Map.Entry<ECKey, TransactionSignature> entry : partialSignatures.entrySet()) {
if(entry.getValue().equals(signature)) {
return entry.getKey();
}
}
}
return null;
}
public Map<ECKey, KeyDerivation> getDerivedPublicKeys() { public Map<ECKey, KeyDerivation> getDerivedPublicKeys() {
return derivedPublicKeys; return derivedPublicKeys;
} }
@ -263,6 +272,8 @@ public class PSBTInput {
} }
} }
//TODO: Implement Bitcoin Script engine to verify finalScriptSig and finalScriptWitness
return true; return true;
} }
} }
@ -275,10 +286,12 @@ public class PSBTInput {
Script signingScript = getNonWitnessUtxo() != null ? getNonWitnessUtxo().getOutputs().get(vout).getScript() : getWitnessUtxo().getScript(); Script signingScript = getNonWitnessUtxo() != null ? getNonWitnessUtxo().getOutputs().get(vout).getScript() : getWitnessUtxo().getScript();
if(ScriptPattern.isP2SH(signingScript)) { if(ScriptPattern.isP2SH(signingScript)) {
if(getRedeemScript() == null) { if(getRedeemScript() != null) {
return null;
} else {
signingScript = getRedeemScript(); 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()); Address address = new P2PKHAddress(signingScript.getPubKeyHash());
signingScript = address.getOutputScript(); signingScript = address.getOutputScript();
} else if(ScriptPattern.isP2WSH(signingScript)) { } else if(ScriptPattern.isP2WSH(signingScript)) {
if(getWitnessScript() == null) { if(getWitnessScript() != null) {
return null;
} else {
signingScript = getWitnessScript(); signingScript = getWitnessScript();
} else if(getFinalScriptWitness() != null) {
List<ScriptChunk> witnessChunks = getFinalScriptWitness().asScriptChunks();
if(witnessChunks.get(witnessChunks.size() - 1).isScript()) {
return witnessChunks.get(witnessChunks.size() - 1).getScript();
}
} else {
return null;
} }
} }

View file

@ -5,4 +5,5 @@ module com.sparrowwallet.drongo {
exports com.sparrowwallet.drongo.psbt; exports com.sparrowwallet.drongo.psbt;
exports com.sparrowwallet.drongo.protocol; exports com.sparrowwallet.drongo.protocol;
exports com.sparrowwallet.drongo.address; exports com.sparrowwallet.drongo.address;
exports com.sparrowwallet.drongo.crypto;
} }

View file

@ -284,6 +284,6 @@ public class PSBTTest {
Assert.assertEquals("00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae", psbt1.getPsbtInputs().get(0).getFinalScriptSig().getProgramAsHex()); Assert.assertEquals("00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae", psbt1.getPsbtInputs().get(0).getFinalScriptSig().getProgramAsHex());
Assert.assertEquals("2200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903", psbt1.getPsbtInputs().get(1).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());
} }
} }