Normative · Part
Data model
7. The change event
Section titled “7. The change event”Every mutation to syncable state is represented as a change event. On the wire an event is a COSE_Sign1 message ([RFC 9052]) — the same container SOP/1 uses for object manifests and access records, so SOVM/1 carries one signature stack.
Event = COSE_Sign1( protected = { alg : ES256, content type : "sovm/ssp-event-v1", kid : site_id (16 bytes) }, unprotected = {}, MUST be empty payload = deterministic CBOR map (fields below), external_aad = mesh_id (16 raw bytes))The unprotected bucket MUST be empty: everything an event carries is covered by the signature, so there is nowhere for an unauthenticated field to hide.
The payload is a deterministic CBOR map with exactly these fields:
| Field | Type | Description |
|---|---|---|
table_schema |
text | Destination schema name. |
table_name |
text | Destination table name. |
row_key |
map | Primary-key column names to values. Non-empty, for both operations. |
operation |
text | "upsert" or "delete". "purge" is reserved and MUST NOT be emitted; see deletion. |
payload |
map or null | Column values. Null for tombstones. |
hlc_physical_ms |
unsigned | HLC physical component, milliseconds since the Unix epoch. |
hlc_logical |
unsigned | HLC logical counter. |
Every field is REQUIRED. Receivers MUST reject an event with a missing or extra payload field — a receiver cannot verify a signature over fields it had to invent, and an extra field it does not interpret is a field it is attesting to blindly.
row_key is the one field with no null form. Both operations address a row, and
every materialization rule is keyed on that address, so a null or empty key names
nothing any rule can act on; a receiver drops such an event rather than inventing a
meaning for it (held and dropped).
payload keeps its null form because a tombstone genuinely carries no columns.
Notes on what is deliberately absent:
- No
event_idmember. The identifier is derived from the message bytes (section 9), so transmitting it inside the message would be self-referential. Deriving it also removes an entire failure class: there is no transmitted identifier to mismatch against. - No
site_idin the payload. The authoring node travels once, in the protectedkidheader, where it is covered by the signature. Two locations for one fact is an invitation for them to disagree. - No encoding or version tags. The protected content type versions the whole
construction. A future change ships as
sovm/ssp-event-v2, and because the content type is inside the signed structure it cannot be swapped in flight. - No
tsmember and no operation synonyms. A timestamp member would duplicatehlc_physical_ms, and accepting"insert"/"update"as synonyms of"upsert"would widen the parser for no benefit; duplicated fields are a divergence hazard.
8. Canonical encoding
Section titled “8. Canonical encoding”Where this specification calls an encoding canonical, it means the Core Deterministic Encoding Requirements of CBOR ([RFC 8949] section 4.2.1): definite lengths, shortest-form integers, preferred float serialization, and bytewise lexicographic map-key ordering.
Deterministic encoding is REQUIRED wherever bytes are signed or hashed — event payloads, record payloads, and every structure named in the registry. Message envelopes that are neither signed nor hashed SHOULD use it too, but interoperability does not depend on that.
Value conventions layered on top, since CBOR has no native types for them:
| Logical type | Canonical representation |
|---|---|
| Timestamp | ISO-8601 text string, UTC, Z suffix |
| Decimal / arbitrary precision | Text string, to avoid binary64 rounding |
| Binary | Byte string |
NaN and the infinities MUST NOT appear, even though CBOR can encode them. A serializer MUST fail closed rather than emit them.
Persist before release. A producer MUST durably persist the exact encoded COSE_Sign1 bytes of an event before those bytes are released to any peer, written to a mailbox, or applied locally, and MUST re-transmit exactly those bytes thereafter rather than re-encoding or re-signing.
Deterministic CBOR makes the payload bytes reproducible. The signature is not:
ECDSA is randomised, and a second signing of the same payload yields a different
COSE_Sign1 byte string (section 9). Exact-byte
persistence, ordered before release, is therefore the only thing standing
between the protocol and duplicate events, and the derived identifier in the
next section depends on it.
The rule has a companion, stated with the clock it constrains. Persisting an event’s bytes is not enough on its own: a producer whose clock does not survive the same crash re-issues an HLC it has already released, which is the one thing convergence does not tolerate. Section 11.2 states that half.
9. Event identity
Section titled “9. Event identity”event_id = lowercase hex BLAKE3 (32-byte output) of the exact COSE_Sign1 bytes as receivedevent_id is derived, never transmitted. A receiver computes it by hashing the
message it received; there is nothing to compare against and therefore nothing to
mismatch.
This is SOVM/1’s one hashing convention, shared with SOP/1: an identifier is the BLAKE3 hash of the exact bytes of a signed structure, and no second hash namespace exists. Deriving from the signed message means there is no hand-assembled pre-image, no separator bytes, and no possibility of a pre-image and the message disagreeing.
event_id is a signing-instance identifier, not a content address, and an
implementer MUST NOT rely on either regime holding.
Deterministic nonces bind every signer
not held in a secure element, which since
section 10.1 includes the event-signing subkey that signs
the ordinary node’s change events: those re-sign to identical bytes and therefore to an
identical event_id. Where a change event is signed by the element directly — a node
that has issued no subkey — the nonce is generated inside the element, is not
reproducible outside it, and two signings of one payload produce two different 64-byte
signatures, two different COSE_Sign1 byte strings and two different event_ids.
Both regimes are conforming and both are in the field. What follows from that is the rule below: the identifier deduplicates re-deliveries of one signed message, and a receiver MUST NOT read it as a content address in either regime.
event_id is an idempotency key, not a trust anchor. It deduplicates
re-deliveries and nothing more — and de-duplicating re-deliveries of one
signed message is exactly what a signing-instance identifier does. Authorship is
established by the signature, and a receiver MUST NOT treat a familiar
event_id as evidence of anything except having seen those bytes before. What
the identifier does not tolerate is re-signing as a recovery action, which is
the next section’s rule.
9.1. Never re-sign under an existing HLC or generation
Section titled “9.1. Never re-sign under an existing HLC or generation”A producer MUST NOT sign a second COSE_Sign1 message under an HLC it has
already signed under. Where a producer cannot establish that a signed message
was never released — a crash between signing and persisting is the case that
matters — it MUST tick the HLC and sign a new event rather than re-sign the
old one.
The same rule reaches chained records. A producer MUST NOT re-sign a role
assignment at an existing generation, or a transport binding at an existing
generation, for that subject or node.
The chained case is the more dangerous of the two. Two distinct signed records
at one generation are a chain fork, and SOVM/1 treats a fork as
detectable evidence, never a silent
divergence — that is, as evidence of
compromise. A producer that re-signs after a crash therefore manufactures a
first-order security alarm about itself, and an operator who learns that forks
are usually benign has lost the signal entirely.
Determinism changes the consequence of a violation without relaxing the rule. Where
change events are signed by an event-signing subkey, a
crash-then-re-sign under the same HLC reproduces byte-identical output, so delivery
deduplication absorbs it and no alarm fires. The rule still stands — a producer MUST
still tick rather than re-sign, because it cannot know which regime a receiver will
apply, and because the rule’s other half does not soften at all. Chained records,
including sovm/ssp-eventkey-v1 records, remain IK-signed and randomised, so
re-signing one still manufactures a first-order fork alarm about the producer itself.
The two halves of §9.1 simply stop being symmetric.
9.2. Ticking instead of re-signing is safe
Section titled “9.2. Ticking instead of re-signing is safe”The alternative looks cheaper, so the reason it is not is worth stating.
Convergence is a pure function of the
delivered event set and rests on exactly one precondition: a node MUST NOT
reuse an HLC across distinct events. Two signings of one payload are
distinct events, so re-signing under the old HLC violates that precondition
directly. Signing under a fresh HLC does not: the result is two distinct events
with distinct HLCs and identical column values, which per-column
last-writer-wins resolves to the same state, and which a duplicated delete
resolves to the same tombstone clock.
The cost is one redundant event in the log. The cost of the alternative is the one precondition the merge rules have.
event_id’s other consumers are unaffected. It is derived, never transmitted,
so there is no transmitted identifier to mismatch.
9.3. RFC 6979 is not the fix
Section titled “9.3. RFC 6979 is not the fix”An implementer will reasonably ask why the specification does not simply require [RFC 6979] deterministic nonces everywhere and recover deterministic signing.
Because RFC 6979 is a rule for software signers and cannot be imposed on a secure element — the element generates the nonce under its own certification and exposes no interface to supply one. Requiring reproducible signing would therefore require the root key to be in software, which is the defect the choice of ES256 exists to avoid. A protocol whose correctness improved as the key moved out of hardware would have exactly the wrong gradient, and the same reasoning SOP/1 §5.2 applies to attestation downgrades applies here.
A determinism claim argued from a property of the signature algorithm rather than from a rule the protocol enforces would be fragile either way: a protocol that depends on an algorithm’s incidental determinism depends on something no future suite is obliged to provide.
10. Event authentication
Section titled “10. Event authentication”Every event is signed by the authoring node, and a receiver MUST verify before apply.
Verification is COSE_Sign1 verification with:
- the public key resolved for the authoring
site_id— the protectedkid— which is that node’sIK_pubfrom the receiver’s trusted-peer keyring, or, where the node has issued an event-signing subkey, theevent_pubselected by section 10.3; external_aadset to the receiver’s own 16-bytemesh_id.
Four properties this construction is chosen for:
- The whole message is covered, protected headers included. The content type
and
kidcannot be altered in flight, and the empty unprotected bucket leaves no unauthenticated surface. mesh_idis bound without being transmitted. The verifier supplies its ownmesh_idas external AAD, so an event captured from one mesh fails verification in any other — including a mesh that happens to trust the same key.- Domain separation is structural. COSE’s
Signature1context string plus the content type make an event signature unconfusable with a vouch, a role record, a transport binding, or any raw-transcript signature in the pairing ceremony. - Event identity does not rest on signature determinism, in either direction. Whether a change event re-signs to identical bytes depends on where its key is held (section 9, section 10.1), and nothing here assumes an answer. What lets event identity be derived is the persist-before-release rule in section 8: a producer signs once, persists the exact bytes, and re-transmits those bytes thereafter.
Failure handling, each fail-closed and each surfaced:
- Unknown
kid— the sender is not in the keyring: drop the event and surface it. Unknown-sender events are not queued pending future trust. - Invalid signature under a known key: drop the event and surface it.
- Above a retirement cutoff — the
kidresolves, the signature is valid, and the event’shlc_physical_msis above the row’sretired_at_hlc: drop the event and surface it. What the cutoff means, and why a retained row answers nothing above it, is identity §20.4’s to state.
No case holds the sender’s group or stalls a watermark. That is deliberate:
events are attributed by kid, not by connection, so if verification failures
held the attributed site’s group, a forger could stall the victim’s stream by
sending garbage carrying the victim’s kid. A forgery must cost the forger, not the
node it impersonates.
10.1. Event-signing subkey
Section titled “10.1. Event-signing subkey”Per-event signing and root-identity signing have opposite duty cycles.
Transport §28 already states the
premise: IK_priv “may live in a secure element that signs rarely and
deliberately”. A change event is neither rare nor deliberate — a bulk import is
tens of thousands of signatures — and a secure element that can produce a few
signatures per second cannot carry that volume. Requiring it would make the
specification unimplementable on exactly the nodes whose custody it most wants in
hardware.
A node MAY therefore issue an event-signing subkey: a software ECDSA P-256 keypair, held outside the secure element, whose public part is bound to the node by a record the element itself signs. The subkey is a chained record and inherits the generation-only narrowing of the transport-binding chain unchanged: it narrows the substrate schema exactly as far as that chain does and no further, and widens nothing. The narrowing is restated at the end of this section, after the three fields that do depart from that precedent.
EventSigningKey = COSE_Sign1( protected = { alg : ES256, content type : "sovm/ssp-eventkey-v1", kid : site_id (16 bytes) }, payload = deterministic CBOR { event_pub : bstr 33, generation : unsigned, not_after : unsigned, prior_revoked_at : unsigned / null }, external_aad = mesh_id (16 raw bytes))| Field | Rule |
|---|---|
event_pub |
The subkey’s public part, SEC 1 compressed P-256, 33 bytes. An uncompressed or hybrid encoding MUST be rejected rather than converted (encoding canonicity). |
generation |
Unsigned, strictly increasing per site_id. A record supersedes all lower generations for that node, and a successor need only exceed its predecessor rather than carry exactly one more — substrate §11’s density rule is inapplicable on a chain with no predecessor member (see the generation-only narrowing below). |
not_after |
The HLC physical component after which the subkey is invalid for all purposes. A verifier MUST reject a record whose not_after is not strictly greater than the record’s own issuance time. |
prior_revoked_at |
null at generation 0. For generation N > 0 it is the HLC physical component after which the generation N−1 subkey is invalid for new events. It MUST NOT be greater than the N−1 record’s not_after. |
Issuance rules, each of which is a MUST:
- The record is signed by
IK, inside the element. It is a SOP/1 §5.4 hardware signature and carries that section’s verify-after-sign obligation at that section’s scope, which is not restated here. Signing it is a rare, deliberate operation, which is the property transport §28 relies on and this section reuses. - One hop, always terminating at the element. A subkey MUST NOT issue a successor
subkey, and a verifier MUST reject an
sovm/ssp-eventkey-v1record whose signature does not verify under theIK_pubof a currently trusted, not retired peer in the trusted-peer keyring. There are no chains of delegation, only a chain of generations. A retired node’s row still holds itsIK_pub, and this predicate is what refuses a record under it: that refusal is the freeze of identity §20.4, enforced at the one place a subkey record is admitted. - The subkey’s private part MUST NOT leave the node, MUST NOT be escrowed, and MUST
NOT be transmitted in any SOVM/1 message. Only
event_pubtravels. - The subkey is a software signer and is therefore bound by deterministic nonces. This is not advisory: a repeated nonce leaks the subkey outright from two signatures.
- Validity windows SHOULD be short. An implementation SHOULD set
not_afterno more than 90 days beyond issuance, and a mesh MAY require less by policy. The bound is what makes the exposure in limitations §40.7 bounded, so it is stated as a number rather than left to taste. - Records travel in the sync exchange’s
recordsmember, on the same path as transport bindings. They are self-verifying.
Three deliberate departures from the §28 precedent, each with its reason:
event_pubisbstr 33(P-256), wherechannel_pubisbstr 32(Ed25519). Transport §28’s asymmetry exists because the transport ecosystem carries Ed25519 and the channel key is bound by a signature rather than by its own custody. No such force acts here: the subkey signsCOSE_Sign1under the substrate profile, which has exactly one signature suite and it is ES256. Copying transport §28’s curve would add a second suite to the signing path for no reason. The channel-key precedent justifies the pattern, not the suite.not_afteris present, where transport §28 has no expiry. A stale channel key merely fails to connect. A stale event-signing key forges history, which is a failure that does not announce itself. Bounded validity is the failure-closed posture for that asymmetry.prior_revoked_atis present. Transport §28 needs no revocation cutoff because a binding is wholly superseded. An event-signing subkey is not: events it signed before revocation must keep passing validity checks, or every rotation would destroy the history signed under the previous generation. See section 10.3.
Like the transport-binding chain, this chain is generation-only: the payload above is
exhaustive and carries no predecessor member, so every record is a genesis record and a
record supersedes all lower generations. That is a narrowing
chained records §12 permits, stated
here on the instantiation’s own page as that section requires. As on
transport §28,
substrate §11’s density rule is thereby
inapplicable rather than weakened: it is stated relative to a record’s predecessor, and
there is none to compare against.
10.2. Scope boundary of the event-signing subkey
Section titled “10.2. Scope boundary of the event-signing subkey”This is the paragraph the rest of the mechanism rests on. It is exhaustive.
An event-signing subkey MAY sign change events —
sovm/ssp-event-v1 — and nothing else.
An event-signing subkey MUST NOT sign any of:
- an
sovm/ssp-eventkey-v1record (no successor subkeys, no delegation chains); - a transport binding,
sovm/ssp-binding-v1; - a role assignment,
sovm/ssp-role-v1; - a vouch certificate,
sovm/ssp-vouch-v1; - a succession record,
sovm/ssp-succession-v1; - a SOP/1 §5.7 membership authorization record;
- any pairing transcript signature or raw-label transcript signature.
A verifier MUST reject any record in that list whose signature verifies under an
event_pub rather than under the node’s IK_pub. The rejection is structural as well
as normative: each of these records carries its own protected content type, and COSE’s
Signature1 context plus the content type make the domains unconfusable
(section 10, property 3). The MUST is stated anyway, because
a property that holds by construction and is not written down is a property the next
revision can lose without noticing.
Everything that authorizes stays with IK_priv. Membership, roles, vouching and
transport identity are unreachable from a compromised subkey, so an extracted subkey
cannot escalate — it can only make false statements about its own node’s rows.
That is a bound on the key, and it becomes a bound on the adversary only where
IK_priv is out of reach of whatever compromised the subkey. Where
§19.1’s element holds it, it is: the scalar
never enters the address space the subkey sat in, and the confinement above is what
makes the exposure in
limitations §40.7
acceptable rather than merely acknowledged. Where custody is software it is not, and
this specification requires that case to exist rather than merely tolerating it:
identity §19.3 declines to ban a software root,
and identity §19.4’s vouch row
records one normatively for every vouched join. An adversary that reads a software
IK_priv out of the process it already holds signs every record in the MUST NOT list
above — by using the root, not by escalating the subkey. The list is exhaustive about
what the subkey reaches. It is not a statement about what an attacker reaches, and
it MUST NOT be cited as one.
10.3. Subkey validity and verification
Section titled “10.3. Subkey validity and verification”A change event’s signing key is selected by the event’s own HLC, against the subkey chain valid at that HLC. Selection is single-candidate and fail-closed.
A verifier presented with a change event MUST:
- Read
kidfrom the protected header. It is the authoringsite_id, unchanged from section 10. - Decode the
COSE_Sign1payload and read its HLC physical component. The payload is cleartext within the signed structure, so reading it before verification is safe: the signature covers those bytes, so an attacker who lies about the HLC selects the wrong generation and fails at step 4 rather than misleading the verifier. - Select the single generation G whose validity window covers that HLC, where the
window is
issued_at(G) ≤ hlc ≤ min( not_after(G), prior_revoked_at(G+1) if a generation-G+1 record is held, otherwise ∞ ). - Verify the
COSE_Sign1signature againstevent_pubof generation G, withexternal_aadset to the verifier’s ownmesh_id. - Fail closed if no generation’s window covers the HLC, if more than one does, or if the signature does not verify: drop the event and surface it, exactly as section 10’s two failure cases require.
A verifier MUST select exactly one candidate generation and MUST NOT iterate over generations trying each in turn. Iteration would let an attacker who controls the HLC brute-force past the selection logic, which is the only thing binding an event to a validity window at all.
Where a node has issued no sovm/ssp-eventkey-v1 record, change events are verified
against IK_pub from the keyring as section 10 describes,
unchanged. Subkey issuance is optional; verifying it is not.
Revocation cascades downward and not upward. Revoking a node at the
§22 level removes IK_pub from the trusted-peer keyring,
at which point every sovm/ssp-eventkey-v1 record for that node stops verifying — the
records are IK-signed, so the whole subkey chain fails with the keyring entry. Node
revocation therefore reaches subkey revocation with no additional mechanism, and it
fails closed. The converse does not hold: rotating or revoking an event-signing
subkey is routine and says nothing about the node’s IK-level trustworthiness.
The subkey mechanism needs no extension for either outcome of
identity §22, and the two mechanisms MUST NOT share
logic. That is a claim about this section, not about identity §22, which does
distinguish removal from retirement.
Retirement retains the chain, bounded by the cutoff. Under
retirement the keyring row survives,
verification-only, with a retired_at_hlc cutoff. Because sovm/ssp-eventkey-v1
records are IK-signed, a retained row that resolved IK_pub alone would keep
history checkable only for nodes that never issued a subkey; resolving the kid of a
retired node’s change event therefore includes running the selection above over its
subkey chain, under the retained ik_pub
(identity §20.4). For a retired node the window of
step 3 is additionally bounded by the cutoff —
issued_at(G) ≤ hlc ≤ min( not_after(G), prior_revoked_at(G+1) if held, retired_at_hlc )
— so an event above the cutoff has no candidate generation and fails closed at step 5,
unchanged.
The chain a verifier resolves is the chain it held when it retired the node, and no
more: the freeze stated at identity §20.4, and
enforced by section 10.1’s admission predicate, refuses
every later sovm/ssp-eventkey-v1 record for that site_id, whatever its issuance
time. The retained read is safe because, and only because, of that freeze; the two are
one rule, and an implementation that has the read without the freeze is a forgery
hole. Removal collapses the chain; retirement bounds it. Retention therefore extends
what is verifiable, never what is authorized.
On what a passing validity check means. An event whose HLC is at or below
prior_revoked_at passes validity checks. That is not the same claim as is
authentic, and this specification does not make the second one. A party who extracted
the subkey could have produced that event at any HLC inside the window. See
limitations §40.7,
which states the residual exposure rather than leaving a reader to infer it.
Forks. An sovm/ssp-eventkey-v1 record is subject to the same
generation-monotonicity rule as every other chained record: two distinct records at one
(site_id, generation) are a fork and are treated as
detectable evidence, never a silent divergence.
Because these records are IK-signed, such a fork is evidence of IK-level compromise
or a node defect — a strictly more informative signal than a subkey-level fork would be.
Normal rotation produces exactly one IK-signed record per generation and cannot raise
a false alarm, and the crash-recovery rule in
section 9.1 applies unchanged:
a producer that cannot establish a generation-N record was never released issues
generation N+1 rather than re-signing.
11. Hybrid logical clock
Section titled “11. Hybrid logical clock”An HLC timestamp is the pair (hlc_physical_ms, hlc_logical), compared
lexicographically. When two events carry equal HLC pairs the tie is broken by
byte-wise comparison of the authoring site_id from the protected kid header.
The comparison including the tiebreak is total: any two distinct events have a
defined order, which is what makes the merge in semantics
deterministic.
Each node maintains one local HLC, which it ticks to stamp a local event and
observes to merge a received one. Both operations are normative. With last the
stored local HLC and now the local wall clock in milliseconds:
tick — stamp a local event:
- if
now > last.physical_ms, the new HLC is(now, 0); - otherwise it is
(last.physical_ms, last.logical + 1).
observe — merge a received remote HLC. The new physical component is
max(last.physical_ms, remote.physical_ms, now). The logical component depends on
which inputs share that maximum:
| Inputs holding the maximum | New logical component |
|---|---|
Both last and remote |
max(last.logical, remote.logical) + 1 |
last only |
last.logical + 1 |
remote only |
remote.logical + 1 |
now alone |
0 |
In both operations the result is stored as the new local HLC. Together they guarantee that a causally later local event sorts after everything the node has already seen, without any coordination.
11.1. Clock skew bound
Section titled “11.1. Clock skew bound”Receivers MUST reject events and watermarks whose hlc_physical_ms exceeds the
receiver’s current wall clock by more than 300 000 ms (5 minutes).
Without this bound a node with a broken or hostile clock can stamp an event far in the future and win every subsequent last-writer-wins comparison indefinitely — a denial-of-replication attack that no amount of correct merge logic recovers from. The bound is one-sided: timestamps arbitrarily far in the past are accepted, since a stale clock only loses comparisons and cannot deny service to others.
Rejection is fail-closed and MUST be surfaced. An implementation MUST NOT clamp the offending timestamp and apply the event anyway.
A rejected event was not applied, so the receiver’s
applied watermark for that
(site_id, schema, table) MUST NOT advance to or past the rejected event’s HLC. The
rejection is also not terminal: a receiver MUST NOT retain the event and MUST NOT
record a standing verdict against it, and MUST apply the identical bytes when they
are re-offered after its own wall clock has advanced to within the bound. Because
the apply chokepoint runs this check
before the idempotent insert, a rejected event_id never enters the duplicate
index, which is what leaves the re-offer able to succeed.
That is what makes a transient skew self-healing rather than lossy. A receiver’s
clock advances monotonically, so an event stamped d ms beyond the bound becomes
acceptable after d ms of real time, and the next sync
cycle re-offers it. Under the other reading — advance
the watermark past the rejection — the event is lost the first time an honest peer’s
clock runs fast, and lost silently, because the watermark tells the sender the
receiver has it.
Advancing would in fact be worse than lossy. A rejected HLC is by construction above every honestly stamped HLC from that site: an honest stamp is at or below the receiver’s present, and a rejected one is more than five minutes beyond it. Since a watermark asserts that everything from that site for that table at or below its HLC has been applied, advancing past a single skewed event would acknowledge the site’s entire unapplied backlog in one step.
Pinning the watermark stalls nothing an attacker chooses. Signature verification precedes this check, so a skew-rejected event is authentically attributed to the site whose watermark it pins and the stall is self-inflicted. That is the reasoning of section 13.6’s signature row reaching the opposite conclusion: there the cost of not advancing would fall on the impersonated node, here it falls on the author.
Self-healing bounds the receiver, not the author. The tick
rule never lowers physical_ms, so a node whose clock
jumps beyond the bound stamps every subsequent event at or above the jumped-to value
and stays rejected by every receiver until real time reaches it — and it cannot
simply set its clock back, because lowering a stored HLC risks the reuse
convergence forbids.
A watermark carrying an out-of-bound hlc_physical_ms is rejected under the same
rule and is simply not recorded: the receiver’s view of that peer’s progress stays
where it was, which is what watermark
monotonicity requires anyway, and the peer
re-reports on a later cycle.
11.2. Clock durability across restart
Section titled “11.2. Clock durability across restart”A node MUST durably persist an HLC high-water mark: a stored HLC pair no lower than every HLC it has used to stamp an event. The mark MUST reach durable storage before the event carrying that HLC is released, on the same ordering persist before release imposes on the event bytes themselves. An event released under an HLC the mark does not yet cover is an event the node cannot promise never to re-issue.
On restart a node MUST initialize its local HLC from the persisted mark and then
tick as section 11 already defines: the next
stamp is (now, 0) where now exceeds the mark’s physical component, and
(mark.physical_ms, mark.logical + 1) otherwise. No new operation is
introduced — restart is the tick applied to a restored last. Initializing
last from the wall clock alone is not permitted, and the case that breaks it is
not an exotic one: a node that stamps several events in the final millisecond
before it stops, and restarts inside that same millisecond, re-issues logical 0
with a perfectly correct clock and no fault anywhere.
A node MAY persist a mark strictly ahead of the highest HLC it has stamped — a reservation — and stamp freely below it without a durable write per event, provided it MUST NOT stamp an HLC above the persisted mark and MUST extend the reservation durably before crossing it. The trade is a bounded forward jump after a crash, which no merge rule is sensitive to, against a synchronous durable write on every local mutation. The window is bounded from above by the clock skew bound: a reservation of 300 000 ms or more resumes the node at a physical component its peers MUST reject, so an implementation MUST keep the window below that bound with margin for the drift the bound also has to absorb.
The mark MUST be integrity-protected, and a node MUST verify that protection
before it initializes a clock from the mark. This is the one piece of node-local
state whose lower values are the dangerous ones, and no other state on the node
contradicts it, so a mark stored as a bare pair trusts whoever can write that
storage to have been honest about it. The protecting key MUST NOT be recoverable
from the mark storage itself; where the platform provides a secure element, the
construction already available is a signature made by the root identity element
over the mark, since section 19.1
delegates all signing to that element and exposes no export operation. An
implementation MUST NOT attempt to derive protection material by extracting
IK_priv — that operation does not exist. The cost lands where the reservation
already put it, on the durable write rather than on every stamp.
A mark that fails verification MUST take the same path as a mark that is absent, and MUST be surfaced as a condition distinct from it: a missing mark is evidence of an interrupted write, a failing one of tampering or storage corruption, and an operator answers those differently.
Integrity protection bounds forgery, not rollback. A mark replaced by an earlier value the node itself wrote verifies correctly, and the node retains nothing above it to contradict the value with. Where the platform exposes rollback-resistant storage or a monotonic counter alongside the signer, an implementation MUST bind the mark to it, so that a replayed mark fails against a counter that only moves forward. Where it does not, what remains is the exposure the snapshot case below describes, reached by a narrower path.
Where the mark is lost, a node MAY reconstruct it as the highest HLC over the
events it authored in its own retained log, but only where it can establish that
no self-authored event has been evicted
— eviction removes exactly the evidence the reconstruction depends on, which is
why the mark is stored in its own right rather than derived on demand. Failing
that, the node MUST NOT stamp further events under that site_id and MUST
surface the condition rather than resume from the wall clock. Peers are not the
fallback: self-echo suppression
requires a receiver to drop events carrying its own kid, and
have_max_hlc is one value over
everything the sender holds rather than a per-site maximum, so nothing on the
wire returns a node its own floor.
Restoring node state from a snapshot taken before events were released rolls the
mark backwards, and no rule here detects it — the restored node reads a mark it
wrote itself and holds no record of what it did afterwards. Such a node MUST be
treated as a new site: re-keyed under a fresh root identity key, and therefore a
fresh site_id (host interface §1), rather
than resumed. Its predecessor’s rows survive and resolve by per-column
last-writer-wins like any other site’s.
Re-keying the successor is only half of that instruction: the predecessor
site_id MUST be revoked before the re-keyed
node joins. A restore rolls back the storage around an identity key, not the key
— section 19.1 puts IK_priv inside a
secure element precisely so that nothing outside it can — so a predecessor still
present in peer keyrings can go on stamping under the old site_id and re-open
the reuse the re-key was meant to close. The successor also inherits no
key_protection standing: a vouched join records software whatever the
predecessor held (section 19.4),
so a deployment that depends on hardware_attested MUST re-establish it by
direct pairing.
Limitations §40.6
records what that leaves exposed.