Part 13 — catching a dishonest relay
The relay cannot read a record, alter one, or forge one. What it could always do — until now — is decline to hand one over, or hand over the version that was replaced. A device asking for a graph it has never seen has no way to know what it was not given.
This is on by default and needs no configuration. What follows is what it does, what it cannot do, and what your app should show.
The two checks
Section titled “The two checks”Against itself. Every write the relay accepts goes in a log it cannot
rewrite without contradiction. If the log says record r7 was written at
revision 900 and this device holds revision 400, the relay served the version
that was replaced. If it holds nothing at all, the record was withheld.
This check needs neither a peer nor a key.
Against other devices. The chain head depends on the log alone, so any two devices that read the same range compute the same value regardless of when they joined. Each publishes its head as an ordinary record — sealed, signed, verified against the roster like anything else — and a relay keeping a private world per member fails here even though it passes the first check perfectly.
What your app should do
Section titled “What your app should do”const log = engine.status.graphs['g:acme']?.log;// { seq: 1204, state: 'ok' | 'unverified' | 'off' | 'alarm', agreed: 2, from: 0 }
engine.on('log.alarm', (e) => { // e.reason: 'omitted' | 'rolled-back' | 'diverged' incident(e.graphId, e.reason, e.detail);});| state | means | show |
|---|---|---|
ok |
everything the log admits to was served here | nothing, or a quiet tick |
agreed > 0 |
another device signed the same history | “verified against N other devices” |
unverified |
could not check — old relay, offline, or a pruned log | “not verified”, never “fine” |
off |
this subscription is filtered, so nothing can be checked | only if your app relies on the check |
alarm |
the relay contradicted its log, or two devices disagree | an incident |
from > 0 |
the relay’s log no longer goes back to the start | “verified from here on” |
alarm latches on purpose. A relay caught omitting one record is not
cleared by behaving for the next thousand, and a warning that clears itself is
a warning nobody sees. The honest response is to stop trusting that
deployment — not to clear the flag.
from is the anchor, and a non-zero one is permanent. A device that first
folded a log which had already been pruned starts its chain in the middle of
history. It still catches an omission or a rollback after that point — that is
the relay’s own account of what it accepted, and it is still checkable — but
the span before it is gone, and its head is a different quantity from the one
a device that folded the whole log holds at the same boundary. So such a
device reports unverified rather than ok, corroborates nobody, and
publishes no checkpoint of its own. Comparing the two would turn an operator
reclaiming disk into a permanent alarm naming a colleague, which is the worst
failure mode this check has: an accusation aimed at a person, raised by
somebody running a DELETE.
What it cannot do
Section titled “What it cannot do”- A relay that shows every member a wholly separate world, their peers’ checkpoints included, is invisible in-band. Catching that needs two people comparing a head out of band. Nothing here does it for you.
- A filtered subscription cannot verify. A device that asked for a subset
cannot tell that from being shown one, so the check reports
off. If you use partial sync and want this protection, keep one device somewhere doing an unfiltered pull. - A device that never syncs learns nothing. Detection is comparative and eventual. The first sync on a brand-new device is exactly when you are least protected and most exposed.
What it costs
Section titled “What it costs”One extra request per sync that has new history, and none at all when a graph is idle. On the relay, about sixty bytes per write, kept forever.
The storage cost is a real trade-off worth considering before you ship: the relay now retains the shape of a record’s edit history, where it used to overwrite it. It never learns more about the contents — it saw each write as it happened either way — but “this document was edited eleven times last night” is now something it remembers.
// Verifying, but not publishing a head of your own:await ArgalaEngine.open({ ..., checkpointEveryMs: -1 });// Off entirely — say why in a comment, because the default is on:await ArgalaEngine.open({ ..., verifyLog: false });