Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java
Original file line number Diff line number Diff line change
Expand Up @@ -2021,6 +2021,49 @@ public List<SilentPayment> computeSilentPaymentOutputs(PSBT psbt, Map<PSBTInput,
return results;
}

/**
* Drops the signatures a quorum has no room for, keeping the ones that opt in.
*
* More signers than the threshold needs leaves a choice, and taking them in key order made it by accident: a 2 of
* 3 whose marked device signs last kept the two that do not opt in, and the transaction went out carrying no
* replay protection having had it. The signature that opts in is the one worth keeping, since one of them protects
* the whole transaction. Only the values are cleared, so every key stays in the map for the script, and the order
* of what is left is untouched, because CHECKMULTISIG requires signatures in the order of the keys.
*/
static void retainThreshold(Map<ECKey, TransactionSignature> pubKeySignatures, int threshold, PSBTInput psbtInput) {
List<ECKey> present = pubKeySignatures.entrySet().stream().filter(entry -> entry.getValue() != null)
.map(Map.Entry::getKey).collect(Collectors.toList());
if(present.size() <= threshold) {
return;
}

//By the pair, not by the signature. A signature does not carry the key that made it and compares by hash type
//and by r and s, so asking whether it is among the verified ones answers yes for a second key that files a
//copy of it: both then took a slot, and the one whose key does not verify it does not spend. Where nothing can
//be checked this finds none preferred and falls back to key order, which is what it did before.
Map<ECKey, TransactionSignature> verified = psbtInput.getVerifiedPartialSignatures(pubKeySignatures.keySet());

List<ECKey> keep = new ArrayList<>();
for(ECKey pubKey : present) {
TransactionSignature signature = pubKeySignatures.get(pubKey);
if(keep.size() < threshold && (signature.sighashFlags & SigHash.UNIFIED_FLAG) != 0
&& signature.equals(verified.get(pubKey))) {
keep.add(pubKey);
}
}
for(ECKey pubKey : present) {
if(keep.size() < threshold && !keep.contains(pubKey)) {
keep.add(pubKey);
}
}

for(ECKey pubKey : present) {
if(!keep.contains(pubKey)) {
pubKeySignatures.put(pubKey, null);
}
}
}

public void finalise(PSBT psbt) {
int threshold = getDefaultPolicy().getNumSignaturesRequired();
Map<PSBTInput, WalletNode> signingNodes = getSigningNodes(psbt);
Expand Down Expand Up @@ -2076,6 +2119,7 @@ public int getIndex() {
throw new IllegalArgumentException("Pubkeys of partial signatures do not match wallet pubkeys");
}

retainThreshold(pubKeySignatures, threshold, psbtInput);
finalizedTxInput = signingWallet.getScriptType().addMultisigSpendingInput(signingWallet.getPolicyType(), transaction, utxo, threshold, pubKeySignatures);
} else {
throw new UnsupportedOperationException("Cannot finalise PSBT for policy type " + signingWallet.getPolicyType());
Expand Down Expand Up @@ -2125,6 +2169,7 @@ private void finaliseExternalInput(PSBTInput psbtInput, TransactionOutput utxo,
return;
}

retainThreshold(pubKeySignatures, inputThreshold, psbtInput);
finalizedTxInput = inputScriptType.addMultisigSpendingInput(PolicyType.MULTI_HD, transaction, utxo, inputThreshold, pubKeySignatures);
} else if(inputThreshold == 1) {
ECKey pubKey;
Expand Down
Loading