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
38 changes: 19 additions & 19 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ members = [".", "crates/sentrix-primitives", "crates/sentrix-wallet", "crates/se
# `version.workspace = true`. Same goes for edition/license/repository so
# they can't drift across crates.
[workspace.package]
version = "2.2.30"
version = "2.2.31"
edition = "2024"
license = "BUSL-1.1"
repository = "https://github.com/sentrix-labs/sentrix"
Expand Down
21 changes: 21 additions & 0 deletions crates/sentrix-core/src/block_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,17 @@ pub(crate) struct BlockchainSnapshot {
/// Restored via `SentrixTrie::set_root` on Pass 2 failure so the
/// next block's `update_trie_for_block` walks the correct state.
trie_root: Option<[u8; 32]>,
/// Consensus/staking state. After STATE_IN_TRIE_HEIGHT these feed the
/// state_root, and the centralized reward bundle (apply_reward_bookkeeping)
/// mutates them inside `apply_block_pass2` BEFORE the #1e state_root
/// check. They were missing here, so a #1e reject left pending_rewards /
/// epoch / liveness incremented — that leak then diverged the next
/// block's computed root and crawled the chain once both forks were live.
/// Snapshot + restore them so the Pass-2 rollback is atomic over
/// everything the state_root now commits.
stake_registry: sentrix_staking::staking::StakeRegistry,
epoch_manager: sentrix_staking::epoch::EpochManager,
slashing: sentrix_staking::slashing::SlashingEngine,
}

/// Frontier Phase F-2 shadow observer. Calls into the F-1 scaffold's
Expand Down Expand Up @@ -606,6 +617,9 @@ impl Blockchain {
total_minted: self.total_minted,
chain_len: self.chain.len(),
trie_root: self.state_trie.as_ref().map(|t| t.root_hash()),
stake_registry: self.stake_registry.clone(),
epoch_manager: self.epoch_manager.clone(),
slashing: self.slashing.clone(),
Comment on lines +620 to +622

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 | 🟠 Major | 🏗️ Heavy lift

Rollback is still non-atomic for pre-#1e side effects.

Restoring stake_registry, epoch_manager, and slashing closes the consensus leak, but apply_block_pass2 still performs success-only side effects before the later state_root reject path can fire. In this same function, TABLE_BLOOM is written at Lines 1507-1517 and subscriber notifications are emitted throughout Pass 2, including emit_new_head / emit_finalized at Lines 1573-1585, while the #1e rejection still happens later at Lines 1632-1748. A block that fails the root check will now rewind staking state but can still leave phantom query data or notify clients about a block that never committed. Please defer those non-rollbackable writes/emits until after the state_root check, or buffer them behind the success path. As per coding guidelines, crates/sentrix-core/src/block_executor** is consensus-critical and state-apply must be deterministic.

Also applies to: 669-675

🤖 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 `@crates/sentrix-core/src/block_executor.rs` around lines 620 - 622, The
non-rollbackable side effects in apply_block_pass2 (notably writes to
TABLE_BLOOM and subscriber notifications like emit_new_head / emit_finalized)
must be deferred until after the state_root check that can still reject the
block; modify apply_block_pass2 to buffer TABLE_BLOOM updates and all subscriber
emits (or gate them behind a success flag) and perform those buffered
writes/emits only after the state_root verification passes, and similarly ensure
the rollback that restores stake_registry, epoch_manager, and slashing (and the
analogous code around the earlier restore at the other mentioned spot) remains
unchanged but occurs before any buffered side-effects are flushed so no phantom
data or notifications escape a failed state_root.

};

match self.apply_block_pass2(block) {
Expand Down Expand Up @@ -652,6 +666,13 @@ impl Blockchain {
self.rebuild_mempool_sidecars();
self.total_minted = snap.total_minted;
self.chain.truncate(snap.chain_len);
// Restore the consensus/staking state too — post STATE_IN_TRIE
// it's in the state_root and the reward bundle mutated it above,
// so leaving it dirty after a #1e reject leaks pending_rewards /
// epoch / liveness into the next block's root.
self.stake_registry = snap.stake_registry;
self.epoch_manager = snap.epoch_manager;
self.slashing = snap.slashing;
// Rewind trie to pre-Pass-2 root if one was captured.
// Orphan nodes from the failed block's partial inserts
// remain in MDBX but are unreachable from any committed
Expand Down
Loading