wallet: apply anti-fee-sniping locktime uniformly across all funded transaction RPCs #35019

pull HouseOfHufflepuff wants to merge 1 commits into bitcoin:master from HouseOfHufflepuff:rpc/uniform-locktime-anti-fee-sniping changing 4 files +92 −5
  1. HouseOfHufflepuff commented at 10:35 PM on April 6, 2026: contributor

    Summary

    Closes #34987.

    Previously, nLockTime anti-fee-sniping (current block height ±100 blocks) was only applied by the send RPC and GUI wallet flow. walletcreatefundedpsbt and fundrawtransaction both defaulted to nLockTime=0, creating a wallet fingerprinting discrepancy.

    Root cause (two issues):

    1. wallet::FundTransaction unconditionally set coinControl.m_locktime = tx.nLockTime even when tx.nLockTime was the default 0. Since coinControl.m_locktime is std::optional<uint32_t>, assigning 0 makes it non-empty, which suppresses DiscourageFeeSniping() in CreateTransaction. Fixed by only setting m_locktime when tx.nLockTime != 0.

    2. walletcreatefundedpsbt documented its locktime parameter default as 0 rather than the anti-fee-sniping default. Updated to match send.

    After this change: all wallet-funded RPCs uniformly apply anti-fee-sniping unless the caller provides an explicit locktime value.

    Behavior

    RPC Before After
    send anti-fee-sniping ✓ anti-fee-sniping ✓ (unchanged)
    walletcreatefundedpsbt (no locktime) nLockTime=0 anti-fee-sniping ✓
    fundrawtransaction (raw tx with default locktime) nLockTime=0 anti-fee-sniping ✓
    Any RPC with explicit locktime=N nLockTime=N nLockTime=N (unchanged)

    Test plan

    • wallet_create_tx.py — new test_walletcreatefundedpsbt_anti_fee_sniping() and test_fundrawtransaction_anti_fee_sniping()
    • rpc_psbt.py — updated existing assertion that expected locktime=0 from walletcreatefundedpsbt with no arguments
    • wallet_send.py, wallet_sendall.py, wallet_fundrawtransaction.py — all pass, no regressions
  2. DrahtBot added the label Wallet on Apr 6, 2026
  3. DrahtBot commented at 10:36 PM on April 6, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

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

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

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

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    No conflicts as of last run.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

    LLM Linter (✨ experimental)

    Possible places where named args for integral literals may be used (e.g. func(x, /*named_arg=*/0) in C++, and func(x, named_arg=0) in Python):

    • self.nodes[0].walletcreatefundedpsbt([], [{addr: 1}], 0) in test/functional/wallet_create_tx.py

    Possible places where comparison-specific test macros should replace generic comparisons:

    • [test/functional/wallet_create_tx.py] assert locktime > 0, f"Expected anti-fee-sniping locktime > 0, got {locktime}" -> use assert_greater_than(locktime, 0)
    • [test/functional/wallet_create_tx.py] assert locktime <= block_height, f"locktime {locktime} exceeds block_height {block_height}" -> use assert_greater_than_or_equal(block_height, locktime) (equivalent check)
    • [test/functional/wallet_create_tx.py] assert locktime >= block_height - 100, f"locktime {locktime} too far below block_height {block_height}" -> use assert_greater_than_or_equal(locktime, block_height - 100)
    • [test/functional/wallet_create_tx.py] assert decoded['tx']['locktime'] == explicit_locktime, \ -> use assert_equal(decoded['tx']['locktime'], explicit_locktime)
    • [test/functional/wallet_create_tx.py] assert decoded['tx']['locktime'] == 0, \ -> use assert_equal(decoded['tx']['locktime'], 0)
    • [test/functional/wallet_create_tx.py] assert locktime > 0, f"Expected anti-fee-sniping locktime > 0, got {locktime}" -> use assert_greater_than(locktime, 0)
    • [test/functional/wallet_create_tx.py] assert locktime <= block_height, f"locktime {locktime} exceeds block_height {block_height}" -> use assert_greater_than_or_equal(block_height, locktime) (equivalent check)
    • [test/functional/wallet_create_tx.py] assert locktime >= block_height - 100, f"locktime {locktime} too far below block_height {block_height}" -> use assert_greater_than_or_equal(locktime, block_height - 100)
    • [test/functional/wallet_create_tx.py] assert tx['locktime'] == 0, f"Expected explicit locktime 0, got {tx['locktime']}" -> use assert_equal(tx['locktime'], 0)
    • [test/functional/wallet_create_tx.py] assert locktime > 0, f"Expected anti-fee-sniping locktime > 0, got {locktime}" -> use assert_greater_than(locktime, 0)
    • [test/functional/wallet_create_tx.py] assert locktime <= block_height, f"locktime {locktime} exceeds block_height {block_height}" -> use assert_greater_than_or_equal(block_height, locktime) (equivalent check)
    • [test/functional/wallet_create_tx.py] assert locktime >= block_height - 100, f"locktime {locktime} too far below block_height {block_height}" -> use assert_greater_than_or_equal(locktime, block_height - 100)
    • [test/functional/wallet_create_tx.py] assert tx.nLockTime == explicit_locktime, \ -> use assert_equal(tx.nLockTime, explicit_locktime)

    <sup>2026-04-08 17:10:52</sup>

  4. HouseOfHufflepuff commented at 12:45 AM on April 7, 2026: contributor
  5. wallet: apply anti-fee-sniping to walletcreatefundedpsbt and fundrawtransaction
    Previously, nLockTime anti-fee-sniping was only applied when using the
    send RPC or GUI wallet flow. walletcreatefundedpsbt and fundrawtransaction
    both defaulted to nLockTime=0.
    
    The root cause was twofold:
    1. wallet::FundTransaction unconditionally set coinControl.m_locktime =
       tx.nLockTime, even when nLockTime was the default 0. Since m_locktime
       is std::optional<uint32_t>, setting it to 0 disables anti-fee-sniping
       in CreateTransaction. Fix: only set m_locktime when nLockTime != 0.
    2. walletcreatefundedpsbt documented its locktime default as 0 rather
       than the anti-fee-sniping default.
    
    After this change all wallet-funded transaction RPCs uniformly apply
    anti-fee-sniping unless the caller provides an explicit locktime.
    
    Fixes #34987.
    11fdec6357
  6. in src/wallet/spend.cpp:1515 in 511daa9e7d outdated
    1512 | -    coinControl.m_locktime = tx.nLockTime;
    1513 | +    // Set the user desired locktime. Only do so when explicitly non-zero;
    1514 | +    // nLockTime == 0 is the default and means anti-fee-sniping should be used.
    1515 | +    if (tx.nLockTime != 0) {
    1516 | +        coinControl.m_locktime = tx.nLockTime;
    1517 | +    }
    


    benalleng commented at 12:19 PM on April 8, 2026:

    Doesn't this completely prevent a user from ever setting the nLocktime to 0 then as it simply assumes a 0 value is coming from the default.

    This doesn't seem like the right way to enforce this.


    HouseOfHufflepuff commented at 5:11 PM on April 8, 2026:

    Good catch, you're right. The if (tx.nLockTime != 0) guard in wallet::FundTransaction is insufficient because nLockTime == 0 is ambiguous — it's indistinguishable from the CMutableTransaction default value and an explicit user-supplied locktime=0.

    Fixed by pushing the detection up to the RPC layer, where the signal is unambiguous:

    In walletcreatefundedpsbt: check !request.params[2].isNull() — null means omitted, non-null (including 0) means explicitly provided. Pre-set coin_control.m_locktime accordingly before calling FundTransaction.

    In the RPC-level FundTransaction: check options.exists("locktime") — presence means explicit, absence means default. This covers send and fundrawtransaction with an explicit locktime option. wallet::FundTransaction still keeps the != 0 guard for the fundrawtransaction case (no locktime option, just the raw tx's nLockTime field), but the RPC layer now correctly pre-populates coinControl.m_locktime for any explicit locktime=0 before reaching this function.

    Added a test case for walletcreatefundedpsbt([], [...], 0) and send(outputs, options={"locktime": 0}) to cover this explicitly. All existing tests pass.

  7. HouseOfHufflepuff force-pushed on Apr 8, 2026
  8. HouseOfHufflepuff requested review from benalleng on Apr 8, 2026
  9. maflcko commented at 10:46 AM on April 10, 2026: member

    Previously, nLockTime anti-fee-sniping (current block height ±100 blocks)

    Is this LLM generated? because it is obviously wrong

  10. HouseOfHufflepuff closed this on Apr 10, 2026


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-04-13 21:12 UTC

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