test: make p2p_eviction actually exercise block-time protection #36470

pull brunoerg wants to merge 1 commits into bitcoin:master from brunoerg:2026-10-test-p2p-eviction-block-protection changing 1 files +57 −43
  1. brunoerg commented at 11:54 AM on October 8, 2026: contributor

    p2p_eviction.py tests that 4 inbound peers that recently sent us a novel block are protected from eviction. However, the test currently passes even when that protection is not in effect.

    The block-sending peers are connected first, so they are the oldest connections. When block-time protection doesn't apply, CompareNodeBlockTime falls back to connection time and protects the 4 oldest peers, which are these same peers. Then the youngest remaining peer gets evicted. The check evicted_peers[0] not in protected_peers therefore holds whether or not block-time protection works.

    This can be shown by deleting the line where the peers send their block (L71 on master):

    block_peer.send_blocks_and_test([block], node, success=True)
    
  2. DrahtBot added the label Tests on Oct 8, 2026
  3. DrahtBot commented at 11:54 AM on October 8, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

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

    <!--006a51241073e994b41acfe9ec718e94-->

    External sites

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

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

    Type Reviewers
    ACK maflcko
    Stale ACK trakshan-mishra

    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. trakshan-mishra commented at 10:47 AM on October 9, 2026: none

    tACK c75cdce1f9430cab029a329fd86c7e6bef7bf444

    Tested on Linux.

    • with the send_blocks_and_test line removed, the test on master still passes (15/15), with this PR it fails (15/15)
    • removed the m_last_block_time check in CompareNodeBlockTime: master's test passes 10/10, this PR's test fails 10/10
    • removed the block-time EraseLastKElements call: same, master passes, this PR fails
    • unmodified, the PR's test passes 30/30
  5. bitcoin blocked a user on Oct 9, 2026
  6. maflcko commented at 2:42 PM on October 12, 2026: member

    review ACK c75cdce1f9430cab029a329fd86c7e6bef7bf444 🏝

    <details><summary>Show signature</summary>

    Signature:

    untrusted comment: signature from minisign secret key on empty file; verify via: minisign -Vm "${path_to_any_empty_file}" -P RWTRmVTMeKV5noAMqVlsMugDDCyyTSbA3Re5AkUrhvLVln0tSaFWglOw -x "${path_to_this_whole_four_line_signature_blob}"
    RUTRmVTMeKV5npGrKx1nqXCw5zeVHdtdYURB/KlyA/LMFgpNCs+SkW9a8N95d+U4AP1RJMi+krxU1A3Yux4bpwZNLvVBKy0wLgM=
    trusted comment: review ACK c75cdce1f9430cab029a329fd86c7e6bef7bf444 🏝
    iowQkSYIobxRbqT/a2g2EKx9ftguBZdUdiW7+G6JCmWAECglHd9yRj+ViTiazJ9UMQxi+75xB5lB1Ki3gSloAw==
    

    </details>

  7. fanquake requested review from instagibbs on Oct 13, 2026
  8. in test/functional/p2p_eviction.py:82 in c75cdce1f9
      78 | @@ -92,6 +79,22 @@ def run_test(self):
      79 |              current_peer += 1
      80 |              self.wait_until(lambda: "ping" in fastpeer.last_message, timeout=10)
      81 |  
      82 | +        # Connect these peers last: without block-time protection, the eviction
    


    instagibbs commented at 8:33 PM on October 13, 2026:

    Apologies for slop, but my clanker says that the regression test doesn't fail under mutants for libc++/MSVC due to sort implementation details with all peers falling under the same netgroup. Suggested alternative to make it stable, feel free to do whatever with it:

    diff --git a/test/functional/p2p_eviction.py b/test/functional/p2p_eviction.py
      index 3d50132..f20ff2a 100755
      --- a/test/functional/p2p_eviction.py
      +++ b/test/functional/p2p_eviction.py
      @@ -26,13 +26,17 @@ from test_framework.p2p import (
           P2PInterface,
       )
       from test_framework.test_framework import BitcoinTestFramework
      -from test_framework.util import assert_equal
      +from test_framework.util import (
      +    assert_equal,
      +    assert_greater_than,
      +)
       from test_framework.wallet import MiniWallet
    
    
      +# Pings slower than SlowP2PInterface, see run_test.
       class SlowP2PDataStore(P2PDataStore):
           def on_ping(self, message):
      -        time.sleep(0.1)
      +        time.sleep(0.3)
               self.send_without_ping(msg_pong(message.nonce))
    
    
      @@ -58,6 +62,12 @@ class P2PEvict(BitcoinTestFramework):
               node = self.nodes[0]
               self.wallet = MiniWallet(node)
    
      +        # Netgroup protection picks 4 arbitrary peers here (all peers share 
      one netgroup,
      +        # so the choice depends on the std::sort implementation). Whatever it
      takes from
      +        # another group is made up from the eviction candidates: they connect
      first, so
      +        # tx- and block-time protection fall back to them as the oldest 
      connections, and
      +        # they ping faster than the tx and block peers. Removing any 
      protection then gets
      +        # one of its peers evicted, unless netgroup protection covered all of
      them.
               self.log.info("Create 5 slow-pinging peers, making them eviction
      candidates")
               for _ in range(5):
                   node.add_p2p_connection(SlowP2PInterface())
      @@ -65,23 +75,15 @@ class P2PEvict(BitcoinTestFramework):
    
               self.log.info("Create 4 peers and protect them from eviction by
      sending us a tx")
               for i in range(4):
      -            txpeer = node.add_p2p_connection(SlowP2PInterface())
      +            txpeer = node.add_p2p_connection(SlowP2PDataStore())
                   current_peer += 1
                   txpeer.sync_with_ping()
    
                   tx = self.wallet.create_self_transfer()['tx']
      -            txpeer.send_without_ping(msg_tx(tx))
      +            txpeer.send_and_ping(msg_tx(tx))
      +            
      assert_greater_than(node.getpeerinfo()[current_peer]["last_transaction"], 0)
                   protected_peers.add(current_peer)
    
      -        self.log.info("Create 8 peers and protect them from eviction by 
      having faster pings")
      -        for _ in range(8):
      -            fastpeer = node.add_p2p_connection(P2PInterface())
      -            current_peer += 1
      -            self.wait_until(lambda: "ping" in fastpeer.last_message, 
      timeout=10)
      -
      -        # Connect these peers last: without block-time protection, the 
      eviction
      -        # logic would fall back to protecting older connections and evict the
      -        # youngest one, so only a working block-time protection keeps them 
      connected.
               self.log.info("Create 4 peers and protect them from eviction by
      sending us a block")
               for _ in range(4):
                   block_peer = node.add_p2p_connection(SlowP2PDataStore())
      @@ -93,13 +95,21 @@ class P2PEvict(BitcoinTestFramework):
                   block = create_block(tip, height=node.getblockcount() + 1,
      ntime=best_block_time + 1)
                   block.solve()
                   block_peer.send_blocks_and_test([block], node, success=True)
      +            
      assert_greater_than(node.getpeerinfo()[current_peer]["last_block"], 0)
                   protected_peers.add(current_peer)
    
      +        self.log.info("Create 8 peers and protect them from eviction by 
      having faster pings")
      +        for _ in range(8):
      +            fastpeer = node.add_p2p_connection(P2PInterface())
      +            current_peer += 1
      +            self.wait_until(lambda: "ping" in fastpeer.last_message, 
      timeout=10)
      +
               # Make sure by asking the node what the actual min pings are
      +        self.wait_until(lambda: all("minping" in p for p in 
      node.getpeerinfo()))
               peerinfo = node.getpeerinfo()
               pings = {}
               for i in range(len(peerinfo)):
      -            pings[i] = peerinfo[i]['minping'] if 'minping' in peerinfo[i] 
      else 1000000
      +            pings[i] = peerinfo[i]['minping']
               sorted_pings = sorted(pings.items(), key=lambda x: x[1])
    
               # Usually the 8 fast peers are protected. In rare case of unreliable
      pings,
    

    brunoerg commented at 11:59 PM on October 13, 2026:

    That makes sense, I will take a better look tomorrow morning. I think I can also reproduce the libc++/MSVC behavior by replacing that to std::stable_sort.


    brunoerg commented at 3:25 PM on October 14, 2026:

    Addressed your suggestion, it makes sense. I Just removed the per-fast-peer ping wait because they have their own minping wait so we may not need it, and improved some comments.

  9. brunoerg force-pushed on Oct 14, 2026
  10. brunoerg commented at 3:23 PM on October 14, 2026: contributor

    Force-pushed addressing #36470 (review) to cover the libc++/MSVC case.

  11. in test/functional/p2p_eviction.py:107 in f9cdfacdb4
     119 | +            node.add_p2p_connection(P2PInterface())
     120 |              current_peer += 1
     121 | -            self.wait_until(lambda: "ping" in fastpeer.last_message, timeout=10)
     122 |  
     123 |          # Make sure by asking the node what the actual min pings are
     124 | +        self.wait_until(lambda: all("minping" in p for p in node.getpeerinfo()))
    


    Crypt-iQ commented at 1:36 PM on October 16, 2026:

    note: this fixes an odd scheduling flake that bedrock ran into where "ping" in fastpeer.last_message succeeds but hasn't arrived yet and so the fast peer gets a value of 1000000

  12. test: make p2p_eviction actually exercise block-time protection
    The 4 peers that send us a block were connected first, making them the
    oldest connections. When block-time protection doesn't apply,
    CompareNodeBlockTime falls through to connection time and protects the
    oldest peers anyway, and the youngest remaining peer gets evicted. So the
    test kept passing with the block-time protection removed from
    SelectNodeToEvict, or with the peers never sending a block.
    
    Simply connecting the block peers last is not enough: netgroup protection
    runs first and, since all test peers share one netgroup, which 4 peers it
    protects depends on the std::sort implementation. With libc++ and MSVC,
    which use insertion sort for small inputs, it keeps the 4 youngest peers.
    
    Make the result independent of which peers netgroup protection takes:
    connect the eviction candidates first and give them a lower ping than the
    tx and block peers, so every protection step (ping, tx-time, block-time)
    makes up any slots taken from its own peers with eviction candidates.
    Removing any of these protections now gets one of its peers evicted.
    2fd9be21e2
  13. in test/functional/p2p_eviction.py:39 in f9cdfacdb4
      36 |  
      37 | +# Pings slower than SlowP2PInterface, so the tx and block peers are never protected by ping.
      38 |  class SlowP2PDataStore(P2PDataStore):
      39 |      def on_ping(self, message):
      40 | -        time.sleep(0.1)
      41 | +        time.sleep(0.3)
    


    maflcko commented at 4:10 PM on October 16, 2026:

    not sure this is reliable. the tests run also in qemu in vms, which may be slow or racy. Also, if someone runs them in bedrock or similar, this may not hold, so the test may fail? Manual sleeps also seem smelly. I think it would be better to replace this with a bumpmocktime(2), and bump(1) in the fast case, so that this is fully deterministic regarless of test env?


    brunoerg commented at 1:20 PM on October 19, 2026:

    Yes, I can change it to control the ping time with mocktime. Basically we can add a add_node helper that connects a peer and use mocktime to make the node to have a minimum ping of exactly ping_time seconds, then we can move the clock forward so each peer gets its own connection time.


    maflcko commented at 1:42 PM on October 19, 2026:

    I think add_p2p_connection with default args is already synchronous, so there should be no race with other connections, and a plain bumpmocktime(2) should work, but I haven't tried this myself. Maybe a helper makes more sense. Happy to review whatever falls out


    brunoerg commented at 1:53 PM on October 19, 2026:

    Going to try but I think bumpmocktime may not be enough, the interface would answer the ping during the handshake before we can bump.


    maflcko commented at 2:02 PM on October 19, 2026:

    I meant that the interface does the bump (in the line I commented here), but anything that works should be fine here.


    brunoerg commented at 2:15 PM on October 19, 2026:

    Done.

  14. brunoerg force-pushed on Oct 19, 2026
  15. brunoerg commented at 2:15 PM on October 19, 2026: contributor

    Force-pushed addressing #36470 (review)

  16. in test/functional/p2p_eviction.py:62 in 2fd9be21e2
      58 | @@ -52,60 +59,67 @@ def set_test_params(self):
      59 |          # 4 by netgroup, 4 that sent us blocks, 4 that sent us transactions and 8 via lowest ping time
      60 |          self.extra_args = [['-maxconnections=53']]
      61 |  
      62 | +    def add_peer(self, peer, ping_time):
    


    maflcko commented at 2:02 PM on October 20, 2026:

    nano-nit: Int options should be kw-only:

        def add_peer(self, peer, *, ping_time):
    

    brunoerg commented at 10:22 AM on October 26, 2026:

    Tks, will leave as-is, but can change in case I need to touch it again.

  17. maflcko commented at 2:54 PM on October 20, 2026: member

    Thanks for taking the nits. The test should now be deterministic and twice as fast with mocktime.

    review ACK 2fd9be21e28fecf298493e28e90ccb8085aa4ae8 🗜

    <details><summary>Show signature</summary>

    Signature:

    untrusted comment: signature from minisign secret key on empty file; verify via: minisign -Vm "${path_to_any_empty_file}" -P RWTRmVTMeKV5noAMqVlsMugDDCyyTSbA3Re5AkUrhvLVln0tSaFWglOw -x "${path_to_this_whole_four_line_signature_blob}"
    RUTRmVTMeKV5npGrKx1nqXCw5zeVHdtdYURB/KlyA/LMFgpNCs+SkW9a8N95d+U4AP1RJMi+krxU1A3Yux4bpwZNLvVBKy0wLgM=
    trusted comment: review ACK 2fd9be21e28fecf298493e28e90ccb8085aa4ae8 🗜
    8H6NZsZgG/T/YvKzHx9lVC7RLKCKwDlJF4+uICAIg7YRM00chlrllnuxY/I1AF9FZHQIP6IaZsyiu/CJDfhpAg==
    

    </details>

  18. maflcko approved
  19. maflcko commented at 2:54 PM on October 20, 2026: member

    .

  20. maflcko commented at 3:07 PM on October 20, 2026: member

    Ralph complains, but to make it happy, one would have to first connect candidates peers, then connect shuffle(tx, block, fast-ping)? Not sure if this is worth it or if it makes Ralph happy. Up to you. This is a solid improvement either way.

  21. fanquake requested review from instagibbs on Oct 21, 2026
  22. brunoerg commented at 10:22 AM on October 26, 2026: contributor

    Ralph complains, but to make it happy, one would have to first connect candidates peers, then connect shuffle(tx, block, fast-ping)? Not sure if this is worth it or if it makes Ralph happy. Up to you. This is a solid improvement either way.

    I'm not sure is worth, will leave as-is for now unless other reviewers prefer that.


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-10-29 06:51 UTC

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