diff --git a/doc/release-notes/release-notes.md b/doc/release-notes/release-notes.md --- a/doc/release-notes/release-notes.md +++ b/doc/release-notes/release-notes.md @@ -15,3 +15,5 @@ - A new `send` RPC with similar syntax to `walletcreatefundedpsbt`, including support for coin selection and a custom fee rate. Using the new `send` method is encouraged: `sendmany` and `sendtoaddress` may be deprecated in a future release. +- The `testmempoolaccept` RPC returns `size` and a `fee` object with the `base` fee + if the transaction passes validation. diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -1058,25 +1058,34 @@ "value, expressed in " + Currency::get().ticker + "/kB\n"}, }, - RPCResult{RPCResult::Type::ARR, - "", - "The result of the mempool acceptance test for each raw " - "transaction in the input array.\n" - "Length is exactly one for now.", - { - {RPCResult::Type::OBJ, - "", - "", - { - {RPCResult::Type::STR_HEX, "txid", - "The transaction hash in hex"}, - {RPCResult::Type::BOOL, "allowed", - "If the mempool allows this tx to be inserted"}, - {RPCResult::Type::STR, "reject-reason", - "Rejection string (only present when 'allowed' is " - "false)"}, - }}, - }}, + RPCResult{ + RPCResult::Type::ARR, + "", + "The result of the mempool acceptance test for each raw " + "transaction in the input array.\n" + "Length is exactly one for now.", + { + {RPCResult::Type::OBJ, + "", + "", + { + {RPCResult::Type::STR_HEX, "txid", + "The transaction hash in hex"}, + {RPCResult::Type::BOOL, "allowed", + "If the mempool allows this tx to be inserted"}, + {RPCResult::Type::NUM, "size", "The transaction size"}, + {RPCResult::Type::OBJ, + "fees", + "Transaction fees (only present if 'allowed' is true)", + { + {RPCResult::Type::STR_AMOUNT, "base", + "transaction fee in " + Currency::get().ticker}, + }}, + {RPCResult::Type::STR, "reject-reason", + "Rejection string (only present when 'allowed' is " + "false)"}, + }}, + }}, RPCExamples{ "\nCreate a transaction\n" + HelpExampleCli( @@ -1127,14 +1136,23 @@ TxValidationState state; bool test_accept_res; + Amount fee; { LOCK(cs_main); test_accept_res = AcceptToMemoryPool( config, mempool, state, std::move(tx), false /* bypass_limits */, - max_raw_tx_fee, true /* test_accept */); + max_raw_tx_fee, true /* test_accept */, &fee); } result_0.pushKV("allowed", test_accept_res); - if (!test_accept_res) { + + // Only return the fee and size if the transaction would pass ATMP. + // These can be used to calculate the feerate. + if (test_accept_res) { + result_0.pushKV("size", virtual_size); + UniValue fees(UniValue::VOBJ); + fees.pushKV("base", fee); + result_0.pushKV("fees", fees); + } else { if (state.IsInvalid()) { if (state.GetResult() == TxValidationResult::TX_MISSING_INPUTS) { result_0.pushKV("reject-reason", "missing-inputs"); diff --git a/src/validation.h b/src/validation.h --- a/src/validation.h +++ b/src/validation.h @@ -285,11 +285,12 @@ /** * (try to) add transaction to memory pool + * @param[out] fee_out optional argument to return tx fee to the caller */ bool AcceptToMemoryPool(const Config &config, CTxMemPool &pool, TxValidationState &state, const CTransactionRef &tx, bool bypass_limits, const Amount nAbsurdFee, - bool test_accept = false) + bool test_accept = false, Amount *fee_out = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_main); /** diff --git a/src/validation.cpp b/src/validation.cpp --- a/src/validation.cpp +++ b/src/validation.cpp @@ -368,6 +368,7 @@ */ std::vector &m_coins_to_uncache; const bool m_test_accept; + Amount *m_fee_out; }; // Single transaction acceptance @@ -555,6 +556,11 @@ return false; } + // If fee_out is passed, return the fee to the caller + if (args.m_fee_out) { + *args.m_fee_out = nFees; + } + // Check for non-standard pay-to-script-hash in inputs if (fRequireStandard && !AreInputsStandard(tx, m_view, ws.m_next_block_script_verify_flags)) { @@ -744,17 +750,16 @@ /** * (try to) add transaction to memory pool with a specified acceptance time. */ -static bool -AcceptToMemoryPoolWithTime(const Config &config, CTxMemPool &pool, - TxValidationState &state, const CTransactionRef &tx, - int64_t nAcceptTime, bool bypass_limits, - const Amount nAbsurdFee, bool test_accept) +static bool AcceptToMemoryPoolWithTime( + const Config &config, CTxMemPool &pool, TxValidationState &state, + const CTransactionRef &tx, int64_t nAcceptTime, bool bypass_limits, + const Amount nAbsurdFee, bool test_accept, Amount *fee_out = nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_main) { AssertLockHeld(cs_main); std::vector coins_to_uncache; MemPoolAccept::ATMPArgs args{config, state, nAcceptTime, bypass_limits, nAbsurdFee, coins_to_uncache, - test_accept}; + test_accept, fee_out}; bool res = MemPoolAccept(pool).AcceptSingleTransaction(tx, args); if (!res) { // Remove coins that were not present in the coins cache before calling @@ -779,9 +784,10 @@ bool AcceptToMemoryPool(const Config &config, CTxMemPool &pool, TxValidationState &state, const CTransactionRef &tx, bool bypass_limits, const Amount nAbsurdFee, - bool test_accept) { + bool test_accept, Amount *fee_out) { return AcceptToMemoryPoolWithTime(config, pool, state, tx, GetTime(), - bypass_limits, nAbsurdFee, test_accept); + bypass_limits, nAbsurdFee, test_accept, + fee_out); } CTransactionRef GetTransaction(const CBlockIndex *const block_index, diff --git a/test/functional/mempool_accept.py b/test/functional/mempool_accept.py --- a/test/functional/mempool_accept.py +++ b/test/functional/mempool_accept.py @@ -4,6 +4,8 @@ # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test mempool acceptance of raw transactions.""" +from decimal import Decimal + from test_framework.key import ECKey from test_framework.messages import ( MAX_BLOCK_BASE_SIZE, @@ -95,22 +97,28 @@ tx = FromHex(CTransaction(), raw_tx_0) txid_0 = tx.rehash() self.check_mempool_result( - result_expected=[{'txid': txid_0, 'allowed': True}], + result_expected=[{'txid': txid_0, 'allowed': True, + 'size': tx.billable_size(), + 'fees': {'base': Decimal(str(fee))}}], rawtxs=[raw_tx_0], ) self.log.info('A final transaction not in the mempool') # Pick a random coin(base) to spend coin = coins.pop() + output_amount = 25_000 raw_tx_final = node.signrawtransactionwithwallet(node.createrawtransaction( inputs=[{'txid': coin['txid'], 'vout': coin['vout'], "sequence": 0xffffffff}], # SEQUENCE_FINAL - outputs=[{node.getnewaddress(): 25000}], + outputs=[{node.getnewaddress(): output_amount}], locktime=node.getblockcount() + 2000, # Can be anything ))['hex'] tx = FromHex(CTransaction(), raw_tx_final) + fee_expected = int(coin['amount']) - output_amount self.check_mempool_result( - result_expected=[{'txid': tx.rehash(), 'allowed': True}], + result_expected=[{'txid': tx.rehash(), 'allowed': True, + 'size': tx.billable_size(), + 'fees': {'base': Decimal(str(fee_expected))}}], rawtxs=[tx.serialize().hex()], maxfeerate=0, ) @@ -198,7 +206,9 @@ tx = FromHex(CTransaction(), raw_tx_reference) # Reference tx should be valid on itself self.check_mempool_result( - result_expected=[{'txid': tx.rehash(), 'allowed': True}], + result_expected=[{'txid': tx.rehash(), 'allowed': True, + 'size': tx.billable_size(), + 'fees': {'base': Decimal(100_000 - 50_000)}}], rawtxs=[ToHex(tx)], maxfeerate=0, )