jonnify·bcs /bcs/ideas/time-inside-the-name/the-law-of-two-clocks

B1.5 · dive — the law of two clocks

Identity time is the id’s; event time is data.

A trading platform runs on two times that look alike and are not. The id’s milliseconds answer exactly one question: when did this architecture first know this entity by name. When the fill occurred at the exchange, when the order was placed by the client, when the risk breach opened — those are claims under someone else’s clock, and they travel as properties in a system’s table, never as readings of the id.

Source: content/bcs1.5.md · What and How; the streams record under bench/valkey-id/ owns the window proof; the rung 1.1 transcript owns the byte-sort proof.

Two clocks, one litmus. Select a side of the law — or the question that draws the line.
identity time owned by the minter inside the id — unix_ms event time owned by someone else’s clock beside the id — a property the litmus whose clock is authoritative? “not ours” → data, not arithmetic
readout · Select a side. Static reading: identity time serves pagination, replay windows, retention cutoffs, first-seen audit, dedupe-by-recency; event time serves matching semantics, regulatory timestamps, PnL bucketing; the litmus is one question — whose clock is authoritative for this claim?
§1

What identity time is

content/bcs1.5.md · What

Per node it is a strict total order — the minting law’s counter never repeats and never reverses — and fleet-wide it is a coarse global order at millisecond grain with node concurrency inside each tick. It is also a free creation instant: the canonical pair in vectors.json decodes USR0NgWEfAEJfs to 320636799581945856 and reads unix_ms as 1780512970164, no column required.

The measured record has exercised both faces. The substrate’s gate paged a byte-sort desc over 2000 minted ids with no timestamp consulted:

content/echo_data/runtimes/elixir/bcs_rung_1_1_check.out · G4frozen
G4 ordered ok -- page_desc(2000) == byte-sort desc over 2000 minted ids; store holds no clock

And the streams experiment addressed a ten-millisecond window purely by id arithmetic — 40960 entries (expected 40960), the first of them 1781000000010-28672, an id readable by eye as time, node, and sequence:

content/echo_data/bench/valkey-id/streams_bench.outfrozen
window [+10ms, +20ms) via branded-derived ids: 40960 entries (expected 40960)
first id in window: "1781000000010-28672"  (low 22 bits = node 7 << 12 | seq 0 = 28672)
§2

What identity time is not

content/bcs1.5.md · What · Lamport 1978

It is not event time, and the distinction has a fifty-year-old foundation. Lamport’s 1978 result examined one event happening before another in a distributed system and showed the relation to “define a partial ordering of the events” — order in a distributed system is a causal construction, not a reading of simultaneous wall clocks, and a timestamp minted here cannot testify about what happened there.

The law of two clocks: identity time is the id’s; event time is data. Every chapter after this one cites the line instead of re-arguing it.

§3

Identity time, as arithmetic

content/bcs1.5.md · How

A window over ORD names is two synthetic cursors, no clock consulted at read time:

# Elixir — min_for: the smallest snowflake a millisecond can carry
min_for = fn ms -> Bitwise.bsl(ms - 1_704_067_200_000, 22) end
from = EchoData.BrandedId.encode!("ORD", min_for.(t0))
til  = EchoData.BrandedId.encode!("ORD", min_for.(t1))
// Go — the same cursor, the same bytes
minFor := func(ms int64) uint64 { return uint64(ms-1704067200000) << 22 }
from := brandedid.MustEncode("ORD", minFor(t0))
til := brandedid.MustEncode("ORD", minFor(t1))
The cursor computer. Three committed milliseconds; the readout computes (ms − 1704067200000) << 22 — the smallest snowflake each can carry.
1780512970164 · the vector’s ms t0 · 1781000000010 window opens (inclusive) t1 · 1781000000020 window closes (exclusive)
readout · Select a millisecond. Static reading: min_for(1780512970164) is 320636799581945856 — exactly the canonical vector’s snowflake, because that vector carries node 0 and sequence 0. The t0/t1 pair brackets the same ten-millisecond window the streams bench addressed.
§4

Event time, as data

content/bcs1.5.md · How + Decisions

The other side of the law is a refusal: the exchange’s stamp rides beside the name, typed as time, never encoded into one.

defmodule Fill do
  defstruct [:id, :order_id, :at_exchange]   # at_exchange: DateTime — their clock
end
type Fill struct {
    ID         string    // FIL… — named when we first knew it
    OrderID    string    // ORD…
    AtExchange time.Time // their clock, our data
}

Same grammar both sides of the bus, and the same prohibition: nothing parses an id to answer an event-time question. The decision of record closes the loop: no created_at beside an id for “first named” — unix_ms is the accessor, and a duplicate column is the second clock returning in disguise. Event timestamps are exempt, because they were never duplicates.

§5

References

sources · related

Sources

Related