# Device operation and reputation What a machine with an identity says about its own operation over time, in a form anyone judges offline without trusting the machine; and a reputation computed from it by a published, deterministic function that returns its figures together with the evidence they come from. The machine's identity (who made it, its device key, its recall list, its status records) is the one of [`../device-identity`](../device-identity/); this module adds the log of what the machine signed after that, its verifier, and the reputation. Node.js 24 only (`node:crypto`: ML-DSA-65, FIPS 204), no dependencies; it imports [`../device-identity`](../device-identity/), [`../identity`](../identity/) and, for the anchor envelope, [`../proof-kinds`](../proof-kinds/). The reputation has a second implementation in Python 3, standard library only. **Read this first.** A record is what the holder of the device key signed. The device key is a **software key** (the device identity README says so, and nothing here changes it): whoever copies it signs records as well as the device. A VALID log says that these records were signed by the key a manufacturer certified, in this order, with no gap, with no other record of the key in force for any of their positions among what the verifier was shown, and, where a witness bounds them, at these times. It does not say that no other record exists: a record signed by a key the log rotated away from, or by a key the manufacturer retired, is named and does not change the verdict (whoever holds a copy of an old key can sign one, see [Equivocation](#equivocation)). **It does not say that the machine did the physical work its records describe**: the state, the counter and the measurement are the word of the device's own sensors and software. See [What it is not](#what-it-is-not). ## The log The device signs, at intervals, an **operation record** with its device key: | field | what it says | |---|---| | `deviceId` | the key that signs (derived from it: `aere-device:` + 20 bytes of the SHA-256 of its SPKI) | | `seq` | its position in the log: 0, 1, 2, ... across key rotations | | `prev` | the hash of the record before it. Record 0 names the hash of the manufacture credential of the first key, so the log is rooted in one identity and cannot be shown under another credential of the same key | | `at` | the device's clock (an RFC 3339 UTC text). Its word, unless a witness bounds it | | `state` | `operating`, `idle`, `fault` or `maintenance` | | `counter` | a whole number the device keeps and never lowers (cycles, energy, distance, seconds of operation) | | `measurement` | the SHA-256 of what the device measured, or `null`. The data stays with the device or its owner; only the digest is signed, so nothing personal is in the log | | `after` | a block the device read before signing (`{ chainId, block, hash }`), or `null`: a lower bound on time (below) | | `next` | a rotation (`{ deviceId, key }`), or `null` (below) | A record is exactly `{ statement, signature }`, the signature exactly `{ alg: "ml-dsa-65", value }` in canonical base64, over a domain of its own (`aere-device-operation/v1/record`): an operation record does not verify as a device attestation, and an attestation does not verify as a record. The **log** is `{ v, kind: "aere-device-operation-log", credentials, records, anchors }`: the manufacture credential of each key used (the first one first), the records in order, and the anchor envelopes. **Rotation.** A record with `next` names the key that signs from the following record on. It counts only when the next manufacture credential of the log certifies exactly that key, and was issued by the same manufacturer for the same model and serial: the old key consents by signing the rotation, the manufacturer by certifying the new key. A key whose holder lost it cannot sign a rotation; the machine then starts a new log, and its reputation starts again. **The witness.** An anchor is an AIP-23 envelope of the existing kind `time` whose `subjectHash` is the hash of a record and whose `source` is that record's device. Notarized, and read back **by the verifier** through the AIP-23 reference verifier under a post-quantum certified anchor, it says the record already existed at the moment the chain wrote, and so did every record before it, which it names through `prev`. A record's `after` names a block; with the verifier's own reading of that block (its hash and its time), the record cannot be older than the block, since a block hash cannot be known before the block exists, and neither can any record after it. ## The verdict `verifyOperation(log, options)` judges each record against the one before it, the key in force, the time and the witness, and makes every check of each record it reaches. Each row is `{ scope, name, pass, detail, rule }`; a check that was not made is `{ pass: null, unjudged: }`. - **INVALID**: a rule refused. The log is accepted up to the first record a rule refuses; the records after it are not judged and are counted (`after-refusal`). - **PARTIAL**: nothing refused, and something was not brought or not read. - **VALID**: everything brought was judged. The verifier does not turn a failure of its own into a refusal: an exception is a defect of the verifier, and it is thrown (exit 4 on the command line). | rule | refuses | |---|---| | `form` | a file that is not an operation log of 1 to 16 manufacture credentials, a list of records and a list of anchors, with nothing else beside them | | `identity` | a manufacture credential the device verifier refuses (each rule it names is in the row: signature, ids, trusted manufacturer, recall list, device key). Its validity window is judged at the time of each record, not on the verifier's clock | | `record-form` | a record that is not exactly the fields above, in their forms (its time from 1970: the facts carry times since 1970 only) | | `record-key` | a record not signed by the key in force at its position | | `record-signature` | a signature that does not verify with the device key, is not in canonical base64, or carries a member more | | `record-seq` | a record that does not carry its position | | `record-prev` | a record that does not name the hash of the one before it (a record removed, changed or moved), or a first record not rooted in the first credential | | `record-time` | a time that goes back, or one ahead of the verifier's clock by more than the tolerance (60 s) | | `record-counter` | a counter that goes down | | `key-window` | a record dated outside the validity of its key's manufacture credential (the last instant of the validity is inside it) | | `key-retired` | a record dated at or after the manufacturer retired its key, or any record of a key whose retirement carries a date that cannot be read | | `rotation` | a rotation to a key the next credential of the log does not certify, from another manufacturer, for another model or serial, to the same key, or to a key its device id is not derived from | | `anchor-envelope` | an anchor that is not a `time` attestation whose hash reproduces, about a record of this log, with that record's device as its source | | `witness-time` | a record dated after its anchor was already on the chain (the earliest reading counts) | | `beacon-hash` | a record that names a block with a hash the verifier did not read there | | `beacon-time` | a record dated before the block it names existed | | `equivocation` | the same key signed another record for a position of the log (see below) | A record's time is compared with the verifier's clock, with a block's time, with an anchor's time and with the start of its key's validity with the tolerance of the verifier's clock (`clockSkewS`, 60 s unless the verifier chooses 0 to 600): a record dated 30 s before its credential's `validFrom`, or 30 s before the block it names, is not refused. The end of a validity and a retirement are compared exactly. | cause | what was not brought or not read | verdict | |---|---|---| | `manufacturer-trust` | the manufacturers the verifier trusts | PARTIAL | | `manufacture-status-unread` | the recall list a credential names | PARTIAL | | `hardware-root` | a hardware root carried by a credential (never judged) | PARTIAL | | `status-unread` | the manufacturer's registry (`statusRecords` not given), a record of the manufacturer this verifier cannot read (another version or state, or a device id that is not a device id: it may be meant for this key), or something handed as a record that is not one | PARTIAL | | `status-other-manufacturer` | a retirement of a key of the log (or a record that cannot be read) signed by another manufacturer the verifier trusts: not applied, and not passed over, as the device verifier does (the same key may carry that manufacturer's credential too) | PARTIAL | | `witness-unread` | a reading of a notarization an anchor claims, or a reading that cannot be used (not whole seconds, or no chain id) | PARTIAL | | `retired-unwitnessed` | records dated before their key was retired that no anchor read shows existed before the retirement: not counted | PARTIAL | | `records-beyond` | records signed by the key in force at the end of the log, at positions after its last record: the log shown is not the latest the verifier holds | PARTIAL | | `records-missing`, `records-unread` | a log with no record; records beyond the 100,000 judged | PARTIAL | | `time-unwitnessed` | counted records with no witnessed upper or lower bound: their time is the device's word | named, the verdict stands | | `beacon-unread` | a block a record names that the verifier did not read | named, the verdict stands | | `rotated-key-used` | a record signed by a key for a position after the log rotated away from it: somebody still holds and uses the old key | named, the verdict stands | | `retired-key-used` | a record handed beside the log, other than the log's own, signed by a key the manufacturer retired: whoever copied the key may have signed it after the retirement, so it is neither a proof of equivocation nor a longer log | named, the verdict stands | | `manufacture-status-none`, `status-future`, `other-ignored`, `after-refusal` | a credential with no recall list; a retirement dated in the verifier's future; records handed beside the log that are not well formed, not signed by a key certified in it, or stand beside a record of the log that is of another key or does not carry its position; records after a refused one | named, the verdict stands | What the verifier brings, and what happens without it: | option | without it | |---|---| | `now` (required) | no verdict: the verifier judges on its own clock | | `trustedManufacturers` | PARTIAL: anyone can issue a manufacture credential with their own keys | | `statusLists` | PARTIAL when a credential names a recall list | | `statusRecords` | PARTIAL: a retirement cannot be seen. `null` says the registry was not read; a list, **empty included**, says what the verifier read. An empty registry and one withheld cannot be told apart: read it from who publishes it, not from the device | | `readings` (`{ statementHash, at: unix seconds, chainId }`) | no upper bound on any time; PARTIAL when an anchor claims a notarization | | `beacons` (`{ chainId, block, hash, at: unix seconds }`, with the `v` and `kind` the `block` command writes beside them, or not) | no lower bound on any time (named) | | `otherRecords` | no equivocation can be seen | Readings and blocks must be the **verifier's own**: a reading handed over by the party being judged is that party's statement. At most 512 status records, 4,096 anchors, 4,096 readings, 100,000 block readings and 4,096 records beside the log are judged; what lies beyond is not read, and the verdict says so. ### Time, said exactly Each counted record gets a **witnessed interval** `[lo, hi]`: `lo` is the time of the latest block read at it or before it (the record was signed after that block existed), `hi` the time of the earliest anchor read at it or after it (the record existed when that anchor was on the chain). Only anchors on accepted records give a bound. Without `lo` and `hi` the time of a record is the device's word, and the verdict names it (`time-unwitnessed`). This matters for what can be manufactured. The holder of the key can sign a year of records in one minute and date them as it likes: the times **claimed** by the device are its word, and so is everything computed from them alone. It cannot make a record whose witnessed interval lies inside a window unless it signed something during that window: it needs a block hash that did not exist before the window, and an anchor on the chain before the window ends. Both bounds rest on the chain's block times. ### Revocation - **A recall by the manufacturer** (its recall list) carries no time: the credential is refused by the device verifier, and the log is INVALID from its first record of that key. A manufacturer that wants a key's history kept retires the key with a dated status record instead. - **A retirement by the manufacturer** (a status record `retired`, signed by the manufacturer of that key's credential) carries the date the manufacturer chose. Records dated at or after it are refused (`key-retired`). Records dated before it count **only when an anchor read shows they existed before it**: a copied key can date a record as it likes, so the others are not counted (`retired-unwitnessed`). For the same reason a record of a retired key handed **beside** the log proves nothing against the device (`retired-key-used`, named): nothing here can show it was signed before the retirement, and without this rule one record signed by whoever copied the key would refuse the log from that position on, undoing the history the retirement keeps. Its price, said: an equivocation the device itself made with that key before the retirement is no longer refused once the key is retired. - The registry is read as the device verifier reads it: a record that names another device is passed over; a record of the key's manufacturer whose device id is not a device id may be meant for this key (`status-unread`); a retirement signed by another manufacturer the verifier trusts is not applied and not passed over (`status-other-manufacturer`). A retirement dated before 1970 holds from the start, as one whose date cannot be read. - A **suspension** concerns what the device may do (the device identity judges it), not what it signed; it is not a revocation of the key here. Status records of an owner are not read here. ### Equivocation Two records signed by one device key for one position are a **proof of equivocation**: `{ v: 1, kind: "aere-device-operation-equivocation", seq, key, a, b }`. `verifyEquivocation(proof)` checks it with nothing but what it carries (both records well formed, of the device the key derives, at that position, different, both signed). The verifier finds one only among the records it is handed beside the log (`otherRecords`: from another party, an earlier copy, a witness); the log is accepted up to the position before it, the proof is kept, and the reputation counts it. Every proof it keeps is one `verifyEquivocation` accepts: the log's record must carry the position too (a log whose records were moved by whoever shows it, with the device's real record handed beside it, is refused for its order, not for an equivocation of the device), and a key the manufacturer retired makes none (above). Two records with the same statement and two signatures are two records (ML-DSA-65 in Node.js 24.14.1 signs with fresh randomness: measured, two signatures of one statement differ), so a device that signs one statement twice, a retry after a lost write for example, has equivocated by this definition: it must keep what it signed. ## The reputation formula The reputation is computed from the **facts** the verifier derives (`result.facts`), never from the records' content again: | fact | what it is | |---|---| | `verdict`, `rules`, `unjudged` | the verdict of the log and its named rules and causes | | `manufacturer`, `keys` | the manufacturer of the first key (`null` when its credential does not carry an aere-id there); the device ids that signed accepted records, in order | | `records` | each counted record: `seq`, `hash`, `at`, `lo`, `hi` (milliseconds since 1970; `lo`, `hi` or `null`), `state`, `counter` | | `uncounted` | every record of the log that is not counted (refused, not judged, beyond the 100,000 judged, or of a retired key with no witness before it) | | `equivocations` | each proof: `seq` and the hashes `a` (the log's record) and `b` (the other), sorted | | `retirements` | each retired key: its device id and the retirement time (`null` when it cannot be read) | | `chains` | the chain ids of the anchors and of the blocks that gave a bound | The parameters are chosen by whoever computes it: `from`, `to` (milliseconds) and `windowS` (seconds). With `W = windowS * 1000`: 1. **Windows.** `n = ceil((to - from) / W)`; window `i` is `[from + i*W, min(from + (i+1)*W, to))`. 2. **In range.** A counted record is in range when `from <= at < to`. `records.inRange` counts them, `records.byState` counts them by state, `records.uncounted` is `uncounted`. 3. **Claimed coverage.** Window `i` is covered by the device's clock when a counted record has its `at` in it. The evidence is the lowest `seq` of such a record. 4. **Witnessed coverage.** Window `i` is witnessed when a counted record has `lo` and `hi`, with `from <= lo <= hi` and `lo` in window `i` and `hi` before the end of window `i`: its whole witnessed interval lies in the window. The evidence is the lowest `seq` of such a record. 5. **Availability.** `availability.claimed` and `availability.witnessed` count the covered windows of each kind; `claimedPerMille = floor(1000 * claimed / n)`, `witnessedPerMille = floor(1000 * witnessed / n)`. 6. **Gaps.** The maximal runs of consecutive windows not covered by the device's clock: `gaps.list` holds `[first window, length]` for each, `gaps.count` their number, `gaps.longestWindows` the longest (0 when none). 7. **Equivocations** and **retirements**: the count and the list, from the facts. 8. **Age.** `age.claimedS = floor((to - at of the first counted record) / 1000)`, when that record is dated before `to`; `age.witnessedS = floor((to - the lowest hi) / 1000)`, when there is one before `to`: the chain shows the log existed since then. Otherwise `null`. 9. **Evidence.** `evidence.windows` lists `[i, claimed seq or null, witnessed seq or null]` for every window covered in either way. 10. `factsHash` is the SHA-256 of the canonical text of the facts (keys sorted, no spaces); the verdict, rules, causes, manufacturer, keys and chains of the facts are copied, with `formula: "aere-device-reputation/1"` and the parameters. Integers only: every time is whole milliseconds, every division is exact, and the bounds (times up to 2^52 ms, windows from 1 s to 366 days, at most 100,000 windows) keep every value a safe integer, so two implementations give the same text. Facts or parameters of another form are refused before anything is computed. The facts the verifier derives are of that form for every log (a record dated before 1970 is refused, a manufacturer that is not an aere-id is `null`), with one bound not guaranteed: more than 64 chains among the anchors or among the blocks that gave a bound, which the verifier's own readings would have to bring. The JSON text is read as JavaScript's `JSON.parse` reads it, in both implementations: each number is one double (2^53 + 1 is 2^53, a whole number of 5,000 digits is Infinity), a member written twice keeps its last value, and the Python reader does not recurse (a claim nested 50,000 deep is in the probe; 200,000 was measured once, by hand): a claim too deep to be written as canonical JSON is not what the formula gives. `checkReputation({ facts, params, claimed })` judges a reputation as somebody states it: VALID when it is exactly what the formula gives for these facts, INVALID `figures` otherwise, INVALID `form` when the facts or the parameters are not of the form. It does not say the facts are those of a log: whoever holds the log derives them again. **What it is not.** Not one number: there are no weights, and no figure stands for the others. Not a token, not tradable, and not written on any chain as a value: anyone who holds the log recomputes it. The figures computed from the device's clock (`claimed`, `gaps`, `age.claimedS`, `byState`) are the device's word; the witnessed ones are bounded by the chain. ## Use ```sh # the identity, with ../device-identity/device-cli.mjs node ../device-identity/device-cli.mjs new-identity --out maker.keys.json node ../device-identity/device-cli.mjs new-device --out d1.key.json node ../device-identity/device-cli.mjs manufacture --manufacturer maker.keys.json --device d1.key.json \ --model AX-200 --serial SN-7 --days 3650 --out m1.json # the device writes its log node operation-cli.mjs start --manufacture m1.json --out log.json node operation-cli.mjs record --device d1.key.json --log log.json --state operating --counter 120 \ --measurement-file reading.bin --after 28001:6157024:0x node operation-cli.mjs rotate --device d1.key.json --next d2.key.json --manufacture m2.json --log log.json --state idle --counter 130 node operation-cli.mjs anchor --log log.json --out anchor.json node operation-cli.mjs notarize --envelope anchor.json --rpc --key-file --out notarized.json node operation-cli.mjs add-anchor --log log.json --envelope notarized.json # the verifier: its own readings, then the verdict, the facts and the reputation node operation-cli.mjs reading --envelope notarized.json --rpc --verify-proof --out reading.json node operation-cli.mjs block --rpc --number 6157024 --out block.json node operation-cli.mjs verify --log log.json --now