RPC: add sendrawtransactiontopeer #33507

pull polespinasa wants to merge 2 commits into bitcoin:master from polespinasa:2025-09-29-sendrawtransactiontopeer changing 4 files +117 −0
  1. polespinasa commented at 1:58 am on September 30, 2025: contributor

    Adds a new RPC sendrawtransactiontopeer, which sends a single tx to a specific peer. After the transaction is sent to the peer it is forgotten and not stored in the local mempool.

    This rpc can serve several purposes. For example, it allows you to “spoof” an initial retransmission from a different “trusted” peer, making it appear as if the original participant was never aware of the transaction. It can also be useful in testing and simulation environments.

    Solves #28636 and #21876 (Partially - see #33507 (review))

  2. DrahtBot added the label RPC/REST/ZMQ on Sep 30, 2025
  3. DrahtBot commented at 1:58 am on September 30, 2025: contributor

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

    Code Coverage & Benchmarks

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

    Reviews

    See the guideline for information on the review process. A summary of reviews will appear here.

  4. in src/rpc/mempool.cpp:168 in 5e4b8120af outdated
    160+                return PackageMempoolAcceptResult(tx->GetWitnessHash(),
    161+                                                  chainman.ProcessTransaction(tx, /*test_accept=*/true));
    162+            }();
    163+
    164+            const auto& tx_result = package_result.m_tx_results.find(tx->GetWitnessHash())->second;
    165+            if (tx_result.m_result_type != MempoolAcceptResult::ResultType::VALID && tx_result.m_result_type != MempoolAcceptResult::ResultType::MEMPOOL_ENTRY) {
    


    polespinasa commented at 2:01 am on September 30, 2025:

    Even if the tx is already in the mempool m_result_type is never ResultType::MEMPOOL_ENTRY making it throw a JSONTransactionError.

    This can be seen in the last test case in 5e4b8120af266026cfbc3ca29fc98ebb4fcf8f55. This should not be the behavior, but I didn’t find a solution yet.

  5. in test/functional/rpc_rawtransaction.py:466 in 5e4b8120af outdated
    461+
    462+        self.log.info("Test error for sendrawtransactiontopeer to non-existing peer")
    463+        rawtx2 = self.wallet.create_self_transfer()['hex']
    464+        assert_raises_rpc_error(-1, "Error: Could not send transaction to peer", self.nodes[2].sendrawtransactiontopeer, hexstring=rawtx2, peer_id=100)
    465+
    466+        self.log.info("Test error for sendrawtransactiontopeer of txs already in the mempool")
    


    polespinasa commented at 2:02 am on September 30, 2025:
    This test case should be deleted as this is not the expected behavior, see #33507 (review)
  6. andrewtoth commented at 2:04 am on September 30, 2025: contributor
    Have you seen #29415? Is there a reason you would want this if we had private broadcast?
  7. polespinasa force-pushed on Sep 30, 2025
  8. in src/rpc/mempool.cpp:127 in fca25c57ef
    119+            {"peer_id", RPCArg::Type::NUM, RPCArg::Optional::NO, "The peer to send the message to."},
    120+            {"maxburnamount", RPCArg::Type::AMOUNT, RPCArg::Default{FormatMoney(DEFAULT_MAX_BURN_AMOUNT)},
    121+             "Reject transactions with provably unspendable outputs (e.g. 'datacarrier' outputs that use the OP_RETURN opcode) greater than the specified value, expressed in " + CURRENCY_UNIT + ".\n"
    122+             "If burning funds through unspendable outputs is desired, increase this value.\n"
    123+             "This check is based on heuristics and does not guarantee spendability of outputs.\n"},
    124+        },
    


    maflcko commented at 6:33 am on September 30, 2025:
    You’ll also have to check the fee? Otherwise, this just seems like a duplicate of sendmsgtopeer 0 "tx" "$txhex"?

    polespinasa commented at 8:29 pm on September 30, 2025:
    You’re right! Done :)
  9. in src/rpc/mempool.cpp:123 in fca25c57ef outdated
    115+        "Submit a raw transaction (serialized, hex-encoded) to a peer node but don't store the transaction on the local mempool.\n"
    116+        "\nRelated RPCs: createrawtransaction, signrawtransactionwithkey\n",
    117+        {
    118+            {"hexstring", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The hex string of the raw transaction"},
    119+            {"peer_id", RPCArg::Type::NUM, RPCArg::Optional::NO, "The peer to send the message to."},
    120+            {"maxburnamount", RPCArg::Type::AMOUNT, RPCArg::Default{FormatMoney(DEFAULT_MAX_BURN_AMOUNT)},
    


    luke-jr commented at 8:32 am on September 30, 2025:
    This feels better as a named-only param.

    luke-jr commented at 8:33 am on September 30, 2025:
    Probably should include the (more likely to be used) “maxfeerate” option too?

    polespinasa commented at 8:28 pm on September 30, 2025:
    You’re right! Done :)

    polespinasa commented at 8:37 pm on September 30, 2025:
    I would rather keep the same format as sendrawtransaction.

    luke-jr commented at 4:29 pm on October 1, 2025:
    You’re not doing that with this.

    polespinasa commented at 5:31 pm on October 1, 2025:
    how so? Care to explain? :)

    luke-jr commented at 12:36 pm on October 2, 2025:
    At the time, you missed maxfeerate, but I see that has since been added
  10. in src/rpc/mempool.cpp:109 in fca25c57ef outdated
    106@@ -107,6 +107,85 @@ static RPCHelpMan sendrawtransaction()
    107     };
    108 }
    109 
    


    luke-jr commented at 8:40 am on September 30, 2025:
    This doesn’t belong in rpc/mempool since it never touches the mempool…?

    polespinasa commented at 7:25 pm on September 30, 2025:
    Agree it doesn’t really fit. Implemented there because of similarity with sendrawtransaction. Any suggestions? Maybe rpc/rawtransaction.cpp or rpc/net.cpp?
  11. in src/rpc/mempool.cpp:187 in fca25c57ef outdated
    172+            msg_ser.data = TryParseHex<unsigned char>(request.params[0].get_str()).value();
    173+            msg_ser.m_type = NetMsgType::TX;
    174+
    175+            bool success = connman.ForNode(peer_id, [&](CNode* node) {
    176+                connman.PushMessage(node, std::move(msg_ser));
    177+                return true;
    


    luke-jr commented at 8:40 am on September 30, 2025:
    Seems like this ought to check if the peer accepted it, and if not, return the rejection reason.

    polespinasa commented at 8:32 pm on September 30, 2025:
    I might be wrong but I don’t think there’s a way to know if the peer accepted it or not.

    luke-jr commented at 4:30 pm on October 1, 2025:
    Right, not since the reject message was removed, oh well. :(
  12. luke-jr changes_requested
  13. polespinasa commented at 6:10 pm on September 30, 2025: contributor

    Have you seen #29415? Is there a reason you would want this if we had private broadcast?

    Yes! I saw it while reviewing the issues; it is a really nice privacy solution, but still I think this can have some use-cases.

    Apart from testing/simulating network stuff, it can be used in an environment where your tx may be dropped/filtered by some of your peers. In that case, the ability to manually select one peer that you know will accept it and route it can be useful while preserving some privacy. #29415 handles this by re-sending the transaction to one new peer after ~15 min, which can be slow; this rpc could speed up the propagation. Eg. If you have 10 peers and NUM_PRIVATE_BROADCAST_PER_TX=2 and 8 of your peers will drop your tx, in the worst case you will wait 1-1.30h for your transaction to be broadcast. Which could lead to your tx fees not being enough or could be a problem for time-sensitive txs.

    This is just an extreme case, but works as an example.

  14. rpc: Add sendrawtransactiontopeer RPC 21208f9fd7
  15. test: test sendrawtransactiontopeer 333c0905d8
  16. polespinasa force-pushed on Sep 30, 2025
  17. theuni commented at 10:53 pm on October 1, 2025: member
    I don’t understand why this is necessary. Why not use sendmsgtopeer if you’re just sending dumb bytes that don’t affect our state?
  18. polespinasa commented at 11:06 pm on October 1, 2025: contributor

    I don’t understand why this is necessary. Why not use sendmsgtopeer if you’re just sending dumb bytes that don’t affect our state?

    We don’t want to send invalid txs or txs that we would not broadcast,. We want to validates the txs before sending it to a peer. Additionally, peer_id argument could take an array of ids and send the tx to multiple peers. (this is not implemented, but shouldn’t be difficult to)

  19. luke-jr commented at 12:39 pm on October 2, 2025: member

    We don’t want to send invalid txs or txs that we would not broadcast,. We want to validates the txs before sending it to a peer.

    Maybe it should just do validation then, and can be followed up by the raw send?

    Additionally, peer_id argument could take an array of ids and send the tx to multiple peers. (this is not implemented, but shouldn’t be difficult to)

    Sounds like a job for batching.

    (But maybe it’s enough complexity to be worth a simplified RPC like this)

  20. polespinasa commented at 6:50 pm on October 2, 2025: contributor

    Maybe it should just do validation then, and can be followed up by the raw send?

    If this is the flow we want to follow then this PR is not necessary at all. We can already do that with testmempoolaccept and then sendmsgtopeer

    Additionally, peer_id argument could take an array of ids and send the tx to multiple peers. (this is not implemented, but shouldn’t be difficult to)

    Sounds like a job for batching.

    Yeah that’s why I didn’t implement it in the first place, but it’s easy to implement here.

    (But maybe it’s enough complexity to be worth a simplified RPC like this)

    Overall sending a tx to multiple desired peers can be already done with testmempoolaccept + N * (sendmsgtopeer). IMHO is worth having it all with one RPC.

  21. theuni commented at 7:50 pm on October 2, 2025: member

    Overall sending a tx to multiple desired peers can be already done with testmempoolaccept + N * (sendmsgtopeer). IMHO is worth having it all with one RPC.

    Strongly disagree. This seems like a case of feature creep to me. RPCs are easy to add and very tough to remove. If there were an issue with atomicity between calls or something, I might agree. But otherwise, chaining rpc calls sounds like the reasonable thing to do.

    Additionally, peer_id argument could take an array of ids and send the tx to multiple peers. (this is not implemented, but shouldn’t be difficult to)

    …and this is a good example of why. Allowing sendmsgtopeer to send to multiple peers (or a new rpc call to do so) would fill in the missing functionality without catering to a single use-case. testmempoolaccept + sendmsgtopeer[M..N] would be composable, and the multi-send may be useful for sending other data as well.

  22. polespinasa commented at 8:02 pm on October 2, 2025: contributor

    I’m closing the PR for lack of support. Thank you all for your time commenting and reviewing :)

    Allowing sendmsgtopeer to send to multiple peers (or a new rpc call to do so) would fill in the missing functionality without catering to a single use-case.

    This approach makes sense to me as long as there are more network messages that might be worth sending to multiple peers. I might investigate that and if so open a PR with sendmsgtopeer[M..N] (probably a new rpc to keep backward compatibility on the original one).

  23. polespinasa closed this on Oct 2, 2025

  24. polespinasa deleted the branch on Oct 2, 2025
  25. mzumsande commented at 10:08 pm on October 2, 2025: contributor
    sendmsgtopeer is a debug-only rpc originally meant only for tests. If we want it to be part of workflows from actual users, we should announce it in release notes (and maybe polish it up a bit). That also means that backwards compatibility is not an issue we need to care about.
  26. polespinasa commented at 11:59 pm on October 2, 2025: contributor

    @mzumsande true.

    Also I got some comments on X about it, proposing the new RPC call to recreate the whole INV - GETDATA - TX flow and not just send a tx message.

  27. vasild commented at 10:00 am on October 7, 2025: contributor

    This looked like an easy way for users to unintentionally dox themselves. Why would one want to send their transaction to a given peer(s) only and not to all like sendrawtransaction RPC?

    1. To obfuscate the transaction origin? Doing this properly is more involved, see #29415 which does exactly that. If we would add an assumption that there is a known trusted peer which is ok to know the transaction origin, then one might as well connect just to them using -connect. Further, even in that scenario, if this is used repeatedly for many transactions, then an outside observer could link those transactions between themselves and link them to that trusted peer, who seems to be the originator. I.e. the trusted peer is kind of doxed.

    2. #29415 is too slow with retries? That is easy to tweak, see commit net_processing: retry private broadcast in that PR (+85/-7 lines). Suggestions are welcome.

    3. We know somehow that a given peer will accept the transaction (e.g. they accept transactions with low fees)? In this case one might as well just add them with addnode to keep a connection with them and then use the regular sendrawtransaction RPC. Then the transaction will be send to that peer as well as to everybody else.

    In general, improved transaction propagation speed and/or privacy of the originator would better be done automatically for everybody and not require manual user intervention or involve trusted peers.


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: 2025-10-10 18:13 UTC

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