Skip to content
Merged
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
4 changes: 4 additions & 0 deletions crates/sentrix-core/src/blockchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,10 @@ impl Blockchain {
crate::fork_heights::is_state_in_trie_height(height)
}

pub fn is_native_state_in_trie_height(height: u64) -> bool {
crate::fork_heights::is_native_state_in_trie_height(height)
}

pub fn is_bft_gate_relax_height(height: u64) -> bool {
crate::fork_heights::is_bft_gate_relax_height(height)
}
Expand Down
46 changes: 45 additions & 1 deletion crates/sentrix-core/src/blockchain_trie_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ use sentrix_primitives::transaction::{PROTOCOL_TREASURY, TOKEN_OP_ADDRESS};
use sentrix_storage::MdbxStorage;
use sentrix_trie::address::{
account_value_bytes, address_to_key, epoch_state_key, epoch_state_value_bytes,
liveness_value_bytes, pending_rewards_value_bytes, total_minted_key, total_minted_value_bytes,
liveness_value_bytes, native_nft_registry_key, native_src20_registry_key,
pending_rewards_value_bytes, total_minted_key, total_minted_value_bytes,
validator_liveness_key, validator_pending_rewards_key,
};

Expand Down Expand Up @@ -517,6 +518,22 @@ impl Blockchain {
}
// Borrow of `stake_registry` / `slashing` / `total_minted` ends here.

// Native-module state commitment. Independent of the SIP-6
// STATE_IN_TRIE gate above (which is already active on testnet —
// reusing it would retroactively fork testnet's state_root). Capture
// the SRC-20 + NFT registry canonical hashes here, before the trie
// mut-borrow, and commit them in Phase 2f. Pre-fork both are None and
// the state_root is unchanged from today.
let native_src20_hash: Option<[u8; 32]>;
let native_nft_hash: Option<[u8; 32]>;
if Self::is_native_state_in_trie_height(block_index) {
native_src20_hash = Some(self.contracts.canonical_hash());
native_nft_hash = Some(self.nft_registry.canonical_hash());
} else {
native_src20_hash = None;
native_nft_hash = None;
}

if trace {
eprintln!("[trie-trace] update_trie_for_block at h={block_index}");
eprintln!("[trie-trace] touched (sorted): {} addresses", updates.len());
Expand Down Expand Up @@ -669,6 +686,33 @@ impl Blockchain {
}
}

// Phase 2f — native-module state commitment (post
// NATIVE_STATE_IN_TRIE fork only). Each registry's canonical hash is
// written under a single fixed key, overwritten every block, so the
// state_root reflects all SRC-20 + NFT state. Always-insert (even when
// empty) — an empty registry has a stable canonical hash distinct from
// any populated one, same insert-always semantics as total_minted.
if let Some(h) = native_src20_hash {
trie.insert(&native_src20_registry_key(), &h)?;
if trace {
eprintln!(
"[trie-trace] native_src20 hash={} → root={}",
hex::encode(&h[..8]),
hex::encode(trie.root_hash())
);
}
}
if let Some(h) = native_nft_hash {
trie.insert(&native_nft_registry_key(), &h)?;
if trace {
eprintln!(
"[trie-trace] native_nft hash={} → root={}",
hex::encode(&h[..8]),
hex::encode(trie.root_hash())
);
}
}

let root = trie.commit(block_index)?;
if trace {
eprintln!(
Expand Down
75 changes: 75 additions & 0 deletions crates/sentrix-core/src/fork_heights.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,17 @@ const STRICT_JUSTIFICATION_HEIGHT_DEFAULT: u64 = u64::MAX;
const STATE_IN_TRIE_HEIGHT_DEFAULT: u64 = u64::MAX;
const STATE_IN_TRIE_HEIGHT_TESTNET_DEFAULT: u64 = 6_026_000;

/// Native-module state-root commitment (SRC-20 ContractRegistry + NFT
/// NftRegistry into the trie). Default `u64::MAX` on BOTH mainnet AND
/// testnet — deliberately NOT reusing `STATE_IN_TRIE_HEIGHT` (already
/// active on testnet at 6,026,000): reusing it would retroactively change
/// testnet's state_root and fork the chain. This is a fresh, independent
/// gate that stays disabled everywhere until an explicit activation
/// height is set via env, preceded by halt-all + state-root-alignment
/// pre-flight + simul-start so every validator commits identical native
/// state at the activation block.
const NATIVE_STATE_IN_TRIE_HEIGHT_DEFAULT: u64 = u64::MAX;

// ── Runtime readers (env → u64, default to compile-time default) ──────

fn configured_chain_id() -> u64 {
Expand Down Expand Up @@ -399,6 +410,17 @@ pub fn get_state_in_trie_height() -> u64 {
)
}

/// Native-module state-root commitment fork height. Default `u64::MAX`
/// on both nets (see [`NATIVE_STATE_IN_TRIE_HEIGHT_DEFAULT`]). Once
/// pinned via env, the apply path commits the SRC-20 + NFT registry
/// canonical hashes into the trie before state_root.
pub fn get_native_state_in_trie_height() -> u64 {
read_height(
"NATIVE_STATE_IN_TRIE_HEIGHT",
NATIVE_STATE_IN_TRIE_HEIGHT_DEFAULT,
)
}

// ── Height predicates ────────────────────────────────────
//
// Every fork-height predicate has the same shape:
Expand Down Expand Up @@ -529,6 +551,18 @@ pub fn is_state_in_trie_height(height: u64) -> bool {
fork != u64::MAX && height >= fork
}

/// Native-module state-root commitment — true once
/// `NATIVE_STATE_IN_TRIE_HEIGHT` activates. Post-fork: the apply path
/// commits the SRC-20 `ContractRegistry` and NFT `NftRegistry` canonical
/// hashes into the state trie before state_root, so divergence in native
/// token/NFT state surfaces as a state_root mismatch instead of relying
/// on deterministic re-execution + the (uncommitted) blob. Pre-fork the
/// state_root is computed exactly as before (native state stays off-trie).
pub fn is_native_state_in_trie_height(height: u64) -> bool {
let fork = get_native_state_in_trie_height();
fork != u64::MAX && height >= fork
}

/// BFT-gate-relax: is the given height at or after the fork?
/// Post-fork: validator-loop's P1 BFT safety gate uses
/// `active >= ⌈2/3 × total⌉` instead of `active >= MIN_BFT_VALIDATORS
Expand Down Expand Up @@ -749,4 +783,45 @@ mod tests {
std::env::remove_var("JAIL_CONSENSUS_HEIGHT");
}
}

/// NATIVE_STATE_IN_TRIE must default to disabled (`u64::MAX`) on BOTH
/// mainnet and testnet — unlike SIP-6 STATE_IN_TRIE, which is already
/// armed on testnet. Reusing that gate would have forked testnet; this
/// fresh gate stays closed everywhere until explicitly pinned.
#[test]
fn native_state_in_trie_disabled_by_default_both_nets() {
let _guard = env_test_lock();
unsafe {
std::env::remove_var("NATIVE_STATE_IN_TRIE_HEIGHT");

std::env::set_var("SENTRIX_CHAIN_ID", MAINNET_CHAIN_ID.to_string());
assert_eq!(get_native_state_in_trie_height(), u64::MAX);
assert!(!is_native_state_in_trie_height(0));
assert!(!is_native_state_in_trie_height(u64::MAX - 1));

std::env::set_var("SENTRIX_CHAIN_ID", TESTNET_CHAIN_ID.to_string());
assert_eq!(
get_native_state_in_trie_height(),
u64::MAX,
"must stay disabled on testnet too (STATE_IN_TRIE is already armed there)"
);
assert!(!is_native_state_in_trie_height(6_026_000));

std::env::remove_var("SENTRIX_CHAIN_ID");
}
}

/// Once pinned, the gate flips open exactly at the activation height.
#[test]
fn native_state_in_trie_activates_at_pinned_height() {
let _guard = env_test_lock();
unsafe {
std::env::set_var("NATIVE_STATE_IN_TRIE_HEIGHT", "1000");
assert_eq!(get_native_state_in_trie_height(), 1000);
assert!(!is_native_state_in_trie_height(999));
assert!(is_native_state_in_trie_height(1000));
assert!(is_native_state_in_trie_height(1001));
std::env::remove_var("NATIVE_STATE_IN_TRIE_HEIGHT");
}
}
}
Loading
Loading