Skip to content

Commit 6b4f231

Browse files
committed
Move transaction combining from signrawtransaction to new RPC
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.
1 parent 0b01935 commit 6b4f231

File tree

4 files changed

+154
-55
lines changed

4 files changed

+154
-55
lines changed

src/rpc/client.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
9595
{ "signrawtransaction", 1, "prevtxs" },
9696
{ "signrawtransaction", 2, "privkeys" },
9797
{ "sendrawtransaction", 1, "allowhighfees" },
98+
{ "combinerawtransaction", 0, "txs" },
9899
{ "fundrawtransaction", 1, "options" },
99100
{ "gettxout", 1, "n" },
100101
{ "gettxout", 2, "include_mempool" },

src/rpc/rawtransaction.cpp

Lines changed: 100 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,93 @@ static void TxInErrorToJSON(const CTxIn& txin, UniValue& vErrorsRet, const std::
554554
vErrorsRet.push_back(entry);
555555
}
556556

557+
UniValue combinerawtransaction(const JSONRPCRequest& request)
558+
{
559+
560+
if (request.fHelp || request.params.size() != 1)
561+
throw std::runtime_error(
562+
"combinerawtransaction [\"hexstring\",...]\n"
563+
"\nCombine multiple partially signed transactions into one transaction.\n"
564+
"The combined transaction may be another partially signed transaction or a \n"
565+
"fully signed transaction."
566+
567+
"\nArguments:\n"
568+
"1. \"txs\" (string) A json array of hex strings of partially signed transactions\n"
569+
" [\n"
570+
" \"hexstring\" (string) A transaction hash\n"
571+
" ,...\n"
572+
" ]\n"
573+
574+
"\nResult:\n"
575+
"\"hex\" : \"value\", (string) The hex-encoded raw transaction with signature(s)\n"
576+
577+
"\nExamples:\n"
578+
+ HelpExampleCli("combinerawtransaction", "[\"myhex1\", \"myhex2\", \"myhex3\"]")
579+
);
580+
581+
582+
UniValue txs = request.params[0].get_array();
583+
std::vector<CMutableTransaction> txVariants(txs.size());
584+
585+
for (unsigned int idx = 0; idx < txs.size(); idx++) {
586+
if (!DecodeHexTx(txVariants[idx], txs[idx].get_str(), true)) {
587+
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, strprintf("TX decode failed for tx %d", idx));
588+
}
589+
}
590+
591+
if (txVariants.empty()) {
592+
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "Missing transactions");
593+
}
594+
595+
// mergedTx will end up with all the signatures; it
596+
// starts as a clone of the rawtx:
597+
CMutableTransaction mergedTx(txVariants[0]);
598+
599+
// Fetch previous transactions (inputs):
600+
CCoinsView viewDummy;
601+
CCoinsViewCache view(&viewDummy);
602+
{
603+
LOCK(cs_main);
604+
LOCK(mempool.cs);
605+
CCoinsViewCache &viewChain = *pcoinsTip;
606+
CCoinsViewMemPool viewMempool(&viewChain, mempool);
607+
view.SetBackend(viewMempool); // temporarily switch cache backend to db+mempool view
608+
609+
for (const CTxIn& txin : mergedTx.vin) {
610+
view.AccessCoin(txin.prevout); // Load entries from viewChain into view; can fail.
611+
}
612+
613+
view.SetBackend(viewDummy); // switch back to avoid locking mempool for too long
614+
}
615+
616+
// Use CTransaction for the constant parts of the
617+
// transaction to avoid rehashing.
618+
const CTransaction txConst(mergedTx);
619+
// Sign what we can:
620+
for (unsigned int i = 0; i < mergedTx.vin.size(); i++) {
621+
CTxIn& txin = mergedTx.vin[i];
622+
const Coin& coin = view.AccessCoin(txin.prevout);
623+
if (coin.IsSpent()) {
624+
throw JSONRPCError(RPC_VERIFY_ERROR, "Input not found or already spent");
625+
}
626+
const CScript& prevPubKey = coin.out.scriptPubKey;
627+
const CAmount& amount = coin.out.nValue;
628+
629+
SignatureData sigdata;
630+
631+
// ... and merge in other signatures:
632+
for (const CMutableTransaction& txv : txVariants) {
633+
if (txv.vin.size() > i) {
634+
sigdata = CombineSignatures(prevPubKey, TransactionSignatureChecker(&txConst, i, amount), sigdata, DataFromTransaction(txv, i));
635+
}
636+
}
637+
638+
UpdateTransaction(mergedTx, i, sigdata);
639+
}
640+
641+
return EncodeHexTx(mergedTx);
642+
}
643+
557644
UniValue signrawtransaction(const JSONRPCRequest& request)
558645
{
559646
#ifdef ENABLE_WALLET
@@ -626,26 +713,9 @@ UniValue signrawtransaction(const JSONRPCRequest& request)
626713
#endif
627714
RPCTypeCheck(request.params, {UniValue::VSTR, UniValue::VARR, UniValue::VARR, UniValue::VSTR}, true);
628715

629-
std::vector<unsigned char> txData(ParseHexV(request.params[0], "argument 1"));
630-
CDataStream ssData(txData, SER_NETWORK, PROTOCOL_VERSION);
631-
std::vector<CMutableTransaction> txVariants;
632-
while (!ssData.empty()) {
633-
try {
634-
CMutableTransaction tx;
635-
ssData >> tx;
636-
txVariants.push_back(tx);
637-
}
638-
catch (const std::exception&) {
639-
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "TX decode failed");
640-
}
641-
}
642-
643-
if (txVariants.empty())
644-
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "Missing transaction");
645-
646-
// mergedTx will end up with all the signatures; it
647-
// starts as a clone of the rawtx:
648-
CMutableTransaction mergedTx(txVariants[0]);
716+
CMutableTransaction mtx;
717+
if (!DecodeHexTx(mtx, request.params[0].get_str(), true))
718+
throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "TX decode failed");
649719

