website_next: move hash logic to clients

This commit is contained in:
nym21
2026-07-01 17:04:29 +02:00
parent 5d83ee4d70
commit 153fcdf4e0
33 changed files with 1206 additions and 189 deletions
Generated
+3 -2
View File
@@ -410,6 +410,7 @@ version = "0.3.6"
dependencies = [
"brk_cohort",
"brk_types",
"rapidhash",
"serde",
"serde_json",
"ureq",
@@ -2458,9 +2459,9 @@ dependencies = [
[[package]]
name = "rapidhash"
version = "4.4.1"
version = "4.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e48930979c155e2f33aa36ab3119b5ee81332beb6482199a8ecd6029b80b59"
checksum = "32b266a82f4aa99bb5c25e28d11cc44ace63d91adbcbcee4d323e2ae3d49ef37"
dependencies = [
"rustversion",
]
+1
View File
@@ -71,6 +71,7 @@ owo-colors = "4.3.0"
parking_lot = "0.12.5"
pco = "1.0.2"
rayon = "1.12.0"
rapidhash = "4.4.2"
rustc-hash = "2.1.2"
schemars = { version = "1.2.1", features = ["indexmap2"] }
serde = "1.0.228"
@@ -688,6 +688,160 @@ const _p = (prefix, acc) => acc ? `${{prefix}}_${{acc}}` : prefix;
"#
)
.unwrap();
output.push_str(r##"
const _MASK_64 = 0xffffffffffffffffn;
const _RAPIDHASH_SECRETS = /** @type {const} */ ([
0x2d358dccaa6c78a5n,
0x8bb84b93962eacc9n,
0x4b33a62ed433d4a3n,
0x4d5a2da51de1aa47n,
0xa0761d6478bd642fn,
0xe7037ed1a0b428dbn,
0x90ed1765281c388cn,
]);
const _RAPIDHASH_SEED = _rapidHashSeed(0n);
/** @param {bigint} value */
function _u64(value) {
return value & _MASK_64;
}
/** @param {bigint} left @param {bigint} right */
function _rapidMix(left, right) {
const result = _u64(left) * _u64(right);
return _u64(result) ^ _u64(result >> 64n);
}
/** @param {bigint} left @param {bigint} right @returns {[bigint, bigint]} */
function _rapidMum(left, right) {
const result = _u64(left) * _u64(right);
return [_u64(result), _u64(result >> 64n)];
}
/** @param {bigint} seed */
function _rapidHashSeed(seed) {
return _u64(seed ^ _rapidMix(seed ^ _RAPIDHASH_SECRETS[2], _RAPIDHASH_SECRETS[1]));
}
/** @param {Uint8Array} bytes @param {number} offset */
function _readU32(bytes, offset) {
return (
BigInt(bytes[offset]) |
(BigInt(bytes[offset + 1]) << 8n) |
(BigInt(bytes[offset + 2]) << 16n) |
(BigInt(bytes[offset + 3]) << 24n)
);
}
/** @param {Uint8Array} bytes @param {number} offset */
function _readU64(bytes, offset) {
return _readU32(bytes, offset) | (_readU32(bytes, offset + 4) << 32n);
}
/** @param {Uint8Array | ArrayBuffer | ArrayBufferView | number[]} payload */
function _asUint8Array(payload) {
if (payload instanceof Uint8Array) return payload;
if (payload instanceof ArrayBuffer) return new Uint8Array(payload);
if (ArrayBuffer.isView(payload)) return new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength);
if (Array.isArray(payload)) return new Uint8Array(payload);
throw new Error("Expected address payload bytes");
}
/** @param {Uint8Array | ArrayBuffer | ArrayBufferView | number[]} payload */
function _rapidHashV3(payload) {
const bytes = _asUint8Array(payload);
const length = bytes.length;
if (length === 0) throw new Error("Expected a non-empty address payload");
if (length > 65) throw new Error("Expected at most 65 address payload bytes");
let seed = _RAPIDHASH_SEED;
let a = 0n;
let b = 0n;
let remainder;
if (length <= 16) {
if (length >= 4) {
seed ^= BigInt(length);
if (length >= 8) {
a ^= _readU64(bytes, 0);
b ^= _readU64(bytes, length - 8);
} else {
a ^= _readU32(bytes, 0);
b ^= _readU32(bytes, length - 4);
}
} else if (length > 0) {
a ^= (BigInt(bytes[0]) << 45n) | BigInt(bytes[length - 1]);
b ^= BigInt(bytes[length >> 1]);
}
remainder = BigInt(length);
} else {
seed = _rapidMix(_readU64(bytes, 0) ^ _RAPIDHASH_SECRETS[2], _readU64(bytes, 8) ^ seed);
if (length > 32) {
seed = _rapidMix(_readU64(bytes, 16) ^ _RAPIDHASH_SECRETS[2], _readU64(bytes, 24) ^ seed);
if (length > 48) {
seed = _rapidMix(_readU64(bytes, 32) ^ _RAPIDHASH_SECRETS[1], _readU64(bytes, 40) ^ seed);
if (length > 64) {
seed = _rapidMix(_readU64(bytes, 48) ^ _RAPIDHASH_SECRETS[1], _readU64(bytes, 56) ^ seed);
}
}
}
remainder = BigInt(length);
a ^= _readU64(bytes, length - 16) ^ remainder;
b ^= _readU64(bytes, length - 8);
}
a ^= _RAPIDHASH_SECRETS[1];
b ^= seed;
[a, b] = _rapidMum(a, b);
return _rapidMix(a ^ 0xaaaaaaaaaaaaaaaan, b ^ _RAPIDHASH_SECRETS[1] ^ remainder);
}
/** @param {number} nibbles */
function _validateHashPrefixNibbles(nibbles) {
if (!Number.isInteger(nibbles) || nibbles < 1 || nibbles > 16) {
throw new Error("Expected hash-prefix length from 1 to 16 hex nibbles");
}
}
/** @param {OutputType} addrType @returns {number[]} */
function _addressPayloadLengths(addrType) {
switch (addrType) {
case "p2a": return [2];
case "p2pk": return [33, 65];
case "p2pkh":
case "p2sh":
case "v0_p2wpkh": return [20];
case "v0_p2wsh":
case "v1_p2tr": return [32];
default:
throw new Error(`Unsupported address type for address payload hash-prefix: ${addrType}`);
}
}
/**
* @param {OutputType} addrType
* @param {Uint8Array | ArrayBuffer | ArrayBufferView | number[]} payload
*/
function _validateAddressPayloadForType(addrType, payload) {
const length = _asUint8Array(payload).length;
const expected = _addressPayloadLengths(addrType);
if (!expected.includes(length)) {
throw new Error(`Expected ${addrType} address payload length ${expected.join(" or ")} bytes`);
}
}
/**
* Compute the RapidHash v3 hash-prefix used by `/api/address/hash-prefix/{addr_type}/{prefix}`.
* @param {Uint8Array | ArrayBuffer | ArrayBufferView | number[]} payload - Raw address payload bytes
* @param {number} nibbles - Prefix length from 1 to 16 hex nibbles
* @returns {string}
*/
function addressPayloadHashPrefix(payload, nibbles) {
_validateHashPrefixNibbles(nibbles);
return _rapidHashV3(payload).toString(16).padStart(16, "0").slice(0, nibbles);
}
"##);
}
/// Generate static constants for the BrkClient class.
@@ -111,6 +111,32 @@ pub fn generate_main_client(
writeln!(output, " this.series = this._buildTree();").unwrap();
writeln!(output, " }}\n").unwrap();
output.push_str(r##" /**
* Compute the RapidHash v3 hash-prefix for raw address payload bytes.
* @param {Uint8Array | ArrayBuffer | ArrayBufferView | number[]} payload
* @param {number} nibbles
* @returns {string}
*/
static addressPayloadHashPrefix(payload, nibbles) {
return addressPayloadHashPrefix(payload, nibbles);
}
/**
* Fetch address hash-prefix matches from raw address payload bytes.
* @param {OutputType} addrType
* @param {Uint8Array | ArrayBuffer | ArrayBufferView | number[]} payload - Raw payload bytes matching addrType length
* @param {number} nibbles
* @param {{ signal?: AbortSignal, onValue?: (value: AddrHashPrefixMatches) => void, cache?: boolean }} [options]
* @returns {Promise<AddrHashPrefixMatches>}
*/
getAddressPayloadHashPrefixMatches(addrType, payload, nibbles, options = {}) {
_validateAddressPayloadForType(addrType, payload);
const prefix = addressPayloadHashPrefix(payload, nibbles);
return this.getAddressHashPrefixMatches(addrType, prefix, options);
}
"##);
writeln!(output, " /**").unwrap();
writeln!(output, " * @private").unwrap();
writeln!(output, " * @returns {{SeriesTree}}").unwrap();
@@ -161,7 +187,11 @@ pub fn generate_main_client(
writeln!(output, "}}\n").unwrap();
writeln!(output, "export {{ BrkClient, BrkError }};").unwrap();
writeln!(
output,
"export {{ BrkClient, BrkError, addressPayloadHashPrefix }};"
)
.unwrap();
}
#[allow(clippy::too_many_arguments)]
@@ -84,6 +84,23 @@ pub fn generate_main_client(output: &mut String, endpoints: &[Endpoint]) {
.unwrap();
writeln!(output, " return _date_to_index(index, d)").unwrap();
writeln!(output).unwrap();
output.push_str(r#" @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))
"#);
// Generate API methods
generate_api_methods(output, endpoints);
}
@@ -147,6 +147,125 @@ def _p(prefix: str, acc: str) -> str:
"#
)
.unwrap();
output.push_str(r#"
_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]
"#);
}
/// Generate the SeriesData and SeriesEndpoint classes
@@ -76,6 +76,36 @@ impl BrkClient {{
)
.unwrap();
output.push_str(r#" /// Decode a mainnet Bitcoin address into the BRK address type and raw payload bytes.
pub fn decode_address_payload(address: &str) -> Result<AddressPayload> {
decode_address_payload(address)
}
/// Compute the RapidHash v3 hash-prefix for raw address payload bytes.
pub fn address_payload_hash_prefix(payload: &[u8], nibbles: usize) -> Result<String> {
address_payload_hash_prefix(payload, nibbles)
}
/// Decode a mainnet Bitcoin address and compute its hash prefix.
pub fn address_hash_prefix(address: &str, nibbles: usize) -> Result<AddressHashPrefix> {
address_hash_prefix(address, nibbles)
}
/// Fetch address hash-prefix matches from raw payload bytes matching `addr_type` length.
pub fn get_address_payload_hash_prefix_matches(&self, addr_type: OutputType, payload: &[u8], nibbles: usize) -> Result<AddrHashPrefixMatches> {
validate_address_payload_for_type(addr_type, payload)?;
let prefix = address_payload_hash_prefix(payload, nibbles)?;
self.get_address_hash_prefix_matches(addr_type, &prefix)
}
/// Fetch address hash-prefix matches for a mainnet Bitcoin address.
pub fn get_address_hash_prefix_matches_for_address(&self, address: &str, nibbles: usize) -> Result<AddrHashPrefixMatches> {
let hashed = address_hash_prefix(address, nibbles)?;
self.get_address_hash_prefix_matches(hashed.addr_type, &hashed.prefix)
}
"#);
generate_api_methods(output, endpoints);
writeln!(output, "}}").unwrap();
@@ -118,6 +148,23 @@ fn generate_get_method(output: &mut String, endpoint: &Endpoint) {
"get_text"
};
if endpoint.path == "/api/address/hash-prefix/{addr_type}/{prefix}" {
writeln!(
output,
" let addr_type = address_payload_type_path(addr_type)?;"
)
.unwrap();
writeln!(
output,
" self.base.{}(&format!(\"{}\"{}))",
fetch_method, path, index_arg
)
.unwrap();
writeln!(output, " }}").unwrap();
writeln!(output).unwrap();
return;
}
if endpoint.query_params.is_empty() {
writeln!(
output,
@@ -11,7 +11,8 @@ use crate::{
pub fn generate_imports(output: &mut String) {
writeln!(
output,
r#"use std::sync::Arc;
r#"use std::str::FromStr;
use std::sync::Arc;
use std::ops::{{Bound, RangeBounds}};
use serde::de::DeserializeOwned;
pub use brk_cohort::*;
@@ -43,6 +44,97 @@ impl std::error::Error for BrkError {{}}
/// Result type for BRK client operations.
pub type Result<T> = std::result::Result<T, BrkError>;
/// BRK address type and raw payload bytes used by the hash-prefix index.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AddressPayload {{
pub addr_type: OutputType,
pub payload: Vec<u8>,
}}
/// BRK address type and leading hex nibbles of the address-payload hash.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AddressHashPrefix {{
pub addr_type: OutputType,
pub prefix: String,
}}
/// Compute the RapidHash v3 hash-prefix used by `/api/address/hash-prefix/{{addr_type}}/{{prefix}}`.
pub fn address_payload_hash_prefix(payload: &[u8], nibbles: usize) -> Result<String> {{
if payload.is_empty() {{
return Err(BrkError {{ message: "Expected a non-empty address payload".to_string() }});
}}
if payload.len() > 65 {{
return Err(BrkError {{ message: "Expected at most 65 address payload bytes".to_string() }});
}}
if !(1..=16).contains(&nibbles) {{
return Err(BrkError {{ message: "Expected hash-prefix length from 1 to 16 hex nibbles".to_string() }});
}}
Ok(format!("{{:016x}}", rapidhash::v3::rapidhash_v3(payload))[..nibbles].to_string())
}}
fn validate_address_payload_for_type(addr_type: OutputType, payload: &[u8]) -> Result<()> {{
let expected: &[usize] = match addr_type {{
OutputType::P2A => &[2],
OutputType::P2PK33 => &[33],
OutputType::P2PK65 => &[65],
OutputType::P2PKH | OutputType::P2SH | OutputType::P2WPKH => &[20],
OutputType::P2WSH | OutputType::P2TR => &[32],
OutputType::P2MS | OutputType::OpReturn | OutputType::Empty | OutputType::Unknown => {{
return Err(BrkError {{ message: format!("Unsupported address type for address payload hash-prefix: {{addr_type:?}}") }});
}},
}};
let addr_type = address_payload_type_path(addr_type)?;
if !expected.contains(&payload.len()) {{
let joined = expected
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(" or ");
return Err(BrkError {{ message: format!("Expected {{addr_type}} address payload length {{joined}} bytes") }});
}}
Ok(())
}}
fn address_payload_type_path(addr_type: OutputType) -> Result<&'static str> {{
match addr_type {{
OutputType::P2A => Ok("p2a"),
OutputType::P2PK33 | OutputType::P2PK65 => Ok("p2pk"),
OutputType::P2PKH => Ok("p2pkh"),
OutputType::P2SH => Ok("p2sh"),
OutputType::P2WPKH => Ok("v0_p2wpkh"),
OutputType::P2WSH => Ok("v0_p2wsh"),
OutputType::P2TR => Ok("v1_p2tr"),
OutputType::P2MS | OutputType::OpReturn | OutputType::Empty | OutputType::Unknown => {{
Err(BrkError {{ message: format!("Unsupported address type for address payload hash-prefix: {{addr_type:?}}") }})
}},
}}
}}
/// Decode a mainnet Bitcoin address into the BRK address type and raw payload bytes.
pub fn decode_address_payload(address: &str) -> Result<AddressPayload> {{
if address.is_empty() {{
return Err(BrkError {{ message: "Expected an address string".to_string() }});
}}
let addr_bytes = AddrBytes::from_str(address).map_err(|e| BrkError {{ message: e.to_string() }})?;
let addr_type = OutputType::from(&addr_bytes);
Ok(AddressPayload {{
addr_type,
payload: addr_bytes.as_slice().to_vec(),
}})
}}
/// Decode a mainnet Bitcoin address and compute its hash prefix.
pub fn address_hash_prefix(address: &str, nibbles: usize) -> Result<AddressHashPrefix> {{
let decoded = decode_address_payload(address)?;
Ok(AddressHashPrefix {{
addr_type: decoded.addr_type,
prefix: address_payload_hash_prefix(&decoded.payload, nibbles)?,
}})
}}
/// Options for configuring the BRK client.
#[derive(Debug, Clone)]
pub struct BrkClientOptions {{
+1
View File
@@ -13,6 +13,7 @@ exclude = ["examples/"]
[dependencies]
brk_cohort = { workspace = true }
brk_types = { workspace = true }
rapidhash = { workspace = true }
ureq = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
+123 -2
View File
@@ -8,6 +8,7 @@
#![allow(clippy::useless_format)]
#![allow(clippy::unnecessary_to_owned)]
use std::str::FromStr;
use std::sync::Arc;
use std::ops::{Bound, RangeBounds};
use serde::de::DeserializeOwned;
@@ -32,6 +33,97 @@ impl std::error::Error for BrkError {}
/// Result type for BRK client operations.
pub type Result<T> = std::result::Result<T, BrkError>;
/// BRK address type and raw payload bytes used by the hash-prefix index.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AddressPayload {
pub addr_type: OutputType,
pub payload: Vec<u8>,
}
/// BRK address type and leading hex nibbles of the address-payload hash.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AddressHashPrefix {
pub addr_type: OutputType,
pub prefix: String,
}
/// Compute the RapidHash v3 hash-prefix used by `/api/address/hash-prefix/{addr_type}/{prefix}`.
pub fn address_payload_hash_prefix(payload: &[u8], nibbles: usize) -> Result<String> {
if payload.is_empty() {
return Err(BrkError { message: "Expected a non-empty address payload".to_string() });
}
if payload.len() > 65 {
return Err(BrkError { message: "Expected at most 65 address payload bytes".to_string() });
}
if !(1..=16).contains(&nibbles) {
return Err(BrkError { message: "Expected hash-prefix length from 1 to 16 hex nibbles".to_string() });
}
Ok(format!("{:016x}", rapidhash::v3::rapidhash_v3(payload))[..nibbles].to_string())
}
fn validate_address_payload_for_type(addr_type: OutputType, payload: &[u8]) -> Result<()> {
let expected: &[usize] = match addr_type {
OutputType::P2A => &[2],
OutputType::P2PK33 => &[33],
OutputType::P2PK65 => &[65],
OutputType::P2PKH | OutputType::P2SH | OutputType::P2WPKH => &[20],
OutputType::P2WSH | OutputType::P2TR => &[32],
OutputType::P2MS | OutputType::OpReturn | OutputType::Empty | OutputType::Unknown => {
return Err(BrkError { message: format!("Unsupported address type for address payload hash-prefix: {addr_type:?}") });
},
};
let addr_type = address_payload_type_path(addr_type)?;
if !expected.contains(&payload.len()) {
let joined = expected
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(" or ");
return Err(BrkError { message: format!("Expected {addr_type} address payload length {joined} bytes") });
}
Ok(())
}
fn address_payload_type_path(addr_type: OutputType) -> Result<&'static str> {
match addr_type {
OutputType::P2A => Ok("p2a"),
OutputType::P2PK33 | OutputType::P2PK65 => Ok("p2pk"),
OutputType::P2PKH => Ok("p2pkh"),
OutputType::P2SH => Ok("p2sh"),
OutputType::P2WPKH => Ok("v0_p2wpkh"),
OutputType::P2WSH => Ok("v0_p2wsh"),
OutputType::P2TR => Ok("v1_p2tr"),
OutputType::P2MS | OutputType::OpReturn | OutputType::Empty | OutputType::Unknown => {
Err(BrkError { message: format!("Unsupported address type for address payload hash-prefix: {addr_type:?}") })
},
}
}
/// Decode a mainnet Bitcoin address into the BRK address type and raw payload bytes.
pub fn decode_address_payload(address: &str) -> Result<AddressPayload> {
if address.is_empty() {
return Err(BrkError { message: "Expected an address string".to_string() });
}
let addr_bytes = AddrBytes::from_str(address).map_err(|e| BrkError { message: e.to_string() })?;
let addr_type = OutputType::from(&addr_bytes);
Ok(AddressPayload {
addr_type,
payload: addr_bytes.as_slice().to_vec(),
})
}
/// Decode a mainnet Bitcoin address and compute its hash prefix.
pub fn address_hash_prefix(address: &str, nibbles: usize) -> Result<AddressHashPrefix> {
let decoded = decode_address_payload(address)?;
Ok(AddressHashPrefix {
addr_type: decoded.addr_type,
prefix: address_payload_hash_prefix(&decoded.payload, nibbles)?,
})
}
/// Options for configuring the BRK client.
#[derive(Debug, Clone)]
pub struct BrkClientOptions {
@@ -9538,7 +9630,7 @@ pub struct BrkClient {
impl BrkClient {
/// Client version.
pub const VERSION: &'static str = "v0.3.4";
pub const VERSION: &'static str = "v0.3.6";
/// Create a new client with the given base URL.
pub fn new(base_url: impl Into<String>) -> Self {
@@ -9592,6 +9684,34 @@ impl BrkClient {
))
}
/// Decode a mainnet Bitcoin address into the BRK address type and raw payload bytes.
pub fn decode_address_payload(address: &str) -> Result<AddressPayload> {
decode_address_payload(address)
}
/// Compute the RapidHash v3 hash-prefix for raw address payload bytes.
pub fn address_payload_hash_prefix(payload: &[u8], nibbles: usize) -> Result<String> {
address_payload_hash_prefix(payload, nibbles)
}
/// Decode a mainnet Bitcoin address and compute its hash prefix.
pub fn address_hash_prefix(address: &str, nibbles: usize) -> Result<AddressHashPrefix> {
address_hash_prefix(address, nibbles)
}
/// Fetch address hash-prefix matches from raw payload bytes matching `addr_type` length.
pub fn get_address_payload_hash_prefix_matches(&self, addr_type: OutputType, payload: &[u8], nibbles: usize) -> Result<AddrHashPrefixMatches> {
validate_address_payload_for_type(addr_type, payload)?;
let prefix = address_payload_hash_prefix(payload, nibbles)?;
self.get_address_hash_prefix_matches(addr_type, &prefix)
}
/// Fetch address hash-prefix matches for a mainnet Bitcoin address.
pub fn get_address_hash_prefix_matches_for_address(&self, address: &str, nibbles: usize) -> Result<AddrHashPrefixMatches> {
let hashed = address_hash_prefix(address, nibbles)?;
self.get_address_hash_prefix_matches(hashed.addr_type, &hashed.prefix)
}
/// Health check
///
/// Liveness probe. Returns server identity, uptime, and indexed/computed heights from local state only (no bitcoind round-trip). For real chain-tip catch-up, see `/api/server/sync`.
@@ -9868,10 +9988,11 @@ impl BrkClient {
/// 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}`
pub fn get_address_hash_prefix_matches(&self, addr_type: OutputType, prefix: &str) -> Result<AddrHashPrefixMatches> {
let addr_type = address_payload_type_path(addr_type)?;
self.base.get_json(&format!("/api/address/hash-prefix/{addr_type}/{prefix}"))
}
+44
View File
@@ -0,0 +1,44 @@
use brk_client::{
BrkClient, OutputType, address_hash_prefix, address_payload_hash_prefix, decode_address_payload,
};
#[test]
fn address_payload_hash_prefix_vectors() {
let vectors = [
(vec![0x4e, 0x73], "58101afa51a1ecfd"),
((0_u8..20).collect::<Vec<_>>(), "c3327ecb8ae1ff23"),
((0_u8..32).collect::<Vec<_>>(), "c0186990f026b180"),
((0_u8..65).collect::<Vec<_>>(), "0d4b77027ae7d700"),
];
for (payload, expected) in vectors.iter() {
assert_eq!(address_payload_hash_prefix(payload, 16).unwrap(), *expected);
assert_eq!(
BrkClient::address_payload_hash_prefix(payload, 8).unwrap(),
&expected[..8]
);
}
}
#[test]
fn address_payload_hash_prefix_validation() {
assert!(address_payload_hash_prefix(&[], 16).is_err());
assert!(address_payload_hash_prefix(&[0; 66], 16).is_err());
assert!(address_payload_hash_prefix(&[1, 2], 0).is_err());
assert!(address_payload_hash_prefix(&[1, 2], 17).is_err());
}
#[test]
fn address_hash_prefix_uses_brk_address_parser() {
let address = "1BoatSLRHtKNngkdXEeobR76b53LETtpyT";
let decoded = decode_address_payload(address).unwrap();
assert_eq!(decoded.addr_type, OutputType::P2PKH);
assert_eq!(decoded.payload.len(), 20);
let hashed = address_hash_prefix(address, 8).unwrap();
assert_eq!(hashed.addr_type, OutputType::P2PKH);
assert_eq!(
hashed.prefix,
address_payload_hash_prefix(&decoded.payload, 8).unwrap()
);
}
+2 -4
View File
@@ -3,9 +3,7 @@ use axum::{
extract::{Path, State},
http::{HeaderMap, Uri},
};
use brk_types::{
AddrHashPrefixMatches, AddrStats, AddrValidation, Transaction, Utxo, Version,
};
use brk_types::{AddrHashPrefixMatches, AddrStats, AddrValidation, Transaction, Utxo, Version};
use crate::{
AppState, CacheStrategy,
@@ -43,7 +41,7 @@ impl AddrRoutes for ApiRouter<AppState> {
.id("get_address_hash_prefix_matches")
.addrs_tag()
.summary("Address hash-prefix matches")
.description("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}`.")
.description("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}`.")
.json_response::<AddrHashPrefixMatches>()
.not_modified()
.bad_request()
+1 -1
View File
@@ -16,7 +16,7 @@ indexmap = { workspace = true }
itoa = "1.0.18"
jiff = { workspace = true }
pco = { workspace = true }
rapidhash = "4.4.1"
rapidhash = { workspace = true }
rustc-hash = { workspace = true }
ryu = "1.0.23"
schemars = { workspace = true }
-1
View File
@@ -6,7 +6,6 @@
[![Supported by OpenSats](https://img.shields.io/badge/supported%20by-opensats-ff7b00)](https://opensats.org/)
[![Discord](https://img.shields.io/discord/1350431684562124850?label=Discord&logo=discord&color=5865F2)](https://discord.gg/WACpShCB7M)
[![X](https://img.shields.io/badge/@_nym21_-000000?logo=x)](https://x.com/_nym21_)
[![Nostr](https://img.shields.io/badge/Nostr-purple?logo=nostr)](https://primal.net/p/nprofile1qqsfw5dacngjlahye34krvgz7u0yghhjgk7gxzl5ptm9v6n2y3sn03sqxu2e6)
> "Shout out to Bitcoin Research Kit. [...] Couldn't recommend them highly enough."
> — James Check (CheckOnChain), [What Bitcoin Did #1000](https://www.whatbitcoindid.com/episodes/wbd1000-checkmate)
+71 -1
View File
@@ -233,6 +233,32 @@ Defined in: [Developer/brk/modules/brk-client/index.js:1894](https://github.com/
***
### addressPayloadHashPrefix()
> `static` **addressPayloadHashPrefix**(`payload`, `nibbles`): `string`
Compute the RapidHash v3 hash-prefix for raw address payload bytes.
#### Parameters
##### payload
`Uint8Array` | `ArrayBuffer` | `ArrayBufferView` | `number`[]
Raw address payload bytes. Must be 1 to 65 bytes.
##### nibbles
`number`
Prefix length from 1 to 16 hex nibbles.
#### Returns
`string`
***
### dateToIndex()
> **dateToIndex**(`index`, `d`): `number`
@@ -427,7 +453,7 @@ Defined in: [Developer/brk/modules/brk-client/index.js:11512](https://github.com
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}`
@@ -461,6 +487,50 @@ Endpoint: `GET /api/address/hash-prefix/{addr_type}/{prefix}`
***
### getAddressPayloadHashPrefixMatches()
> **getAddressPayloadHashPrefixMatches**(`addrType`, `payload`, `nibbles`, `options?`): `Promise`\<[`AddrHashPrefixMatches`](../interfaces/AddrHashPrefixMatches.md)\>
Fetch address hash-prefix matches from raw payload bytes matching `addrType` length.
#### Parameters
##### addrType
[`OutputType`](../type-aliases/OutputType.md)
##### payload
`Uint8Array` | `ArrayBuffer` | `ArrayBufferView` | `number`[]
Raw payload bytes matching `addrType` length.
##### nibbles
`number`
Prefix length from 1 to 16 hex nibbles.
##### options?
###### cache?
`boolean`
###### onValue?
(`value`) => `void`
###### signal?
`AbortSignal`
#### Returns
`Promise`\<[`AddrHashPrefixMatches`](../interfaces/AddrHashPrefixMatches.md)\>
***
### getAddressMempoolTxs()
> **getAddressMempoolTxs**(`address`, `options?`): `Promise`\<[`Transaction`](../interfaces/Transaction.md)[]\>
@@ -0,0 +1,30 @@
[**brk-client**](../README.md)
***
[brk-client](../globals.md) / addressPayloadHashPrefix
# Function: addressPayloadHashPrefix()
> **addressPayloadHashPrefix**(`payload`, `nibbles`): `string`
Compute the RapidHash v3 hash-prefix used by `/api/address/hash-prefix/{addr_type}/{prefix}`.
## Parameters
### payload
`Uint8Array` | `ArrayBuffer` | `ArrayBufferView` | `number`[]
Raw address payload bytes. Must be 1 to 65 bytes.
### nibbles
`number`
Prefix length from 1 to 16 hex nibbles.
## Returns
`string`
+4
View File
@@ -9,6 +9,10 @@
- [BrkClient](classes/BrkClient.md)
- [BrkError](classes/BrkError.md)
## Functions
- [addressPayloadHashPrefix](functions/addressPayloadHashPrefix.md)
## Interfaces
- [\_0sdM0M1M1sdM2M2sdM3sdP0P1P1sdP2P2sdP3sdSdZscorePattern](interfaces/0sdM0M1M1sdM2M2sdM3sdP0P1P1sdP2P2sdP3sdSdZscorePattern.md)
+179 -2
View File
@@ -2132,6 +2132,159 @@ const _m = (acc, s) => s ? (acc ? `${acc}_${s}` : s) : acc;
const _p = (prefix, acc) => acc ? `${prefix}_${acc}` : prefix;
const _MASK_64 = 0xffffffffffffffffn;
const _RAPIDHASH_SECRETS = /** @type {const} */ ([
0x2d358dccaa6c78a5n,
0x8bb84b93962eacc9n,
0x4b33a62ed433d4a3n,
0x4d5a2da51de1aa47n,
0xa0761d6478bd642fn,
0xe7037ed1a0b428dbn,
0x90ed1765281c388cn,
]);
const _RAPIDHASH_SEED = _rapidHashSeed(0n);
/** @param {bigint} value */
function _u64(value) {
return value & _MASK_64;
}
/** @param {bigint} left @param {bigint} right */
function _rapidMix(left, right) {
const result = _u64(left) * _u64(right);
return _u64(result) ^ _u64(result >> 64n);
}
/** @param {bigint} left @param {bigint} right @returns {[bigint, bigint]} */
function _rapidMum(left, right) {
const result = _u64(left) * _u64(right);
return [_u64(result), _u64(result >> 64n)];
}
/** @param {bigint} seed */
function _rapidHashSeed(seed) {
return _u64(seed ^ _rapidMix(seed ^ _RAPIDHASH_SECRETS[2], _RAPIDHASH_SECRETS[1]));
}
/** @param {Uint8Array} bytes @param {number} offset */
function _readU32(bytes, offset) {
return (
BigInt(bytes[offset]) |
(BigInt(bytes[offset + 1]) << 8n) |
(BigInt(bytes[offset + 2]) << 16n) |
(BigInt(bytes[offset + 3]) << 24n)
);
}
/** @param {Uint8Array} bytes @param {number} offset */
function _readU64(bytes, offset) {
return _readU32(bytes, offset) | (_readU32(bytes, offset + 4) << 32n);
}
/** @param {Uint8Array | ArrayBuffer | ArrayBufferView | number[]} payload */
function _asUint8Array(payload) {
if (payload instanceof Uint8Array) return payload;
if (payload instanceof ArrayBuffer) return new Uint8Array(payload);
if (ArrayBuffer.isView(payload)) return new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength);
if (Array.isArray(payload)) return new Uint8Array(payload);
throw new Error("Expected address payload bytes");
}
/** @param {Uint8Array | ArrayBuffer | ArrayBufferView | number[]} payload */
function _rapidHashV3(payload) {
const bytes = _asUint8Array(payload);
const length = bytes.length;
if (length === 0) throw new Error("Expected a non-empty address payload");
if (length > 65) throw new Error("Expected at most 65 address payload bytes");
let seed = _RAPIDHASH_SEED;
let a = 0n;
let b = 0n;
let remainder;
if (length <= 16) {
if (length >= 4) {
seed ^= BigInt(length);
if (length >= 8) {
a ^= _readU64(bytes, 0);
b ^= _readU64(bytes, length - 8);
} else {
a ^= _readU32(bytes, 0);
b ^= _readU32(bytes, length - 4);
}
} else if (length > 0) {
a ^= (BigInt(bytes[0]) << 45n) | BigInt(bytes[length - 1]);
b ^= BigInt(bytes[length >> 1]);
}
remainder = BigInt(length);
} else {
seed = _rapidMix(_readU64(bytes, 0) ^ _RAPIDHASH_SECRETS[2], _readU64(bytes, 8) ^ seed);
if (length > 32) {
seed = _rapidMix(_readU64(bytes, 16) ^ _RAPIDHASH_SECRETS[2], _readU64(bytes, 24) ^ seed);
if (length > 48) {
seed = _rapidMix(_readU64(bytes, 32) ^ _RAPIDHASH_SECRETS[1], _readU64(bytes, 40) ^ seed);
if (length > 64) {
seed = _rapidMix(_readU64(bytes, 48) ^ _RAPIDHASH_SECRETS[1], _readU64(bytes, 56) ^ seed);
}
}
}
remainder = BigInt(length);
a ^= _readU64(bytes, length - 16) ^ remainder;
b ^= _readU64(bytes, length - 8);
}
a ^= _RAPIDHASH_SECRETS[1];
b ^= seed;
[a, b] = _rapidMum(a, b);
return _rapidMix(a ^ 0xaaaaaaaaaaaaaaaan, b ^ _RAPIDHASH_SECRETS[1] ^ remainder);
}
/** @param {number} nibbles */
function _validateHashPrefixNibbles(nibbles) {
if (!Number.isInteger(nibbles) || nibbles < 1 || nibbles > 16) {
throw new Error("Expected hash-prefix length from 1 to 16 hex nibbles");
}
}
/** @param {OutputType} addrType @returns {number[]} */
function _addressPayloadLengths(addrType) {
switch (addrType) {
case "p2a": return [2];
case "p2pk": return [33, 65];
case "p2pkh":
case "p2sh":
case "v0_p2wpkh": return [20];
case "v0_p2wsh":
case "v1_p2tr": return [32];
default:
throw new Error(`Unsupported address type for address payload hash-prefix: ${addrType}`);
}
}
/**
* @param {OutputType} addrType
* @param {Uint8Array | ArrayBuffer | ArrayBufferView | number[]} payload
*/
function _validateAddressPayloadForType(addrType, payload) {
const length = _asUint8Array(payload).length;
const expected = _addressPayloadLengths(addrType);
if (!expected.includes(length)) {
throw new Error(`Expected ${addrType} address payload length ${expected.join(" or ")} bytes`);
}
}
/**
* Compute the RapidHash v3 hash-prefix used by `/api/address/hash-prefix/{addr_type}/{prefix}`.
* @param {Uint8Array | ArrayBuffer | ArrayBufferView | number[]} payload - Raw address payload bytes
* @param {number} nibbles - Prefix length from 1 to 16 hex nibbles
* @returns {string}
*/
function addressPayloadHashPrefix(payload, nibbles) {
_validateHashPrefixNibbles(nibbles);
return _rapidHashV3(payload).toString(16).padStart(16, "0").slice(0, nibbles);
}
// Index group constants and factory
const _i1 = /** @type {const} */ (["minute10", "minute30", "hour1", "hour4", "hour12", "day1", "day3", "week1", "month1", "month3", "month6", "year1", "year10", "halving", "epoch", "height"]);
@@ -9091,6 +9244,30 @@ class BrkClient extends BrkClientBase {
this.series = this._buildTree();
}
/**
* Compute the RapidHash v3 hash-prefix for raw address payload bytes.
* @param {Uint8Array | ArrayBuffer | ArrayBufferView | number[]} payload
* @param {number} nibbles
* @returns {string}
*/
static addressPayloadHashPrefix(payload, nibbles) {
return addressPayloadHashPrefix(payload, nibbles);
}
/**
* Fetch address hash-prefix matches from raw address payload bytes.
* @param {OutputType} addrType
* @param {Uint8Array | ArrayBuffer | ArrayBufferView | number[]} payload - Raw payload bytes matching addrType length
* @param {number} nibbles
* @param {{ signal?: AbortSignal, onValue?: (value: AddrHashPrefixMatches) => void, cache?: boolean }} [options]
* @returns {Promise<AddrHashPrefixMatches>}
*/
getAddressPayloadHashPrefixMatches(addrType, payload, nibbles, options = {}) {
_validateAddressPayloadForType(addrType, payload);
const prefix = addressPayloadHashPrefix(payload, nibbles);
return this.getAddressHashPrefixMatches(addrType, prefix, options);
}
/**
* @private
* @returns {SeriesTree}
@@ -11500,7 +11677,7 @@ class BrkClient extends BrkClientBase {
/**
* 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}`
*
@@ -12766,4 +12943,4 @@ class BrkClient extends BrkClientBase {
}
export { BrkClient, BrkError };
export { BrkClient, BrkError, addressPayloadHashPrefix };
+1
View File
@@ -5,6 +5,7 @@
"scripts": {
"test": "node tests/basic.js && node tests/tree.js",
"test:basic": "node tests/basic.js",
"test:hash-prefix": "node tests/hash_prefix.js",
"test:tree": "node tests/tree.js"
},
"description": "Bitcoin on-chain analytics client — thousands of metrics, block explorer, and address index",
+34
View File
@@ -0,0 +1,34 @@
import assert from "node:assert/strict";
import { BrkClient, addressPayloadHashPrefix } from "../index.js";
const vectors = [
[Uint8Array.of(0x4e, 0x73), "58101afa51a1ecfd"],
[Uint8Array.from({ length: 20 }, (_, i) => i), "c3327ecb8ae1ff23"],
[Uint8Array.from({ length: 32 }, (_, i) => i), "c0186990f026b180"],
[Uint8Array.from({ length: 65 }, (_, i) => i), "0d4b77027ae7d700"],
];
for (const [payload, hash] of vectors) {
assert.equal(addressPayloadHashPrefix(payload, 16), hash);
assert.equal(BrkClient.addressPayloadHashPrefix(payload, 8), hash.slice(0, 8));
}
assert.throws(() => addressPayloadHashPrefix(Uint8Array.of(), 16), /non-empty/);
assert.throws(() => addressPayloadHashPrefix(new Uint8Array(66), 16), /at most 65/);
assert.throws(() => addressPayloadHashPrefix(Uint8Array.of(1, 2), 0), /1 to 16/);
const client = new BrkClient("http://127.0.0.1:0");
client.getAddressHashPrefixMatches = (addrType, prefix) => ({ addrType, prefix, truncated: false, addresses: [] });
assert.deepEqual(
client.getAddressPayloadHashPrefixMatches("p2pkh", Uint8Array.from({ length: 20 }, (_, i) => i), 8),
{ addrType: "p2pkh", prefix: "c3327ecb", truncated: false, addresses: [] },
);
assert.throws(
() => client.getAddressPayloadHashPrefixMatches("p2pkh", Uint8Array.of(1, 2), 8),
/p2pkh address payload length 20 bytes/,
);
assert.throws(
() => client.getAddressPayloadHashPrefixMatches("op_return", Uint8Array.of(1, 2), 8),
/Unsupported address type/,
);
+27 -2
View File
@@ -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`
+134 -2
View File
@@ -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)
+3 -14
View File
@@ -1,24 +1,15 @@
import { rapidHashV3Prefix } from "./hash.js";
const MIN_PREFIX_NIBBLES = 4;
const MAX_PREFIX_NIBBLES = 16;
/**
* @typedef {import("../../modules/brk-client/index.js").AddrHashPrefixMatches} AddrHashPrefixMatches
* @typedef {import("../derive/index.js").AddressType} AddressType
* @typedef {import("../derive/index.js").GeneratedAddress} GeneratedAddress
*/
/**
* @typedef {Object} AddrHashPrefixMatches
* @property {AddressType} addrType
* @property {string} prefix
* @property {boolean} truncated
* @property {string[]} addresses
*/
/**
* @typedef {Object} AddressClient
* @property {(addrType: AddressType, prefix: string, options?: { cache?: boolean }) => Promise<unknown>} getAddressHashPrefixMatches
* @property {(addrType: AddressType, payload: Uint8Array, nibbles: number, options?: { cache?: boolean }) => Promise<AddrHashPrefixMatches>} getAddressPayloadHashPrefixMatches
*/
/**
@@ -28,10 +19,8 @@ const MAX_PREFIX_NIBBLES = 16;
* @returns {Promise<AddrHashPrefixMatches>}
*/
async function fetchPrefixMatches(client, generated, nibbles) {
const prefix = rapidHashV3Prefix(generated.payload, nibbles);
return /** @type {AddrHashPrefixMatches} */ (
await client.getAddressHashPrefixMatches(generated.addrType, prefix, {
await client.getAddressPayloadHashPrefixMatches(generated.addrType, generated.payload, nibbles, {
cache: false,
})
);
-107
View File
@@ -1,107 +0,0 @@
const MASK_64 = 0xffffffffffffffffn;
const DEFAULT_SECRETS = /** @type {const} */ ([
0x2d358dccaa6c78a5n,
0x8bb84b93962eacc9n,
0x4b33a62ed433d4a3n,
0x4d5a2da51de1aa47n,
0xa0761d6478bd642fn,
0xe7037ed1a0b428dbn,
0x90ed1765281c388cn,
]);
const DEFAULT_SEED = rapidHashSeed(0n);
/**
* @param {bigint} value
*/
function u64(value) {
return value & MASK_64;
}
/**
* @param {bigint} left
* @param {bigint} right
*/
function rapidMix(left, right) {
const result = u64(left) * u64(right);
return u64(result) ^ u64(result >> 64n);
}
/**
* @param {bigint} left
* @param {bigint} right
* @returns {[bigint, bigint]}
*/
function rapidMum(left, right) {
const result = u64(left) * u64(right);
return [u64(result), u64(result >> 64n)];
}
/**
* @param {bigint} seed
*/
function rapidHashSeed(seed) {
return u64(seed ^ rapidMix(seed ^ DEFAULT_SECRETS[2], DEFAULT_SECRETS[1]));
}
/**
* @param {Uint8Array} bytes
* @param {number} offset
*/
function readU64(bytes, offset) {
return (
BigInt(bytes[offset]) |
(BigInt(bytes[offset + 1]) << 8n) |
(BigInt(bytes[offset + 2]) << 16n) |
(BigInt(bytes[offset + 3]) << 24n) |
(BigInt(bytes[offset + 4]) << 32n) |
(BigInt(bytes[offset + 5]) << 40n) |
(BigInt(bytes[offset + 6]) << 48n) |
(BigInt(bytes[offset + 7]) << 56n)
);
}
/**
* @param {Uint8Array} bytes
*/
function rapidHashV3(bytes) {
const length = bytes.length;
if (length <= 16) {
throw new Error("Expected more than 16 bytes");
}
if (length > 32) {
throw new Error("Expected at most 32 bytes");
}
let seed = rapidMix(
readU64(bytes, 0) ^ DEFAULT_SECRETS[2],
readU64(bytes, 8) ^ DEFAULT_SEED,
);
let a = readU64(bytes, length - 16) ^ BigInt(length);
let b = readU64(bytes, length - 8);
a ^= DEFAULT_SECRETS[1];
b ^= seed;
[a, b] = rapidMum(a, b);
return rapidMix(a ^ 0xaaaaaaaaaaaaaaaan, b ^ DEFAULT_SECRETS[1] ^ BigInt(length));
}
/**
* @param {Uint8Array} bytes
*/
function rapidHashV3Hex(bytes) {
return rapidHashV3(bytes).toString(16).padStart(16, "0");
}
/**
* @param {Uint8Array} bytes
* @param {number} nibbles
*/
export function rapidHashV3Prefix(bytes, nibbles) {
return rapidHashV3Hex(bytes).slice(0, nibbles);
}
+5 -8
View File
@@ -16,6 +16,7 @@ const LOOKUP_CONCURRENCY = 8;
/**
* @typedef {import("./stats.js").AddressStats} AddressStats
* @typedef {import("./bucket.js").AddrHashPrefixMatches} AddrHashPrefixMatches
*/
/**
@@ -37,8 +38,8 @@ const LOOKUP_CONCURRENCY = 8;
/**
* @typedef {Object} AddressClient
* @property {string} domain
* @property {(address: string, options?: { cache?: boolean }) => Promise<unknown>} getAddress
* @property {(addrType: AddressType, prefix: string, options?: { cache?: boolean }) => Promise<unknown>} getAddressHashPrefixMatches
* @property {(address: string, options?: { cache?: boolean }) => Promise<AddressStats>} getAddress
* @property {(addrType: AddressType, payload: Uint8Array, nibbles: number, options?: { cache?: boolean }) => Promise<AddrHashPrefixMatches>} getAddressPayloadHashPrefixMatches
*/
/**
@@ -115,9 +116,7 @@ async function fetchBucketMetadata(client, addresses, cache) {
if (!cache.has(address)) {
cache.set(
address,
client.getAddress(address, { cache: false }).then(
(stats) => /** @type {AddressStats} */ (stats),
),
client.getAddress(address, { cache: false }),
);
}
}
@@ -132,9 +131,7 @@ async function fetchBucketMetadata(client, addresses, cache) {
*/
async function fetchDirectWalletAddress(client, generated) {
try {
const stats = /** @type {AddressStats} */ (
await client.getAddress(generated.address, { cache: false })
);
const stats = await client.getAddress(generated.address, { cache: false });
const historyAddresses =
getAddressTxCount(stats) > 0 ? [generated.address] : [];
+1 -8
View File
@@ -8,17 +8,10 @@ const MAX_SCANNED_ADDRESSES = 1_000;
/**
* @typedef {import("../derive/address.js").AddressScript} AddressScript
* @typedef {import("../derive/index.js").AddressType} AddressType
* @typedef {import("../lookup/index.js").AddressClient} AddressClient
* @typedef {import("../lookup/index.js").WalletAddress} WalletAddress
*/
/**
* @typedef {Object} AddressClient
* @property {string} domain
* @property {(address: string, options?: { cache?: boolean }) => Promise<unknown>} getAddress
* @property {(addrType: AddressType, prefix: string, options?: { cache?: boolean }) => Promise<unknown>} getAddressHashPrefixMatches
*/
/**
* @typedef {Object} ScanProgress
* @property {number} scannedCount
+1 -8
View File
@@ -25,17 +25,10 @@ const descriptorBranches = /** @type {const} */ ([
/**
* @typedef {import("../derive/address.js").AddressScript} AddressScript
* @typedef {import("../derive/index.js").AddressType} AddressType
* @typedef {import("../lookup/index.js").AddressClient} AddressClient
* @typedef {import("./branch.js").WalletAddress} WalletAddress
*/
/**
* @typedef {Object} AddressClient
* @property {string} domain
* @property {(address: string, options?: { cache?: boolean }) => Promise<unknown>} getAddress
* @property {(addrType: AddressType, prefix: string, options?: { cache?: boolean }) => Promise<unknown>} getAddressHashPrefixMatches
*/
/**
* @typedef {WalletAddress & {
* branchId: WalletBranchId,
+5 -9
View File
@@ -5,7 +5,7 @@ import { addressScripts } from "../derive/script.js";
/**
* @typedef {import("../derive/address.js").AddressScript} AddressScript
* @typedef {import("../derive/index.js").AddressType} AddressType
* @typedef {import("../lookup/index.js").AddressClient} AddressClient
* @typedef {Awaited<ReturnType<typeof scanBranches>>["addresses"][number]} WalletAddress
* @typedef {Awaited<ReturnType<typeof scanBranches>>} ScriptScan
*/
@@ -18,11 +18,9 @@ import { addressScripts } from "../derive/script.js";
*/
/**
* @typedef {Object} WalletScanClient
* @property {string} domain
* @property {(address: string, options?: { cache?: boolean }) => Promise<unknown>} getAddress
* @property {(addrType: AddressType, prefix: string, options?: { cache?: boolean }) => Promise<unknown>} getAddressHashPrefixMatches
* @property {(options?: { cache?: boolean }) => Promise<unknown>} getLivePrice
* @typedef {AddressClient & {
* getLivePrice(options?: { cache?: boolean }): Promise<number>,
* }} WalletScanClient
*/
/**
@@ -135,9 +133,7 @@ export async function scanWalletAddresses({
const addresses = scans.flatMap((scan) => scan.addresses)
.sort(compareWalletAddresses);
const btcUsdPrice = /** @type {number} */ (
await client.getLivePrice({ cache: false })
);
const btcUsdPrice = await client.getLivePrice({ cache: false });
return {
addresses,
@@ -4,20 +4,21 @@ const HISTORY_CONCURRENCY = 4;
const MAX_SELECTED_ADDRESS_TXS = 100;
const historyByBucketKey =
/** @type {Map<string, Promise<Map<string, unknown[]>>>} */ (new Map());
/** @type {Map<string, Promise<Map<string, ApiTransaction[]>>>} */ (new Map());
/**
* @typedef {import("../../scan/index.js").WalletAddress} WalletAddress
* @typedef {import("./transaction.js").ApiTransaction} ApiTransaction
*/
/**
* @typedef {Object} AddressHistoryClient
* @property {(address: string, options?: { cache?: boolean }) => Promise<unknown>} getAddressTxs
* @property {(address: string, options?: { cache?: boolean }) => Promise<ApiTransaction[]>} getAddressTxs
*/
/**
* @typedef {Object} AddressHistory
* @property {unknown[]} transactions
* @property {ApiTransaction[]} transactions
*/
/**
@@ -30,16 +31,14 @@ function createBucketKey(addresses) {
/**
* @param {AddressHistoryClient} client
* @param {readonly string[]} addresses
* @returns {Promise<Map<string, unknown[]>>}
* @returns {Promise<Map<string, ApiTransaction[]>>}
*/
async function fetchBucketHistory(client, addresses) {
const entries = await mapConcurrent(
addresses,
HISTORY_CONCURRENCY,
async (address) => {
const transactions = /** @type {unknown[]} */ (
await client.getAddressTxs(address, { cache: false })
);
const transactions = await client.getAddressTxs(address, { cache: false });
return /** @type {const} */ ([address, transactions]);
},
+2 -1
View File
@@ -3,12 +3,13 @@ import { readWalletTransaction } from "./transaction.js";
/**
* @typedef {import("../../scan/index.js").WalletAddress} WalletAddress
* @typedef {import("./transaction.js").ApiTransaction} ApiTransaction
* @typedef {import("./transaction.js").WalletTransaction} WalletTransaction
*/
/**
* @typedef {Object} TransactionClient
* @property {(address: string, options?: { cache?: boolean }) => Promise<unknown>} getAddressTxs
* @property {(address: string, options?: { cache?: boolean }) => Promise<ApiTransaction[]>} getAddressTxs
*/
/**
@@ -1,5 +1,6 @@
/**
* @typedef {import("../../scan/index.js").WalletAddress} WalletAddress
* @typedef {Record<string, unknown>} ApiTransaction
*
* @typedef {Object} WalletTransactionAddress
* @property {WalletAddress} walletAddress
@@ -186,7 +187,7 @@ function getExternalOutputValue(transaction, walletAddressSet) {
}
/**
* @param {unknown} transaction
* @param {ApiTransaction} transaction
* @param {readonly WalletAddress[]} walletAddresses
* @returns {WalletTransaction}
*/
+17 -7
View File
@@ -3,20 +3,31 @@ import { mapConcurrent } from "../../concurrent.js";
const UTXO_CONCURRENCY = 4;
const utxosByBucketKey =
/** @type {Map<string, Promise<Map<string, unknown[]>>>} */ (new Map());
/** @type {Map<string, Promise<Map<string, ApiUtxo[]>>>} */ (new Map());
/**
* @typedef {import("../../scan/index.js").WalletAddress} WalletAddress
*/
/**
* @typedef {Object} ApiUtxoStatus
* @property {boolean} confirmed
*
* @typedef {Object} ApiUtxo
* @property {string} txid
* @property {number} vout
* @property {number} value
* @property {ApiUtxoStatus} status
*/
/**
* @typedef {Object} UtxoClient
* @property {(address: string, options?: { cache?: boolean }) => Promise<unknown>} getAddressUtxos
* @property {(address: string, options?: { cache?: boolean }) => Promise<ApiUtxo[]>} getAddressUtxos
*/
/**
* @typedef {Object} AddressUtxos
* @property {unknown[]} utxos
* @property {ApiUtxo[]} utxos
*/
/**
@@ -39,12 +50,11 @@ function createBucketKey(addresses) {
/**
* @param {UtxoClient} client
* @param {string} address
* @returns {Promise<ApiUtxo[]>}
*/
async function fetchAddressUtxos(client, address) {
try {
return /** @type {unknown[]} */ (
await client.getAddressUtxos(address, { cache: false })
);
return await client.getAddressUtxos(address, { cache: false });
} catch (error) {
if (isNotFound(error)) return [];
@@ -55,7 +65,7 @@ async function fetchAddressUtxos(client, address) {
/**
* @param {UtxoClient} client
* @param {readonly string[]} addresses
* @returns {Promise<Map<string, unknown[]>>}
* @returns {Promise<Map<string, ApiUtxo[]>>}
*/
async function fetchBucketUtxos(client, addresses) {
const entries = await mapConcurrent(