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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,6 @@ __pycache__/
.cur[a-z]or/
target-docker/
*.proptest-regressions

# ── Local docs preview build (node_modules + build cache only) ──
docs-site/
25 changes: 25 additions & 0 deletions bin/sentrix/src/commands/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,31 @@ fn render_json(r: &PreflightReport) -> String {
}

/// `sentrix state preflight [--json]` — read-only activation readiness report.
/// Dump per-component state fingerprints for the chain.db at SENTRIX_DATA_DIR.
/// Run against each node's data dir (node STOPPED) and diff the output to find
/// WHICH state component diverges across nodes at the same height. Added
/// 2026-06-04 to chase the post-#782 determinism drift.
pub fn cmd_state_fingerprint() -> anyhow::Result<()> {
Comment on lines 352 to +357
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix the stale docstring on this command.

Line 352 still describes state preflight, but this function is the fingerprint command. This is misleading in generated docs and code navigation.

Suggested patch
-/// `sentrix state preflight [--json]` — read-only activation readiness report.
+/// `sentrix state fingerprint` — dump per-component state fingerprints (read-only).
 /// Dump per-component state fingerprints for the chain.db at SENTRIX_DATA_DIR.
 /// Run against each node's data dir (node STOPPED) and diff the output to find
 /// WHICH state component diverges across nodes at the same height. Added
 /// 2026-06-04 to chase the post-#782 determinism drift.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// `sentrix state preflight [--json]` — read-only activation readiness report.
/// Dump per-component state fingerprints for the chain.db at SENTRIX_DATA_DIR.
/// Run against each node's data dir (node STOPPED) and diff the output to find
/// WHICH state component diverges across nodes at the same height. Added
/// 2026-06-04 to chase the post-#782 determinism drift.
pub fn cmd_state_fingerprint() -> anyhow::Result<()> {
/// `sentrix state fingerprint` — dump per-component state fingerprints (read-only).
/// Dump per-component state fingerprints for the chain.db at SENTRIX_DATA_DIR.
/// Run against each node's data dir (node STOPPED) and diff the output to find
/// WHICH state component diverges across nodes at the same height. Added
/// 2026-06-04 to chase the post-#782 determinism drift.
pub fn cmd_state_fingerprint() -> anyhow::Result<()> {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bin/sentrix/src/commands/state.rs` around lines 352 - 357, The docstring
above the function cmd_state_fingerprint incorrectly describes "state
preflight"; update that comment to accurately describe the "sentrix state
fingerprint [--json]" command — a per-component state fingerprint dump for the
chain.db in SENTRIX_DATA_DIR (run against stopped nodes to diff state components
across nodes at the same height). Ensure the first line, short description, and
any following lines refer to "fingerprint" rather than "preflight" so generated
docs and code navigation reflect the correct command.

let storage = Storage::open(&get_db_path())?;
let bc = storage
.load_blockchain()?
.ok_or_else(|| anyhow::anyhow!("Chain not initialized."))?;

let c = sentrix::core::block_executor::state_components(&bc);
let hx = |b: &[u8; 32]| b.iter().map(|x| format!("{x:02x}")).collect::<String>();

println!("height {}", bc.height());
println!("account_count {}", c.account_count);
println!("total_minted {}", c.total_minted);
println!("total_burned {}", c.total_burned);
println!("accounts_fp {}", hx(&c.accounts_fp));
println!("contract_code_fp {}", hx(&c.contract_code_fp));
println!("contract_stor_fp {}", hx(&c.contract_storage_fp));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use the documented output key name for storage fingerprint.

Line 372 prints contract_stor_fp, but the API/docs use contract_storage_fp. This can break simple parser/diff scripts expecting the canonical field name.

Suggested patch
-    println!("contract_stor_fp  {}", hx(&c.contract_storage_fp));
+    println!("contract_storage_fp {}", hx(&c.contract_storage_fp));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
println!("contract_stor_fp {}", hx(&c.contract_storage_fp));
println!("contract_storage_fp {}", hx(&c.contract_storage_fp));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bin/sentrix/src/commands/state.rs` at line 372, The printed output uses the
wrong key name "contract_stor_fp" which differs from the documented/canonical
"contract_storage_fp"; update the println in the state command (the println!
call that references c.contract_storage_fp) to print the key
"contract_storage_fp" instead of "contract_stor_fp" so external parsers see the
documented field name.

println!("src20_fp {}", hx(&c.src20_fp));
println!("nft_fp {}", hx(&c.nft_fp));
Ok(())
}

pub fn cmd_state_preflight(json: bool) -> anyhow::Result<()> {
let storage = Storage::open(&get_db_path())?;
let bc = storage
Expand Down
6 changes: 6 additions & 0 deletions bin/sentrix/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,11 @@ enum StateCommands {
#[arg(long)]
json: bool,
},
/// Dump per-component state fingerprints (accounts / EVM code / EVM
/// storage / total_minted / total_burned / SRC-20 / NFT) for the chain.db
/// at SENTRIX_DATA_DIR. Run per node (STOPPED) and diff to localize which
/// component diverges across nodes — drift diagnosis.
Fingerprint,
}

#[derive(Subcommand)]
Expand Down Expand Up @@ -894,6 +899,7 @@ async fn main() -> anyhow::Result<()> {
}
StateCommands::Verify { input } => commands::state::cmd_state_verify(&input)?,
StateCommands::Preflight { json } => commands::state::cmd_state_preflight(json)?,
StateCommands::Fingerprint => commands::state::cmd_state_fingerprint()?,
},

Commands::Mempool { action } => match action {
Expand Down
71 changes: 71 additions & 0 deletions crates/sentrix-core/src/block_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1967,6 +1967,77 @@ fn compute_state_fingerprint(bc: &Blockchain) -> ([u8; 32], [u8; 32]) {
(acc_fp, fp)
}

/// Per-component state fingerprints for cross-node drift diagnosis.
///
/// `compute_state_fingerprint` folds everything into one root, which tells
/// you THAT two nodes diverged but not WHERE. This splits each component out
/// (accounts / EVM code / EVM storage / mint / burn / SRC-20 / NFT) so a diff
/// across nodes at the same height points straight at the divergent subsystem.
/// Added 2026-06-04 chasing the post-#782 state-determinism drift.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StateComponents {
/// balance + nonce + code_hash + storage_root over sorted accounts.
pub accounts_fp: [u8; 32],
/// hash over sorted EVM contract bytecode.
pub contract_code_fp: [u8; 32],
/// hash over sorted EVM contract storage slots.
pub contract_storage_fp: [u8; 32],
pub total_minted: u64,
pub total_burned: u64,
/// SRC-20 native registry canonical hash.
pub src20_fp: [u8; 32],
/// NFT native registry canonical hash.
pub nft_fp: [u8; 32],
/// account count (cheap sanity signal alongside the hashes).
pub account_count: usize,
}

/// Compute the per-component fingerprints — see [`StateComponents`].
pub fn state_components(bc: &Blockchain) -> StateComponents {
use sha2::{Digest, Sha256};

let mut accounts: Vec<(&String, &sentrix_primitives::account::Account)> =
bc.accounts.accounts.iter().collect();
accounts.sort_by(|a, b| a.0.cmp(b.0));
let account_count = accounts.len();
let mut acc_h = Sha256::new();
for (addr, account) in accounts {
acc_h.update(addr.as_bytes());
acc_h.update(account.balance.to_be_bytes());
acc_h.update(account.nonce.to_be_bytes());
acc_h.update(account.code_hash);
acc_h.update(account.storage_root);
}

let mut codes: Vec<(&String, &Vec<u8>)> = bc.accounts.contract_code.iter().collect();
codes.sort_by(|a, b| a.0.cmp(b.0));
let mut code_h = Sha256::new();
for (k, v) in codes {
code_h.update(k.as_bytes());
let h: [u8; 32] = Sha256::digest(v).into();
code_h.update(h);
}

let mut storage: Vec<(&String, &Vec<u8>)> = bc.accounts.contract_storage.iter().collect();
storage.sort_by(|a, b| a.0.cmp(b.0));
let mut stor_h = Sha256::new();
for (k, v) in storage {
stor_h.update(k.as_bytes());
stor_h.update(v);
}

StateComponents {
accounts_fp: acc_h.finalize().into(),
contract_code_fp: code_h.finalize().into(),
contract_storage_fp: stor_h.finalize().into(),
total_minted: bc.total_minted,
total_burned: bc.accounts.total_burned,
src20_fp: bc.contracts.canonical_hash(),
nft_fp: bc.nft_registry.canonical_hash(),
account_count,
}
}

// ── Tests ─────────────────────────────────────────────────
#[cfg(test)]
mod tests {
Expand Down
Loading