Skip to main content

Timestamps

Two rules cover every time-dependent thing in the contract, and both have consequences a client has to know about.

Everything is milliseconds of block time​

UnitMilliseconds. Every stored time, every argument, every event field.
SourceThe block timestamp, in milliseconds, of the block executing the call.
NeverA server's wall clock. Wall clocks drift and can be wrong; a block timestamp is a fact the whole network agreed on.

In the API the same rule holds, with names that keep the two apart: chainTimeMs for chain time, serverTimeMs for ours. Member names end in Ms, and the only places seconds appear are where an external standard requires them — each with a millisecond twin in the body. (Conventions)

Some useful constants, as milliseconds:

A standard plan period2_592_000_000 (30 days)
Notice before a tariff increase172_800_000 (48 hours)
Notice before a Relay Unit schedule change172_800_000 (48 hours)
A lease60_000
A quote120_000

Nothing is scheduled; everything activates lazily​

There is no keeper. No cron job, no bot, no transaction that has to fire at midnight for the system to be correct. Every time rule is a pure function of the block timestamp, evaluated by whoever touches the state next:

  • a scheduled tariff becomes effective;
  • a Relay Unit schedule version becomes effective;
  • a period rolls over;
  • a queued plan block starts;
  • a grant expires;
  • a delayed role change takes effect.
// The shape of every one of them.
fn effective_tariff(&self, now: u64) -> u64 {
let pending = self.pending_tariff().get();
if pending != 0 && now >= self.pending_tariff_effective_ms().get() { pending }
else { self.tariff().get() }
}

Note that it compares rather than subtracting. Timestamp arithmetic that could wrap is avoided on purpose, as defence in depth.

What that means for you​

A value can be effective without an event having been emitted.

An activation emits nothing at the moment it becomes effective. The event arrives with the next state-changing call, which may be hours later. So:

DoDo not
Derive activation from the scheduled event plus its effective_msWait for an "activated" event before believing a value has changed
Call a view: the views apply the lazy rule without writingAssume the last event you saw reflects the current effective value
Compare against chain timeCompare against your own wall clock

The views are the honest reading. getEffectiveTariff() applies the rule and tells you what is in force right now; getTariff() gives you the current value, the pending one and when it becomes effective. The reported version accounts for a due-but-not-yet-materialised change, so readers off chain and the chain itself always agree.

Asking "what will it be then?"​

Several views take an optional trailing at_ms and evaluate the lazy rule at that moment instead of now:

getEffectiveTariff(at_ms?) getPrice(tier, at_ms?) getPaygPrice(tier, at_ms?)
getRuSchedule(at_ms?) getPricingConfig(at_ms?) getSwapBudget(at_ms?)

This is how a quote that straddles an activation is priced correctly, and how the backend computes the "from date X the price will be" line in the pricing document.

The horizon is bounded — at_ms more than 24 hours ahead is rejected. For the full 48-hour notice window, multiply the tier's price units by the pending tariff yourself; both numbers are readable.

After a chain stall​

Block timestamps jump when a chain resumes after a stall. The comparison rule is unaffected: a pending value whose effective time has passed simply becomes effective at the first call that looks. Nothing has to catch up, and nothing is skipped.

This is the practical argument for the lazy design. A keeper-based schedule has to be running at the right moment; a lazily evaluated one only has to be looked at eventually, and the answer is the same whenever that happens.