Step-Scoped State

Definition

A step is a discrete, sequential interval identified by a monotonically increasing u64. Rs defines the step contract — the interface between a cell and its runtime:

  • current_step() -> u64 — returns the current step number
  • reset_step_state() — called by the runtime at the boundary between step N and step N+1

Rs enforces: step state is only accessed within cell context (RS401), and #[step] generates correct reset implementations.

Rs does not define: what triggers step transitions, how long a step lasts, or how steps relate to external events (blocks, time intervals, signals). The runtime provides the step driver.

Problem

Long-running systems accumulate state. A variable set in step N might still be non-zero in step N+1000 because someone forgot to clear it. This is a source of non-determinism and state leaks.

Solution

The #[step] attribute marks state that is automatically reset at step boundaries.

Syntax

#[step]
static PENDING_TXS: Mutex<BoundedVec<Transaction, 10_000>> =
    Mutex::new(BoundedVec::new());

#[step]
static VOTES_THIS_ROUND: AtomicU32 = AtomicU32::new(0);

fn process_transaction(tx: Transaction) {
    PENDING_TXS.lock().push(tx);
    // At step boundary, PENDING_TXS is automatically reset to empty
    // No manual cleanup required. Forgetting is impossible.
}

Semantics

The runtime calls StepState::reset() at the beginning of each step. This is injected by the cell infrastructure, not by user code.

On statics

// Compiler generates this trait impl for every #[step] static:
impl StepReset for BoundedVec<Transaction, 10_000> {
    fn reset(&mut self) {
        self.clear();
    }
}

// At step boundary (generated by cell! macro):
fn __step_reset() {
    PENDING_TXS.lock().reset();
    VOTES_THIS_ROUND.store(0, Ordering::SeqCst);
}

On structs

When #[step] is placed on a struct (as used in cell! generated code for step_state blocks), it generates a StepReset impl that resets all fields:

#[step]
pub struct ConsensusStepState {
    round_votes: BoundedVec<Vote, MAX_VALIDATORS>,
    proposed_block: Option<Block>,
}

// Generated:
impl StepReset for ConsensusStepState {
    fn reset(&mut self) {
        self.round_votes.clear();
        self.proposed_block = None;
    }
}

Reset rules by field type:

Type Reset to
BoundedVec<T, N> clear()
BoundedMap<K, V, N> clear()
Option<T> None
AtomicU32 / AtomicU64 store(0)
bool false
Integer types 0
Custom types must implement StepReset

Compile-Time Checks

In edition = "rs", accessing a #[step] variable outside of a step context is an error:

#[step]
static COUNTER: AtomicU64 = AtomicU64::new(0);

// OK: inside a cell function (has step context)
cell! {
    name: MyCell,
    pub fn tick(&self) {
        COUNTER.fetch_add(1, Ordering::SeqCst);  // OK
    }
}

// ERROR: outside cell context
fn standalone() {
    COUNTER.fetch_add(1, Ordering::SeqCst);
    //~^ error[RS401]: #[step] state accessed outside of cell context
}

Implementation: ~300 lines (attribute handling + lint pass).

Error Reference

See errors/step.md for detailed description of RS401.

Dimensions

step
one tick of consensus time. signals enter, achieve finality, and the tru recomputes cyberank from the new state discover all concepts
rs/reference/errors/step
Step Errors (RS401) [Back to Error Catalog](/rs-reference-errors) | Spec: [step.md](/rs-reference-step) Enforcement: rsc lint (rs edition only). RS401: Step state outside cell context `#[step]` state is automatically reset at step boundaries by the cell runtime. Accessing it outside a cell context…

Local Graph