-
Notifications
You must be signed in to change notification settings - Fork 299
Add pallet-block-timestamps for historical block timestamp access #2547
Description
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:
on_initialize-pallet_timestamp::Nowstill holds previous block's value- Inherent
pallet_timestamp::set()→ writesNow→ callsOnTimestampSet→ our pallet storesTimestamps[N] - 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
- Should the retention window be a
Configconstant or hardcoded? - Should we store milliseconds (matching
pallet_timestamp::Now) or seconds (matching Unix convention and drand)?