Skip to content

Reinstate JSON-based history feature flag and add staking reward snapshots #3311

Description

@miazn

Reinstate JSON-based history feature flag and add staking reward snapshots

Background

The history feature flag previously wrote full JSON snapshots of key credits.aleo mappings at each finalized block height (commit before 09f729f7). This was behind #[cfg(all(feature = "history", feature = "rocks"))] and only ran in FinalizeMode::RealRun. It wrote five mappings per block:

  • delegated
  • bonded
  • metadata
  • unbonding
  • withdraw

These were written as JSON files at the time of finalization, allowing external consumers (e.g. block explorers and data pipelines) to efficiently reconstruct historical staking state at any block height.

Problem

In commit 09f729f7 ("perf: revamp historical mapping storage", Feb 2026), this approach was replaced with a per-key RocksDB store in ledger/store/src/program/finalize.rs. The new approach writes every individual mapping key change across all programs at every block, storing (program_id, mapping_name, key, height) → value in RocksDB.

This is not performant for external data consumers that only care about specific, well-known mappings (the five credits.aleo staking mappings above). The old approach was targeted and cheap — it wrote exactly the data needed once per block, as a single coherent snapshot. The new approach writes orders of magnitude more data with no scoping.

Proposal

1. Reinstate the JSON-based history feature flag

Restore the original behavior: when compiled with --features history, write JSON snapshots of the five credits.aleo mappings at the end of each FinalizeMode::RealRun finalization. The removed code was in synthesizer/src/vm/finalize.rs inside the credits finalize block:

#[cfg(all(feature = "history", feature = "rocks"))]
{
    if IS_FINALIZE {
        let history = History::new(N::ID, store.storage_mode());
        history.store_mapping(state.block_height(), MappingName::Delegated, &next_delegated_map)?;
        history.store_mapping(state.block_height(), MappingName::Bonded, &next_bonded_map)?;
        // ...metadata, unbonding, withdraw
    }
}

And the History struct itself (deleted in synthesizer/src/vm/helpers/history.rs) needs to be restored.

2. Extend the history flag to also write staking rewards per block

During credits finalization, the VM already computes per-staker rewards via staking_rewards(). This produces a map of:

staker_address → (validator_address, reward_microcredits, new_total_stake)

This data is computed but not persisted anywhere in the canonical chain state — it is derived, ephemeral, and only available at finalization time. External consumers that want to reconstruct historical staking rewards (e.g. for APY calculations, attribution, or tax reporting) currently cannot access this without replaying the entire chain.

We'd like the history flag to also write this computed staking reward data alongside the mapping snapshots, one JSON entry per staker per block. This does not need to be stored in the credits program state or in RocksDB — a JSON file per block height alongside the existing mapping history files is sufficient.

The history-staking-rewards feature that was briefly present in this repo (commit 62a05e3ee, later disabled) attempted to do this via store.staking_rewards_map(). We'd prefer this to be merged into the history feature itself and written to disk in the same JSON snapshot pattern rather than introduced as a separate RocksDB map.

3. Keep slipstream-plugins as a separate, independent feature

The slipstream-plugins feature flag should remain decoupled from history and history-staking-rewards. We'd like these two concerns kept orthogonal — enabling history snapshots should not require or imply slipstream, and vice versa.

What we're not asking for

  • We're not asking for staking rewards to be written into credits.aleo state or be part of consensus.
  • We're not asking for real-time indexing or streaming. A file per finalized block is fine.

Why this matters

Without the scoped JSON snapshot approach, external nodes that need historical staking state must either replay the full chain or maintain their own side-channel tracking at significant operational cost. For block explorers, attribution dashboards, and validators tracking delegator rewards, the targeted per-block snapshot approach is the right interface.

The old implementation was minimal, correct, and required no changes to consensus — it was purely a compile-time opt-in for node operators who wanted this data.

Questions

  • Is there appetite to restore synthesizer/src/vm/helpers/history.rs and the associated snapshot writes in finalize.rs?

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions