Skip to content

Commit 702e8b7

Browse files
committed
Merge #11872: [rpc] createrawtransaction: Accept sorted outputs
fac7013 rpc: Update createrawtransaction examples (MarcoFalke) fa06dfc [rpc] createrawtransaction: Accept sorted outputs (MarcoFalke) 8acd25d rpc: Allow typeAny in RPCTypeCheck (MarcoFalke) Pull request description: The second parameter of the `createrawtransaction` is a dictionary of the outputs. This comes with at least two drawbacks: * In case of duplicate keys, either of them might silently disappear, with no user feedback at all. A user needs to make other mistakes, but this could eventually lead to abnormal tx fees. * A dictionary does not guarantee that keys are sorted. Again, a user needs to keep this in mind, as it could eventually lead to excessive tx fees. Even though my scenario of loss-of-funds is unlikely to happen, I see it as a inconvenience that should be fixed. Tree-SHA512: cd562f34f7f9f79c7d3433805971325c388c2035611be283980f4049066a622df4f0afdc11d7ac96662260ec0115147cb65e1ab5268f5a1b063242f3fe425f77
2 parents 0f0229d + fac7013 commit 702e8b7

File tree

6 files changed

+111
-42
lines changed

6 files changed

+111
-42
lines changed

doc/release-notes.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ RPC changes
6161

6262
### Low-level changes
6363

64-
- The `fundrawtransaction` rpc will reject the previously deprecated `reserveChangeKey` option.
64+
- The `createrawtransaction` RPC will now accept an array or dictionary (kept for compatibility) for the `outputs` parameter. This means the order of transaction outputs can be specified by the client.
65+
- The `fundrawtransaction` RPC will reject the previously deprecated `reserveChangeKey` option.
6566

6667
External wallet files
6768
---------------------

src/rpc/rawtransaction.cpp

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -316,9 +316,10 @@ UniValue verifytxoutproof(const JSONRPCRequest& request)
316316

