diff --git a/src/node/transaction.h b/src/node/transaction.h --- a/src/node/transaction.h +++ b/src/node/transaction.h @@ -20,6 +20,7 @@ MEMPOOL_REJECTED, MEMPOOL_ERROR, INVALID_PSBT, + PSBT_MISMATCH, SIGHASH_MISMATCH, ERROR_COUNT diff --git a/src/node/transaction.cpp b/src/node/transaction.cpp --- a/src/node/transaction.cpp +++ b/src/node/transaction.cpp @@ -31,6 +31,8 @@ return "AcceptToMemoryPool failed"; case TransactionError::INVALID_PSBT: return "PSBT is not sane"; + case TransactionError::PSBT_MISMATCH: + return "PSBTs not compatible (different transactions)"; case TransactionError::SIGHASH_MISMATCH: return "Specified sighash value does not match existing value"; diff --git a/src/psbt.h b/src/psbt.h --- a/src/psbt.h +++ b/src/psbt.h @@ -479,4 +479,38 @@ PartiallySignedTransaction &psbt, int index, SigHashType sighash = SigHashType()); +/** + * Finalizes a PSBT if possible, combining partial signatures. + * + * @param[in,out] &psbtx reference to PartiallySignedTransaction to finalize + * return True if the PSBT is now complete, false otherwise + */ +bool FinalizePSBT(PartiallySignedTransaction &psbtx); + +/** + * Finalizes a PSBT if possible, and extracts it to a CMutableTransaction if it + * could be finalized. + * + * @param[in] &psbtx reference to PartiallySignedTransaction + * @param[out] result CMutableTransaction representing the complete transaction, + * if successful + * @return True if we successfully extracted the transaction, false otherwise + */ +bool FinalizeAndExtractPSBT(PartiallySignedTransaction &psbtx, + CMutableTransaction &result); + +/** + * Combines PSBTs with the same underlying transaction, resulting in a single + * PSBT with all partial signatures from each input. + * + * @param[out] &out the combined PSBT, if successful + * @param[out] &error reference to TransactionError to fill with error info on + * failure + * @param[in] psbtxs the PSBTs to combine + * @return True if we successfully combined the transactions, false if they were + * not compatible + */ +bool CombinePSBTs(PartiallySignedTransaction &out, TransactionError &error, + const std::vector &psbtxs); + #endif // BITCOIN_PSBT_H diff --git a/src/psbt.cpp b/src/psbt.cpp --- a/src/psbt.cpp +++ b/src/psbt.cpp @@ -176,3 +176,54 @@ return sig_complete; } + +bool FinalizePSBT(PartiallySignedTransaction &psbtx) { + // Finalize input signatures -- in case we have partial signatures that add + // up to a complete + // signature, but have not combined them yet (e.g. because the combiner + // that created this PartiallySignedTransaction did not understand them), + // this will combine them into a final script. + bool complete = true; + for (size_t i = 0; i < psbtx.tx->vin.size(); ++i) { + complete &= + SignPSBTInput(DUMMY_SIGNING_PROVIDER, psbtx, i, SigHashType()); + } + + return complete; +} + +bool FinalizeAndExtractPSBT(PartiallySignedTransaction &psbtx, + CMutableTransaction &result) { + // It's not safe to extract a PSBT that isn't finalized, and there's no easy + // way to check + // whether a PSBT is finalized without finalizing it, so we just do this. + if (!FinalizePSBT(psbtx)) { + return false; + } + + result = *psbtx.tx; + for (size_t i = 0; i < result.vin.size(); ++i) { + result.vin[i].scriptSig = psbtx.inputs[i].final_script_sig; + } + return true; +} + +bool CombinePSBTs(PartiallySignedTransaction &out, TransactionError &error, + const std::vector &psbtxs) { + // Copy the first one + out = psbtxs[0]; + + // Merge + for (auto it = std::next(psbtxs.begin()); it != psbtxs.end(); ++it) { + if (!out.Merge(*it)) { + error = TransactionError::PSBT_MISMATCH; + return false; + } + } + if (!out.IsSane()) { + error = TransactionError::INVALID_PSBT; + return false; + } + + return true; +} diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -1725,19 +1725,10 @@ psbtxs.push_back(psbtx); } - // Copy the first one - PartiallySignedTransaction merged_psbt(psbtxs[0]); - - // Merge - for (auto it = std::next(psbtxs.begin()); it != psbtxs.end(); ++it) { - if (!merged_psbt.Merge(*it)) { - throw JSONRPCError(RPC_INVALID_PARAMETER, - "PSBTs do not refer to the same transactions."); - } - } - if (!merged_psbt.IsSane()) { - throw JSONRPCError(RPC_INVALID_PARAMETER, - "Merged PSBT is inconsistent"); + PartiallySignedTransaction merged_psbt; + TransactionError error; + if (!CombinePSBTs(merged_psbt, error, psbtxs)) { + throw JSONRPCTransactionError(error); } UniValue result(UniValue::VOBJ); @@ -1795,31 +1786,24 @@ strprintf("TX decode failed %s", error)); } - // Finalize input signatures -- in case we have partial signatures that add - // up to a complete - // signature, but have not combined them yet (e.g. because the combiner - // that created this PartiallySignedTransaction did not understand them), - // this will combine them into a final script. - bool complete = true; - for (size_t i = 0; i < psbtx.tx->vin.size(); ++i) { - complete &= - SignPSBTInput(DUMMY_SIGNING_PROVIDER, psbtx, i, SigHashType()); - } + bool extract = request.params[1].isNull() || (!request.params[1].isNull() && + request.params[1].get_bool()); + + CMutableTransaction mtx; + bool complete = FinalizeAndExtractPSBT(psbtx, mtx); UniValue result(UniValue::VOBJ); CDataStream ssTx(SER_NETWORK, PROTOCOL_VERSION); - bool extract = request.params[1].isNull() || (!request.params[1].isNull() && - request.params[1].get_bool()); + std::string result_str; + if (complete && extract) { - CMutableTransaction mtx(*psbtx.tx); - for (size_t i = 0; i < mtx.vin.size(); ++i) { - mtx.vin[i].scriptSig = psbtx.inputs[i].final_script_sig; - } ssTx << mtx; - result.pushKV("hex", HexStr(ssTx.str())); + result_str = HexStr(ssTx.str()); + result.pushKV("hex", result_str); } else { ssTx << psbtx; - result.pushKV("psbt", EncodeBase64(ssTx.str())); + result_str = EncodeBase64(ssTx.str()); + result.pushKV("psbt", result_str); } result.pushKV("complete", complete); diff --git a/src/rpc/util.cpp b/src/rpc/util.cpp --- a/src/rpc/util.cpp +++ b/src/rpc/util.cpp @@ -124,6 +124,8 @@ case TransactionError::P2P_DISABLED: return RPC_CLIENT_P2P_DISABLED; case TransactionError::INVALID_PSBT: + case TransactionError::PSBT_MISMATCH: + return RPC_INVALID_PARAMETER; case TransactionError::SIGHASH_MISMATCH: return RPC_DESERIALIZATION_ERROR; default: