-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Labels
Description
Problem
The StakingPrecompile stores all validator staking state in an in-memory HashMap. This state is lost when the node restarts, causing:
- All staked validators to disappear
- Total stake to reset to zero
- Validator set to become empty
Location
crates/execution/src/precompiles/staking.rs:128-148 and 210-231
Code Analysis
/// Staking state managed by the precompile.
#[derive(Debug, Clone)]
pub struct StakingState {
/// Active validators (address -> ValidatorInfo).
pub validators: HashMap<Address, ValidatorInfo>, // IN-MEMORY!
/// Total staked amount.
pub total_stake: U256,
/// Current epoch number.
pub epoch: u64,
}
// ...
/// Staking precompile implementation.
#[derive(Debug, Clone)]
pub struct StakingPrecompile {
state: Arc<RwLock<StakingState>>, // Arc<RwLock<>> around ephemeral state
}
impl StakingPrecompile {
/// Create a new staking precompile with empty state.
pub fn new() -> Self {
Self {
state: Arc::new(RwLock::new(StakingState::default())), // Starts empty
}
}
}Impact
- Node restart loses all stake: After restarting, validators must re-register
- Slashing history lost: Slashed amounts and pending exits are forgotten
- Epoch state lost: Current epoch resets to 0
- Consensus breaks: ValidatorSetManager relies on staking data which disappears
Root Cause
The precompile was designed for testing but deployed in production. There's a comment in new():
/// Create with existing state (for testing).
pub fn with_state(state: StakingState) -> Self {This suggests awareness that state needs to be loaded somehow, but no actual persistence mechanism exists.
Suggested Fix
Option 1: Integrate with EVM state trie (preferred for EVM compatibility):
// Store validator data in contract storage slots at STAKING_PRECOMPILE_ADDRESS
// Slot layout:
// 0: validator_count
// keccak(validator_address, 1): ValidatorInfo for that address
impl StakingPrecompile {
pub fn run_with_db<DB: Database>(
&self,
db: &mut DB,
input: &Bytes,
// ...
) -> PrecompileResult {
// Load state from DB
let state = self.load_state_from_db(db)?;
// Execute operation
let result = self.run_internal(&state, input, ...)?;
// Persist state changes to DB
self.save_state_to_db(db, &state)?;
Ok(result)
}
}Option 2: Integrate with MDBX storage:
// Use crates/storage/src/staking.rs StakingStore trait
pub struct StakingPrecompile {
store: Arc<dyn StakingStore>, // Persistent storage
}
impl StakingPrecompile {
pub fn new(store: Arc<dyn StakingStore>) -> Self {
Self { store }
}
fn register_validator(&self, ...) -> PrecompileResult {
// Persist immediately
self.store.put_validator(validator)?;
// ...
}
}Option 3: Load from genesis on startup:
// In node initialization
let genesis_validators = load_genesis_validators(&genesis_config)?;
let staking_state = StakingState {
validators: genesis_validators.into_iter()
.map(|v| (v.address, v))
.collect(),
total_stake: genesis_validators.iter().map(|v| v.stake).sum(),
epoch: 0,
};
let staking_precompile = StakingPrecompile::with_state(staking_state);Related
The storage crate already has StakingStore trait (crates/storage/src/staking.rs) that should be used:
pub trait StakingStore: Send + Sync {
fn get_validator(&self, address: &Address) -> StakingStoreResult<Option<StoredValidator>>;
fn put_validator(&self, validator: StoredValidator) -> StakingStoreResult<()>;
fn delete_validator(&self, address: &Address) -> StakingStoreResult<bool>;
// ...
}Severity
Critical - All staking state is lost on node restart, breaking consensus validator set.