p2p_fingerprint.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2017-2021 The Limenka developers
   3  # Distributed under the MIT software license, see the accompanying
   4  # file COPYING or http://www.opensource.org/licenses/mit-license.php.
   5  """Test various fingerprinting protections.
   6  
   7  If a stale block more than a month old or its header are requested by a peer,
   8  the node should pretend that it does not have it to avoid fingerprinting.
   9  """
  10  
  11  import time
  12  
  13  from test_framework.blocktools import (create_block, create_coinbase)
  14  from test_framework.messages import CInv, MSG_BLOCK
  15  from test_framework.p2p import (
  16      P2PInterface,
  17      msg_headers,
  18      msg_block,
  19      msg_getdata,
  20      msg_getheaders,
  21      p2p_lock,
  22  )
  23  from test_framework.test_framework import LimenkaTestFramework
  24  from test_framework.util import (
  25      assert_equal,
  26  )
  27  
  28  
  29  class P2PFingerprintTest(LimenkaTestFramework):
  30      def set_test_params(self):
  31          self.setup_clean_chain = True
  32          self.num_nodes = 1
  33  
  34      # Build a chain of blocks on top of given one
  35      def build_chain(self, nblocks, prev_hash, prev_height, prev_median_time):
  36          blocks = []
  37          for _ in range(nblocks):
  38              coinbase = create_coinbase(prev_height + 1)
  39              block_time = prev_median_time + 1
  40              block = create_block(int(prev_hash, 16), coinbase, block_time)
  41              block.solve()
  42  
  43              blocks.append(block)
  44              prev_hash = block.hash
  45              prev_height += 1
  46              prev_median_time = block_time
  47          return blocks
  48  
  49      # Send a getdata request for a given block hash
  50      def send_block_request(self, block_hash, node):
  51          msg = msg_getdata()
  52          msg.inv.append(CInv(MSG_BLOCK, block_hash))
  53          node.send_message(msg)
  54  
  55      # Send a getheaders request for a given single block hash
  56      def send_header_request(self, block_hash, node):
  57          msg = msg_getheaders()
  58          msg.hashstop = block_hash
  59          node.send_message(msg)
  60  
  61      # Checks that stale blocks timestamped more than a month ago are not served
  62      # by the node while recent stale blocks and old active chain blocks are.
  63      # This does not currently test that stale blocks timestamped within the
  64      # last month but that have over a month's worth of work are also withheld.
  65      def run_test(self):
  66          node0 = self.nodes[0].add_p2p_connection(P2PInterface())
  67  
  68          # Set node time to 60 days ago
  69          self.nodes[0].setmocktime(int(time.time()) - 60 * 24 * 60 * 60)
  70  
  71          # Generating a chain of 10 blocks
  72          block_hashes = self.generatetoaddress(self.nodes[0], 10, self.nodes[0].get_deterministic_priv_key().address)
  73  
  74          # Create longer chain starting 2 blocks before current tip
  75          height = len(block_hashes) - 2
  76          block_hash = block_hashes[height - 1]
  77          block_time = self.nodes[0].getblockheader(block_hash)["mediantime"] + 1
  78          new_blocks = self.build_chain(5, block_hash, height, block_time)
  79  
  80          # Force reorg to a longer chain
  81          node0.send_message(msg_headers(new_blocks))
  82          node0.wait_for_getdata([x.sha256 for x in new_blocks])
  83          for block in new_blocks:
  84              node0.send_and_ping(msg_block(block))
  85  
  86          # Check that reorg succeeded
  87          assert_equal(self.nodes[0].getblockcount(), 13)
  88  
  89          stale_hash = int(block_hashes[-1], 16)
  90  
  91          # Check that getdata request for stale block succeeds
  92          self.send_block_request(stale_hash, node0)
  93          node0.wait_for_block(stale_hash, timeout=3)
  94  
  95          # Check that getheader request for stale block header succeeds
  96          self.send_header_request(stale_hash, node0)
  97          node0.wait_for_header(hex(stale_hash), timeout=3)
  98  
  99          # Longest chain is extended so stale is much older than chain tip
 100          self.nodes[0].setmocktime(0)
 101          block_hash = int(self.generatetoaddress(self.nodes[0], 1, self.nodes[0].get_deterministic_priv_key().address)[-1], 16)
 102          assert_equal(self.nodes[0].getblockcount(), 14)
 103          node0.wait_for_block(block_hash, timeout=3)
 104  
 105          # Request for very old stale block should now fail
 106          with p2p_lock:
 107              node0.last_message.pop("block", None)
 108          self.send_block_request(stale_hash, node0)
 109          node0.sync_with_ping()
 110          assert "block" not in node0.last_message
 111  
 112          # Request for very old stale block header should now fail
 113          with p2p_lock:
 114              node0.last_message.pop("headers", None)
 115          self.send_header_request(stale_hash, node0)
 116          node0.sync_with_ping()
 117          assert "headers" not in node0.last_message
 118  
 119          # Verify we can fetch very old blocks and headers on the active chain
 120          block_hash = int(block_hashes[2], 16)
 121          self.send_block_request(block_hash, node0)
 122          self.send_header_request(block_hash, node0)
 123          node0.sync_with_ping()
 124  
 125          self.send_block_request(block_hash, node0)
 126          node0.wait_for_block(block_hash, timeout=3)
 127  
 128          self.send_header_request(block_hash, node0)
 129          node0.wait_for_header(hex(block_hash), timeout=3)
 130  
 131  
 132  if __name__ == '__main__':
 133      P2PFingerprintTest(__file__).main()
 134