Wallet: Don't backdate locktime rbf #36040

pull Bicaru20 wants to merge 2 commits into bitcoin:master from Bicaru20:2026-dont-backdate-locktime-RBF changing 5 files +115 −8
  1. Bicaru20 commented at 3:28 PM on August 20, 2026: contributor

    Closes #26526

    Currently, in Bitcoin Core, given an original transaction A and its replacement B, we refer to backdating when the locktime of A is higher than the locktime of B (A.locktime > B.locktime). This can happen because Bitcoin Core enables anti-fee-sniping by default, which sets the transaction's locktime to the current block height. For privacy, 10% of the time, it instead sets a different locktime, randomly chosen between the current height and the current height minus 100 blocks. This can lead into having a replacement of a transaction with a locktime older than its original transaction. This is unrealistics and can be used as a wallet fingerprint.

    You can find a functional test to reproduce the behaviour mentioned here

    The approach proposed in this PR adds a new parameter to CoinControl to keep track of the previous locktime in the case of a bumpfee. We then pass this parameter to the DiscourageFeeSniping function through a new parameter, minimum_height, whose default value is set to 0.

    <details> <summary>Alternative approach:</summary> Since the locktime of the new transaction is 0, we also considered setting it to the previous locktime value and then, inside `DiscourageFeeSniping`, saving the previous locktime and setting the transaction's locktime to the block height before applying any of the backdating logic. This way, we could avoid passing a new parameter to the function. We ultimately decided not to go with this approach, as it makes the code more difficult to follow. </details>

    The only RPC that is affected by this changes is bumpfee.

    The pr also includes a functional test in wallet_bumpfee.py to test that when replacing a transation using bumpfee the locktime is not backdated.

    <details> <summary>We also conducted a small analysis to see how many the backdating in RBF transactions actually happen.</summary> We have data on the replaced transactions from 2025-05-01 to 2026-06-01. With that we have been able to detect this many backdatings: <img width="1782" height="891" alt="image" src="https://github.com/user-attachments/assets/1d2a83d5-aa61-4850-82e5-cb434b9ee958" />

    We took the date of the last transaction of the replacement chain (A replacement chain are all the transactions that replace themselves)

    In total we have over 1,000,000 rbf transactions, but we see that on average there are only about 200-300 hundred replaccements backdating per week. Looking at the percentages we see that on average is less than 2% of all the transactions that have been replaced.

    So, it is clear that backdating in replacement transactions is unusual. However, the few transactions that do exhibit backdating are easily fingerprintable as having been replaced using bumpfee in Bitcoin Core or Electrum.

    </details>

  2. DrahtBot added the label Wallet on Aug 20, 2026
  3. DrahtBot commented at 3:28 PM on August 20, 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/36040.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

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

    Type Reviewers
    Concept ACK nervana21, molnard
    Approach ACK polespinasa

    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.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #35472 (test: add coverage for feebumper uncomputable cluster error path by 151henry151)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  4. Bicaru20 force-pushed on Aug 20, 2026
  5. DrahtBot added the label CI failed on Aug 20, 2026
  6. DrahtBot removed the label CI failed on Aug 20, 2026
  7. nervana21 commented at 12:34 AM on August 21, 2026: contributor

    Concept ACK

  8. in src/wallet/feebumper.cpp:317 in f26586b91d
     312 | @@ -313,6 +313,10 @@ Result CreateRateBumpTransaction(CWallet& wallet, const Txid& txid, const CCoinC
     313 |  
     314 |      // We cannot source new unconfirmed inputs(bip125 rule 2)
     315 |      new_coin_control.m_min_depth = 1;
     316 | +    // If no locktime is set, we save the previous one for anti fee sniping
     317 | +    if (!new_coin_control.m_locktime){
    


    nervana21 commented at 5:07 PM on August 23, 2026:

    b7935c708edc225e20dcab0820dff774f841736b: Wallet: Do not allow bumpfee to backdate the replacement transaction lockitme

        if (!new_coin_control.m_locktime.has_value()){
    

    nit


    polespinasa commented at 2:03 PM on August 25, 2026:

    in b7935c708edc225e20dcab0820dff774f841736b Wallet: Do not allow bumpfee to backdate the replacement transaction lockitme

    nit: missing space between ) and {

  9. in src/wallet/spend.cpp:1034 in f26586b91d
    1030 | +            if (static_cast<uint32_t>(block_height) >= minimum_height) {
    1031 | +                int locktime_range = std::min(100, int(block_height - minimum_height));
    1032 | +                if (locktime_range > 0){
    1033 | +                    tx.nLockTime = std::max(int(minimum_height), int(tx.nLockTime) - int(rng_fast.randrange(locktime_range)));
    1034 | +                }
    1035 | +            }
    


    nervana21 commented at 5:08 PM on August 23, 2026:

    b7935c708edc225e20dcab0820dff774f841736b: Wallet: Do not allow bumpfee to backdate the replacement transaction lockitme

                const int tip_floor_distance = block_height - static_cast<int>(minimum_height);
                if (tip_floor_distance > 0) {
                    const int bound = minimum_height > 0
                        ? tip_floor_distance + 1
                        : std::min(100, tip_floor_distance + 1);
                    const int back = rng_fast.randrange(bound);
                    tx.nLockTime = block_height - back;
                }
    

    There's an OBO in this logic. In the inclusive range [minimum_height, block_height] there are (block_height - minimum_height) + 1 possible values. randrange(block_height - minimum_height) only draws block_height - minimum_height possible values and can never pick the minimum_height value.

    Also, when minimum_height == 0, we retain the prior behavior and backdate at most 99 blocks from tip. When minimum_height > 0, we should use the full inclusive window without the 100 block cap.


    Bicaru20 commented at 10:18 AM on August 28, 2026:

    Also, when minimum_height == 0, we retain the prior behavior and backdate at most 99 blocks from tip. When minimum_height > 0, we should use the full inclusive window without the 100 block cap.

    The idea here is to avoid backdating when using bumpfee, without making these changes distinguishable from other transactions that use antifee sniping. If we were to use only minimum_height as the inclusive window, we could end up with replacements with a locktime more than 100 blocks behind the current tip. That would be easily fingerprintable as coming from Core, which is exactly what we want to avoid.

  10. in src/wallet/spend.cpp:1040 in f26586b91d


    nervana21 commented at 5:10 PM on August 23, 2026:

    b7935c708edc225e20dcab0820dff774f841736b: Wallet: Do not allow bumpfee to backdate the replacement transaction lockitme

            // the privacy of high-latency transactions. Use minimum_height so new
            // sends still get a constant 0 fingerprint, while bumpfee keeps the prior 
            // height and does not make the replacement older than the original.
            // If that height is ahead of the local tip, use 0 so the tx stays final
            // and we do not fingerprint the lagging tip.
            if (minimum_height <= static_cast<uint32_t>(block_height)) {
                tx.nLockTime = minimum_height;
            } else {
                tx.nLockTime = 0;
    

    In the case where we're fee bumping an already-nLockTimed tx, we should not reset its nLockTime back below that established minimum_height. Otherwise we fingerprint the tx. minimum_height defaults to 0, so new sends on a stale chain still retain the prior behavior of nLockTime = 0.

    In the case where the minimum_height is ahead of the local tip, set nLockTime = 0


    molnard commented at 8:13 AM on August 26, 2026:

    Suppose the original tx nLockTime is 96400. The node goes offline for more than 8 hours, and the user calls bumpfee. The PR passes 9640 into DiscourageFeeSniping() but the chain considered stale, so the execution goes directly to tx.nLockTime = 0;. Therefore:

    original:    900,000
    replacement:       0
    

    Bicaru20 commented at 10:20 AM on August 28, 2026:

    In the case where we're fee bumping an already-nLockTimed tx, we should not reset its nLockTime back below that established minimum_height. Otherwise we fingerprint the tx. minimum_height defaults to 0, so new sends on a stale chain still retain the prior behavior of nLockTime = 0.

    The only time when this can happen is if the node is still downloading blocks or if the last block is from more than 8 horus ago. This males the block_height unrelaibale to compre and it could happen that minimum_height is greater than the block_height beacuse we don't have blockchain up to date. This could lead to setting a transaction with a locktime to 0 because even though minimum_height is less than the current block tip becasue the blockhain is not up to date. Example: We have the originstl transaction txA with locktime 12. The current tip is 15 but the node has only downloaded till 10. If we replace the transaction, that would set the locktime to 0 (since minimum_height is 12 and current_height is 10), and with that logic this would tell that the node is not up to date. Plus I do not belive that setting the rpelacement set to 0 fingerprints the transactions as there are lots of wallets than when bumping fee set the locktime to 0.


    Bicaru20 commented at 10:21 AM on August 28, 2026:

    In the case where the minimum_height is ahead of the local tip, set nLockTime = 0

    But that would disable the antifee sniping completly, wouldn't it be beeter to leave the locktime in the current_tip? That way we are still antifee sniping.


    Bicaru20 commented at 10:26 AM on August 28, 2026:

    I think that is how it should be. See my previous answer: #36040 (review)


    Bicaru20 commented at 10:29 AM on August 28, 2026:

    In the case where the minimum_height is ahead of the local tip, set nLockTime = 0

    You're right about this one, otherwise we would be setting a locktime older than the original transaction.


    danielabrozzoni commented at 2:14 PM on August 28, 2026:

    In the case where we're fee bumping an already-nLockTimed tx, we should not reset its nLockTime back below that established minimum_height. Otherwise we fingerprint the tx.

    I think this makes sense. For example:

    • Current block height is 15, we create txA with locktime = 15
    • A few more blocks come in, txA is not included in any, but we fall behind the tip
    • We want to replace txA with txB; minimum_height = 15. Here we could either: a) set txB's locktime to 0, or b) set txB's locktime to minimum_height

    If we go with a), an observer would say that we are either:

    1. using a walelt that does anti-feesniping, but sets nlocktime to 0 when feebumping (assume there are any, I'm not sure)
    2. using Bitcoin Core or equivalent, but we're not synced to the tip. In the imaginary future where we all use the same exact anti-fee sniping strategy, 1. doesn't exist, and the observer would notice we're not synced to the tip

    If we go with b), an observer would say that we are either:

    1. using a wallet that does anti-fee sniping on txs and replacements
    2. using some protocol based on presigned transactions that had a locktime set (and there's no fee sniping at all)

    The reason 2. wouldn't exist in case a) is that it's pretty weird that in a protocol that uses locktime, a txA has a locktime, but its replacement doesn't.


    molnard commented at 11:26 AM on September 1, 2026:

    I agree with the axiom of replacement.nLockTime >= original.nLockTime in any case. Good privacy strategy: do not reveal more than you already had, if possible.

    Following that, the solution of setting replacement.nLockTime = original.nLockTime preserves the information already visible in the original, while other solutions introduce a new, unusual transition - since we cannot do better because our chain is stale.

  11. in src/wallet/spend.h:193 in f26586b91d outdated
     187 | @@ -188,9 +188,11 @@ util::Result<SelectionResult> SelectCoins(const CWallet& wallet, CoinsResult& av
     188 |  
     189 |  /**
     190 |   * Set a height-based locktime for new transactions (uses the height of the
     191 | - * current chain tip unless we are not synced with the current chain
     192 | + * current chain tip unless we are not synced with the current chain.
     193 | + * The locktime is occasionally backdated, but never by more than 100 blocks,
     194 | + * and never below minimum_height.
    


    nervana21 commented at 5:12 PM on August 23, 2026:

    b7935c708edc225e20dcab0820dff774f841736b: Wallet: Do not allow bumpfee to backdate the replacement transaction lockitme

     * current chain tip unless we are not synced with the current chain).
     * The locktime is occasionally backdated, but never below minimum_height.
     * When minimum_height is 0, backdating is capped at 99 blocks from tip.
     * When minimum_height is set (bumpfee), backdating is uniform on [minimum_height, tip].
     * If the chain is not current and minimum_height is above the local tip, locktime is 0.
    
  12. nervana21 commented at 5:15 PM on August 23, 2026: contributor

    b7935c708edc225e20dcab0820dff774f841736b: Wallet: Do not allow bumpfee to backdate the replacement transaction lockitme

    nit: Commit subject and body say "lockitme". Should be "locktime".

    Commit body says "This changes change the behavior". It should be "This changes the behavior".

  13. in src/wallet/spend.cpp:1029 in b7935c708e
    1023 | @@ -1024,7 +1024,14 @@ void DiscourageFeeSniping(CMutableTransaction& tx, FastRandomContext& rng_fast,
    1024 |          // e.g. high-latency mix networks and some CoinJoin implementations, have
    1025 |          // better privacy.
    1026 |          if (rng_fast.randrange(10) == 0) {
    1027 | -            tx.nLockTime = std::max(0, int(tx.nLockTime) - int(rng_fast.randrange(100)));
    1028 | +            // If a previous locktime is passed (like in the bump fee case), the
    1029 | +            // backdating is limited between the current height and the previous locktime
    1030 | +            if (static_cast<uint32_t>(block_height) >= minimum_height) {
    


    polespinasa commented at 10:52 AM on August 25, 2026:

    in b7935c708edc225e20dcab0820dff774f841736b Wallet: Do not allow bumpfee to backdate the replacement transaction lockitme

    Is this if needed? can we have a block_height smaller than the minimum_height? I think at least we will always be at the minimum_height, I can only think of a re-org, but then we must swich to a chain with more PoW so the height will likely be higher too.

    Also I think this can be simplified into:

    $ git diff
    diff --git a/src/wallet/spend.cpp b/src/wallet/spend.cpp
    index 40eeb78f17..a34c95fcb3 100644
    --- a/src/wallet/spend.cpp
    +++ b/src/wallet/spend.cpp
    @@ -1023,15 +1023,11 @@ void DiscourageFeeSniping(CMutableTransaction& tx, FastRandomContext& rng_fast,
             // that transactions that are delayed after signing for whatever reason,
             // e.g. high-latency mix networks and some CoinJoin implementations, have
             // better privacy.
    -        if (rng_fast.randrange(10) == 0) {
    +        if (rng_fast.randrange(10) == 0 && tx.nLockTime > minimum_height) {
                 // If a previous locktime is passed (like in the bump fee case), the
                 // backdating is limited between the current height and the previous locktime
    -            if (static_cast<uint32_t>(block_height) >= minimum_height) {
    -                int locktime_range = std::min(100, int(block_height - minimum_height));
    -                if (locktime_range > 0){
    -                    tx.nLockTime = std::max(int(minimum_height), int(tx.nLockTime) - int(rng_fast.randrange(locktime_range)));
    -                }
    -            }
    +            int range = std::min(100, int(block_height - minimum_height + 1));
    +            tx.nLockTime = std::max(int(minimum_height), int(tx.nLockTime) - int(rng_fast.randrange(range)));
             }
         } else {
             // If our chain is lagging behind, we can't discourage fee sniping nor help
    sliv3r@sliv3r-tuxedo:~/Documentos/Projectes/BitcoinCore/bitcoin$ 
    
    

    This version I propose has several improvements:

    1. Removes some redundant if conditions.
    2. Fixes a missing + 1 case that you are missing by just taking block_height - minimum_height.
    3. Keeps the old behavior for the default case. Before this PR, short chains with less than a 100blocks would backdate locktime to 0, while this commit clamps it at 1.

    molnard commented at 8:24 AM on August 26, 2026:

    When minimum_height is greater, nothing happens. The transaction keeps the already assigned block_height,, which is below the declared minimum.

    This can happen, for example, if:

    • The chain has moved backward after a reorganization or manual invalidateblock.
    • The original wallet transaction was dropped or never broadcast.
    • The original transaction was created with a future height locktime.

    Bicaru20 commented at 10:22 AM on August 28, 2026:

    Is this if needed? can we have a block_height smaller than the minimum_height? I think at least we will always be at the minimum_height, I can only think of a re-org, but then we must swich to a chain with more PoW so the height will likely be higher too.

    If the original transaction has a final sequence number and a locktime greater than the current tip, the locktime is not enforced because of the sequence number, so the transaction is valid with a locktime far greater than the current tip. This would make minimum_height in the replacement greater than the current tip. This is the only case I can think of. The other solution would be to check the sequence number of the original transaction.


    Bicaru20 commented at 10:29 AM on August 28, 2026:

    Yes, you're rigth. I will fix it.

  14. in src/wallet/spend.cpp:1031 in b7935c708e
    1023 | @@ -1024,7 +1024,14 @@ void DiscourageFeeSniping(CMutableTransaction& tx, FastRandomContext& rng_fast,
    1024 |          // e.g. high-latency mix networks and some CoinJoin implementations, have
    1025 |          // better privacy.
    1026 |          if (rng_fast.randrange(10) == 0) {
    1027 | -            tx.nLockTime = std::max(0, int(tx.nLockTime) - int(rng_fast.randrange(100)));
    1028 | +            // If a previous locktime is passed (like in the bump fee case), the
    1029 | +            // backdating is limited between the current height and the previous locktime
    1030 | +            if (static_cast<uint32_t>(block_height) >= minimum_height) {
    1031 | +                int locktime_range = std::min(100, int(block_height - minimum_height));
    1032 | +                if (locktime_range > 0){
    


    polespinasa commented at 2:04 PM on August 25, 2026:

    in b7935c7 Wallet: Do not allow bumpfee to backdate the replacement transaction lockitme

    nit: again, space between ) and {

  15. in src/wallet/spend.cpp:1336 in b7935c708e
    1331 |      if (coin_control.m_locktime) {
    1332 |          txNew.nLockTime = coin_control.m_locktime.value();
    1333 |          // If we have a locktime set, we can't use anti-fee-sniping
    1334 |          use_anti_fee_sniping = false;
    1335 | +    } else if (coin_control.m_previous_locktime.has_value() && coin_control.m_previous_locktime < LOCKTIME_THRESHOLD) {
    1336 | +            minimum_height = coin_control.m_previous_locktime.value();
    


    polespinasa commented at 2:05 PM on August 25, 2026:

    in b7935c7 Wallet: Do not allow bumpfee to backdate the replacement transaction lockitme

    nit: this is over indented, there are 8 spaces instead of 4.

  16. in test/functional/wallet_bumpfee.py:858 in f26586b91d
     853 | +
     854 | +        # Replacement with higher fee_rate
     855 | +        change_addr = get_change_address(tx["txid"], wallet)[0]
     856 | +        bumped = wallet.bumpfee(txid=tx["txid"], options={"fee_rate":5, "outputs": [{change_addr: 10}]})
     857 | +
     858 | +        replaced_locktime = rbf_node.getrawtransaction(bumped["txid"],True)["locktime"]
    


    polespinasa commented at 2:07 PM on August 25, 2026:

    in f26586b91de16a03d4c265a7e8ee9fefb728dd65 Test: bumpfee does not backdate the locktime

    Missing commas between arguments, probably True con go with verbose=True so it is easier to understand.

  17. in test/functional/wallet_bumpfee.py:847 in f26586b91d outdated
     842 | +    current_height = rbf_node.getblockchaininfo()["blocks"]
     843 | +    # The original tx has an older locktime so we can differentiate when the replacement backdates
     844 | +    replaced_locktime = current_height
     845 | +
     846 | +    # Exit the loop when the locktime backdates
     847 | +    while current_height == replaced_locktime:
    


    polespinasa commented at 2:11 PM on August 25, 2026:

    in f26586b Test: bumpfee does not backdate the locktime

    The test is weak, if the backdating code is broken and it does not backdate, this loop would run forever and never fail. Probably should add a big max num of tries so if after N iterations it did not pass, we can assume the code is wrong.

    I would suggest a unit test for the function anyway, that way we can just test the backdating function.


    molnard commented at 8:56 AM on August 26, 2026:

    Small chance to that the test can pass with the unpatched code, when the first nonzero random backdate is only one or two blocks.

  18. polespinasa commented at 2:25 PM on August 25, 2026: member

    Approach ACK

    reviewed f26586b91de16a03d4c265a7e8ee9fefb728dd65

    in f26586b91de16a03d4c265a7e8ee9fefb728dd65 there as a typo in the commit message. It ends with - should be a .

  19. in src/wallet/spend.cpp:1032 in f26586b91d
    1028 | +            // If a previous locktime is passed (like in the bump fee case), the
    1029 | +            // backdating is limited between the current height and the previous locktime
    1030 | +            if (static_cast<uint32_t>(block_height) >= minimum_height) {
    1031 | +                int locktime_range = std::min(100, int(block_height - minimum_height));
    1032 | +                if (locktime_range > 0){
    1033 | +                    tx.nLockTime = std::max(int(minimum_height), int(tx.nLockTime) - int(rng_fast.randrange(locktime_range)));
    


    molnard commented at 8:35 AM on August 26, 2026:

    A true lower-bound implementation would retain the original distribution.

    Example: rng_fast.randrange(50) => 0...49.

    Therefore, the subtraction can never reach or cross minimum_height.


    Bicaru20 commented at 10:24 AM on August 28, 2026:

    Fixed in c72998bc4e

  20. in src/wallet/coincontrol.h:120 in f26586b91d outdated
     115 | @@ -116,6 +116,8 @@ class CCoinControl
     116 |      uint32_t m_version = DEFAULT_WALLET_TX_VERSION;
     117 |      //! Locktime
     118 |      std::optional<uint32_t> m_locktime;
     119 | +    //! Save the previous locktime for replacements
     120 | +    std::optional<uint32_t> m_previous_locktime;
    


    molnard commented at 8:46 AM on August 26, 2026:

    CCoinControl describes how a new transaction should be constructed - but m_previous_locktime is different. A normal transaction has no previous transaction. The concept exists only because CreateRateBumpTransaction() is constructing an RBF replacement.

    There is a hidden coupling now. The generic transaction builder must now understand a fee-bumping detail => maintenance complexity later.

    What if we try to find better ownership for this fee-bump-specific improvement? The fee-bumper already possesses both values, so it could enforce the relationship locally:

    mtx = CMutableTransaction(*txr.tx);
    
    if (!coin_control.m_locktime &&
        tx->nLockTime < LOCKTIME_THRESHOLD) {
        mtx.nLockTime =
            std::max(mtx.nLockTime, tx->nLockTime);
    }
    

    The generic builder creates a normal transaction. The fee-bumper then applies the replacement-specific invariant before signing.


    Bicaru20 commented at 10:23 AM on August 28, 2026:

    I'm not sure if doing it like this is a good idea. When backdating, if we apply this, the Discuragefeesniping will give a locktime between the [current_height, current_height-100], and most of the times this locktime will be older than the previous locktime since most of replacements are done after a few blocks. That means that we will have either a replacement transaction with a locktime set on the current_height or on the previous lockitme most of the time. If we keep the current logic, we'll have a more diverse distribution of locktimes for replacement transactions.


    molnard commented at 11:30 AM on September 1, 2026:

    Fair point—the std::max() example may not be the right implementation.

    My main point was architectural rather than about that exact implementation. What do you think about keeping this replacement-specific behavior in CreateRateBumpTransaction()? The fee-bumper owns both the original and replacement transactions and therefore knows the invariant it must enforce.

    We could still generate the locktime using the desired distribution, possibly through a shared helper, while avoiding m_previous_locktime in the generic CCoinControl and keeping the generic transaction builder unaware of RBF history.


    Bicaru20 commented at 9:18 PM on September 8, 2026:

    I'd rather keep all the anti fee sniping logic inside the Discuragefeesniping. I think this way it is easier to follow the code and not delegate part of this logic outside the function because of the RBF case. I feel that otherwise we would be duplicating code when the current approach just needs the m_previous_locktime in CCoinControl.

  21. molnard commented at 9:07 AM on August 26, 2026: none

    Concept ACK.

    I think this can be simplified by enforcing the replacement-specific invariant in CreateRateBumpTransaction() (feebumper.cpp), after creation and before signing. It avoids adding state to CCoinControl, preserves the existing random distribution, and covers stale and previous_locktime > block_height cases that the current implementation misses.

    The test appears flaky: unpatched implementation still has about a chance of passing. Could this regression be tested deterministically, perhaps at the unit level with controlled randomness?

  22. Bicaru20 force-pushed on Aug 28, 2026
  23. Bicaru20 commented at 10:24 AM on August 28, 2026: contributor

    Thanks for the reviews!

    • c72998bc4e: Fix the nits. Also applied Pol suggestion to simplify the code in Discuragefeesniping and at the same time fixing the OBO problem.
    • f911ae8345: Add verbose=True

    For now putting the pr as a draft. Seeing some of the feedback I see now that the functional test is weak and that a unit test is better. Once the unit test is completed I will open the pr again. During this time, feel free to respond to my replies to your comments so we can agree on the best approach.

  24. Bicaru20 marked this as a draft on Aug 28, 2026
  25. Wallet: Do not allow bumpfee to backdate the replacement transaction locktime
    This changes the behavior of bumpfee so when used, the replacement
    transaction doesn't have an older lockitme than the original transaction.
    Now the replacement transaction will have a locktime between the block_height
    and the locktime of the original transaction, except when the minimum_height
    is greter than block_height. This could happen if the original transaction
    has a high locktime and a sequence number that disable locktime validation.
    In this case the tx is valid because the locktime is not used.
    
    Co-Authored-By: danielabrozzoni <danielabrozzoni@protonmail.com>
    cc8f4784cf
  26. Bicaru20 force-pushed on Sep 8, 2026
  27. Test: bumpfee does not backdate the locktime
    Unit test to check that the DiscourageFeeSniping function behaves as expected
    when the minimum_height parameter is passed.
    We test that the backdating occurs between the block_height and minimum_height
    range.
    We test that when the transaction doesn't pass IsCurrentForAntiFeeSniping the
    locktime is set to minimum_height if block_height > minimum_height,
    or to 0 if block_height < minimum_height.
    
    Co-Authored-By: danielabrozzoni <danielabrozzoni@protonmail.com>
    903d2bfa67
  28. Bicaru20 force-pushed on Sep 8, 2026
  29. DrahtBot added the label CI failed on Sep 8, 2026
  30. DrahtBot commented at 10:00 PM on September 8, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task OpenBSD Cross: https://github.com/bitcoin/bitcoin/actions/runs/34280727979/job/102244563839</sub> <sub>LLM reason (✨ experimental): CI failed due to a Clang build error from -Wthread-safety-analysis/-Werror in wallet/test/spend_tests.cpp (calling chainman->ActiveChain() without exclusively holding cs_main).</sub>

    <details><summary>Hints</summary>

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

    </details>

  31. Bicaru20 commented at 10:06 PM on September 8, 2026: contributor

    Changes made:

    • cc8f4784cf9ba77fb22209adbfa9c2176b82228a: I adpoted @nervana21 suggestion. Now we don't reset the nLocktime below the established minimum_height even if we are on a stal chain. The only case when we reset the nlocktime to 0 is if the minimum_height is higher than the current block_height. In this case, we set it to 0.

    • 903d2bfa675f1acfc24e21fbbeff9ceb8b0e019e: I've eliminated the functional test and added a unit test in the spend_tests.cpp of the wallet. I added the following cases:

      1. First, the case to check that the backdating when having a minimum_height occurs between the expected range ([minimum_height, block_height]).
      2. Second, the case to check that the backdating can reach the minimum_height. This way we prove that the OBO error is not there and also check that when calculating the range in spend.cpp this is not 0 as this would cause an error later on rng_fast.randrange(range).
      3. Third, the case where the minimum_height > block_height. In this case we set the locktime to 0.
      4. Fourth, the case where we have a stale chain and minimum_height <= block_height, in this case we set the locktime to minimum_height
      5. Finally, the case where we have a stale chain and minimum_height > block_height, in this case we set the locktime to 0.
  32. Bicaru20 marked this as ready for review on Sep 8, 2026
  33. DrahtBot removed the label CI failed on Sep 8, 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-09-09 07:56 UTC