650720
// Fetch previous transactions (inputs):
651721
CCoinsView viewDummy;
@@ -656,7 +726,7 @@ UniValue signrawtransaction(const JSONRPCRequest& request)
656726
CCoinsViewMemPool viewMempool(&viewChain, mempool);
657727
view.SetBackend(viewMempool); // temporarily switch cache backend to db+mempool view
658728

659-
for (const CTxIn& txin : mergedTx.vin) {
729+
for (const CTxIn& txin : mtx.vin) {
660730
view.AccessCoin(txin.prevout); // Load entries from viewChain into view; can fail.
661731
}
662732

@@ -781,10 +851,10 @@ UniValue signrawtransaction(const JSONRPCRequest& request)
781851

782852
// Use CTransaction for the constant parts of the
783853
// transaction to avoid rehashing.
784-
const CTransaction txConst(mergedTx);
854+
const CTransaction txConst(mtx);
785855
// Sign what we can:
786-
for (unsigned int i = 0; i < mergedTx.vin.size(); i++) {
787-
CTxIn& txin = mergedTx.vin[i];
856+
for (unsigned int i = 0; i < mtx.vin.size(); i++) {
857+
CTxIn& txin = mtx.vin[i];
788858
const Coin& coin = view.AccessCoin(txin.prevout);
789859
if (coin.IsSpent()) {
790860
TxInErrorToJSON(txin, vErrors, "Input not found or already spent");
@@ -795,17 +865,11 @@ UniValue signrawtransaction(const JSONRPCRequest& request)
795865

796866
SignatureData sigdata;
797867
// Only sign SIGHASH_SINGLE if there's a corresponding output:
798-
if (!fHashSingle || (i < mergedTx.vout.size()))
799-
ProduceSignature(MutableTransactionSignatureCreator(&keystore, &mergedTx, i, amount, nHashType), prevPubKey, sigdata);
868+
if (!fHashSingle || (i < mtx.vout.size()))
869+
ProduceSignature(MutableTransactionSignatureCreator(&keystore, &mtx, i, amount, nHashType), prevPubKey, sigdata);
870+
sigdata = CombineSignatures(prevPubKey, TransactionSignatureChecker(&txConst, i, amount), sigdata, DataFromTransaction(mtx, i));
800871

801-
// ... and merge in other signatures:
802-
for (const CMutableTransaction& txv : txVariants) {
803-
if (txv.vin.size() > i) {
804-
sigdata = CombineSignatures(prevPubKey, TransactionSignatureChecker(&txConst, i, amount), sigdata, DataFromTransaction(txv, i));
805-
}
806-
}
807-
808-
UpdateTransaction(mergedTx, i, sigdata);
872+
UpdateTransaction(mtx, i, sigdata);
809873

810874
ScriptError serror = SCRIPT_ERR_OK;
811875
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)
815879
bool fComplete = vErrors.empty();
816880

817881
UniValue result(UniValue::VOBJ);
818-
result.push_back(Pair("hex", EncodeHexTx(mergedTx)));
882+
result.push_back(Pair("hex", EncodeHexTx(mtx)));
819883
result.push_back(Pair("complete", fComplete));
820884
if (!vErrors.empty()) {
821885
result.push_back(Pair("errors", vErrors));
@@ -905,6 +969,7 @@ static const CRPCCommand commands[] =
905969
{ "rawtransactions", "decoderawtransaction", &decoderawtransaction, true, {"hexstring"} },
906970
{ "rawtransactions", "decodescript", &decodescript, true, {"hexstring"} },
907971
{ "rawtransactions", "sendrawtransaction", &sendrawtransaction, false, {"hexstring","allowhighfees"} },
972+
{ "rawtransactions", "combinerawtransaction", &combinerawtransaction, true, {"txs"} },
908973
{ "rawtransactions", "signrawtransaction", &signrawtransaction, false, {"hexstring","prevtxs","privkeys","sighashtype"} }, /* uses wallet if enabled */
909974

910975
{ "blockchain", "gettxoutproof", &gettxoutproof, true, {"txids", "blockhash"} },

test/functional/rawtransactions.py

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ def run_test(self):
114114
rawTx = self.nodes[2].createrawtransaction(inputs, outputs)
115115
rawTxPartialSigned = self.nodes[1].signrawtransaction(rawTx, inputs)
116116
assert_equal(rawTxPartialSigned['complete'], False) #node1 only has one key, can't comp. sign the tx
117-
117+
118118
rawTxSigned = self.nodes[2].signrawtransaction(rawTx, inputs)
119119
assert_equal(rawTxSigned['complete'], True) #node2 can sign the tx compl., own two of three keys
120120
self.nodes[2].sendrawtransaction(rawTxSigned['hex'])
@@ -124,6 +124,55 @@ def run_test(self):
124124
self.sync_all()
125125
assert_equal(self.nodes[0].getbalance(), bal+Decimal('50.00000000')+Decimal('2.19000000')) #block reward + tx
126126

127+
# 2of2 test for combining transactions
128+
bal = self.nodes[2].getbalance()
129+
addr1 = self.nodes[1].getnewaddress()
130+
addr2 = self.nodes[2].getnewaddress()
131+
132+
addr1Obj = self.nodes[1].validateaddress(addr1)
133+
addr2Obj = self.nodes[2].validateaddress(addr2)
134+
135+
self.nodes[1].addmultisigaddress(2, [addr1Obj['pubkey'], addr2Obj['pubkey']])
136+
mSigObj = self.nodes[2].addmultisigaddress(2, [addr1Obj['pubkey'], addr2Obj['pubkey']])
137+
mSigObjValid = self.nodes[2].validateaddress(mSigObj)
138+
139+
txId = self.nodes[0].sendtoaddress(mSigObj, 2.2)
140+
decTx = self.nodes[0].gettransaction(txId)
141+
rawTx2 = self.nodes[0].decoderawtransaction(decTx['hex'])
142+
self.sync_all()
143+
self.nodes[0].generate(1)
144+
self.sync_all()
145+
146+
assert_equal(self.nodes[2].getbalance(), bal) # the funds of a 2of2 multisig tx should not be marked as spendable
147+
148+
txDetails = self.nodes[0].gettransaction(txId, True)
149+
rawTx2 = self.nodes[0].decoderawtransaction(txDetails['hex'])
150+
vout = False
151+
for outpoint in rawTx2['vout']:
152+
if outpoint['value'] == Decimal('2.20000000'):
153+
vout = outpoint
154+
break
155+
156+
bal = self.nodes[0].getbalance()
157+
inputs = [{ "txid" : txId, "vout" : vout['n'], "scriptPubKey" : vout['scriptPubKey']['hex'], "redeemScript" : mSigObjValid['hex']}]
158+
outputs = { self.nodes[0].getnewaddress() : 2.19 }
159+
rawTx2 = self.nodes[2].createrawtransaction(inputs, outputs)
160+
rawTxPartialSigned1 = self.nodes[1].signrawtransaction(rawTx2, inputs)
161+
self.log.info(rawTxPartialSigned1)
162+
assert_equal(rawTxPartialSigned['complete'], False) #node1 only has one key, can't comp. sign the tx
163+
164+
rawTxPartialSigned2 = self.nodes[2].signrawtransaction(rawTx2, inputs)
165+
self.log.info(rawTxPartialSigned2)
166+
assert_equal(rawTxPartialSigned2['complete'], False) #node2 only has one key, can't comp. sign the tx
167+
rawTxComb = self.nodes[2].combinerawtransaction([rawTxPartialSigned1['hex'], rawTxPartialSigned2['hex']])
168+
self.log.info(rawTxComb)
169+
self.nodes[2].sendrawtransaction(rawTxComb)
170+
rawTx2 = self.nodes[0].decoderawtransaction(rawTxComb)
171+
self.sync_all()
172+
self.nodes[0].generate(1)
173+
self.sync_all()
174+
assert_equal(self.nodes[0].getbalance(), bal+Decimal('50.00000000')+Decimal('2.19000000')) #block reward + tx
175+
127176
# getrawtransaction tests
128177
# 1. valid parameters - only supply txid
129178
txHash = rawTx["hash"]
@@ -156,17 +205,17 @@ def run_test(self):
156205
rawtx = self.nodes[0].createrawtransaction(inputs, outputs)
157206
decrawtx= self.nodes[0].decoderawtransaction(rawtx)
158207
assert_equal(decrawtx['vin'][0]['sequence'], 1000)
159-
208+
160209
# 9. invalid parameters - sequence number out of range
161210
inputs = [ {'txid' : "1d1d4e24ed99057e84c3f80fd8fbec79ed9e1acee37da269356ecea000000000", 'vout' : 1, 'sequence' : -1}]
162211
outputs = { self.nodes[0].getnewaddress() : 1 }
163212
assert_raises_jsonrpc(-8, 'Invalid parameter, sequence number is out of range', self.nodes[0].createrawtransaction, inputs, outputs)
164-
213+
165214
# 10. invalid parameters - sequence number out of range
166215
inputs = [ {'txid' : "1d1d4e24ed99057e84c3f80fd8fbec79ed9e1acee37da269356ecea000000000", 'vout' : 1, 'sequence' : 4294967296}]
167216
outputs = { self.nodes[0].getnewaddress() : 1 }
168217
assert_raises_jsonrpc(-8, 'Invalid parameter, sequence number is out of range', self.nodes[0].createrawtransaction, inputs, outputs)
169-
218+
170219
inputs = [ {'txid' : "1d1d4e24ed99057e84c3f80fd8fbec79ed9e1acee37da269356ecea000000000", 'vout' : 1, 'sequence' : 4294967294}]
171220
outputs = { self.nodes[0].getnewaddress() : 1 }
172221
rawtx = self.nodes[0].createrawtransaction(inputs, outputs)

test/functional/signrawtransactions.py

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,22 +43,6 @@ def successful_signing_test(self):
4343
# 2) No script verification error occurred
4444
assert 'errors' not in rawTxSigned
4545

46-
# Check that signrawtransaction doesn't blow up on garbage merge attempts
47-
dummyTxInconsistent = self.nodes[0].createrawtransaction([inputs[0]], outputs)
48-
rawTxUnsigned = self.nodes[0].signrawtransaction(rawTx + dummyTxInconsistent, inputs)
49-
50-
assert 'complete' in rawTxUnsigned
51-
assert_equal(rawTxUnsigned['complete'], False)
52-
53-
# Check that signrawtransaction properly merges unsigned and signed txn, even with garbage in the middle
54-
rawTxSigned2 = self.nodes[0].signrawtransaction(rawTxUnsigned["hex"] + dummyTxInconsistent + rawTxSigned["hex"], inputs)
55-
56-
assert 'complete' in rawTxSigned2
57-
assert_equal(rawTxSigned2['complete'], True)
58-
59-
assert 'errors' not in rawTxSigned2
60-
61-
6246
def script_verification_error_test(self):
6347
"""Create and sign a raw transaction with valid (vin 0), invalid (vin 1) and one missing (vin 2) input script.
6448

0 commit comments

Comments
 (0)