Skip to content

Commit 6294f32

Browse files
committed
gettxoutproof() should return consistent result
We can call gettxoutproof() with a list of transactions. Currently, if the first transaction is unspent (and all other transactions are in the same block), then the call will succeed. If the first transaction has been spent, then the call will fail. The means that the following two calls will return different results: gettxoutproof(unspent_tx1, spent_tx1) gettxoutproof(spent_tx1, unspent_tx1) This commit makes behaviour independent of transaction ordering by looping through all transactions provided and trying to find which block they're in. This commit also increases the test coverage and tests more failure cases for gettxoutproof()
1 parent 300f8e7 commit 6294f32

File tree

2 files changed

+23
-14
lines changed

2 files changed

+23
-14
lines changed

src/rpc/rawtransaction.cpp

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -219,9 +219,13 @@ UniValue gettxoutproof(const JSONRPCRequest& request)
219219
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found");
220220
pblockindex = mapBlockIndex[hashBlock];
221221
} else {
222-
const Coin& coin = AccessByTxid(*pcoinsTip, oneTxid);
223-
if (!coin.IsSpent() && coin.nHeight > 0 && coin.nHeight <= chainActive.Height()) {
224-
pblockindex = chainActive[coin.nHeight];
222+
// Loop through txids and try to find which block they're in. Exit loop once a block is found.
223+
for (const auto& tx : setTxids) {
224+
const Coin& coin = AccessByTxid(*pcoinsTip, tx);
225+
if (!coin.IsSpent()) {
226+
pblockindex = chainActive[coin.nHeight];
227+
break;
228+
}
225229
}
226230
}
227231

@@ -244,7 +248,7 @@ UniValue gettxoutproof(const JSONRPCRequest& request)
244248
if (setTxids.count(tx->GetHash()))
245249
ntxFound++;
246250
if (ntxFound != setTxids.size())
247-
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "(Not all) transactions not found in specified block");
251+
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Not all transactions found in specified or retrieved block");
248252

249253
CDataStream ssMB(SER_NETWORK, PROTOCOL_VERSION | SERIALIZE_TRANSACTION_NO_WITNESS);
250254
CMerkleBlock mb(block, setTxids);

test/functional/merkle_blocks.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ def run_test(self):
3939
txid1 = self.nodes[0].sendrawtransaction(self.nodes[0].signrawtransaction(tx1)["hex"])
4040
tx2 = self.nodes[0].createrawtransaction([node0utxos.pop()], {self.nodes[1].getnewaddress(): 49.99})
4141
txid2 = self.nodes[0].sendrawtransaction(self.nodes[0].signrawtransaction(tx2)["hex"])
42-
assert_raises(JSONRPCException, self.nodes[0].gettxoutproof, [txid1])
42+
# This will raise an exception because the transaction is not yet in a block
43+
assert_raises_jsonrpc(-5, "Transaction not yet in block", self.nodes[0].gettxoutproof, [txid1])
4344

4445
self.nodes[0].generate(1)
4546
blockhash = self.nodes[0].getblockhash(chain_height + 1)
@@ -56,25 +57,29 @@ def run_test(self):
5657

5758
txin_spent = self.nodes[1].listunspent(1).pop()
5859
tx3 = self.nodes[1].createrawtransaction([txin_spent], {self.nodes[0].getnewaddress(): 49.98})
59-
self.nodes[0].sendrawtransaction(self.nodes[1].signrawtransaction(tx3)["hex"])
60+
txid3 = self.nodes[0].sendrawtransaction(self.nodes[1].signrawtransaction(tx3)["hex"])
6061
self.nodes[0].generate(1)
6162
self.sync_all()
6263

6364
txid_spent = txin_spent["txid"]
6465
txid_unspent = txid1 if txin_spent["txid"] != txid1 else txid2
6566

6667
# We can't find the block from a fully-spent tx
67-
assert_raises(JSONRPCException, self.nodes[2].gettxoutproof, [txid_spent])
68-
# ...but we can if we specify the block
68+
assert_raises_jsonrpc(-5, "Transaction not yet in block", self.nodes[2].gettxoutproof, [txid_spent])
69+
# We can get the proof if we specify the block
6970
assert_equal(self.nodes[2].verifytxoutproof(self.nodes[2].gettxoutproof([txid_spent], blockhash)), [txid_spent])
70-
# ...or if the first tx is not fully-spent
71+
# We can't get the proof if we specify a non-existent block
72+
assert_raises_jsonrpc(-5, "Block not found", self.nodes[2].gettxoutproof, [txid_spent], "00000000000000000000000000000000")
73+
# We can get the proof if the transaction is unspent
7174
assert_equal(self.nodes[2].verifytxoutproof(self.nodes[2].gettxoutproof([txid_unspent])), [txid_unspent])
72-
try:
73-
assert_equal(self.nodes[2].verifytxoutproof(self.nodes[2].gettxoutproof([txid1, txid2])), txlist)
74-
except JSONRPCException:
75-
assert_equal(self.nodes[2].verifytxoutproof(self.nodes[2].gettxoutproof([txid2, txid1])), txlist)
76-
# ...or if we have a -txindex
75+
# We can get the proof if we provide a list of transactions and one of them is unspent. The ordering of the list should not matter.
76+
assert_equal(sorted(self.nodes[2].verifytxoutproof(self.nodes[2].gettxoutproof([txid1, txid2]))), sorted(txlist))
77+
assert_equal(sorted(self.nodes[2].verifytxoutproof(self.nodes[2].gettxoutproof([txid2, txid1]))), sorted(txlist))
78+
# We can always get a proof if we have a -txindex
7779
assert_equal(self.nodes[2].verifytxoutproof(self.nodes[3].gettxoutproof([txid_spent])), [txid_spent])
80+
# We can't get a proof if we specify transactions from different blocks
81+
assert_raises_jsonrpc(-5, "Not all transactions found in specified or retrieved block", self.nodes[2].gettxoutproof, [txid1, txid3])
82+
7883

7984
if __name__ == '__main__':
8085
MerkleBlockTest().main()

0 commit comments

Comments
 (0)