rpc: avoid quadratic JSON construction when keys are unique #36096

pull l0rinc wants to merge 3 commits into bitcoin:master from l0rinc:l0rinc/getprioritisedtransactions-linear-json changing 5 files +22 −20
  1. l0rinc commented at 2:58 AM on August 27, 2026: contributor

    Problem: getprioritisedtransactions lets node operators inspect fee adjustments. While building the response, the RPC checks each transaction ID against all previous IDs, even though duplicates are impossible. The same unnecessary search appears in a few other RPC responses built directly from std::map or std::set keys.

    Fix: Each changed response key comes from an std::map or std::set, where keys are unique, so insertion can skip the linear findKey() call.

    Reproducer: On a Rpi 4 the test below took almost a minute before the fix and about half that time after. The other changed map and set loops perform the same per-key search, so their response construction has the same quadratic-to-linear scaling as the number of entries grows.

    <details> <summary>Reproducer commands</summary>

    diff --git a/test/functional/mining_prioritisetransaction.py b/test/functional/mining_prioritisetransaction.py
    --- a/test/functional/mining_prioritisetransaction.py
    +++ b/test/functional/mining_prioritisetransaction.py
    @@ -11,6 +11,7 @@ from test_framework.blocktools import NORMAL_GBT_REQUEST_PARAMS
     from test_framework.messages import (
         COIN,
         MAX_BLOCK_WEIGHT,
    +    ser_uint256,
     )
     from test_framework.test_framework import BitcoinTestFramework
     from test_framework.util import (
    @@ -215,6 +216,12 @@ class PrioritiseTransactionTest(BitcoinTestFramework):
             assert_raises_rpc_error(-1, "getprioritisedtransactions",
                     self.nodes[0].getprioritisedtransactions, True)
     
    +        self.log.info("Test getprioritisedtransactions order")
    +        txids = [ser_uint256(i).hex() for i in range(20_000, 0, -1)]
    +        self.nodes[0].batch([self.nodes[0].prioritisetransaction.get_request(txid, 0, 1) for txid in txids])
    +        assert_equal(list(self.nodes[0].getprioritisedtransactions()), txids[::-1])
    +        self.clear_prioritisation(self.nodes[0])
    +
             # Test `prioritisetransaction` invalid `txid`
    

    </details>

  2. DrahtBot added the label RPC/REST/ZMQ on Aug 27, 2026
  3. DrahtBot commented at 2:58 AM on August 27, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/36096.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    ACK sedited, hodlinator

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  4. instagibbs commented at 10:10 AM on August 27, 2026: member

    Do we generally rely on the replacing functionality at all? I feel like this could be swapped in almost at every site?

  5. l0rinc commented at 3:58 PM on August 27, 2026: contributor

    Do we generally rely on the replacing functionality at all?

    Valid question, based on the comments we have at least one such case: https://github.com/bitcoin/bitcoin/blob/a24110cef7fa36138620f1876cd0722944618d07/src/rpc/client.cpp#L513-L516

    There may be a few other sites where we cannot have duplicated and could use pushKVEnd(), but here that guarantee comes directly from mapDeltas, it's trivial to prove why the lookup isn't needed. Quick LLM search for other candidates if reviewers prefer we include them here:

    Strong pushKVEnd() candidates:

    • src/rpc/rawtransaction.cpp:1165: eight loops over PSBT maps, including unknown fields and hash preimages. This is the strongest performance follow-up because these objects can be large.
    • src/rpc/mempool.cpp:786 and src/rpc/mempool.cpp:851: both iterate CTxMemPool::setEntries, whose comparator makes txids unique. getrawmempool already uses pushKVEnd() for the same reason.
    • src/rpc/net.cpp:295: two loops over std::map<std::string, uint64_t> message counters.
    • src/rpc/blockchain.cpp:2210: iterates a std::set<std::string>.
    • src/rpc/node.cpp:263: category names come from a std::map.
    • estimaterawfee: iterates the three distinct fee-estimation horizons.

    The last four are tiny objects, so there is little performance value. The worthwhile separate audits are decodepsbt, followed by the two verbose mempool RPCs. I found no open upstream PR covering them.

    Edit: ended up pushing the trivial ones in a second commit, thanks for the comment.

  6. l0rinc force-pushed on Aug 27, 2026
  7. l0rinc renamed this:
    rpc: avoid quadratic JSON construction in getprioritisedtransactions
    rpc: avoid quadratic JSON construction when keys are unique
    on Aug 27, 2026
  8. hodlinator approved
  9. hodlinator commented at 1:18 PM on August 31, 2026: contributor

    crACK 882cf7741583cf8cd00d221e01fd9987dee57952

    Verified that all pushKVEnd() calls are for unique keys.

    Went down a similar rabbit hole in a5f6d47c444c5ecc777152b8cc9a1faf54eb5925 in May which I dropped on the floor. Maybe worth picking up in some form, probably as another PR.

  10. in src/rpc/net.cpp:298 in 882cf77415
     294 | @@ -295,14 +295,14 @@ static RPCMethod getpeerinfo()
     295 |          UniValue sendPerMsgType(UniValue::VOBJ);
     296 |          for (const auto& i : stats.mapSendBytesPerMsgType) {
     297 |              if (i.second > 0)
     298 | -                sendPerMsgType.pushKV(i.first, i.second);
     299 | +                sendPerMsgType.pushKVEnd(i.first, i.second);
    


    maflcko commented at 9:34 AM on September 1, 2026:

    Use structured-binding in the loop while touching this line?


    l0rinc commented at 4:51 PM on September 1, 2026:

    Done in a separate commit, thanks

  11. in src/rpc/rawtransaction.cpp:1168 in 882cf77415
    1164 | @@ -1165,7 +1165,7 @@ static RPCMethod decodepsbt()
    1165 |      // Unknown data
    1166 |      UniValue unknowns(UniValue::VOBJ);
    1167 |      for (auto entry : psbtx.unknown) {
    1168 | -        unknowns.pushKV(HexStr(entry.first), HexStr(entry.second));
    1169 | +        unknowns.pushKVEnd(HexStr(entry.first), HexStr(entry.second));
    


    maflcko commented at 9:35 AM on September 1, 2026:

    same?


    l0rinc commented at 4:51 PM on September 1, 2026:

    I always apply the review comments to every other case I can find, thanks, done

  12. maflcko approved
  13. maflcko commented at 9:39 AM on September 1, 2026: member

    Seems fine, left some nits

  14. rpc: avoid quadratic prioritised transaction JSON
    `GetPrioritisedTransactions()` returns unique keys in map order.
    Append them directly instead of scanning the growing UniValue object for duplicates.
    23e512a58e
  15. rpc: append unique container keys directly
    RPC responses that build objects from `std::map` or `std::set` keys already have unique keys.
    
    Use `pushKVEnd()` for direct map and set loops in `decodepsbt` and verbose mempool ancestor and descendant results.
    Do the same for `getpeerinfo` message counters and selected `getblockstats` results.
    21d5d5cb73
  16. refactor: use structured bindings for map entries 74ddf1c0a0
  17. l0rinc force-pushed on Sep 1, 2026
  18. l0rinc commented at 4:56 PM on September 1, 2026: contributor

    Rebased, used structured bindings for the map iterations and added braces to the affected code

  19. sedited approved
  20. sedited commented at 6:53 PM on September 1, 2026: contributor

    ACK 74ddf1c0a0ef8447686f59044b2fc2ee8d78c0e4

  21. DrahtBot requested review from hodlinator on Sep 1, 2026
  22. hodlinator approved
  23. hodlinator commented at 4:26 PM on September 3, 2026: contributor

    re-ACK 74ddf1c0a0ef8447686f59044b2fc2ee8d78c0e4

    Would prefer the last commit was merged into the middle one, but like this is fine too.


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-09-04 07:51 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me