From 35bd1b4a3b6ce81b3df7507eadc19bb3a8e178d0 Mon Sep 17 00:00:00 2001 From: Maya Parbhoe <73040345+MeABzZz@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:54:36 -0300 Subject: [PATCH 1/2] fix(node): anchor assume-valid per network with Core's hash and minimum work Startup gave consensus assume_valid_height with hash none and n_minimum_chain_work 0. Connect skips scripts when height < assume_valid_height && chainwork >= n_minimum_chain_work, so with a floor of 0 any header chain skipped every script below the height. On mainnet the blvm binary seeds height 912683 (Core v30) with no hash; on a zero-config regtest start the library default height 938343 applied to regtest. Add core_assume_valid_for_network with Bitcoin Core v31.1 chainparams (defaultAssumeValid height and hash, nMinimumChainWork) for mainnet, testnet3 and default signet. Testnet4 keeps height 0 (full verification, as before) and takes Core's minimum work. Regtest and custom signet get none. resolve_assume_valid picks the height from env, then config, then network; a configured height equal to the network's keeps the network's hash, so the binary's height-only seed keeps the anchor. Minimum work always comes from the network. The startup log prints it. Tests: tests/assumevalid_tests.rs checks every value against Core v31.1 display hex, and that the binary seed, zero-config, custom-height, env and custom-signet cases resolve as described. --- src/config/mod.rs | 119 ++++++++++++++++++++++++++++-- src/node/mod.rs | 32 ++++++-- tests/assumevalid_tests.rs | 147 +++++++++++++++++++++++++++++++++++-- 3 files changed, 277 insertions(+), 21 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index a405a1b7..0d15bdd2 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -701,14 +701,121 @@ pub fn parse_signet_challenge_hex(hex_str: &str) -> Result> { Ok(bytes) } +/// Bitcoin Core chainparams `defaultAssumeValid` and `nMinimumChainWork` for one network. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct NetworkAssumeValid { + /// Height of the assume-valid block; 0 turns the skip off. + pub height: u64, + /// Hash of that block in internal (little-endian) byte order. + pub hash: Option<[u8; 32]>, + /// Scripts are skipped only when the best header chain has at least this much work. + pub min_chain_work: u128, +} + +/// Display (RPC) hex to internal byte order, at compile time. +const fn hash_from_display(hex: &str) -> [u8; 32] { + const fn nibble(c: u8) -> u8 { + match c { + b'0'..=b'9' => c - b'0', + b'a'..=b'f' => c - b'a' + 10, + _ => panic!("hash must be lowercase hex"), + } + } + let b = hex.as_bytes(); + assert!(b.len() == 64, "hash must be 64 hex digits"); + let mut out = [0u8; 32]; + let mut i = 0; + while i < 32 { + out[31 - i] = (nibble(b[2 * i]) << 4) | nibble(b[2 * i + 1]); + i += 1; + } + out +} + +/// Values from Bitcoin Core v31.1 `src/kernel/chainparams.cpp`. +const CORE_MAINNET: NetworkAssumeValid = NetworkAssumeValid { + height: 938_343, + hash: Some(hash_from_display( + "00000000000000000000ccebd6d74d9194d8dcdc1d177c478e094bfad51ba5ac", + )), + min_chain_work: 0x0001_1287_50f8_2f4c_3661_53a3_a030, +}; +const CORE_TESTNET3: NetworkAssumeValid = NetworkAssumeValid { + height: 4_842_348, + hash: Some(hash_from_display( + "000000007a61e4230b28ac5cb6b5e5a0130de37ac1faf2f8987d2fa6505b67f4", + )), + min_chain_work: 0x17dd_e1c6_49f3_708d_14b6, +}; +/// Core sets testnet4 assume-valid at 123613; BLVM keeps full verification there and +/// takes only Core's minimum work. +const CORE_TESTNET4: NetworkAssumeValid = NetworkAssumeValid { + height: 0, + hash: None, + min_chain_work: 0x09a0_fe15_d017_7d08_6304, +}; +/// Default signet only. A custom signet challenge has no assume-valid in Core. +const CORE_SIGNET: NetworkAssumeValid = NetworkAssumeValid { + height: 293_175, + hash: Some(hash_from_display( + "00000008414aab61092ef93f1aacc54cf9e9f16af29ddad493b908a01ff5c329", + )), + min_chain_work: 0x0b46_3ea0_a4b8, +}; +const NO_ASSUME_VALID: NetworkAssumeValid = NetworkAssumeValid { + height: 0, + hash: None, + min_chain_work: 0, +}; + +/// Per-network assume-valid anchor and minimum chain work (Core v31.1 chainparams). +pub fn core_assume_valid_for_network(network: &str) -> NetworkAssumeValid { + match network.to_lowercase().as_str() { + "mainnet" | "bitcoinv1" => CORE_MAINNET, + "testnet" | "testnet3" => CORE_TESTNET3, + "testnet4" => CORE_TESTNET4, + "signet" => CORE_SIGNET, + _ => NO_ASSUME_VALID, // Regtest and unknown: validate all + } +} + /// Per-network default assume-valid height (Core chainparams defaultAssumeValid) pub fn default_assume_valid_height_for_network(network: &str) -> u64 { - match network.to_lowercase().as_str() { - "mainnet" | "bitcoinv1" => 912_683, // Core mainnet default - "testnet" | "testnet3" => 4_550_000, // Core testnet default - "testnet4" => 0, // full verification; no assume-valid skip - "signet" => 267_665, // Core signet default - _ => 0, // Regtest and unknown: validate all + core_assume_valid_for_network(network).height +} + +/// Assume-valid settings handed to consensus at startup. +/// +/// Minimum work always comes from the network: without it any header chain, however +/// little work it has, would skip scripts below the height. The height comes from the +/// env override, then the config file, then the network. A configured height that +/// equals the network's keeps the network's hash, so the `blvm` binary's height-only +/// seed does not drop the anchor. +pub fn resolve_assume_valid( + network: &str, + custom_signet: bool, + file: Option<&BlockValidationNodeConfig>, + env_height: Option, +) -> NetworkAssumeValid { + let net = if custom_signet { + NO_ASSUME_VALID + } else { + core_assume_valid_for_network(network) + }; + let hash_for = |height: u64| if height == net.height { net.hash } else { None }; + let (height, hash) = match (env_height, file) { + (Some(h), _) => (h, hash_for(h)), + (None, Some(bv)) => ( + bv.assume_valid_height, + bv.assume_valid_hash + .or_else(|| hash_for(bv.assume_valid_height)), + ), + (None, None) => (net.height, net.hash), + }; + NetworkAssumeValid { + height, + hash, + min_chain_work: net.min_chain_work, } } diff --git a/src/node/mod.rs b/src/node/mod.rs index 91c51d61..1a9ed8bb 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -207,12 +207,28 @@ impl Node { .as_ref() .map(|bv| bv.assume_valid_height) .unwrap_or(0); - if let Some(ref bv) = config.block_validation { - if std::env::var("BLVM_ASSUME_VALID_HEIGHT").is_err() { - consensus_config.block_validation.assume_valid_height = bv.assume_valid_height; - consensus_config.block_validation.assume_valid_hash = bv.assume_valid_hash; - } - } + let network_name = match self.protocol_version { + ProtocolVersion::BitcoinV1 => "mainnet", + ProtocolVersion::Testnet3 => "testnet3", + ProtocolVersion::Testnet4 => "testnet4", + ProtocolVersion::Signet => "signet", + ProtocolVersion::Regtest => "regtest", + }; + let custom_signet = self.protocol_version == ProtocolVersion::Signet + && config + .signet_challenge + .as_deref() + .is_some_and(|h| !h.trim().is_empty()); + let resolved = crate::config::resolve_assume_valid( + network_name, + custom_signet, + config.block_validation.as_ref(), + assume_valid_from_env + .then_some(consensus_config.block_validation.assume_valid_height), + ); + consensus_config.block_validation.assume_valid_height = resolved.height; + consensus_config.block_validation.assume_valid_hash = resolved.hash; + consensus_config.block_validation.n_minimum_chain_work = resolved.min_chain_work; let height = consensus_config.block_validation.assume_valid_height; let hash = consensus_config.block_validation.assume_valid_hash; blvm_protocol::consensus_config::init_consensus_config(consensus_config); @@ -228,8 +244,8 @@ impl Node { if height > 0 { let hash_s = hash.map(hex::encode).unwrap_or_else(|| "none".to_string()); info!( - "Consensus assume-valid: height={} hash={} source={} (blocks before this height may skip script checks per network policy)", - height, hash_s, source + "Consensus assume-valid: height={} hash={} min_chain_work={:#x} source={} (blocks before this height may skip script checks per network policy)", + height, hash_s, resolved.min_chain_work, source ); } else { info!( diff --git a/tests/assumevalid_tests.rs b/tests/assumevalid_tests.rs index fbd9ffbd..6bf1f70a 100644 --- a/tests/assumevalid_tests.rs +++ b/tests/assumevalid_tests.rs @@ -2,25 +2,28 @@ //! //! Verifies -assumevalid CLI, config wiring, and per-network defaults. -use blvm_node::config::{BlockValidationNodeConfig, default_assume_valid_height_for_network}; +use blvm_node::config::{ + BlockValidationNodeConfig, core_assume_valid_for_network, + default_assume_valid_height_for_network, resolve_assume_valid, +}; #[test] fn test_default_assume_valid_height_per_network() { - assert_eq!(default_assume_valid_height_for_network("mainnet"), 912_683); - assert_eq!(default_assume_valid_height_for_network("Mainnet"), 912_683); + assert_eq!(default_assume_valid_height_for_network("mainnet"), 938_343); + assert_eq!(default_assume_valid_height_for_network("Mainnet"), 938_343); assert_eq!( default_assume_valid_height_for_network("bitcoinv1"), - 912_683 + 938_343 ); assert_eq!( default_assume_valid_height_for_network("testnet"), - 4_550_000 + 4_842_348 ); assert_eq!( default_assume_valid_height_for_network("Testnet"), - 4_550_000 + 4_842_348 ); - assert_eq!(default_assume_valid_height_for_network("signet"), 267_665); + assert_eq!(default_assume_valid_height_for_network("signet"), 293_175); assert_eq!(default_assume_valid_height_for_network("regtest"), 0); assert_eq!(default_assume_valid_height_for_network("unknown"), 0); } @@ -54,3 +57,133 @@ fn test_noassumevalid_disables_skip() { }; assert_eq!(config.assume_valid_height, 0); } + +/// Display (RPC) hex to internal byte order. +fn from_display(hex: &str) -> [u8; 32] { + let mut out = [0u8; 32]; + for (i, b) in out.iter_mut().enumerate() { + *b = u8::from_str_radix(&hex[2 * i..2 * i + 2], 16).unwrap(); + } + out.reverse(); + out +} + +fn work(hex: &str) -> u128 { + let trimmed = hex.trim_start_matches('0'); + u128::from_str_radix(trimmed, 16).unwrap() +} + +/// Bitcoin Core v31.1 `src/kernel/chainparams.cpp`, copied verbatim. +#[test] +fn test_assume_valid_anchors_match_core_v31_1() { + let main = core_assume_valid_for_network("mainnet"); + assert_eq!(main.height, 938_343); + assert_eq!( + main.hash, + Some(from_display( + "00000000000000000000ccebd6d74d9194d8dcdc1d177c478e094bfad51ba5ac" + )) + ); + assert_eq!( + main.min_chain_work, + work("0000000000000000000000000000000000000001128750f82f4c366153a3a030") + ); + + let test3 = core_assume_valid_for_network("testnet"); + assert_eq!(test3.height, 4_842_348); + assert_eq!( + test3.hash, + Some(from_display( + "000000007a61e4230b28ac5cb6b5e5a0130de37ac1faf2f8987d2fa6505b67f4" + )) + ); + assert_eq!( + test3.min_chain_work, + work("0000000000000000000000000000000000000000000017dde1c649f3708d14b6") + ); + + let test4 = core_assume_valid_for_network("testnet4"); + assert_eq!((test4.height, test4.hash), (0, None)); + assert_eq!( + test4.min_chain_work, + work("0000000000000000000000000000000000000000000009a0fe15d0177d086304") + ); + + let sig = core_assume_valid_for_network("signet"); + assert_eq!(sig.height, 293_175); + assert_eq!( + sig.hash, + Some(from_display( + "00000008414aab61092ef93f1aacc54cf9e9f16af29ddad493b908a01ff5c329" + )) + ); + assert_eq!( + sig.min_chain_work, + work("00000000000000000000000000000000000000000000000000000b463ea0a4b8") + ); + + let reg = core_assume_valid_for_network("regtest"); + assert_eq!((reg.height, reg.hash, reg.min_chain_work), (0, None, 0)); +} + +/// The `blvm` binary seeds `[block_validation]` with the network height and no hash. +/// That seed must keep the anchor and the minimum work; before, it reached consensus +/// as height 912683, hash none, minimum work 0. +#[test] +fn test_binary_height_seed_keeps_hash_and_minimum_work() { + let main = core_assume_valid_for_network("mainnet"); + let seed = BlockValidationNodeConfig { + assume_valid_height: default_assume_valid_height_for_network("mainnet"), + assume_valid_hash: None, + }; + let got = resolve_assume_valid("mainnet", false, Some(&seed), None); + assert_eq!(got, main); + assert!(got.hash.is_some()); + assert_ne!(got.min_chain_work, 0); +} + +#[test] +fn test_zero_config_uses_network_values() { + assert_eq!( + resolve_assume_valid("mainnet", false, None, None), + core_assume_valid_for_network("mainnet") + ); + // Regtest must not inherit the mainnet height: any regtest chain would skip scripts. + let reg = resolve_assume_valid("regtest", false, None, None); + assert_eq!((reg.height, reg.hash, reg.min_chain_work), (0, None, 0)); +} + +#[test] +fn test_custom_height_drops_hash_keeps_minimum_work() { + let cfg = BlockValidationNodeConfig { + assume_valid_height: 700_000, + assume_valid_hash: None, + }; + let got = resolve_assume_valid("mainnet", false, Some(&cfg), None); + assert_eq!(got.height, 700_000); + assert_eq!(got.hash, None); + assert_eq!( + got.min_chain_work, + core_assume_valid_for_network("mainnet").min_chain_work + ); +} + +#[test] +fn test_env_and_noassumevalid_override_height_not_minimum_work() { + let env = resolve_assume_valid("mainnet", false, None, Some(0)); + assert_eq!((env.height, env.hash), (0, None)); + assert_ne!(env.min_chain_work, 0); + + let off = BlockValidationNodeConfig { + assume_valid_height: 0, + assume_valid_hash: None, + }; + let got = resolve_assume_valid("mainnet", false, Some(&off), None); + assert_eq!((got.height, got.hash), (0, None)); +} + +#[test] +fn test_custom_signet_has_no_assume_valid() { + let got = resolve_assume_valid("signet", true, None, None); + assert_eq!((got.height, got.hash, got.min_chain_work), (0, None, 0)); +} From 8ac954c99ad3e63943076ada300dd345178c977b Mon Sep 17 00:00:00 2001 From: MeABzZz <73040345+MeABzZz@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:08:12 -0300 Subject: [PATCH 2/2] test(node): describe the height-only seed after blvm 19da720 --- tests/assumevalid_tests.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/assumevalid_tests.rs b/tests/assumevalid_tests.rs index 6bf1f70a..e62fbd95 100644 --- a/tests/assumevalid_tests.rs +++ b/tests/assumevalid_tests.rs @@ -126,9 +126,10 @@ fn test_assume_valid_anchors_match_core_v31_1() { assert_eq!((reg.height, reg.hash, reg.min_chain_work), (0, None, 0)); } -/// The `blvm` binary seeds `[block_validation]` with the network height and no hash. -/// That seed must keep the anchor and the minimum work; before, it reached consensus -/// as height 912683, hash none, minimum work 0. +/// A `[block_validation]` that gives only the network's default height and no hash (what +/// the `blvm` binary seeded before blvm 19da720, and what old config files still say) must +/// keep the anchor and the minimum work; before, it reached consensus as height 912683, +/// hash none, minimum work 0. #[test] fn test_binary_height_seed_keeps_hash_and_minimum_work() { let main = core_assume_valid_for_network("mainnet");