fix transaction signature ordering bug

This commit is contained in:
Craig Raw 2020-08-10 13:19:24 +02:00
parent fff658a3ab
commit 1a38b8e662
4 changed files with 101 additions and 46 deletions

View file

@ -909,4 +909,26 @@ public class ECKey implements EncryptableItem {
public String toString() {
return pub.toString();
}
public static class LexicographicECKeyComparator implements Comparator<ECKey> {
@Override
public int compare(ECKey leftKey, ECKey rightKey) {
byte[] left = leftKey.getPubKey();
byte[] right = rightKey.getPubKey();
int minLength = Math.min(left.length, right.length);
for (int i = 0; i < minLength; i++) {
int result = compare(left[i], right[i]);
if (result != 0) {
return result;
}
}
return left.length - right.length;
}
public static int compare(byte a, byte b) {
return Byte.toUnsignedInt(a) - Byte.toUnsignedInt(b);
}
}
}

View file

@ -111,12 +111,12 @@ public enum ScriptType {
}
@Override
public Script getMultisigScriptSig(Script scriptPubKey, int threshold, List<ECKey> pubKeys, List<TransactionSignature> signatures) {
public Script getMultisigScriptSig(Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
throw new ProtocolException(getName() + " is not a multisig script type");
}
@Override
public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, List<ECKey> pubKeys, List<TransactionSignature> signatures) {
public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
throw new ProtocolException(getName() + " is not a multisig script type");
}
@ -219,12 +219,12 @@ public enum ScriptType {
}
@Override
public Script getMultisigScriptSig(Script scriptPubKey, int threshold, List<ECKey> pubKeys, List<TransactionSignature> signatures) {
public Script getMultisigScriptSig(Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
throw new ProtocolException(getName() + " is not a multisig script type");
}
@Override
public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, List<ECKey> pubKeys, List<TransactionSignature> signatures) {
public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
throw new ProtocolException(getName() + " is not a multisig script type");
}
@ -274,7 +274,7 @@ public enum ScriptType {
}
@Override
public Script getOutputScript(int threshold, List<ECKey> pubKeys) {
public Script getOutputScript(int threshold, Collection<ECKey> pubKeys) {
if(threshold > pubKeys.size()) {
throw new ProtocolException("Threshold of " + threshold + " is greater than number of pubKeys provided (" + pubKeys.size() + ")");
}
@ -382,11 +382,13 @@ public enum ScriptType {
}
@Override
public Script getMultisigScriptSig(Script scriptPubKey, int threshold, List<ECKey> pubKeys, List<TransactionSignature> signatures) {
public Script getMultisigScriptSig(Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
if(!isScriptType(scriptPubKey)) {
throw new ProtocolException("Provided scriptPubKey is not a " + getName() + " script");
}
if(threshold != signatures.size()) {
List<TransactionSignature> signatures = pubKeySignatures.values().stream().filter(Objects::nonNull).collect(Collectors.toList());
if(signatures.size() < threshold) {
throw new ProtocolException("Only " + signatures.size() + " signatures provided to meet a multisig threshold of " + threshold);
}
@ -402,8 +404,8 @@ public enum ScriptType {
}
@Override
public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, List<ECKey> pubKeys, List<TransactionSignature> signatures) {
Script scriptSig = getMultisigScriptSig(prevOutput.getScript(), threshold, pubKeys, signatures);
public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
Script scriptSig = getMultisigScriptSig(prevOutput.getScript(), threshold, pubKeySignatures);
return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig);
}
@ -503,17 +505,17 @@ public enum ScriptType {
}
@Override
public Script getMultisigScriptSig(Script scriptPubKey, int threshold, List<ECKey> pubKeys, List<TransactionSignature> signatures) {
public Script getMultisigScriptSig(Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
if(!isScriptType(scriptPubKey)) {
throw new ProtocolException("Provided scriptPubKey is not a " + getName() + " script");
}
Script redeemScript = MULTISIG.getOutputScript(threshold, pubKeys);
Script redeemScript = MULTISIG.getOutputScript(threshold, pubKeySignatures.keySet());
if(!scriptPubKey.equals(getOutputScript(redeemScript))) {
throw new ProtocolException("P2SH scriptPubKey hash does not match constructed redeem script hash");
}
Script multisigScript = MULTISIG.getMultisigScriptSig(redeemScript, threshold, pubKeys, signatures);
Script multisigScript = MULTISIG.getMultisigScriptSig(redeemScript, threshold, pubKeySignatures);
List<ScriptChunk> chunks = new ArrayList<>(multisigScript.getChunks());
ScriptChunk redeemScriptChunk = ScriptChunk.fromData(redeemScript.getProgram());
chunks.add(redeemScriptChunk);
@ -522,8 +524,8 @@ public enum ScriptType {
}
@Override
public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, List<ECKey> pubKeys, List<TransactionSignature> signatures) {
Script scriptSig = getMultisigScriptSig(prevOutput.getScript(), threshold, pubKeys, signatures);
public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
Script scriptSig = getMultisigScriptSig(prevOutput.getScript(), threshold, pubKeySignatures);
return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig);
}
@ -616,12 +618,12 @@ public enum ScriptType {
}
@Override
public Script getMultisigScriptSig(Script scriptPubKey, int threshold, List<ECKey> pubKeys, List<TransactionSignature> signatures) {
public Script getMultisigScriptSig(Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
throw new ProtocolException(getName() + " is not a multisig script type");
}
@Override
public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, List<ECKey> pubKeys, List<TransactionSignature> signatures) {
public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
throw new ProtocolException(getName() + " is not a multisig script type");
}
@ -698,12 +700,12 @@ public enum ScriptType {
}
@Override
public Script getMultisigScriptSig(Script scriptPubKey, int threshold, List<ECKey> pubKeys, List<TransactionSignature> signatures) {
public Script getMultisigScriptSig(Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
if(!isScriptType(scriptPubKey)) {
throw new ProtocolException("Provided scriptPubKey is not a " + getName() + " script");
}
Script witnessScript = MULTISIG.getOutputScript(threshold, pubKeys);
Script witnessScript = MULTISIG.getOutputScript(threshold, pubKeySignatures.keySet());
Script redeemScript = P2WSH.getOutputScript(witnessScript);
if(!scriptPubKey.equals(P2SH.getOutputScript(redeemScript))) {
throw new ProtocolException("P2SH scriptPubKey hash does not match constructed redeem script hash");
@ -714,10 +716,10 @@ public enum ScriptType {
}
@Override
public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, List<ECKey> pubKeys, List<TransactionSignature> signatures) {
Script scriptSig = getMultisigScriptSig(prevOutput.getScript(), threshold, pubKeys, signatures);
Script witnessScript = MULTISIG.getOutputScript(threshold, pubKeys);
TransactionWitness witness = new TransactionWitness(transaction, signatures, witnessScript);
public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
Script scriptSig = getMultisigScriptSig(prevOutput.getScript(), threshold, pubKeySignatures);
Script witnessScript = MULTISIG.getOutputScript(threshold, pubKeySignatures.keySet());
TransactionWitness witness = new TransactionWitness(transaction, pubKeySignatures.values().stream().filter(Objects::nonNull).collect(Collectors.toList()), witnessScript);
return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig, witness);
}
@ -812,12 +814,12 @@ public enum ScriptType {
}
@Override
public Script getMultisigScriptSig(Script scriptPubKey, int threshold, List<ECKey> pubKeys, List<TransactionSignature> signatures) {
public Script getMultisigScriptSig(Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
throw new ProtocolException(getName() + " is not a multisig script type");
}
@Override
public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, List<ECKey> pubKeys, List<TransactionSignature> signatures) {
public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
throw new ProtocolException(getName() + " is not a multisig script type");
}
@ -906,12 +908,12 @@ public enum ScriptType {
}
@Override
public Script getMultisigScriptSig(Script scriptPubKey, int threshold, List<ECKey> pubKeys, List<TransactionSignature> signatures) {
public Script getMultisigScriptSig(Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
if(!isScriptType(scriptPubKey)) {
throw new ProtocolException("Provided scriptPubKey is not a " + getName() + " script");
}
Script witnessScript = MULTISIG.getOutputScript(threshold, pubKeys);
Script witnessScript = MULTISIG.getOutputScript(threshold, pubKeySignatures.keySet());
if(!scriptPubKey.equals(P2WSH.getOutputScript(witnessScript))) {
throw new ProtocolException("P2WSH scriptPubKey hash does not match constructed witness script hash");
}
@ -920,10 +922,10 @@ public enum ScriptType {
}
@Override
public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, List<ECKey> pubKeys, List<TransactionSignature> signatures) {
Script scriptSig = getMultisigScriptSig(prevOutput.getScript(), threshold, pubKeys, signatures);
Script witnessScript = MULTISIG.getOutputScript(threshold, pubKeys);
TransactionWitness witness = new TransactionWitness(transaction, signatures, witnessScript);
public TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures) {
Script scriptSig = getMultisigScriptSig(prevOutput.getScript(), threshold, pubKeySignatures);
Script witnessScript = MULTISIG.getOutputScript(threshold, pubKeySignatures.keySet());
TransactionWitness witness = new TransactionWitness(transaction, pubKeySignatures.values().stream().filter(Objects::nonNull).collect(Collectors.toList()), witnessScript);
return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig, witness);
}
@ -993,7 +995,7 @@ public enum ScriptType {
public abstract Script getOutputScript(Script script);
public Script getOutputScript(int threshold, List<ECKey> pubKeys) {
public Script getOutputScript(int threshold, Collection<ECKey> pubKeys) {
throw new UnsupportedOperationException("Only defined for MULTISIG script type");
}
@ -1025,9 +1027,9 @@ public enum ScriptType {
public abstract TransactionInput addSpendingInput(Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature);
public abstract Script getMultisigScriptSig(Script scriptPubKey, int threshold, List<ECKey> pubKeys, List<TransactionSignature> signatures);
public abstract Script getMultisigScriptSig(Script scriptPubKey, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures);
public abstract TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, List<ECKey> pubKeys, List<TransactionSignature> signatures);
public abstract TransactionInput addMultisigSpendingInput(Transaction transaction, TransactionOutput prevOutput, int threshold, Map<ECKey, TransactionSignature> pubKeySignatures);
public static final ScriptType[] SINGLE_HASH_TYPES = {P2PKH, P2SH, P2SH_P2WPKH, P2SH_P2WSH, P2WPKH, P2WSH};

View file

@ -2,6 +2,7 @@ package com.sparrowwallet.drongo.wallet;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.crypto.Key;
@ -14,7 +15,6 @@ import com.sparrowwallet.drongo.psbt.PSBTInput;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static com.sparrowwallet.drongo.protocol.Transaction.WITNESS_SCALE_FACTOR;
@ -390,11 +390,11 @@ public class Wallet {
} else if(getPolicyType().equals(PolicyType.MULTI)) {
List<ECKey> pubKeys = getPubKeys(receiveNode);
int threshold = getDefaultPolicy().getNumSignaturesRequired();
List<TransactionSignature> signatures = new ArrayList<>(threshold);
for(int i = 0; i < threshold; i++) {
signatures.add(TransactionSignature.dummy());
Map<ECKey, TransactionSignature> pubKeySignatures = new TreeMap<>(new ECKey.LexicographicECKeyComparator());
for(int i = 0; i < pubKeys.size(); i++) {
pubKeySignatures.put(pubKeys.get(i), i < threshold ? TransactionSignature.dummy() : null);
}
txInput = getScriptType().addMultisigSpendingInput(transaction, prevTxOut, threshold, pubKeys, signatures);
txInput = getScriptType().addMultisigSpendingInput(transaction, prevTxOut, threshold, pubKeySignatures);
}
assert txInput != null;
@ -491,8 +491,11 @@ public class Wallet {
} else if(getPolicyType().equals(PolicyType.MULTI)) {
List<ECKey> pubKeys = getPubKeys(walletNode);
int threshold = getDefaultPolicy().getNumSignaturesRequired();
List<TransactionSignature> signatures = IntStream.range(0, threshold).mapToObj(i -> TransactionSignature.dummy()).collect(Collectors.toList());
return getScriptType().addMultisigSpendingInput(transaction, prevTxOut, threshold, pubKeys, signatures);
Map<ECKey, TransactionSignature> pubKeySignatures = new TreeMap<>(new ECKey.LexicographicECKeyComparator());
for(int i = 0; i < pubKeys.size(); i++) {
pubKeySignatures.put(pubKeys.get(i), i < threshold ? TransactionSignature.dummy() : null);
}
return getScriptType().addMultisigSpendingInput(transaction, prevTxOut, threshold, pubKeySignatures);
} else {
throw new UnsupportedOperationException("Cannot create transaction for policy type " + getPolicyType());
}
@ -647,6 +650,9 @@ public class Wallet {
}
public void finalise(PSBT psbt) {
System.out.println("Before finalise: ");
System.out.println(Utils.bytesToHex(psbt.serialize()));
int threshold = getDefaultPolicy().getNumSignaturesRequired();
Map<PSBTInput, WalletNode> signingNodes = getSigningNodes(psbt);
@ -683,12 +689,18 @@ public class Wallet {
finalizedTxInput = getScriptType().addSpendingInput(transaction, utxo, pubKey, transactionSignature);
} else if(getPolicyType().equals(PolicyType.MULTI)) {
List<ECKey> pubKeys = getPubKeys(signingNode);
List<TransactionSignature> signatures = pubKeys.stream().map(psbtInput::getPartialSignature).filter(Objects::nonNull).collect(Collectors.toList());
Map<ECKey, TransactionSignature> pubKeySignatures = new TreeMap<>(new ECKey.LexicographicECKeyComparator());
for(ECKey pubKey : pubKeys) {
pubKeySignatures.put(pubKey, psbtInput.getPartialSignature(pubKey));
}
List<TransactionSignature> signatures = pubKeySignatures.values().stream().filter(Objects::nonNull).collect(Collectors.toList());
if(signatures.size() < threshold) {
throw new IllegalArgumentException("Pubkeys of partial signatures do not match wallet pubkeys");
}
finalizedTxInput = getScriptType().addMultisigSpendingInput(transaction, utxo, threshold, pubKeys, signatures);
finalizedTxInput = getScriptType().addMultisigSpendingInput(transaction, utxo, threshold, pubKeySignatures);
} else {
throw new UnsupportedOperationException("Cannot finalise PSBT for policy type " + getPolicyType());
}
@ -698,6 +710,9 @@ public class Wallet {
psbtInput.clearFinalised();
}
}
System.out.println("After finalise: ");
System.out.println(Utils.bytesToHex(psbt.serialize()));
}
public BitcoinUnit getAutoUnit() {

View file

@ -9,7 +9,8 @@ import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
public class TransactionTest {
@Test
@ -302,8 +303,13 @@ public class TransactionTest {
ECKey key1 = redeemScript.getChunks().get(2).getPubKey();
ECKey key2 = redeemScript.getChunks().get(3).getPubKey();
Map<ECKey, TransactionSignature> pubKeySignatures = new TreeMap<>(new ECKey.LexicographicECKeyComparator());
pubKeySignatures.put(key0, signature0);
pubKeySignatures.put(key1, signature1);
pubKeySignatures.put(key2, null);
Transaction transaction = new Transaction();
spent0ScriptType.addMultisigSpendingInput(transaction, spent0Output, 2, List.of(key0, key1, key2), List.of(signature0, signature1));
spent0ScriptType.addMultisigSpendingInput(transaction, spent0Output, 2, pubKeySignatures);
transaction.addOutput(833300, Address.fromString("31mKrRn3xQoGppLY5dU92Dbm4kN4ddkknE"));
transaction.addOutput(1222480000, Address.fromString("1CL9kj1seXif6agPfeh6vpKkzc2Hxq1UpM"));
@ -376,9 +382,14 @@ public class TransactionTest {
ECKey key1 = witnessScript.getChunks().get(2).getPubKey();
ECKey key2 = witnessScript.getChunks().get(3).getPubKey();
Map<ECKey, TransactionSignature> pubKeySignatures = new TreeMap<>(new ECKey.LexicographicECKeyComparator());
pubKeySignatures.put(key0, signature0);
pubKeySignatures.put(key1, signature1);
pubKeySignatures.put(key2, null);
Transaction transaction = new Transaction();
transaction.setSegwitVersion(1);
TransactionInput input = ScriptType.P2SH_P2WSH.addMultisigSpendingInput(transaction, spent0Output, 2, List.of(key0, key1, key2), List.of(signature0, signature1));
TransactionInput input = ScriptType.P2SH_P2WSH.addMultisigSpendingInput(transaction, spent0Output, 2, pubKeySignatures);
transaction.addOutput(59287429, Address.fromString("3PBjKH4FRuEKy4sD3NfL7tqfZTG5K42owu"));
transaction.addOutput(212571, Address.fromString("3KRUgU4XGuErXkjBtFhksPzTGJ4AMwF4jB"));
@ -447,9 +458,14 @@ public class TransactionTest {
ECKey key1 = witnessScript.getChunks().get(2).getPubKey();
ECKey key2 = witnessScript.getChunks().get(3).getPubKey();
Map<ECKey, TransactionSignature> pubKeySignatures = new TreeMap<>(new ECKey.LexicographicECKeyComparator());
pubKeySignatures.put(key0, signature0);
pubKeySignatures.put(key1, signature1);
pubKeySignatures.put(key2, null);
Transaction transaction = new Transaction();
transaction.setSegwitVersion(1);
spent0ScriptType.addMultisigSpendingInput(transaction, spent0Output, 2, List.of(key0, key1, key2), List.of(signature0, signature1));
spent0ScriptType.addMultisigSpendingInput(transaction, spent0Output, 2, pubKeySignatures);
transaction.addOutput(10900000, Address.fromString("3Dt17mpd8FDXBjP56rCD7a4Sx7wpL91uhn"));
transaction.addOutput(332500000, Address.fromString("1K6igqzm36x8jxRTavPhgWXLVcVZVDTGc9"));