Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 113 additions & 6 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -701,14 +701,121 @@ pub fn parse_signet_challenge_hex(hex_str: &str) -> Result<Vec<u8>> {
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<u64>,
) -> 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,
}
}

Expand Down
32 changes: 24 additions & 8 deletions src/node/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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!(
Expand Down
148 changes: 141 additions & 7 deletions tests/assumevalid_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -54,3 +57,134 @@ 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));
}

/// 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");
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));
}