Skip to content

Add pallet-block-timestamps for historical block timestamp access #2547

@fine135

Description

@fine135

Motivation

Currently, drand randomness stored on-chain has no direct EVM read path - smart contracts cannot access it through a precompile. I'm building an EVM precompile to expose drand randomness, but the underlying drand implementation is changing soon: it will move from round-based (multiple rounds per block, delivery not guaranteed) to block-based (one deterministic randomness value per block). The only way to build a precompile that survives this transition is to key reads by block number rather than internal drand round IDs. This means the precompile needs to map blockNumber → blockTimestamp → drand round, which requires access to historical block timestamps from within the runtime.

Problem Statement

Substrate does not expose historical block timestamps from within the runtime. pallet_timestamp::Now stores only the current block's value and is overwritten every block. frame_system::BlockHash retains hashes for recent blocks, but these are opaque - the full header (containing the Aura slot in its digest) is stored in the client's off-chain database, inaccessible to runtime code.

This means no pallet or EVM precompile can answer "what was the timestamp of block N?" for any past block.

Why Aura slot estimation doesn't work

Since Aura slots have a deterministic relationship with time (slot = timestamp_ms / slot_duration), it's tempting to estimate past timestamps as (current_slot - (current_block - N)) * slot_duration. However, this breaks when a validator misses a slot - the result for the same block number changes depending on when you query it:

  • At block 100: slot(50) = current_slot - 50
  • At block 200 (one missed slot in between): slot(50) = (current_slot + 101) - 150 = current_slot - 49

Different slot → different timestamp → non-deterministic results for the same input, which is unacceptable for precompiles and on-chain logic.

Describe the solution you'd like

Proposed Design

A minimal new pallet (pallet-block-timestamps) with a single storage map:

#[pallet::storage]
pub type Timestamps<T: Config> =
    StorageMap<_, Blake2_128Concat, BlockNumberFor<T>, u64, OptionQuery>;

Recording

The pallet implements OnTimestampSet and is wired into pallet_timestamp::Config alongside Aura:

// runtime config
impl pallet_timestamp::Config for Runtime {
    type Moment = u64;
    type OnTimestampSet = (Aura, BlockTimestamps);
    // ...
}
// pallet implementation
impl<T: Config> OnTimestampSet<T::Moment> for Pallet<T> {
    fn on_timestamp_set(moment: T::Moment) {
        let block_number = frame_system::Pallet::<T>::block_number();
        let secs: u64 = moment.into() / 1000;
        Timestamps::<T>::insert(block_number, secs);
    }
}

This fires the moment the timestamp inherent is applied - before any extrinsics or EVM transactions execute. The block execution order is:

  1. on_initialize - pallet_timestamp::Now still holds previous block's value
  2. Inherent pallet_timestamp::set() → writes Now → calls OnTimestampSetour pallet stores Timestamps[N]
  3. Extrinsics / EVM - Timestamps[current_block] is already available

This guarantees that the current block's timestamp is accessible to precompiles and other runtime code during the same block.

Pruning

Entries older than a configurable retention window are pruned in on_initialize - not inside on_timestamp_set, since adding variable work to a Mandatory inherent is unsafe (it bypasses normal block weight limits). Since we add exactly one entry per block, pruning is trivial: check if current_block - oldest > MAX_KEPT_TIMESTAMPS, and if so remove the single oldest entry. No loop or batching needed - constant cost of 1 read + at most 2 writes per block. A sensible default is one week (MAX_KEPT_TIMESTAMPS = 50,400 blocks at 12s block time), matching the retention window of drand pulses.

Open Questions

  1. Should the retention window be a Config constant or hardcoded?
  2. Should we store milliseconds (matching pallet_timestamp::Now) or seconds (matching Unix convention and drand)?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions