Skip to content

Commit e25de33

Browse files
committed
Merge bitcoin/bitcoin#26341: test: add BIP158 false-positive element check in rpc_scanblocks.py
fa54d30 test: check for false-positives in rpc_scanblocks.py (Sebastian Falbesoner) 3bca6cd test: add compact block filter (BIP158) helper routines (Sebastian Falbesoner) 25ee74d test: add SipHash implementation for generic data in Python (Sebastian Falbesoner) Pull request description: This PR adds a fixed false-positive element check to the functional test rpc_scanblocks.py by using a pre-calculated scriptPubKey that collides with the regtest genesis block's coinbase output. Note that determining a BIP158 false-positive at runtime would also be possible, but take too long (we'd need to create and check ~800k output scripts on average, which took at least 2 minutes on average on my machine). The introduced check is related to issue #26322 and more concretely inspired by PR #26325 which introduces an "accurate" mode that filters out these false-positives. The introduced cryptography routines (siphash for generic data) and helpers (BIP158 ranged hash calculation, relevant scriptPubKey per block determination) could potentially also be useful for more tests in the future that involve compact block filters. ACKs for top commit: achow101: ACK fa54d30 Tree-SHA512: c6af50864146028228d197fb022ba2ff24d1ef48dc7d171bccfb21e62dd50ac80db5fae0c53f5d205edabd48b3493c7aa2040f628a223e68df086ec2243e5a93
2 parents 88502ec + fa54d30 commit e25de33

File tree

3 files changed

+104
-27
lines changed

3 files changed

+104
-27
lines changed