317317
UniValue createrawtransaction(const JSONRPCRequest& request)
318318
{
319-
if (request.fHelp || request.params.size() < 2 || request.params.size() > 4)
319+
if (request.fHelp || request.params.size() < 2 || request.params.size() > 4) {
320320
throw std::runtime_error(
321-
"createrawtransaction [{\"txid\":\"id\",\"vout\":n},...] {\"address\":amount,\"data\":\"hex\",...} ( locktime ) ( replaceable )\n"
321+
// clang-format off
322+
"createrawtransaction [{\"txid\":\"id\",\"vout\":n},...] [{\"address\":amount},{\"data\":\"hex\"},...] ( locktime ) ( replaceable )\n"
322323
"\nCreate a transaction spending the given inputs and creating new outputs.\n"
323324
"Outputs can be addresses or data.\n"
324325
"Returns hex-encoded raw transaction.\n"
@@ -329,37 +330,53 @@ UniValue createrawtransaction(const JSONRPCRequest& request)
329330
"1. \"inputs\" (array, required) A json array of json objects\n"
330331
" [\n"
331332
" {\n"
332-
" \"txid\":\"id\", (string, required) The transaction id\n"
333+
" \"txid\":\"id\", (string, required) The transaction id\n"
333334
" \"vout\":n, (numeric, required) The output number\n"
334335
" \"sequence\":n (numeric, optional) The sequence number\n"
335336
" } \n"
336337
" ,...\n"
337338
" ]\n"
338-
"2. \"outputs\" (object, required) a json object with outputs\n"
339+
"2. \"outputs\" (array, required) a json array with outputs (key-value pairs)\n"
340+
" [\n"
339341
" {\n"
340-
" \"address\": x.xxx, (numeric or string, required) The key is the bitcoin address, the numeric value (can be string) is the " + CURRENCY_UNIT + " amount\n"
341-
" \"data\": \"hex\" (string, required) The key is \"data\", the value is hex encoded data\n"
342-
" ,...\n"
342+
" \"address\": x.xxx, (obj, optional) A key-value pair. The key (string) is the bitcoin address, the value (float or string) is the amount in " + CURRENCY_UNIT + "\n"
343+
" },\n"
344+
" {\n"
345+
" \"data\": \"hex\" (obj, optional) A key-value pair. The key must be \"data\", the value is hex encoded data\n"
343346
" }\n"
347+
" ,... More key-value pairs of the above form. For compatibility reasons, a dictionary, which holds the key-value pairs directly, is also\n"
348+
" accepted as second parameter.\n"
349+
" ]\n"
344350
"3. locktime (numeric, optional, default=0) Raw locktime. Non-0 value also locktime-activates inputs\n"
345351
"4. replaceable (boolean, optional, default=false) Marks this transaction as BIP125 replaceable.\n"
346352
" Allows this transaction to be replaced by a transaction with higher fees. If provided, it is an error if explicit sequence numbers are incompatible.\n"
347353
"\nResult:\n"
348354
"\"transaction\" (string) hex string of the transaction\n"
349355

350356
"\nExamples:\n"
351-
+ HelpExampleCli("createrawtransaction", "\"[{\\\"txid\\\":\\\"myid\\\",\\\"vout\\\":0}]\" \"{\\\"address\\\":0.01}\"")
352-
+ HelpExampleCli("createrawtransaction", "\"[{\\\"txid\\\":\\\"myid\\\",\\\"vout\\\":0}]\" \"{\\\"data\\\":\\\"00010203\\\"}\"")
353-
+ HelpExampleRpc("createrawtransaction", "\"[{\\\"txid\\\":\\\"myid\\\",\\\"vout\\\":0}]\", \"{\\\"address\\\":0.01}\"")
354-
+ HelpExampleRpc("createrawtransaction", "\"[{\\\"txid\\\":\\\"myid\\\",\\\"vout\\\":0}]\", \"{\\\"data\\\":\\\"00010203\\\"}\"")
357+
+ HelpExampleCli("createrawtransaction", "\"[{\\\"txid\\\":\\\"myid\\\",\\\"vout\\\":0}]\" \"[{\\\"address\\\":0.01}]\"")
358+
+ HelpExampleCli("createrawtransaction", "\"[{\\\"txid\\\":\\\"myid\\\",\\\"vout\\\":0}]\" \"[{\\\"data\\\":\\\"00010203\\\"}]\"")
359+
+ HelpExampleRpc("createrawtransaction", "\"[{\\\"txid\\\":\\\"myid\\\",\\\"vout\\\":0}]\", \"[{\\\"address\\\":0.01}]\"")
360+
+ HelpExampleRpc("createrawtransaction", "\"[{\\\"txid\\\":\\\"myid\\\",\\\"vout\\\":0}]\", \"[{\\\"data\\\":\\\"00010203\\\"}]\"")
361+
// clang-format on
355362
);
363+
}
356364

357-
RPCTypeCheck(request.params, {UniValue::VARR, UniValue::VOBJ, UniValue::VNUM, UniValue::VBOOL}, true);
365+
RPCTypeCheck(request.params, {
366+
UniValue::VARR,
367+
UniValueType(), // ARR or OBJ, checked later
368+
UniValue::VNUM,
369+
UniValue::VBOOL
370+
}, true
371+
);
358372
if (request.params[0].isNull() || request.params[1].isNull())
359373
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, arguments 1 and 2 must be non-null");
360374

361375
UniValue inputs = request.params[0].get_array();
362-
UniValue sendTo = request.params[1].get_obj();
376+
const bool outputs_is_obj = request.params[1].isObject();
377+
UniValue outputs = outputs_is_obj ?
378+
request.params[1].get_obj() :
379+
request.params[1].get_array();
363380

364381
CMutableTransaction rawTx;
365382

@@ -411,11 +428,24 @@ UniValue createrawtransaction(const JSONRPCRequest& request)
411428
}
412429

