Merge #10571: [RPC]Move transaction combining from signrawtransaction to new RPC
6b4f231
Move transaction combining from signrawtransaction to new RPC (Andrew Chow)
Pull request description:
Create a combinerawtransaction RPC which accepts a json array of hex raw transactions to combine them into one transaction. Signrawtransaction is changed to no longer combine transactions and only accept one transaction at a time.
The tests have been updated to test this. Tests for the signrawtransaction merge have also been removed.
This is part of #10570
Tree-SHA512: 035aebbd6537c1c017d5c8e06d309228b4c23fe52d5b31ffde19741c81a11a6346ddbbdc582b77b02a47f4c22b1952b69d3c2ee1109c29b3f0f1b612d8de53ed
This commit is contained in:
commit
adf170daf9
4 changed files with 154 additions and 55 deletions
|
@ -95,6 +95,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
|
|||
{ "signrawtransaction", 1, "prevtxs" },
|
||||
{ "signrawtransaction", 2, "privkeys" },
|
||||
{ "sendrawtransaction", 1, "allowhighfees" },
|
||||
{ "combinerawtransaction", 0, "txs" },
|
||||
{ "fundrawtransaction", 1, "options" },
|
||||
{ "gettxout", 1, "n" },
|
||||
{ "gettxout", 2, "include_mempool" },
|
||||
|
|
|
@ -554,6 +554,93 @@ static void TxInErrorToJSON(const CTxIn& txin, UniValue& vErrorsRet, const std::
|
|||
vErrorsRet.push_back(entry);
|
||||
}
|
||||
|
||||
UniValue combinerawtransaction(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<CMutableTransaction> txVariants(txs.size());
|
||||
|
||||
for (unsigned int idx = 0; idx < txs.size(); idx++) {
|
||||
if (!DecodeHexTx(txVariants[idx], txs[idx].get_str(), true)) {
|
||||
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);
|
||||
view.SetBackend(viewMempool); // temporarily switch cache backend to db+mempool view
|
||||
|
||||
for (const CTxIn& txin : mergedTx.vin) {
|
||||
view.AccessCoin(txin.prevout); // Load entries from viewChain into view; can fail.
|
||||
}
|
||||
|
||||
view.SetBackend(viewDummy); // switch back to avoid locking mempool for too long
|
||||
}
|
||||
|
||||
// Use CTransaction for the constant parts of the
|
||||
// transaction to avoid rehashing.
|
||||
const CTransaction txConst(mergedTx);
|
||||
// Sign what we can:
|
||||
for (unsigned int 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.out.scriptPubKey;
|
||||
const CAmount& amount = coin.out.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(mergedTx);
|
||||
}
|
||||
|
||||
UniValue signrawtransaction(const JSONRPCRequest& request)
|
||||
{
|
||||
#ifdef ENABLE_WALLET
|
||||
|
@ -626,26 +713,9 @@ UniValue signrawtransaction(const JSONRPCRequest& request)
|
|||
#endif
|
||||
RPCTypeCheck(request.params, {UniValue::VSTR, UniValue::VARR, UniValue::VARR, UniValue::VSTR}, true);
|
||||
|
||||
std::vector<unsigned char> txData(ParseHexV(request.params[0], "argument 1"));
|
||||
CDataStream ssData(txData, SER_NETWORK, PROTOCOL_VERSION);
|
||||
std::vector<CMutableTransaction> 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");
|
||||
|
||||
// mergedTx will end up with all the signatures; it
|
||||
// starts as a clone of the rawtx:
|
||||
CMutableTransaction mergedTx(txVariants[0]);
|
||||
CMutableTransaction mtx;
|
||||
if (!DecodeHexTx(mtx, request.params[0].get_str(), true))
|
||||
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "TX decode failed");
|
||||
|
||||
// Fetch previous transactions (inputs):
|
||||
CCoinsView viewDummy;
|
||||
|
@ -656,7 +726,7 @@ UniValue signrawtransaction(const JSONRPCRequest& request)
|
|||
CCoinsViewMemPool viewMempool(&viewChain, mempool);
|
||||
view.SetBackend(viewMempool); // temporarily switch cache backend to db+mempool view
|
||||
|
||||
for (const CTxIn& txin : mergedTx.vin) {
|
||||
for (const CTxIn& txin : mtx.vin) {
|
||||
view.AccessCoin(txin.prevout); // Load entries from viewChain into view; can fail.
|
||||
}
|
||||
|
||||
|
@ -781,10 +851,10 @@ UniValue signrawtransaction(const JSONRPCRequest& request)
|
|||
|
||||
// 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 (unsigned int i = 0; i < mergedTx.vin.size(); i++) {
|
||||
CTxIn& txin = mergedTx.vin[i];
|
||||
for (unsigned int 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");
|
||||
|
@ -795,17 +865,11 @@ UniValue signrawtransaction(const JSONRPCRequest& request)
|
|||
|
||||
SignatureData sigdata;
|
||||
// Only sign SIGHASH_SINGLE if there's a corresponding output:
|
||||
if (!fHashSingle || (i < mergedTx.vout.size()))
|
||||
ProduceSignature(MutableTransactionSignatureCreator(&keystore, &mergedTx, i, amount, nHashType), prevPubKey, sigdata);
|
||||
if (!fHashSingle || (i < mtx.vout.size()))
|
||||
ProduceSignature(MutableTransactionSignatureCreator(&keystore, &mtx, i, amount, nHashType), 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(txin.scriptSig, prevPubKey, &txin.scriptWitness, STANDARD_SCRIPT_VERIFY_FLAGS, TransactionSignatureChecker(&txConst, i, amount), &serror)) {
|
||||
|
@ -815,7 +879,7 @@ UniValue signrawtransaction(const JSONRPCRequest& request)
|
|||
bool fComplete = vErrors.empty();
|
||||
|
||||
UniValue result(UniValue::VOBJ);
|
||||
result.push_back(Pair("hex", EncodeHexTx(mergedTx)));
|
||||
result.push_back(Pair("hex", EncodeHexTx(mtx)));
|
||||
result.push_back(Pair("complete", fComplete));
|
||||
if (!vErrors.empty()) {
|
||||
result.push_back(Pair("errors", vErrors));
|
||||
|
@ -905,6 +969,7 @@ static const CRPCCommand commands[] =
|
|||
{ "rawtransactions", "decoderawtransaction", &decoderawtransaction, true, {"hexstring"} },
|
||||
{ "rawtransactions", "decodescript", &decodescript, true, {"hexstring"} },
|
||||
{ "rawtransactions", "sendrawtransaction", &sendrawtransaction, false, {"hexstring","allowhighfees"} },
|
||||
{ "rawtransactions", "combinerawtransaction", &combinerawtransaction, true, {"txs"} },
|
||||
{ "rawtransactions", "signrawtransaction", &signrawtransaction, false, {"hexstring","prevtxs","privkeys","sighashtype"} }, /* uses wallet if enabled */
|
||||
|
||||
{ "blockchain", "gettxoutproof", &gettxoutproof, true, {"txids", "blockhash"} },
|
||||
|
|
|
@ -114,7 +114,7 @@ class RawTransactionsTest(BitcoinTestFramework):
|
|||
rawTx = self.nodes[2].createrawtransaction(inputs, outputs)
|
||||
rawTxPartialSigned = self.nodes[1].signrawtransaction(rawTx, inputs)
|
||||
assert_equal(rawTxPartialSigned['complete'], False) #node1 only has one key, can't comp. sign the tx
|
||||
|
||||
|
||||
rawTxSigned = self.nodes[2].signrawtransaction(rawTx, inputs)
|
||||
assert_equal(rawTxSigned['complete'], True) #node2 can sign the tx compl., own two of three keys
|
||||
self.nodes[2].sendrawtransaction(rawTxSigned['hex'])
|
||||
|
@ -124,6 +124,55 @@ class RawTransactionsTest(BitcoinTestFramework):
|
|||
self.sync_all()
|
||||
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()
|
||||
|
||||
assert_equal(self.nodes[2].getbalance(), bal) # the funds of a 2of2 multisig tx should not be marked as spendable
|
||||
|
||||
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']}]
|
||||
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)
|
||||
assert_equal(rawTxPartialSigned['complete'], False) #node1 only has one key, can't comp. sign the tx
|
||||
|
||||
rawTxPartialSigned2 = self.nodes[2].signrawtransaction(rawTx2, inputs)
|
||||
self.log.info(rawTxPartialSigned2)
|
||||
assert_equal(rawTxPartialSigned2['complete'], False) #node2 only has one key, can't comp. sign the tx
|
||||
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"]
|
||||
|
@ -156,17 +205,17 @@ class RawTransactionsTest(BitcoinTestFramework):
|
|||
rawtx = self.nodes[0].createrawtransaction(inputs, outputs)
|
||||
decrawtx= self.nodes[0].decoderawtransaction(rawtx)
|
||||
assert_equal(decrawtx['vin'][0]['sequence'], 1000)
|
||||
|
||||
|
||||
# 9. invalid parameters - sequence number out of range
|
||||
inputs = [ {'txid' : "1d1d4e24ed99057e84c3f80fd8fbec79ed9e1acee37da269356ecea000000000", 'vout' : 1, 'sequence' : -1}]
|
||||
outputs = { self.nodes[0].getnewaddress() : 1 }
|
||||
assert_raises_jsonrpc(-8, 'Invalid parameter, sequence number is out of range', self.nodes[0].createrawtransaction, inputs, outputs)
|
||||
|
||||
|
||||
# 10. invalid parameters - sequence number out of range
|
||||
inputs = [ {'txid' : "1d1d4e24ed99057e84c3f80fd8fbec79ed9e1acee37da269356ecea000000000", 'vout' : 1, 'sequence' : 4294967296}]
|
||||
outputs = { self.nodes[0].getnewaddress() : 1 }
|
||||
assert_raises_jsonrpc(-8, 'Invalid parameter, sequence number is out of range', self.nodes[0].createrawtransaction, inputs, outputs)
|
||||
|
||||
|
||||
inputs = [ {'txid' : "1d1d4e24ed99057e84c3f80fd8fbec79ed9e1acee37da269356ecea000000000", 'vout' : 1, 'sequence' : 4294967294}]
|
||||
outputs = { self.nodes[0].getnewaddress() : 1 }
|
||||
rawtx = self.nodes[0].createrawtransaction(inputs, outputs)
|
||||
|
|
|
@ -43,22 +43,6 @@ class SignRawTransactionsTest(BitcoinTestFramework):
|
|||
# 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):
|
||||
"""Create and sign a raw transaction with valid (vin 0), invalid (vin 1) and one missing (vin 2) input script.
|
||||
|
||||
|
|
Loading…
Reference in a new issue