test/functional/rpc_scanblocks.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
# Distributed under the MIT software license, see the accompanying
44
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
55
"""Test the scanblocks RPC call."""
6+
from test_framework.blockfilter import (
7+
bip158_basic_element_hash,
8+
bip158_relevant_scriptpubkeys,
9+
)
610
from test_framework.messages import COIN
711
from test_framework.test_framework import BitcoinTestFramework
812
from test_framework.util import (
@@ -71,6 +75,28 @@ def run_test(self):
7175
assert(blockhash in node.scanblocks(
7276
"start", [{"desc": f"pkh({parent_key}/*)", "range": [0, 100]}], height)['relevant_blocks'])
7377

78+
# check that false-positives are included in the result now; note that
79+
# finding a false-positive at runtime would take too long, hence we simply
80+
# use a pre-calculated one that collides with the regtest genesis block's
81+
# coinbase output and verify that their BIP158 ranged hashes match
82+
genesis_blockhash = node.getblockhash(0)
83+
genesis_spks = bip158_relevant_scriptpubkeys(node, genesis_blockhash)
84+
assert_equal(len(genesis_spks), 1)
85+
genesis_coinbase_spk = list(genesis_spks)[0]
86+
false_positive_spk = bytes.fromhex("001400000000000000000000000000000000000cadcb")
87+
88+
genesis_coinbase_hash = bip158_basic_element_hash(genesis_coinbase_spk, 1, genesis_blockhash)
89+
false_positive_hash = bip158_basic_element_hash(false_positive_spk, 1, genesis_blockhash)
90+
assert_equal(genesis_coinbase_hash, false_positive_hash)
91+
92+
assert(genesis_blockhash in node.scanblocks(
93+
"start", [{"desc": f"raw({genesis_coinbase_spk.hex()})"}], 0, 0)['relevant_blocks'])
94+
assert(genesis_blockhash in node.scanblocks(
95+
"start", [{"desc": f"raw({false_positive_spk.hex()})"}], 0, 0)['relevant_blocks'])
96+
97+
# TODO: after an "accurate" mode for scanblocks is implemented (e.g. PR #26325)
98+
# check here that it filters out the false-positive
99+
74100
# test node with disabled blockfilterindex
75101
assert_raises_rpc_error(-1, "Index is not enabled for filtertype basic",
76102
self.nodes[1].scanblocks, "start", [f"addr({addr_1})"])
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2022 The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
"""Helper routines relevant for compact block filters (BIP158).
6+
"""
7+
from .siphash import siphash
8+
9+
10+
def bip158_basic_element_hash(script_pub_key, N, block_hash):
11+
""" Calculates the ranged hash of a filter element as defined in BIP158:
12+
13+
'The first step in the filter construction is hashing the variable-sized
14+
raw items in the set to the range [0, F), where F = N * M.'
15+
16+
'The items are first passed through the pseudorandom function SipHash, which takes a
17+
128-bit key k and a variable-sized byte vector and produces a uniformly random 64-bit
18+
output. Implementations of this BIP MUST use the SipHash parameters c = 2 and d = 4.'
19+
20+
'The parameter k MUST be set to the first 16 bytes of the hash (in standard
21+
little-endian representation) of the block for which the filter is constructed. This
22+
ensures the key is deterministic while still varying from block to block.'
23+
"""
24+
M = 784931
25+
block_hash_bytes = bytes.fromhex(block_hash)[::-1]
26+
k0 = int.from_bytes(block_hash_bytes[0:8], 'little')
27+
k1 = int.from_bytes(block_hash_bytes[8:16], 'little')
28+
return (siphash(k0, k1, script_pub_key) * (N * M)) >> 64
29+
30+
31+
def bip158_relevant_scriptpubkeys(node, block_hash):
32+
""" Determines the basic filter relvant scriptPubKeys as defined in BIP158:
33+
34+
'A basic filter MUST contain exactly the following items for each transaction in a block:
35+
- The previous output script (the script being spent) for each input, except for
36+
the coinbase transaction.
37+
- The scriptPubKey of each output, aside from all OP_RETURN output scripts.'
38+
"""
39+
spks = set()
40+
for tx in node.getblock(blockhash=block_hash, verbosity=3)['tx']:
41+
# gather prevout scripts
42+
for i in tx['vin']:
43+
if 'prevout' in i:
44+
spks.add(bytes.fromhex(i['prevout']['scriptPubKey']['hex']))
45+
# gather output scripts
46+
for o in tx['vout']:
47+
if o['scriptPubKey']['type'] != 'nulldata':
48+
spks.add(bytes.fromhex(o['scriptPubKey']['hex']))
49+
return spks
Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
#!/usr/bin/env python3
2-
# Copyright (c) 2016-2018 The Bitcoin Core developers
2+
# Copyright (c) 2016-2022 The Bitcoin Core developers
33
# Distributed under the MIT software license, see the accompanying
44
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5-
"""Specialized SipHash-2-4 implementations.
5+
"""SipHash-2-4 implementation.
66
7-
This implements SipHash-2-4 for 256-bit integers.
7+
This implements SipHash-2-4. For convenience, an interface taking 256-bit
8+
integers is provided in addition to the one accepting generic data.
89
"""
910

1011
def rotl64(n, b):
1112
return n >> (64 - b) | (n & ((1 << (64 - b)) - 1)) << b
1213

14+
1315
def siphash_round(v0, v1, v2, v3):
1416
v0 = (v0 + v1) & ((1 << 64) - 1)
1517
v1 = rotl64(v1, 13)
@@ -27,37 +29,37 @@ def siphash_round(v0, v1, v2, v3):
2729
v2 = rotl64(v2, 32)
2830
return (v0, v1, v2, v3)
2931

30-
def siphash256(k0, k1, h):
31-
n0 = h & ((1 << 64) - 1)
32-
n1 = (h >> 64) & ((1 << 64) - 1)
33-
n2 = (h >> 128) & ((1 << 64) - 1)
34-
n3 = (h >> 192) & ((1 << 64) - 1)
32+
33+
def siphash(k0, k1, data):
34+
assert(type(data) == bytes)
3535
v0 = 0x736f6d6570736575 ^ k0
3636
v1 = 0x646f72616e646f6d ^ k1
3737
v2 = 0x6c7967656e657261 ^ k0
38-
v3 = 0x7465646279746573 ^ k1 ^ n0
39-
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
40-
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
41-
v0 ^= n0
42-
v3 ^= n1
43-
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
44-
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
45-
v0 ^= n1
46-
v3 ^= n2
38+
v3 = 0x7465646279746573 ^ k1
39+
c = 0
40+
t = 0
41+
for d in data:
42+
t |= d << (8 * (c % 8))
43+
c = (c + 1) & 0xff
44+
if (c & 7) == 0:
45+
v3 ^= t
46+
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
47+
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
48+
v0 ^= t
49+
t = 0
50+
t = t | (c << 56)
51+
v3 ^= t
4752
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
4853
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
49-
v0 ^= n2
50-
v3 ^= n3
51-
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
52-
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
53-
v0 ^= n3
54-
v3 ^= 0x2000000000000000
55-
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
56-
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
57-
v0 ^= 0x2000000000000000
58-
v2 ^= 0xFF
54+
v0 ^= t
55+
v2 ^= 0xff
5956
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
6057
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
6158
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
6259
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
6360
return v0 ^ v1 ^ v2 ^ v3
61+
62+
63+
def siphash256(k0, k1, num):
64+
assert(type(num) == int)
65+
return siphash(k0, k1, num.to_bytes(32, 'little'))

0 commit comments

Comments
 (0)