# Dual Attestation Protocol **Specification v1.0 (draft)** Sovereign Circuit · Born Between 2 Generals 29 August 2026 > **Status.** Draft for review. This is a specification, not a commitment or an offer. > Nothing in this document has been legally reviewed. See §12. --- ## 1. What this protocol is for Every hardware program that takes equipment in and gives value back has the same unsolved problem: **somebody has to say what the used thing is worth.** In practice one side says it, and the other side accepts it. That is how phone carrier trade-ins work, and it is the single most resented moment in the entire subscription-hardware model. The customer hands over a working device, is told a number they have no way to check, and has no recourse. The vendor holds all of the information and all of the discretion. Dual Attestation removes the discretion. Both sides value the unit **independently and privately**, both publish a cryptographic commitment to their number **before either number is visible**, and then both reveal. If the two numbers are close, the settlement is the midpoint, automatically, with no negotiation. If they are far apart, the tie is broken by a published index rather than by whoever has more leverage. The property this buys is precise and worth stating plainly: > **Neither side can move their number after seeing the other side's number.** That is the whole protocol. Everything below is the engineering required to make that sentence true in the real world. ### 1.1 The doctrine it comes from The rest of the portfolio already refuses to present an assertion as evidence. This protocol applies the same rule to money: the holder's claim is an **assertion**, the inspector's finding is an **observation**, and the sealed record keeps **both** rather than collapsing them into a single price. The settled figure is derived from the two, and the derivation is published. ### 1.2 Where it is used Four places in the Sovereign Circuit loop, all of them the same protocol: | # | Moment | Holder side | Inspector side | |---|--------|-------------|----------------| | 1 | Intake of a retiring fleet | The supplying company | The program bench | | 2 | Residual at scheduled upgrade | The subscriber | The program bench | | 3 | Cascade re-grade between tiers | Outgoing tier record | The program bench | | 4 | Certified material recovery | The program | The recovery partner | Moment 2 is the one that matters commercially. It is the exact moment carriers lose customer trust, and doing it under sealed dual attestation turns the most resented event in the model into the most defensible one. --- ## 2. Terminology | Term | Meaning | |------|---------| | **Round** | One valuation of one asset, identified by `round_id`. | | **Holder** | The party currently in possession of the unit. Asserts a value. | | **Inspector** | The party performing the technical assessment. Observes a value. | | **Commitment** | A SHA-256 digest binding a party to an amount, published before reveal. | | **Nonce** | 32 random bytes, unique per party per round, that hide the amount. | | **Reveal** | Publication of the amount, grade and nonce that reproduce the commitment. | | **Tolerance band** | The spread inside which settlement is automatic. Default 1000 bp. | | **bp** | Basis point. 1 bp = 0.01%. 1000 bp = 10%. | | **Index** | A published comparable-value reference used only for escalation. | | **Sealed record** | The append-only, hash-chained entry that closes the round. | Requirement keywords **MUST**, **MUST NOT**, **SHOULD** and **MAY** are used in the ordinary IETF sense. --- ## 3. Cryptographic dependencies | Function | Standard | |----------|----------| | Hash | SHA-256, [FIPS 180-4](https://csrc.nist.gov/pubs/fips/180-4/upd1/final) | | Canonical serialization | [RFC 8785, JSON Canonicalization Scheme](https://www.rfc-editor.org/info/rfc8785/) | | Nonce generation | CSPRNG per [NIST SP 800-90A Rev. 1](https://csrc.nist.gov/pubs/sp/800/90/a/r1/final) | | Message authentication | HMAC, [RFC 2104](https://www.rfc-editor.org/info/rfc2104/) — transport only | | Party signatures (optional) | Ed25519, [RFC 8032](https://www.rfc-editor.org/info/rfc8032/) | | Independent timestamping (optional) | [RFC 3161 TSP](https://www.rfc-editor.org/info/rfc3161/) | The commit-reveal construction is the standard sealed-bid primitive: participants publish a hash of their bid plus a blinding nonce, then reveal, and the hash proves the revealed value is the one they were bound to ([Chainlink on commit-and-reveal](https://chain.link/article/commit-and-reveal-schemes)). The known failure mode of naive commit-reveal is the **last-revealer advantage** — the party who reveals last can compute the outcome and choose to abort. §7 addresses that directly, and it is the reason non-reveal is penalised rather than merely logged. ### 3.1 Canonicalization All hashes are computed over RFC 8785-canonical JSON: keys sorted by code point, no insignificant whitespace, UTF-8. **No protocol field may be a floating-point number.** The reference implementation raises on any float it encounters rather than attempting to canonicalize it, because IEEE-754 serialization differences between two implementations would silently produce non-matching digests. All money is **integer cents**. All rates are **integer basis points**. ### 3.2 Domain separation Every digest is prefixed with exactly one domain tag: ``` TAG_COMMIT = "SCDA1|commit|" TAG_RECORD = "SCDA1|record|" TAG_ROUND = "SCDA1|round|" ``` Without domain separation, a digest computed for one purpose could be replayed as a digest for another purpose. This is cheap and there is no reason to omit it. --- ## 4. Commitment ``` C = SHA256( TAG_COMMIT || JCS(commit_body) ) ``` where `commit_body` is: ```json { "protocol": "sovereign-circuit/dual-attestation/1", "round_id": "R-2026-08-0417", "asset_id": "SC-LT-000391", "party_id": "ACME-HOLDINGS", "role": "holder", "amount_cents": 42000, "grade": "B2", "nonce": "a1a1…a1a1" } ``` ### 4.1 Why each field is in the commitment - `round_id` — binds the commitment to one round. Without it, a commitment can be replayed in a later round where the same number happens to be favourable. - `asset_id` — binds it to one unit. - `party_id` and `role` — prevents a commitment from being attributed to the other side. Without `role`, an adversarial coordinator holding both commitments could swap which is which after seeing the reveals and pick the better outcome. - `amount_cents` and `grade` — the substance. Grade is committed alongside amount so a party cannot retroactively justify a price with a different condition claim. - `nonce` — **hiding**. This field is not optional and 32 bytes is not excessive. The plausible price range for a used business laptop is a few thousand distinct cent values in practice. Without a nonce, anyone holding the commitment can enumerate the entire space in microseconds and read the sealed amount. The nonce is what makes the commitment actually sealed. ### 4.2 Nonce requirements - 32 bytes from a CSPRNG. **MUST NOT** be derived from the amount, the round id, a timestamp, or a counter. - Fresh per party per round. Reuse across rounds by the same party leaks equality of amounts. - Retained until reveal, then published as part of the reveal. It is not a long-term secret and **MUST NOT** be reused as a key. ### 4.3 Grade vocabulary `grade` is a short token from a published rubric, e.g. `A1`…`D4`. The rubric is a separate document and is **out of scope here**; this spec only requires that the token space is published, that grading criteria are testable rather than subjective, and that the rubric version is captured in the policy hash. See §11. --- ## 5. Round lifecycle ``` ┌──────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ ┌────────┐ │ 1 OPEN │──▶│ 2 COMMIT │──▶│ 3 REVEAL │──▶│ 4 SETTLE │──▶│ 5 SEAL │ └──────────┘ └──────────────┘ └──────────────┘ └──────────┘ └────────┘ policy is both parties both parties deterministic appended to published publish C publish nonce arithmetic, the hash and hashed privately + amount no discretion chain ``` ### Stage 1 — Open The coordinator publishes `round_id`, `asset_id`, the **policy** (§6.1), the commit deadline `T_c` and the reveal deadline `T_r`, and the index source that would be used on escalation. `policy_hash` **MUST** be published before the commit window opens. This is the load-bearing rule of the whole stage: if the policy could be changed after the commitments were made, the coordinator could pick a tolerance band that produced the outcome they preferred. Publishing the hash first makes the rules immutable for that round. ### Stage 2 — Commit Each party computes its valuation **without access to the other party's figure** and publishes only `C`. The coordinator stores both commitments and **MUST NOT** disclose either one to the counterparty before `T_c` closes. Publishing them to both parties after `T_c` is permitted and **SHOULD** be done, because it lets each side confirm the other was genuinely bound before reveals began. If only one party commits by `T_c`, the round is **void** and reopened. No default applies at commit stage — a party who never committed was never bound, and treating silence as agreement to a number they never saw would be unfair. ### Stage 3 — Reveal Each party publishes `amount_cents`, `grade` and `nonce`. The coordinator recomputes `C` and rejects any reveal that does not match. Reveal ordering is **not** significant to the arithmetic. ### Stage 4 — Settle Deterministic (§6). Given the same two revealed amounts and the same policy, every conforming implementation **MUST** produce the same result to the cent. ### Stage 5 — Seal The round record (§8) is appended to the hash chain. The chain head advances. --- ## 6. Settlement ### 6.1 Policy The default policy, and its hash: ```json { "tolerance_bp": 1000, "floor_cents": 2500, "absolute_floor_bp_exempt_cents": 5000, "bonus_bp": 250, "bonus_inner_bp": 300, "bonus_cap_cents": 7500, "late_reveal_penalty": "counterparty_amount", "index_name": "SC-COMP-A" } ``` ``` policy_hash = f5bbc16aaba884177e6581aa528aa6b84c489aa842e592a9998909154ed5c0c1 ``` ### 6.2 Spread ``` spread_bp = round_half_up( (max − min) × 10000 / min ) ``` The spread is measured **against the lower of the two amounts**. This is the conservative choice: it produces a larger number than measuring against the midpoint or the higher amount, so the band is harder to satisfy. Any round that auto-settles under this definition would also auto-settle under the looser ones. In plain language: the gap is expressed as a percentage of the smaller number. ### 6.3 The ladder **1. Inside the band** — if `spread_bp ≤ tolerance_bp`, settle at the midpoint: ``` amount = round_half_up( (holder + inspector) / 2 ) ``` Half-up rounding, not banker's rounding. Two independent implementations must agree to the cent, and half-up is what a non-technical participant expects when they check the arithmetic by hand. **2. Low-value exemption** — if both amounts are at or below `absolute_floor_bp_exempt_cents` and the absolute gap is at or below `floor_cents`, settle at the midpoint regardless of the percentage spread. On a $32 accessory a $8 gap is 2500 bp and means nothing; escalating it costs more than the item. **3. Outside the band** — escalate to the published index, then **clamp the index into the revealed range**: ``` amount = max( min_revealed, min( max_revealed, index ) ) ``` The clamp is important and is not merely defensive. It guarantees escalation can never land outside what both parties actually claimed. If the index says $200 and the holder said $900 while the inspector said $300, settlement is $300 — the inspector's own number — not $200. A stale, thin or manipulated index cannot be used to pay a participant less than the program's own inspector said the unit was worth. **4. Verified-condition bonus** — if `spread_bp ≤ bonus_inner_bp`, add `bonus_bp` of the settled amount, capped at `bonus_cap_cents`. This is the economic engine of the protocol and deserves an explicit statement of intent. The bonus is not goodwill. It **pays participants for accurate self-assessment**. A holder who grades honestly lands inside the inner band and earns more than a holder who inflates. Over a fleet, that converts inspection from an adversarial cost centre into a spot-check, because the incoming grades are already close. Cheaper inspection is the return on the bonus. ### 6.4 Non-reveal If one party commits and then fails to reveal by `T_r`: - Settlement is **the counterparty's revealed amount**. - **No bonus** is paid. - The default is recorded as such in the sealed record. The revealing party does **not** get to name the price unilaterally. If they did, there would be a direct incentive to induce a non-reveal — delay the counterparty, run out the clock, then name any number. Settling at the counterparty's amount means the silent party is treated as having accepted the figure that was already sealed before the deadline, which is the only reading that does not reward gamesmanship. Repeated non-reveal by the same `party_id` **SHOULD** trigger review outside the protocol. ### 6.5 What the protocol deliberately does not do - It does not decide whether a unit is worth anything. That is the grading rubric. - It does not price subscriptions. Published Sentinel prices remain a floor and are not undercut by any credit produced here. - It does not arbitrate disputes about physical condition. It records both positions and settles the money; a condition dispute is a separate process. --- ## 7. Security analysis | Attack | Mitigation | |--------|------------| | Move your number after seeing theirs | Commitment published before reveal; digest will not match. | | Brute-force a commitment to read the sealed amount | 32-byte nonce; search space is 2²⁵⁶, not the price range. | | Replay a commitment into a different round or unit | `round_id` and `asset_id` are inside the digest. | | Swap which commitment belongs to which side | `party_id` and `role` are inside the digest. | | Change the rules after seeing the numbers | `policy_hash` published before the commit window opens. | | Last-revealer aborts an unfavourable round | §6.4 — non-reveal settles at the counterparty's amount. | | Coordinator leaks one commitment to the other party | Commitments are hashes: leaking one reveals no amount. Leaking a *reveal* early does break the protocol, so reveals **MUST** be gated until both are in or `T_r` passes. | | Manipulate or stale-date the index | Index is only reachable outside the band, and is clamped into the revealed range (§6.3). | | Silently rewrite a settled round | Hash chain: any edit changes the entry hash and breaks every subsequent link. | | Coordinator is not neutral | The coordinator is the program, which is a party in most rounds. This is the protocol's **main residual weakness** — see below. | ### 7.1 Residual weakness: the coordinator is not neutral In most rounds the program is both the inspector and the coordinator. The cryptography prevents it from changing its own number after seeing the holder's number, and prevents it from rewriting a sealed round. It does **not** structurally prevent it from mis-implementing the arithmetic, or from withholding a reveal it dislikes and claiming the counterparty defaulted. Three mitigations, in increasing strength: 1. **Publish the reference implementation and the test vectors** (§10) so any holder can independently recompute their own settlement from the record. This is cheap and should be done from day one. 2. **Independent timestamping** of commitments via RFC 3161, so the existence and timing of a commitment is attested by a third party rather than by the program's own log. 3. **A third attesting party** for high-value rounds — the supplying company's own auditor or insurer, committing alongside. The protocol generalises to *n* parties without structural change: the band test becomes max-vs-min across all reveals and settlement becomes the median rather than the midpoint. This is noted as a v1.1 extension and is **not** specified here. Honesty about this is the point. The protocol makes a specific, verifiable promise — neither side can move after seeing the other — and does not pretend to make a larger one. --- ## 8. The sealed record ```json { "protocol": "sovereign-circuit/dual-attestation/1", "round_id": "R-2026-08-0417", "asset_id": "SC-LT-000391", "policy_hash": "f5bbc16a…d5c0c1", "commitments": { "holder": "…", "inspector": "…" }, "reveals": { "holder": { "party_id": "ACME-HOLDINGS", "role": "holder", "amount_cents": 42000, "grade": "B2", "nonce": "…" }, "inspector": { "party_id": "SC-BENCH-04", "role": "inspector", "amount_cents": 39000, "grade": "B3", "nonce": "…" } }, "settlement": { "outcome": "auto_settled", "amount_cents": 40500, "spread_bp": 769, "bonus_cents": 0, "total_cents": 40500, "reason": "spread 769 bp within 1000 bp band; midpoint settlement" }, "timestamps": { "commit_opened": "…", "commit_closed": "…", "reveal_closed": "…", "settled": "…" } } ``` **The record retains both revealed amounts, not just the settled figure.** The holder's claim stays visible as an assertion and the inspector's finding stays visible as an observation. Collapsing them into one price would destroy the evidentiary value of the record and would be inconsistent with the rest of the portfolio. The `reason` string is human-readable on purpose. A participant reading their own record should be able to see the arithmetic without a decoder. ### 8.1 Hash chain ``` entry_hash = SHA256( TAG_RECORD || JCS({ prev, seq, body }) ) ``` with `prev` of the genesis entry set to 64 zeros. This reuses the audit-chain pattern already used elsewhere in the platform. Any modification to a sealed entry changes its hash and breaks every subsequent link. A Merkle-tree log per [RFC 6962](https://www.rfc-editor.org/info/rfc6962/) is the natural upgrade when efficient inclusion proofs are needed — a holder proving their single round is in the log without downloading it. A linear chain is sufficient at pilot volume and is much easier to audit by reading. --- ## 9. Reference implementation | File | Purpose | |------|---------| | `refimpl/attestation.py` | The protocol. Dependency-free Python, standard library only, so it can be audited by reading it. Integer cents throughout, no floats in the settlement path, floats rejected at the canonicalizer. | | `refimpl/make_vectors.py` | Regenerates every vector in §10 from the implementation. | | `refimpl/test_spec.py` | Asserts that this document matches the implementation. | | `test-vectors.json` | Machine-readable form of §10. | ```bash cd refimpl && python3 make_vectors.py # regenerate cd refimpl && python3 test_spec.py # verify this document ``` Everything in §10 was produced by `make_vectors.py`, not written by hand. `test_spec.py` re-asserts every printed vector plus four invariants that are not printed: escalation without an index raises, a short nonce raises, a float amount raises, settlement is symmetric in its two arguments, and the escalation clamp never leaves the revealed range across a sweep of adversarial index values including 0 and 10⁹. Current status: **PASS**. --- ## 10. Normative test vectors A conforming implementation **MUST** reproduce all of these exactly. ### 10.1 Commitment Input: ```json { "round_id": "R-2026-08-0417", "asset_id": "SC-LT-000391", "party_id": "ACME-HOLDINGS", "role": "holder", "amount_cents": 42000, "grade": "B2", "nonce": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1" } ``` Expected commitment: ``` f40ccaef3378cbfb0574d0499f8a8d3bfd35d2990edcf7943e5f163ed2db9165 ``` Negative cases, all of which **MUST** be rejected against that commitment: | Mutation | Result | |----------|--------| | `amount_cents` → 42001 | rejected | | `round_id` → `R-2026-08-0418` | rejected | | `role` → `inspector` | rejected | ### 10.2 Settlement Amounts in cents. Policy is the §6.1 default. | # | Case | Holder | Inspector | Index | Spread | Outcome | Settled | Bonus | Total | |---|------|-------:|----------:|------:|-------:|---------|--------:|------:|------:| | TV-1 | inside band | 42000 | 39000 | — | 769 bp | auto_settled | 40500 | 0 | **40500** | | TV-2 | identical figures | 40000 | 40000 | — | 0 bp | auto_settled | 40000 | 1000 | **41000** | | TV-3 | edge, exactly at band | 44000 | 40000 | — | 1000 bp | auto_settled | 42000 | 0 | **42000** | | TV-4 | just outside band | 44100 | 40000 | 41500 | 1025 bp | escalated | 41500 | 0 | **41500** | | TV-5 | far apart, index below range | 90000 | 30000 | 20000 | 20000 bp | escalated | 30000 | 0 | **30000** | | TV-6 | inner band, bonus | 41000 | 40000 | — | 250 bp | auto_settled | 40500 | 1013 | **41513** | | TV-7 | low-value exemption | 4000 | 3200 | — | 2500 bp | auto_settled | 3600 | 0 | **3600** | | TV-8 | non-reveal default | 38500 | — | — | n/a | defaulted | 38500 | 0 | **38500** | Notes on the interesting rows: - **TV-3** confirms the band is inclusive. `spread ≤ tolerance`, not `<`. - **TV-5** is the clamp doing its job. The index said 20000, below the inspector's own 30000, so settlement is 30000. The program cannot use a low index to pay less than its own bench assessed. - **TV-6** shows the bonus: 250 bp of 40500 = 1012.5, half-up to **1013**. This vector exists specifically to pin the rounding direction. - **TV-8** has no spread; the field is not meaningful for a default. ### 10.3 Hash chain Two entries: a settled round, then a placement event. ``` entry[0].hash = 5472ffe25bd5e7e2f2cbfc38351017781c8280d6f14670c4c3bbb246c787f133 entry[1].prev = 5472ffe25bd5e7e2f2cbfc38351017781c8280d6f14670c4c3bbb246c787f133 entry[1].hash = 375e31cf0216123658529e6575925543c865b83964def4393a34625b8b1ae1ad head = 375e31cf0216123658529e6575925543c865b83964def4393a34625b8b1ae1ad ``` `verify()` returns true. Changing `settlement.amount_cents` in entry 0 from 40500 to 41000 causes `verify()` to return false — the tamper-detection case is in the vector generator and passes. --- ## 11. Open items These are genuinely unresolved and are called out rather than papered over. 1. **The grading rubric.** The protocol is only as fair as the grade tokens it commits to. The rubric needs testable criteria — firmware support status, TPM version, RAM, storage class, battery health — so a demotion reason is objective rather than a judgment call. This is the single biggest dependency. 2. **The index.** `SC-COMP-A` does not exist yet. It needs a defined construction, a publication cadence, a source list, and an archive so a historical round can be re-verified against the index value that was current at the time. An index that can be silently restated is not a tie-breaker. 3. **Tolerance band calibration.** 1000 bp is a starting guess. It should be set from real paired data — bench grade versus holder self-grade across a first batch — not chosen because it is a round number. 4. **Bonus cap.** $75 is a placeholder. The cap must sit below the marginal cost of a full inspection, or the bonus stops paying for itself. 5. **Deadlines.** `T_c` and `T_r` need defaults. Too short disadvantages a holder without technical staff; too long stalls the loop. 6. **Multi-party rounds.** The *n*-party generalisation in §7.1 is sketched, not specified. 7. **Identity and signatures.** Ed25519 signing of commitments and reveals is listed as optional. It becomes necessary the moment a participant might deny having committed. Key distribution is unspecified. 8. **Legal characterisation.** See below. --- ## 12. Legal note **Do not market any part of this as insurance, and do not describe the settled figure as an appraisal, until counsel has reviewed it.** Both words are regulated terms of art. Insurance and insurance-adjacent products carry licensing obligations, and there is a well-documented distinction between a warranty, a service contract and insurance that turns on who bears the risk and whether the obligor is the party providing the goods ([IRMI](https://www.irmi.com/articles/expert-commentary/warranty-service-contract-and-insurance)). Service-contract obligors face state-by-state registration and reserve requirements ([NCOIL overview](https://ncoil.org/wp-content/uploads/2021/07/Introduction-and-Overview-of-Warranty-Legislative-Regulatory-Landscape.pdf)), and states such as Virginia have specific extended service contract statutes ([Virginia Extended Service Contract Act](https://law.lis.virginia.gov/vacodepopularnames/extended-service-contract-act/)). California draws the line by obligor: a dealer-obligor arrangement is treated differently from mechanical breakdown insurance ([ConsumerAffairs summary of California law](https://www.consumeraffairs.com/automotive/california-extended-warranty-law.html)). For this protocol specifically, the safe framing is **a contractual valuation mechanism inside a subscription agreement**. Terms to prefer: | Use | Avoid | |-----|-------| | settled value, agreed value | appraised value, fair market value opinion | | protection plan, swap-on-failure | insurance, coverage, policy | | participation credit | rebate, cash-back, payout | | service-level obligation | claim, indemnity | "Appraisal" in particular can imply a licensed appraiser in some contexts. The program is not producing an appraisal; it is producing a contractually agreed figure derived from two sealed inputs under published rules. That is a materially different and much more defensible claim — and it happens to be a more accurate description of what the protocol actually does. --- ## 13. Why build this first It requires **no hardware**. The entire protocol is specifiable, implementable, testable and demonstrable before a single unit is acquired — as this document and its passing test vectors demonstrate. It is also the best sales artifact the program has. A supplying company's procurement or finance team does not need to be persuaded that a laptop can be wiped; they need to be persuaded they will not be low-balled on a fleet. A working demonstration where they commit their own number, watch the program commit blind, and see the midpoint settle automatically is a more convincing five minutes than any deck. --- *Sovereign Circuit — Dual Attestation Protocol v1.0 draft. Internal concept of record. Figures are targets, not measured results. Nothing here is a commitment or an offer.*