diff --git a/doc/release-notes.md b/doc/release-notes.md --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -16,3 +16,4 @@ - Backport getblock RPC's new verbosity mode from bitcoin core for retrieving all transactions of a given block in full. - Added 'parked' state to getchaintips RPC - RPC `listreceivedbyaddress` now accepts an address filter + - Backport combinerawtransaction RPC from bitcoin core to combine multiple partially signed transactions into one transaction. \ No newline at end of file diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -87,6 +87,7 @@ {"signrawtransaction", 1, "prevtxs"}, {"signrawtransaction", 2, "privkeys"}, {"sendrawtransaction", 1, "allowhighfees"}, + {"combinerawtransaction", 0, "txs"}, {"fundrawtransaction", 1, "options"}, {"gettxout", 1, "n"}, {"gettxout", 2, "include_mempool"}, diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -724,6 +724,105 @@ vErrorsRet.push_back(entry); } +UniValue combinerawtransaction(const Config &config, + const JSONRPCRequest &request) { + + if (request.fHelp || request.params.size() != 1) { + throw std::runtime_error( + "combinerawtransaction [\"hexstring\",...]\n" + "\nCombine multiple partially signed transactions into one " + "transaction.\n" + "The combined transaction may be another partially signed " + "transaction or a \n" + "fully signed transaction." + + "\nArguments:\n" + "1. \"txs\" (string) A json array of hex strings of " + "partially signed transactions\n" + " [\n" + " \"hexstring\" (string) A transaction hash\n" + " ,...\n" + " ]\n" + + "\nResult:\n" + "\"hex\" : \"value\", (string) The hex-encoded raw " + "transaction with signature(s)\n" + + "\nExamples:\n" + + HelpExampleCli("combinerawtransaction", + "[\"myhex1\", \"myhex2\", \"myhex3\"]")); + } + + UniValue txs = request.params[0].get_array(); + std::vector txVariants(txs.size()); + + for (unsigned int idx = 0; idx < txs.size(); idx++) { + if (!DecodeHexTx(txVariants[idx], txs[idx].get_str())) { + throw JSONRPCError(RPC_DESERIALIZATION_ERROR, + strprintf("TX decode failed for tx %d", idx)); + } + } + + if (txVariants.empty()) { + throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "Missing transactions"); + } + + // mergedTx will end up with all the signatures; it + // starts as a clone of the rawtx: + CMutableTransaction mergedTx(txVariants[0]); + + // Fetch previous transactions (inputs): + CCoinsView viewDummy; + CCoinsViewCache view(&viewDummy); + { + LOCK(cs_main); + LOCK(mempool.cs); + CCoinsViewCache &viewChain = *pcoinsTip; + CCoinsViewMemPool viewMempool(&viewChain, mempool); + // temporarily switch cache backend to db+mempool view + view.SetBackend(viewMempool); + + for (const CTxIn &txin : mergedTx.vin) { + // Load entries from viewChain into view; can fail. + view.AccessCoin(txin.prevout); + } + + // switch back to avoid locking mempool for too long + view.SetBackend(viewDummy); + } + + // Use CTransaction for the constant parts of the + // transaction to avoid rehashing. + const CTransaction txConst(mergedTx); + // Sign what we can: + for (size_t i = 0; i < mergedTx.vin.size(); i++) { + CTxIn &txin = mergedTx.vin[i]; + const Coin &coin = view.AccessCoin(txin.prevout); + if (coin.IsSpent()) { + throw JSONRPCError(RPC_VERIFY_ERROR, + "Input not found or already spent"); + } + const CScript &prevPubKey = coin.GetTxOut().scriptPubKey; + const Amount &amount = coin.GetTxOut().nValue; + + SignatureData sigdata; + + // ... and merge in other signatures: + for (const CMutableTransaction &txv : txVariants) { + if (txv.vin.size() > i) { + sigdata = CombineSignatures( + prevPubKey, + TransactionSignatureChecker(&txConst, i, amount), sigdata, + DataFromTransaction(txv, i)); + } + } + + UpdateTransaction(mergedTx, i, sigdata); + } + + return EncodeHexTx(CTransaction(mergedTx)); +} + static UniValue signrawtransaction(const Config &config, const JSONRPCRequest &request) { #ifdef ENABLE_WALLET @@ -833,27 +932,11 @@ request.params, {UniValue::VSTR, UniValue::VARR, UniValue::VARR, UniValue::VSTR}, true); - std::vector txData(ParseHexV(request.params[0], "argument 1")); - CDataStream ssData(txData, SER_NETWORK, PROTOCOL_VERSION); - std::vector txVariants; - while (!ssData.empty()) { - try { - CMutableTransaction tx; - ssData >> tx; - txVariants.push_back(tx); - } catch (const std::exception &) { - throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "TX decode failed"); - } - } - - if (txVariants.empty()) { - throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "Missing transaction"); + CMutableTransaction mtx; + if (!DecodeHexTx(mtx, request.params[0].get_str())) { + throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "TX decode failed"); } - // mergedTx will end up with all the signatures; it starts as a clone of the - // rawtx: - CMutableTransaction mergedTx(txVariants[0]); - // Fetch previous transactions (inputs): CCoinsView viewDummy; CCoinsViewCache view(&viewDummy); @@ -864,7 +947,7 @@ // Temporarily switch cache backend to db+mempool view. view.SetBackend(viewMempool); - for (const CTxIn &txin : mergedTx.vin) { + for (const CTxIn &txin : mtx.vin) { // Load entries from viewChain into view; can fail. view.AccessCoin(txin.prevout); } @@ -1035,10 +1118,10 @@ // Use CTransaction for the constant parts of the transaction to avoid // rehashing. - const CTransaction txConst(mergedTx); + const CTransaction txConst(mtx); // Sign what we can: - for (size_t i = 0; i < mergedTx.vin.size(); i++) { - CTxIn &txin = mergedTx.vin[i]; + for (size_t i = 0; i < mtx.vin.size(); i++) { + CTxIn &txin = mtx.vin[i]; const Coin &coin = view.AccessCoin(txin.prevout); if (coin.IsSpent()) { TxInErrorToJSON(txin, vErrors, "Input not found or already spent"); @@ -1051,23 +1134,16 @@ SignatureData sigdata; // Only sign SIGHASH_SINGLE if there's a corresponding output: if ((sigHashType.getBaseType() != BaseSigHashType::SINGLE) || - (i < mergedTx.vout.size())) { + (i < mtx.vout.size())) { ProduceSignature(MutableTransactionSignatureCreator( - &keystore, &mergedTx, i, amount, sigHashType), + &keystore, &mtx, i, amount, sigHashType), prevPubKey, sigdata); } + sigdata = CombineSignatures( + prevPubKey, TransactionSignatureChecker(&txConst, i, amount), + sigdata, DataFromTransaction(mtx, i)); - // ... and merge in other signatures: - for (const CMutableTransaction &txv : txVariants) { - if (txv.vin.size() > i) { - sigdata = CombineSignatures( - prevPubKey, - TransactionSignatureChecker(&txConst, i, amount), sigdata, - DataFromTransaction(txv, i)); - } - } - - UpdateTransaction(mergedTx, i, sigdata); + UpdateTransaction(mtx, i, sigdata); ScriptError serror = SCRIPT_ERR_OK; if (!VerifyScript( @@ -1080,7 +1156,7 @@ bool fComplete = vErrors.empty(); UniValue result(UniValue::VOBJ); - result.pushKV("hex", EncodeHexTx(CTransaction(mergedTx))); + result.pushKV("hex", EncodeHexTx(CTransaction(mtx))); result.pushKV("complete", fComplete); if (!vErrors.empty()) { result.pushKV("errors", vErrors); @@ -1191,6 +1267,7 @@ { "rawtransactions", "decoderawtransaction", decoderawtransaction, {"hexstring"} }, { "rawtransactions", "decodescript", decodescript, {"hexstring"} }, { "rawtransactions", "sendrawtransaction", sendrawtransaction, {"hexstring","allowhighfees"} }, + { "rawtransactions", "combinerawtransaction", combinerawtransaction, {"txs"} }, { "rawtransactions", "signrawtransaction", signrawtransaction, {"hexstring","prevtxs","privkeys","sighashtype"} }, /* uses wallet if enabled */ { "blockchain", "gettxoutproof", gettxoutproof, {"txids", "blockhash"} }, diff --git a/test/functional/rawtransactions.py b/test/functional/rawtransactions.py --- a/test/functional/rawtransactions.py +++ b/test/functional/rawtransactions.py @@ -143,6 +143,63 @@ assert_equal(self.nodes[0].getbalance(), bal + Decimal( '50.00000000') + Decimal('2.19000000')) # block reward + tx + # 2of2 test for combining transactions + bal = self.nodes[2].getbalance() + addr1 = self.nodes[1].getnewaddress() + addr2 = self.nodes[2].getnewaddress() + + addr1Obj = self.nodes[1].validateaddress(addr1) + addr2Obj = self.nodes[2].validateaddress(addr2) + + self.nodes[1].addmultisigaddress( + 2, [addr1Obj['pubkey'], addr2Obj['pubkey']]) + mSigObj = self.nodes[2].addmultisigaddress( + 2, [addr1Obj['pubkey'], addr2Obj['pubkey']]) + mSigObjValid = self.nodes[2].validateaddress(mSigObj) + + txId = self.nodes[0].sendtoaddress(mSigObj, 2.2) + decTx = self.nodes[0].gettransaction(txId) + rawTx2 = self.nodes[0].decoderawtransaction(decTx['hex']) + self.sync_all() + self.nodes[0].generate(1) + self.sync_all() + + # the funds of a 2of2 multisig tx should not be marked as spendable + assert_equal(self.nodes[2].getbalance(), bal) + + txDetails = self.nodes[0].gettransaction(txId, True) + rawTx2 = self.nodes[0].decoderawtransaction(txDetails['hex']) + vout = False + for outpoint in rawTx2['vout']: + if outpoint['value'] == Decimal('2.20000000'): + vout = outpoint + break + + bal = self.nodes[0].getbalance() + inputs = [{"txid": txId, "vout": vout['n'], "scriptPubKey": vout['scriptPubKey'] + ['hex'], "redeemScript": mSigObjValid['hex'], "amount": vout['value']}] + outputs = {self.nodes[0].getnewaddress(): 2.19} + rawTx2 = self.nodes[2].createrawtransaction(inputs, outputs) + rawTxPartialSigned1 = self.nodes[1].signrawtransaction(rawTx2, inputs) + self.log.info(rawTxPartialSigned1) + # node1 only has one key, can't comp. sign the tx + assert_equal(rawTxPartialSigned['complete'], False) + + rawTxPartialSigned2 = self.nodes[2].signrawtransaction(rawTx2, inputs) + self.log.info(rawTxPartialSigned2) + # node2 only has one key, can't comp. sign the tx + assert_equal(rawTxPartialSigned2['complete'], False) + rawTxComb = self.nodes[2].combinerawtransaction( + [rawTxPartialSigned1['hex'], rawTxPartialSigned2['hex']]) + self.log.info(rawTxComb) + self.nodes[2].sendrawtransaction(rawTxComb) + rawTx2 = self.nodes[0].decoderawtransaction(rawTxComb) + self.sync_all() + self.nodes[0].generate(1) + self.sync_all() + assert_equal(self.nodes[0].getbalance( + ), bal+Decimal('50.00000000')+Decimal('2.19000000')) # block reward + tx + # getrawtransaction tests # 1. valid parameters - only supply txid txHash = rawTx["hash"] diff --git a/test/functional/signrawtransactions.py b/test/functional/signrawtransactions.py --- a/test/functional/signrawtransactions.py +++ b/test/functional/signrawtransactions.py @@ -44,26 +44,6 @@ # 2) No script verification error occurred assert 'errors' not in rawTxSigned - # Check that signrawtransaction doesn't blow up on garbage merge - # attempts - dummyTxInconsistent = self.nodes[ - 0].createrawtransaction([inputs[0]], outputs) - rawTxUnsigned = self.nodes[0].signrawtransaction( - rawTx + dummyTxInconsistent, inputs) - - assert 'complete' in rawTxUnsigned - assert_equal(rawTxUnsigned['complete'], False) - - # Check that signrawtransaction properly merges unsigned and signed - # txn, even with garbage in the middle - rawTxSigned2 = self.nodes[0].signrawtransaction( - rawTxUnsigned["hex"] + dummyTxInconsistent + rawTxSigned["hex"], inputs) - - assert 'complete' in rawTxSigned2 - assert_equal(rawTxSigned2['complete'], True) - - assert 'errors' not in rawTxSigned2 - def script_verification_error_test(self): """Creates and signs a raw transaction with valid (vin 0), invalid (vin 1) and one missing (vin 2) input script.