← Research blog

RESEARCH / CONSENSUS

A Block Timestamp, from Construction to Finalization

Polkadot's producer, validator, and finalization hook form one lifecycle; only then can we test whether a reachable block satisfies it.

Read the block lifecycle as one program

The timestamp pallet is not only a validator for a number. At the pinned Polkadot SDK commit, create_inherent constructs a set call from inherent data and stored Now; check_inherent checks a proposed call; set updates state; and on_finalize checks whether that update happened. Reading one function in isolation would miss the block-level contract.

RECONSTRUCTED SOURCE MAPTimestamp inherent lifecycle
Polkadot SDK bf0d89d

Scroll the diagram horizontally

Stored Now and inherent timestamp data feed create_inherent. The proposed set call is checked against both inputs, while set writes Now and DidUpdate before on_finalize requires the update.
PINNED SOURCE

The diagram is a source map, not a local block trace. Its important distinction is that the producer and checker use overlapping but different inputs. The constructor chooses a value no earlier than Now + MinimumPeriod; the checker also bounds it against the supplied inherent timestamp. The update hook then requires one timestamp update within the block. The upstream tests show successful set and rejected duplicate or too-small values, giving concrete controls for these roles.

Only after mapping production, validation, state update, and finalization can we ask whether a reachable state leaves the producer with no valid value. The pallet’s equations identify a boundary worth testing; they do not show that a deployed runtime can reach it.

Three source points, one contract

The Polkadot SDK timestamp pallet at commit bf0d89d constructs an inherent timestamp using supplied data and a minimum period:

let next_time = cmp::max(data, Now::<T>::get() + T::MinimumPeriod::get());
Some(Call::set { now: next_time })

In the same file, check_inherent rejects a timestamp outside its accepted interval. The on_finalize hook asserts DidUpdate::<T>::take(). These are three observable parts of one runtime component: construction, validation, and an end-of-block update check.

Follow the value through the file

The following is an annotated source trace at the pinned commit, not a test result:

  1. create_inherent, lines 284–299 chooses max(data, Now + MinimumPeriod) and returns Call::set. The constructor enforces the lower bound before the call is checked.
  2. check_inherent, lines 302–325 checks the proposed value against both Now + MinimumPeriod and the supplied timestamp plus the 30-second drift allowance. This is a distinct upper bound the constructor does not calculate.
  3. set, lines 260–269 checks that the update occurs once, checks the minimum increment, and writes DidUpdate = true. on_finalize, lines 223–231 requires that update by the end of block execution.

In ordinary representable ranges, the comparison worth writing down is Now + MinimumPeriod <= data + drift: if the lower bound exceeds the validator’s upper bound, the value chosen by max cannot satisfy both checks. Reproducing that boundary requires actual runtime values, the surrounding block-production path, and a valid control block.

Producer and validator conditions

When a protocol requires a future object, reading only its constructor misses the rule that validates it; reading only the validator misses whether omission is allowed. The interesting design question is whether the accepted input domain leaves the producer at least one valid output. The cited pallet shows where to locate those rules.

To study another component, place its admission predicate, constructor, validator, and omission rule side by side. A local fixture at a boundary value can test whether the chosen representation is accepted. The fixture needs an independently checked valid continuation; a constructor error alone does not answer that question.

Source assertions that separate the rules

The timestamp pallet’s set call is marked DispatchClass::Mandatory. It requires an unsigned origin, rejects a second update in the same block, requires the configured minimum increment unless the previous value is zero, and records DidUpdate = true. The on_finalize assertion consumes that flag. Thus omission is not simply an alternative to a rejected value: under these hooks, omission also has a defined failure branch.

The public timestamp tests at the same commit contain concrete controls. One sets Now = 46, calls set(69), and expects Now = 69; another expects a panic on a second timestamp; a third sets Now = 44, tries set(46), and expects the minimum-period panic. These are upstream test cases, not a local run or evidence of an unconstructible block.

Turn the apparent contradiction into a precise test

The source gives a lower bound L = Now + MinimumPeriod and a separate upper bound U = inherentData + 30,000 ms in check_inherent. The constructor chooses max(inherentData, L). In ordinary ranges where the arithmetic and conversions represent the same values, L > U would make the constructor’s result fail the upper-bound check. To make this more than an algebraic observation, a reproduction must establish all of the following:

1. The input state and timestamp data are admitted by the actual runtime.
2. The constructor is called with that data and returns the claimed value.
3. The validator rejects that very value under the same state.
4. Omitting the call is rejected by finalization.
5. A normal control block succeeds under otherwise identical conditions.

Numeric toy inputs alone cannot satisfy the first condition. Saturating conversions, the runtime’s Moment type, minimum period, and the production source of inherent data all affect reachability.

A boundary calculation, explicitly synthetic

Assume for this synthetic example that all values are ordinary milliseconds, conversions preserve them, Now = 100,000, and MinimumPeriod = 5,000. If the supplied inherent time is 100,001, the constructor chooses 105,000; the validator’s upper bound is 130,001, so the constructed value lies within the interval. If the supplied time is instead 70,000, the constructor still chooses 105,000, while the upper bound is 100,000; the validator’s upper-bound branch would reject it. Reachability depends on whether a supported runtime can supply that stale timestamp.

The zero-state special case is also material. The set call exempts prev.is_zero() from its minimum-increment assertion, whereas check_inherent still computes its bounds. A meaningful fixture must set Now deliberately and state which of these branches it exercises. Treating the first block and an ordinary later block as the same case can produce a false positive.

This review technique generalizes to required protocol objects: write down the admission domain, the producer’s output function, the validator’s accepted set, and the behavior if the object is omitted. Then test whether the producer’s output belongs to the accepted set for reachable states. A constructor returning an error is not enough when the protocol can legitimately skip the item; a validator rejection is not enough when a different valid construction exists. The complete claim needs all four relations and a valid control continuation.

MORE RESEARCH

Explore more research.

Research blog