413430
std::set<CTxDestination> destinations;
414-
std::vector<std::string> addrList = sendTo.getKeys();
415-
for (const std::string& name_ : addrList) {
416-
431+
if (!outputs_is_obj) {
432+
// Translate array of key-value pairs into dict
433+
UniValue outputs_dict = UniValue(UniValue::VOBJ);
434+
for (size_t i = 0; i < outputs.size(); ++i) {
435+
const UniValue& output = outputs[i];
436+
if (!output.isObject()) {
437+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, key-value pair not an object as expected");
438+
}
439+
if (output.size() != 1) {
440+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, key-value pair must contain exactly one key");
441+
}
442+
outputs_dict.pushKVs(output);
443+
}
444+
outputs = std::move(outputs_dict);
445+
}
446+
for (const std::string& name_ : outputs.getKeys()) {
417447
if (name_ == "data") {
418-
std::vector<unsigned char> data = ParseHexV(sendTo[name_].getValStr(),"Data");
448+
std::vector<unsigned char> data = ParseHexV(outputs[name_].getValStr(), "Data");
419449

420450
CTxOut out(0, CScript() << OP_RETURN << data);
421451
rawTx.vout.push_back(out);
@@ -430,7 +460,7 @@ UniValue createrawtransaction(const JSONRPCRequest& request)
430460
}
431461

432462
CScript scriptPubKey = GetScriptForDestination(destination);
433-
CAmount nAmount = AmountFromValue(sendTo[name_]);
463+
CAmount nAmount = AmountFromValue(outputs[name_]);
434464

435465
CTxOut out(nAmount, scriptPubKey);
436466
rawTx.vout.push_back(out);

src/rpc/server.cpp

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,11 @@ void RPCServer::OnStopped(std::function<void ()> slot)
5050
}
5151

5252
void RPCTypeCheck(const UniValue& params,
53-
const std::list<UniValue::VType>& typesExpected,
53+
const std::list<UniValueType>& typesExpected,
5454
bool fAllowNull)
5555
{
5656
unsigned int i = 0;
57-
for (UniValue::VType t : typesExpected)
58-
{
57+
for (const UniValueType& t : typesExpected) {
5958
if (params.size() <= i)
6059
break;
6160

@@ -67,10 +66,10 @@ void RPCTypeCheck(const UniValue& params,
6766
}
6867
}
6968

70-
void RPCTypeCheckArgument(const UniValue& value, UniValue::VType typeExpected)
69+
void RPCTypeCheckArgument(const UniValue& value, const UniValueType& typeExpected)
7170
{
72-
if (value.type() != typeExpected) {
73-
throw JSONRPCError(RPC_TYPE_ERROR, strprintf("Expected type %s, got %s", uvTypeName(typeExpected), uvTypeName(value.type())));
71+
if (!typeExpected.typeAny && value.type() != typeExpected.type) {
72+
throw JSONRPCError(RPC_TYPE_ERROR, strprintf("Expected type %s, got %s", uvTypeName(typeExpected.type), uvTypeName(value.type())));
7473
}
7574
}
7675

src/rpc/server.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ namespace RPCServer
2828
}
2929

3030
/** Wrapper for UniValue::VType, which includes typeAny:
31-
* Used to denote don't care type. Only used by RPCTypeCheckObj */
31+
* Used to denote don't care type. */
3232
struct UniValueType {
33-
explicit UniValueType(UniValue::VType _type) : typeAny(false), type(_type) {}
33+
UniValueType(UniValue::VType _type) : typeAny(false), type(_type) {}
3434
UniValueType() : typeAny(true) {}
3535
bool typeAny;
3636
UniValue::VType type;
@@ -69,12 +69,12 @@ bool RPCIsInWarmup(std::string *outStatus);
6969
* the right number of arguments are passed, just that any passed are the correct type.
7070
*/
7171
void RPCTypeCheck(const UniValue& params,
72-
const std::list<UniValue::VType>& typesExpected, bool fAllowNull=false);
72+
const std::list<UniValueType>& typesExpected, bool fAllowNull=false);
7373

7474
/**
7575
* Type-check one argument; throws JSONRPCError if wrong type given.
7676
*/
77-
void RPCTypeCheckArgument(const UniValue& value, UniValue::VType typeExpected);
77+
void RPCTypeCheckArgument(const UniValue& value, const UniValueType& typeExpected);
7878

7979
/*
8080
Check for expected keys/value types in an Object.

src/test/rpc_tests.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ BOOST_AUTO_TEST_CASE(rpc_rawparams)
5252
BOOST_CHECK_THROW(CallRPC("createrawtransaction"), std::runtime_error);
5353
BOOST_CHECK_THROW(CallRPC("createrawtransaction null null"), std::runtime_error);
5454
BOOST_CHECK_THROW(CallRPC("createrawtransaction not_array"), std::runtime_error);
55-
BOOST_CHECK_THROW(CallRPC("createrawtransaction [] []"), std::runtime_error);
5655
BOOST_CHECK_THROW(CallRPC("createrawtransaction {} {}"), std::runtime_error);
5756
BOOST_CHECK_NO_THROW(CallRPC("createrawtransaction [] {}"));
5857
BOOST_CHECK_THROW(CallRPC("createrawtransaction [] {} extra"), std::runtime_error);

test/functional/rpc_rawtransaction.py

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
- getrawtransaction
1313
"""
1414

15+
from collections import OrderedDict
16+
from io import BytesIO
1517
from test_framework.test_framework import BitcoinTestFramework
18+
from test_framework.messages import (
19+
CTransaction,
20+
)
1621
from test_framework.util import *
1722

1823

@@ -43,11 +48,10 @@ def set_test_params(self):
4348

4449
def setup_network(self, split=False):
4550
super().setup_network()
46-
connect_nodes_bi(self.nodes,0,2)
51+
connect_nodes_bi(self.nodes, 0, 2)
4752

4853
def run_test(self):
49-
50-
#prepare some coins for multiple *rawtransaction commands
54+
self.log.info('prepare some coins for multiple *rawtransaction commands')
5155
self.nodes[2].generate(1)
5256
self.sync_all()
5357
self.nodes[0].generate(101)
@@ -59,10 +63,11 @@ def run_test(self):
5963
self.nodes[0].generate(5)
6064
self.sync_all()
6165

62-
# Test getrawtransaction on genesis block coinbase returns an error
66+
self.log.info('Test getrawtransaction on genesis block coinbase returns an error')
6367
block = self.nodes[0].getblock(self.nodes[0].getblockhash(0))
6468
assert_raises_rpc_error(-5, "The genesis block coinbase is not considered an ordinary transaction", self.nodes[0].getrawtransaction, block['merkleroot'])
6569

70+
self.log.info('Check parameter types and required parameters of createrawtransaction')
6671
# Test `createrawtransaction` required parameters
6772
assert_raises_rpc_error(-1, "createrawtransaction", self.nodes[0].createrawtransaction)
6873
assert_raises_rpc_error(-1, "createrawtransaction", self.nodes[0].createrawtransaction, [])
@@ -83,12 +88,18 @@ def run_test(self):
8388

8489
# Test `createrawtransaction` invalid `outputs`
8590
address = self.nodes[0].getnewaddress()
86-
assert_raises_rpc_error(-3, "Expected type object", self.nodes[0].createrawtransaction, [], 'foo')
91+
address2 = self.nodes[0].getnewaddress()
92+
assert_raises_rpc_error(-1, "JSON value is not an array as expected", self.nodes[0].createrawtransaction, [], 'foo')
93+
self.nodes[0].createrawtransaction(inputs=[], outputs={}) # Should not throw for backwards compatibility
94+
self.nodes[0].createrawtransaction(inputs=[], outputs=[])
8795
assert_raises_rpc_error(-8, "Data must be hexadecimal string", self.nodes[0].createrawtransaction, [], {'data': 'foo'})
8896
assert_raises_rpc_error(-5, "Invalid Bitcoin address", self.nodes[0].createrawtransaction, [], {'foo': 0})
8997
assert_raises_rpc_error(-3, "Invalid amount", self.nodes[0].createrawtransaction, [], {address: 'foo'})
9098
assert_raises_rpc_error(-3, "Amount out of range", self.nodes[0].createrawtransaction, [], {address: -1})
9199
assert_raises_rpc_error(-8, "Invalid parameter, duplicated address: %s" % address, self.nodes[0].createrawtransaction, [], multidict([(address, 1), (address, 1)]))
100+
assert_raises_rpc_error(-8, "Invalid parameter, duplicated address: %s" % address, self.nodes[0].createrawtransaction, [], [{address: 1}, {address: 1}])
101+
assert_raises_rpc_error(-8, "Invalid parameter, key-value pair must contain exactly one key", self.nodes[0].createrawtransaction, [], [{'a': 1, 'b': 2}])
102+
assert_raises_rpc_error(-8, "Invalid parameter, key-value pair not an object as expected", self.nodes[0].createrawtransaction, [], [['key-value pair1'], ['2']])
92103

93104
# Test `createrawtransaction` invalid `locktime`
94105
assert_raises_rpc_error(-3, "Expected type number", self.nodes[0].createrawtransaction, [], {}, 'foo')
@@ -98,9 +109,38 @@ def run_test(self):
98109
# Test `createrawtransaction` invalid `replaceable`
99110
assert_raises_rpc_error(-3, "Expected type bool", self.nodes[0].createrawtransaction, [], {}, 0, 'foo')
100111

101-
#########################################
102-
# sendrawtransaction with missing input #
103-
#########################################
112+
self.log.info('Check that createrawtransaction accepts an array and object as outputs')
113+
tx = CTransaction()
114+
# One output
115+
tx.deserialize(BytesIO(hex_str_to_bytes(self.nodes[2].createrawtransaction(inputs=[{'txid': txid, 'vout': 9}], outputs={address: 99}))))
116+
assert_equal(len(tx.vout), 1)
117+
assert_equal(
118+
bytes_to_hex_str(tx.serialize()),
119+
self.nodes[2].createrawtransaction(inputs=[{'txid': txid, 'vout': 9}], outputs=[{address: 99}]),
120+
)
121+
# Two outputs
122+
tx.deserialize(BytesIO(hex_str_to_bytes(self.nodes[2].createrawtransaction(inputs=[{'txid': txid, 'vout': 9}], outputs=OrderedDict([(address, 99), (address2, 99)])))))
123+
assert_equal(len(tx.vout), 2)
124+
assert_equal(
125+
bytes_to_hex_str(tx.serialize()),
126+
self.nodes[2].createrawtransaction(inputs=[{'txid': txid, 'vout': 9}], outputs=[{address: 99}, {address2: 99}]),
127+
)
128+
# Two data outputs
129+
tx.deserialize(BytesIO(hex_str_to_bytes(self.nodes[2].createrawtransaction(inputs=[{'txid': txid, 'vout': 9}], outputs=multidict([('data', '99'), ('data', '99')])))))
130+
assert_equal(len(tx.vout), 2)
131+
assert_equal(
132+
bytes_to_hex_str(tx.serialize()),
133+
self.nodes[2].createrawtransaction(inputs=[{'txid': txid, 'vout': 9}], outputs=[{'data': '99'}, {'data': '99'}]),
134+
)
135+
# Multiple mixed outputs
136+
tx.deserialize(BytesIO(hex_str_to_bytes(self.nodes[2].createrawtransaction(inputs=[{'txid': txid, 'vout': 9}], outputs=multidict([(address, 99), ('data', '99'), ('data', '99')])))))
137+
assert_equal(len(tx.vout), 3)
138+
assert_equal(
139+
bytes_to_hex_str(tx.serialize()),
140+
self.nodes[2].createrawtransaction(inputs=[{'txid': txid, 'vout': 9}], outputs=[{address: 99}, {'data': '99'}, {'data': '99'}]),
141+
)
142+
143+
self.log.info('sendrawtransaction with missing input')
104144
inputs = [ {'txid' : "1d1d4e24ed99057e84c3f80fd8fbec79ed9e1acee37da269356ecea000000000", 'vout' : 1}] #won't exists
105145
outputs = { self.nodes[0].getnewaddress() : 4.998 }
106146
rawtx = self.nodes[2].createrawtransaction(inputs, outputs)
@@ -248,14 +288,14 @@ def run_test(self):
248288
outputs = { self.nodes[0].getnewaddress() : 2.19 }
249289
rawTx2 = self.nodes[2].createrawtransaction(inputs, outputs)
250290
rawTxPartialSigned1 = self.nodes[1].signrawtransactionwithwallet(rawTx2, inputs)
251-
self.log.info(rawTxPartialSigned1)
291+
self.log.debug(rawTxPartialSigned1)
252292
assert_equal(rawTxPartialSigned['complete'], False) #node1 only has one key, can't comp. sign the tx
253293

254294
rawTxPartialSigned2 = self.nodes[2].signrawtransactionwithwallet(rawTx2, inputs)
255-
self.log.info(rawTxPartialSigned2)
295+
self.log.debug(rawTxPartialSigned2)
256296
assert_equal(rawTxPartialSigned2['complete'], False) #node2 only has one key, can't comp. sign the tx
257297
rawTxComb = self.nodes[2].combinerawtransaction([rawTxPartialSigned1['hex'], rawTxPartialSigned2['hex']])
258-
self.log.info(rawTxComb)
298+
self.log.debug(rawTxComb)
259299
self.nodes[2].sendrawtransaction(rawTxComb)
260300
rawTx2 = self.nodes[0].decoderawtransaction(rawTxComb)
261301
self.sync_all()
@@ -273,7 +313,7 @@ def run_test(self):
273313
encrawtx = "01000000010000000000000072c1a6a246ae63f74f931e8365e15a089c68d61900000000000000000000ffffffff0100e1f505000000000000000000"
274314
decrawtx = self.nodes[0].decoderawtransaction(encrawtx, False) # decode as non-witness transaction
275315
assert_equal(decrawtx['vout'][0]['value'], Decimal('1.00000000'))
276-
316+
277317
# getrawtransaction tests
278318
# 1. valid parameters - only supply txid
279319
txHash = rawTx["hash"]

0 commit comments

Comments
 (0)