mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-01 22:39:03 -07:00
website_next: move hash logic to clients
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
* [series\_endpoint](#brk_client.BrkClient.series_endpoint)
|
||||
* [index\_to\_date](#brk_client.BrkClient.index_to_date)
|
||||
* [date\_to\_index](#brk_client.BrkClient.date_to_index)
|
||||
* [address\_payload\_hash\_prefix](#brk_client.BrkClient.address_payload_hash_prefix)
|
||||
* [get\_health](#brk_client.BrkClient.get_health)
|
||||
* [get\_version](#brk_client.BrkClient.get_version)
|
||||
* [get\_sync\_status](#brk_client.BrkClient.get_sync_status)
|
||||
@@ -47,6 +48,7 @@
|
||||
* [get\_difficulty\_adjustment](#brk_client.BrkClient.get_difficulty_adjustment)
|
||||
* [get\_prices](#brk_client.BrkClient.get_prices)
|
||||
* [get\_historical\_price](#brk_client.BrkClient.get_historical_price)
|
||||
* [get\_address\_payload\_hash\_prefix\_matches](#brk_client.BrkClient.get_address_payload_hash_prefix_matches)
|
||||
* [get\_address\_hash\_prefix\_matches](#brk_client.BrkClient.get_address_hash_prefix_matches)
|
||||
* [get\_address](#brk_client.BrkClient.get_address)
|
||||
* [get\_address\_txs](#brk_client.BrkClient.get_address_txs)
|
||||
@@ -255,6 +257,17 @@ def date_to_index(index: Index, d: Union[date, datetime]) -> int
|
||||
|
||||
Convert a date/datetime to an index value for date-based indexes.
|
||||
|
||||
<a id="brk_client.BrkClient.address_payload_hash_prefix"></a>
|
||||
|
||||
#### address\_payload\_hash\_prefix
|
||||
|
||||
```python
|
||||
def address_payload_hash_prefix(
|
||||
payload: Union[bytes, bytearray, memoryview], nibbles: int) -> str
|
||||
```
|
||||
|
||||
Compute the RapidHash v3 hash-prefix for raw address payload bytes.
|
||||
|
||||
<a id="brk_client.BrkClient.get_health"></a>
|
||||
|
||||
#### get\_health
|
||||
@@ -607,6 +620,19 @@ Get historical BTC/USD price. Optionally specify a UNIX timestamp to get the pri
|
||||
|
||||
Endpoint: `GET /api/v1/historical-price`
|
||||
|
||||
<a id="brk_client.BrkClient.get_address_payload_hash_prefix_matches"></a>
|
||||
|
||||
#### get\_address\_payload\_hash\_prefix\_matches
|
||||
|
||||
```python
|
||||
def get_address_payload_hash_prefix_matches(
|
||||
addr_type: OutputType,
|
||||
payload: Union[bytes, bytearray, memoryview],
|
||||
nibbles: int) -> AddrHashPrefixMatches
|
||||
```
|
||||
|
||||
Fetch address hash-prefix matches from raw payload bytes matching `addr_type` length.
|
||||
|
||||
<a id="brk_client.BrkClient.get_address_hash_prefix_matches"></a>
|
||||
|
||||
#### get\_address\_hash\_prefix\_matches
|
||||
@@ -618,7 +644,7 @@ def get_address_hash_prefix_matches(addr_type: OutputType,
|
||||
|
||||
Address hash-prefix matches.
|
||||
|
||||
Find addresses by address type and address-payload hash prefix. Intended for privacy-preserving client-side wallet discovery without sending raw addresses or xpubs. Fetch metadata for the returned addresses through `/api/address/{address}`.
|
||||
Find addresses by address type and by the first 1-16 hex nibbles of RapidHash v3 over the raw address payload bytes. Intended for privacy-preserving client-side wallet discovery without sending raw addresses or xpubs. Fetch metadata for the returned addresses through `/api/address/{address}`.
|
||||
|
||||
Endpoint: `GET /api/address/hash-prefix/{addr_type}/{prefix}`
|
||||
|
||||
@@ -1769,4 +1795,3 @@ Compact OpenAPI specification.
|
||||
Compact OpenAPI specification optimized for LLM consumption. Removes redundant fields while preserving essential API information. Full spec available at `/openapi.json`.
|
||||
|
||||
Endpoint: `GET /api.json`
|
||||
|
||||
|
||||
@@ -1912,6 +1912,124 @@ def _p(prefix: str, acc: str) -> str:
|
||||
return f"{prefix}_{acc}" if acc else prefix
|
||||
|
||||
|
||||
|
||||
_MASK_64 = (1 << 64) - 1
|
||||
_RAPIDHASH_SECRETS = (
|
||||
0x2d358dccaa6c78a5,
|
||||
0x8bb84b93962eacc9,
|
||||
0x4b33a62ed433d4a3,
|
||||
0x4d5a2da51de1aa47,
|
||||
0xa0761d6478bd642f,
|
||||
0xe7037ed1a0b428db,
|
||||
0x90ed1765281c388c,
|
||||
)
|
||||
_RAPIDHASH_SEED = 0
|
||||
|
||||
|
||||
def _u64(value: int) -> int:
|
||||
return value & _MASK_64
|
||||
|
||||
|
||||
def _rapid_mix(left: int, right: int) -> int:
|
||||
result = _u64(left) * _u64(right)
|
||||
return _u64(result) ^ _u64(result >> 64)
|
||||
|
||||
|
||||
def _rapid_mum(left: int, right: int) -> Tuple[int, int]:
|
||||
result = _u64(left) * _u64(right)
|
||||
return _u64(result), _u64(result >> 64)
|
||||
|
||||
|
||||
def _rapid_hash_seed(seed: int) -> int:
|
||||
return _u64(seed ^ _rapid_mix(seed ^ _RAPIDHASH_SECRETS[2], _RAPIDHASH_SECRETS[1]))
|
||||
|
||||
|
||||
_RAPIDHASH_SEED = _rapid_hash_seed(0)
|
||||
|
||||
|
||||
def _read_u32(data: bytes, offset: int) -> int:
|
||||
return int.from_bytes(data[offset:offset + 4], "little")
|
||||
|
||||
|
||||
def _read_u64(data: bytes, offset: int) -> int:
|
||||
return int.from_bytes(data[offset:offset + 8], "little")
|
||||
|
||||
|
||||
def _rapid_hash_v3(payload: Union[bytes, bytearray, memoryview]) -> int:
|
||||
data = bytes(payload)
|
||||
length = len(data)
|
||||
if length == 0:
|
||||
raise ValueError("Expected a non-empty address payload")
|
||||
if length > 65:
|
||||
raise ValueError("Expected at most 65 address payload bytes")
|
||||
|
||||
seed = _RAPIDHASH_SEED
|
||||
a = 0
|
||||
b = 0
|
||||
|
||||
if length <= 16:
|
||||
if length >= 4:
|
||||
seed ^= length
|
||||
if length >= 8:
|
||||
a ^= _read_u64(data, 0)
|
||||
b ^= _read_u64(data, length - 8)
|
||||
else:
|
||||
a ^= _read_u32(data, 0)
|
||||
b ^= _read_u32(data, length - 4)
|
||||
elif length > 0:
|
||||
a ^= (data[0] << 45) | data[length - 1]
|
||||
b ^= data[length >> 1]
|
||||
remainder = length
|
||||
else:
|
||||
if length > 16:
|
||||
seed = _rapid_mix(_read_u64(data, 0) ^ _RAPIDHASH_SECRETS[2], _read_u64(data, 8) ^ seed)
|
||||
if length > 32:
|
||||
seed = _rapid_mix(_read_u64(data, 16) ^ _RAPIDHASH_SECRETS[2], _read_u64(data, 24) ^ seed)
|
||||
if length > 48:
|
||||
seed = _rapid_mix(_read_u64(data, 32) ^ _RAPIDHASH_SECRETS[1], _read_u64(data, 40) ^ seed)
|
||||
if length > 64:
|
||||
seed = _rapid_mix(_read_u64(data, 48) ^ _RAPIDHASH_SECRETS[1], _read_u64(data, 56) ^ seed)
|
||||
remainder = length
|
||||
a ^= _read_u64(data, length - 16) ^ remainder
|
||||
b ^= _read_u64(data, length - 8)
|
||||
|
||||
a ^= _RAPIDHASH_SECRETS[1]
|
||||
b ^= seed
|
||||
a, b = _rapid_mum(a, b)
|
||||
return _rapid_mix(a ^ 0xaaaaaaaaaaaaaaaa, b ^ _RAPIDHASH_SECRETS[1] ^ remainder)
|
||||
|
||||
|
||||
def _validate_hash_prefix_nibbles(nibbles: int) -> None:
|
||||
if isinstance(nibbles, bool) or not isinstance(nibbles, int) or nibbles < 1 or nibbles > 16:
|
||||
raise ValueError("Expected hash-prefix length from 1 to 16 hex nibbles")
|
||||
|
||||
|
||||
def _address_payload_lengths(addr_type: OutputType) -> Tuple[int, ...]:
|
||||
if addr_type == "p2a":
|
||||
return (2,)
|
||||
if addr_type == "p2pk":
|
||||
return (33, 65)
|
||||
if addr_type in ("p2pkh", "p2sh", "v0_p2wpkh"):
|
||||
return (20,)
|
||||
if addr_type in ("v0_p2wsh", "v1_p2tr"):
|
||||
return (32,)
|
||||
raise ValueError(f"Unsupported address type for address payload hash-prefix: {addr_type}")
|
||||
|
||||
|
||||
def _validate_address_payload_for_type(addr_type: OutputType, payload: Union[bytes, bytearray, memoryview]) -> None:
|
||||
length = len(bytes(payload))
|
||||
expected = _address_payload_lengths(addr_type)
|
||||
if length not in expected:
|
||||
joined = " or ".join(str(value) for value in expected)
|
||||
raise ValueError(f"Expected {addr_type} address payload length {joined} bytes")
|
||||
|
||||
|
||||
def address_payload_hash_prefix(payload: Union[bytes, bytearray, memoryview], nibbles: int) -> str:
|
||||
"""Compute the RapidHash v3 hash-prefix used by `/api/address/hash-prefix/{addr_type}/{prefix}`."""
|
||||
_validate_hash_prefix_nibbles(nibbles)
|
||||
return f"{_rapid_hash_v3(payload):016x}"[:nibbles]
|
||||
|
||||
|
||||
# Date conversion constants
|
||||
_GENESIS = date(2009, 1, 3) # day1 0, week1 0
|
||||
_DAY_ONE = date(2009, 1, 9) # day1 1 (6 day gap after genesis)
|
||||
@@ -8201,6 +8319,21 @@ class BrkClient(BrkClientBase):
|
||||
"""Convert a date/datetime to an index value for date-based indexes."""
|
||||
return _date_to_index(index, d)
|
||||
|
||||
@staticmethod
|
||||
def address_payload_hash_prefix(payload: Union[bytes, bytearray, memoryview], nibbles: int) -> str:
|
||||
"""Compute the RapidHash v3 hash-prefix for raw address payload bytes."""
|
||||
return address_payload_hash_prefix(payload, nibbles)
|
||||
|
||||
def get_address_payload_hash_prefix_matches(
|
||||
self,
|
||||
addr_type: OutputType,
|
||||
payload: Union[bytes, bytearray, memoryview],
|
||||
nibbles: int,
|
||||
) -> AddrHashPrefixMatches:
|
||||
"""Fetch address hash-prefix matches from raw payload bytes matching addr_type length."""
|
||||
_validate_address_payload_for_type(addr_type, payload)
|
||||
return self.get_address_hash_prefix_matches(addr_type, address_payload_hash_prefix(payload, nibbles))
|
||||
|
||||
def get_health(self) -> Health:
|
||||
"""Health check.
|
||||
|
||||
@@ -8449,7 +8582,7 @@ class BrkClient(BrkClientBase):
|
||||
def get_address_hash_prefix_matches(self, addr_type: OutputType, prefix: str) -> AddrHashPrefixMatches:
|
||||
"""Address hash-prefix matches.
|
||||
|
||||
Find addresses by address type and address-payload hash prefix. Intended for privacy-preserving client-side wallet discovery without sending raw addresses or xpubs. Fetch metadata for the returned addresses through `/api/address/{address}`.
|
||||
Find addresses by address type and by the first 1-16 hex nibbles of RapidHash v3 over the raw address payload bytes. Intended for privacy-preserving client-side wallet discovery without sending raw addresses or xpubs. Fetch metadata for the returned addresses through `/api/address/{address}`.
|
||||
|
||||
Endpoint: `GET /api/address/hash-prefix/{addr_type}/{prefix}`"""
|
||||
return self.get_json(f'/api/address/hash-prefix/{addr_type}/{prefix}')
|
||||
@@ -9163,4 +9296,3 @@ class BrkClient(BrkClientBase):
|
||||
|
||||
Endpoint: `GET /api.json`"""
|
||||
return self.get_json('/api.json')
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import pytest
|
||||
|
||||
from brk_client import BrkClient, address_payload_hash_prefix
|
||||
|
||||
|
||||
VECTORS = (
|
||||
(bytes([0x4E, 0x73]), "58101afa51a1ecfd"),
|
||||
(bytes(range(20)), "c3327ecb8ae1ff23"),
|
||||
(bytes(range(32)), "c0186990f026b180"),
|
||||
(bytes(range(65)), "0d4b77027ae7d700"),
|
||||
)
|
||||
|
||||
|
||||
def test_address_payload_hash_prefix_vectors():
|
||||
for payload, expected in VECTORS:
|
||||
assert address_payload_hash_prefix(payload, 16) == expected
|
||||
assert BrkClient.address_payload_hash_prefix(payload, 8) == expected[:8]
|
||||
|
||||
|
||||
def test_address_payload_hash_prefix_validation():
|
||||
with pytest.raises(ValueError, match="non-empty"):
|
||||
address_payload_hash_prefix(b"", 16)
|
||||
with pytest.raises(ValueError, match="at most 65"):
|
||||
address_payload_hash_prefix(bytes(range(66)), 16)
|
||||
with pytest.raises(ValueError, match="1 to 16"):
|
||||
address_payload_hash_prefix(b"\x01\x02", 0)
|
||||
|
||||
|
||||
def test_address_payload_hash_prefix_match_validation():
|
||||
client = BrkClient("http://127.0.0.1:0")
|
||||
client.get_address_hash_prefix_matches = lambda addr_type, prefix: {
|
||||
"addr_type": addr_type,
|
||||
"prefix": prefix,
|
||||
"truncated": False,
|
||||
"addresses": [],
|
||||
}
|
||||
|
||||
assert client.get_address_payload_hash_prefix_matches("p2pkh", bytes(range(20)), 8) == {
|
||||
"addr_type": "p2pkh",
|
||||
"prefix": "c3327ecb",
|
||||
"truncated": False,
|
||||
"addresses": [],
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="p2pkh address payload length 20 bytes"):
|
||||
client.get_address_payload_hash_prefix_matches("p2pkh", b"\x01\x02", 8)
|
||||
with pytest.raises(ValueError, match="Unsupported address type"):
|
||||
client.get_address_payload_hash_prefix_matches("op_return", b"\x01\x02", 8)
|
||||
Reference in New Issue
Block a user