From 4c5166a6ea886622d57254444e95efa043ba302f Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 30 Jul 2020 15:57:50 +0200 Subject: [PATCH] signing fixes --- .../drongo/protocol/TransactionWitness.java | 5 +- .../sparrowwallet/drongo/psbt/PSBTInput.java | 27 +++++++++- .../drongo/wallet/FinalizingPSBTWallet.java | 52 +++++++++++++++++-- .../sparrowwallet/drongo/wallet/Wallet.java | 9 ++-- 4 files changed, 82 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/TransactionWitness.java b/src/main/java/com/sparrowwallet/drongo/protocol/TransactionWitness.java index fb4e796..34be7d6 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/TransactionWitness.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/TransactionWitness.java @@ -24,8 +24,9 @@ public class TransactionWitness extends ChildMessage { public TransactionWitness(Transaction transaction, List signatures, Script witnessScript) { setParent(transaction); this.pushes = new ArrayList<>(); + //If a multisig witness script, add a zero byte witness element to handle the multisig off by one bug if(ScriptType.MULTISIG.isScriptType(witnessScript)) { - pushes.add(new byte[] { ScriptOpCodes.OP_0 }); + pushes.add(new byte[0]); } for(TransactionSignature signature : signatures) { pushes.add(signature.encodeToBitcoin()); @@ -95,7 +96,7 @@ public class TransactionWitness extends ChildMessage { protected void bitcoinSerializeToStream(OutputStream stream) throws IOException { stream.write(new VarInt(pushes.size()).encode()); - for (int i = 0; i < pushes.size(); i++) { + for(int i = 0; i < pushes.size(); i++) { byte[] push = pushes.get(i); if(push.length == 1 && push[0] == 0) { stream.write(push); diff --git a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java index fa3d396..230ff2c 100644 --- a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java +++ b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java @@ -386,7 +386,7 @@ public class PSBTInput { //All partial sigs are already verified int reqSigs = getSigningScript().getNumRequiredSignatures(); int sigs = getPartialSignatures().size(); - return sigs == reqSigs; + return sigs >= reqSigs; } catch(NonStandardScriptException e) { return false; } @@ -457,6 +457,31 @@ public class PSBTInput { return false; } + public ScriptType getScriptType() { + Script signingScript = getUtxo().getScript(); + + boolean p2sh = false; + if(P2SH.isScriptType(signingScript)) { + p2sh = true; + + if(getRedeemScript() != null) { + signingScript = getRedeemScript(); + } else if(getFinalScriptSig() != null) { + signingScript = getFinalScriptSig().getFirstNestedScript(); + } else { + return null; + } + } + + if(P2WPKH.isScriptType(signingScript)) { + return p2sh ? P2SH_P2WPKH : P2WPKH; + } else if(P2WSH.isScriptType(signingScript)) { + return p2sh ? P2SH_P2WSH : P2WSH; + } + + return ScriptType.getType(signingScript); + } + public Script getSigningScript() { Script signingScript = getUtxo().getScript(); diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/FinalizingPSBTWallet.java b/src/main/java/com/sparrowwallet/drongo/wallet/FinalizingPSBTWallet.java index fd1c81c..07df1f1 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/FinalizingPSBTWallet.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/FinalizingPSBTWallet.java @@ -1,9 +1,12 @@ package com.sparrowwallet.drongo.wallet; +import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.policy.Miniscript; import com.sparrowwallet.drongo.policy.Policy; import com.sparrowwallet.drongo.policy.PolicyType; +import com.sparrowwallet.drongo.protocol.NonStandardScriptException; +import com.sparrowwallet.drongo.protocol.Script; import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.TransactionSignature; import com.sparrowwallet.drongo.psbt.PSBT; @@ -15,7 +18,7 @@ import java.util.stream.IntStream; /** * This is a special wallet that is used solely to finalize a fully signed PSBT by reading from the partial signatures and UTXO scriptPubKey - * + * It is used when the normal wallet is not available. */ public class FinalizingPSBTWallet extends Wallet { private final Map signedInputNodes = new LinkedHashMap<>(); @@ -25,15 +28,43 @@ public class FinalizingPSBTWallet extends Wallet { public FinalizingPSBTWallet(PSBT psbt) { super("Finalizing PSBT Wallet"); - Map signingNodes = new LinkedHashMap<>(); + if(!psbt.isSigned() || psbt.isFinalized()) { + throw new IllegalArgumentException("Only a fully signed, unfinalized PSBT can be used"); + } + + WalletNode purposeNode = getNode(KeyPurpose.RECEIVE); + List signedNodes = new ArrayList<>(purposeNode.getChildren()); + for(int i = 0; i < psbt.getPsbtInputs().size(); i++) { PSBTInput psbtInput = psbt.getPsbtInputs().get(i); Set keys = psbtInput.getPartialSignatures().keySet(); - WalletNode signedNode = new WalletNode("m/" + i); + + WalletNode signedNode = signedNodes.get(i); signedInputNodes.put(psbtInput, signedNode); signedNodeKeys.put(signedNode, new ArrayList<>(keys)); - numSignatures = keys.size(); - setScriptType(ScriptType.getType(psbtInput.getUtxo().getScript())); + + ScriptType scriptType = psbtInput.getScriptType(); + if(scriptType == null || (getScriptType() != null && scriptType.equals(getScriptType()))) { + throw new IllegalArgumentException("Cannot determine a single script type from the PSBT"); + } else { + setScriptType(scriptType); + } + + try { + Script signingScript = psbtInput.getSigningScript(); + int sigsRequired = signingScript.getNumRequiredSignatures(); + if(numSignatures > 0 && sigsRequired != numSignatures) { + throw new IllegalArgumentException("Different number of signatures required in PSBT inputs"); + } else { + numSignatures = sigsRequired; + } + + if(ScriptType.MULTISIG.isScriptType(signingScript)) { + signedNodeKeys.put(signedNode, Arrays.asList(ScriptType.MULTISIG.getPublicKeysFromScript(signingScript))); + } + } catch(NonStandardScriptException e) { + throw new IllegalArgumentException(e.getMessage()); + } } setPolicyType(numSignatures == 1 ? PolicyType.SINGLE : PolicyType.MULTI); @@ -73,4 +104,15 @@ public class FinalizingPSBTWallet extends Wallet { public List getPubKeys(WalletNode node) { return signedNodeKeys.get(node); } + + @Override + public Script getOutputScript(WalletNode node) { + for(Map.Entry entry : signedInputNodes.entrySet()) { + if(node.equals(entry.getValue())) { + return entry.getKey().getUtxo().getScript(); + } + } + + return new Script(new byte[10]); + } } \ No newline at end of file diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java b/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java index 7d90ff1..9e3f52b 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java @@ -637,7 +637,10 @@ public class Wallet { for(Map.Entry signingEntry : signingNodes.entrySet()) { ECKey privKey = keystore.getKey(signingEntry.getValue()); PSBTInput psbtInput = signingEntry.getKey(); - psbtInput.sign(privKey); + + if(!psbtInput.isSigned()) { + psbtInput.sign(privKey); + } } } } @@ -680,8 +683,8 @@ public class Wallet { finalizedTxInput = getScriptType().addSpendingInput(transaction, utxo, pubKey, transactionSignature); } else if(getPolicyType().equals(PolicyType.MULTI)) { List pubKeys = getPubKeys(signingNode); - List signatures = pubKeys.stream().map(psbtInput::getPartialSignature).collect(Collectors.toList()); - if(pubKeys.size() != signatures.size()) { + List signatures = pubKeys.stream().map(psbtInput::getPartialSignature).filter(Objects::nonNull).collect(Collectors.toList()); + if(signatures.size() < threshold) { throw new IllegalArgumentException("Pubkeys of partial signatures do not match wallet pubkeys"); }