Skip to content

[Bug] Staking precompile state is not persisted across restarts #97

@qj0r9j0vc2

Description

@qj0r9j0vc2

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

  1. Node restart loses all stake: After restarting, validators must re-register
  2. Slashing history lost: Slashed amounts and pending exits are forgotten
  3. Epoch state lost: Current epoch resets to 0
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions