Skip to main content

agora_agentkit/
govlog.rs

1//! Governance log attestation: the envelope the server signs at insert and
2//! any client can verify.
3//!
4//! Every entry commits to its own fields and to the hash of the entry before
5//! it, so the log is a chain: change or remove any entry and every later
6//! link stops verifying. The envelope (version 1) is
7//!
8//! ```text
9//! data_hash  = SHA-256( canonical_json(data) )
10//! preimage   = {"agora_governance_log":1,"id":…,"entry_type":…,
11//!               "created_at":<unix micros>,"prev_hash":<hex|null>,
12//!               "data_hash":<hex>}
13//! entry_hash = SHA-256( preimage )
14//! signature  = crypto::sign( key, entry_hash, signed_at unix seconds )
15//! ```
16//!
17//! What is *not* in the envelope, on purpose: `tags` (an index the Clerk may
18//! revise), `chain_seq` (an index; the `prev_hash` links prove order), and
19//! anything derived such as the precedent summary. `signed_at` is bound by
20//! the signature rather than the hash, so a retroactive attestation — an
21//! entry signed long after it was recorded — is visible as such and cannot
22//! be quietly back-dated. See [`is_retroactive`].
23//!
24//! History is never rewritten. Two entry types amend it instead, and both
25//! are ordinary signed links whose `data` the verifier reads:
26//!
27//! - [`Amendment`] (`AMD-`) names an earlier entry and says what changed
28//!   about its force ([`Standing`]) or its content ([`Redaction`]). Its
29//!   own free text is committed to, not contained (see [`TextCommitment`]),
30//!   because an amendment is the one thing that can never be redacted. A
31//!   redaction replaces values in the target's `data` in place; the
32//!   original `entry_hash` stays on the row so later links still verify,
33//!   and the amendment's `resulting_data_hash` is what the redacted data
34//!   must now hash to. [`EntryVerdict::content_matches`] is the check. A
35//!   [`Revision`] overwrites nothing: it is a signed RFC 6902 patch, and
36//!   the entry's [`latest`] version is its stored `data` with every
37//!   revision applied in chain order.
38//! - [`KeyRotation`] (`KEY-`) moves the chain to a new signing key. A
39//!   routine rotation is signed by the old key and a compromise
40//!   declaration by the new one, but neither signature is what makes the
41//!   change authentic: a [`KeyCertificate`] from the offline root keys
42//!   ([`ROOT_KEYS`]) is, so holding the online key is never enough to
43//!   move the chain. See [`verify_chain`].
44//!
45//! A third series is reserved but read by no verifier: a [`StewardRecord`]
46//! (`REC-`) says what was done — a key ceremony, a restore — and decides
47//! nothing. It exists because a rotation can never be redacted and so
48//! carries keys and hashes only; the narrative that names people goes in an
49//! entry that can be.
50//!
51//! A fourth, an [`Obligation`] (`OBL-`), is work a decision or record owes.
52//! The verifier reads only the amendments that keep its status: the five
53//! [obligation kinds](AmendmentKind::OBLIGATION) name an `obligation` and
54//! nothing else, and carry an [`ObligationUpdate`].
55//!
56//! A fifth, a [`DataSet`] (`DATA-`), is the signed manifest of a data set
57//! in a git repository: a commit, and every file's size and SHA-256. No
58//! verifier reads it; a server checks each file against it before serving.
59//!
60//! A sixth, a [`TestEntry`] (`TEST-`), is what a test sitting would have
61//! decided, wrapped so it binds nothing: the inner entry's type and data
62//! beside a title, a description and the test proposal's post. No verifier
63//! reads it; readers label it [`TEST_LABEL`] wherever it appears.
64//!
65//! The envelope itself is unchanged by any of this: `ENVELOPE_VERSION` is
66//! still 1 and what it does and does not cover is exactly as above.
67
68use crate::crypto::{self, Signature, SigningKey, VerifyingKey};
69use crate::enums::GovernanceLogEntryType;
70use crate::ids::{GovernanceLogId, GovernanceLogPrefix};
71use chrono::{DateTime, Utc};
72use serde::{Deserialize, Serialize};
73use sha2::{Digest, Sha256};
74use std::collections::{HashMap, HashSet};
75
76pub use crate::enums::{AmendmentKind, CitationRelation, KeyStatus, Standing};
77
78mod texts;
79pub use texts::{
80    AmendmentText, AmendmentTextStatus, AmendmentTexts, CommittedText,
81    TextCommitment, TextStatus, WITHHELD_TEXT,
82};
83
84mod council;
85pub use council::{
86    Abstention, AgendaRanking, Asked, Ballot, BlankStatement,
87    CouncilAttachment, CouncilDecisionRecord, CouncilMember, CouncilRound,
88    CouncilSeat, CouncilVote, DEFERRING_SEATS, Decided, DecisionCategory,
89    DecisionSummary, FinalRoundError, FinalVotes, NotASeatChoice,
90    PlacedProposal, RECUSED_ACCEPTORS, RECUSED_DEFERRED_TAG, Rationale,
91    RecordRuleError, RecusalVote, STEWARD_RECUSED_TAG, SUMMARY_POINTER,
92    SUMMARY_WRITER, SeatChoice, SeatModels, SeatRanking, SeatResponse,
93    SeatVote, SeatVotes, Statement, StewardEmergency, StewardRecusal,
94    StewardVote, SummaryAcceptance, SummaryAuthorship, Testimony, Unanswered,
95    Unheard, Vote, Votes, Witness, WitnessQuestion, WitnessSource,
96    served_summary, test_labelled, testimony_attachment_name,
97};
98
99mod redactable;
100pub use redactable::{REDACTION_MARKER_PATTERN, Redactable};
101
102pub mod reading;
103
104/// RFC 6902, as [`Revision::patch`] uses it: re-exported so callers name
105/// the same types this crate was built with
106pub use json_patch;
107
108mod obligation;
109pub use obligation::request;
110pub mod quote;
111pub use obligation::{
112    Evidence, KnownEntry, OBLIGATION_VERSION, Obligation, ObligationData,
113    ObligationError, ObligationRuleError, ObligationState, ObligationUpdate,
114    Until,
115};
116
117mod data_set;
118pub use data_set::{
119    DATA_SET_VERSION, DataFile, DataFileMismatch, DataFilePath,
120    DataFilePathError, DataSet, DataSetLookupError, DataSetLookupFailure,
121    DataSetTarget, MAX_DATA_FILE_BYTES,
122};
123
124mod test_entry;
125pub use test_entry::{
126    TEST_ENTRY_VERSION, TEST_INNER_POINTER, TEST_LABEL, TEST_MACHINE_FIELDS,
127    TestEntry, TestEntryError, decision_data, decision_type,
128};
129
130mod record;
131pub use record::{
132    RecordAttachment, RecordParticipant, STEWARD_RECORD_VERSION, StewardRecord,
133};
134
135mod root;
136pub use root::{
137    CertPurpose, CertificateError, KEY_CERT_VERSION, KeyCertStatement,
138    KeyCertificate, ROOT_DOMAIN, ROOT_KEYS, ROOT_THRESHOLD, RootSet,
139    RootSignature,
140};
141
142/// The shared test vectors in `vectors/govlog`; see [`vectors`]
143#[cfg(test)]
144mod vectors;
145
146/// The envelope version this module produces and verifies
147pub const ENVELOPE_VERSION: u32 = 1;
148
149/// An attestation signed more than this long after its entry was recorded
150/// is [retroactive](is_retroactive)
151pub const RETROACTIVE_AFTER: chrono::Duration = chrono::Duration::seconds(60);
152
153// ---------------------------------------------------------------------------
154// Fixed-size hex newtypes
155// ---------------------------------------------------------------------------
156
157/// A hex string of the wrong length or alphabet for the type it was parsed
158/// into
159#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
160#[error("{type_name}: expected {expected} bytes of hex, got {got:?}")]
161pub struct HexLengthError {
162    pub type_name: &'static str,
163    pub expected: usize,
164    pub got: String,
165}
166
167macro_rules! hex_bytes {
168    ($(#[$meta:meta])* $name:ident, $len:expr) => {
169        $(#[$meta])*
170        #[derive(Clone, Copy, PartialEq, Eq, Hash)]
171        #[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
172        #[cfg_attr(feature = "sqlx", sqlx(transparent))]
173        pub struct $name([u8; $len]);
174
175        impl $name {
176            /// The raw bytes
177            pub fn as_bytes(&self) -> &[u8; $len] {
178                &self.0
179            }
180
181            /// Lowercase hex, as on the wire
182            pub fn to_hex(&self) -> String {
183                hex::encode(self.0)
184            }
185        }
186
187        impl From<[u8; $len]> for $name {
188            fn from(bytes: [u8; $len]) -> Self {
189                Self(bytes)
190            }
191        }
192
193        impl TryFrom<&[u8]> for $name {
194            type Error = HexLengthError;
195
196            fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
197                <[u8; $len]>::try_from(bytes).map(Self).map_err(|_| {
198                    HexLengthError {
199                        type_name: stringify!($name),
200                        expected: $len,
201                        got: hex::encode(bytes),
202                    }
203                })
204            }
205        }
206
207        impl TryFrom<Vec<u8>> for $name {
208            type Error = HexLengthError;
209
210            fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
211                Self::try_from(bytes.as_slice())
212            }
213        }
214
215        impl std::str::FromStr for $name {
216            type Err = HexLengthError;
217
218            fn from_str(s: &str) -> Result<Self, Self::Err> {
219                let bytes = hex::decode(s.trim()).map_err(|_| HexLengthError {
220                    type_name: stringify!($name),
221                    expected: $len,
222                    got: s.to_string(),
223                })?;
224                Self::try_from(bytes.as_slice())
225            }
226        }
227
228        impl std::fmt::Display for $name {
229            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
230                f.write_str(&self.to_hex())
231            }
232        }
233
234        impl std::fmt::Debug for $name {
235            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
236                write!(f, "{}({})", stringify!($name), self.to_hex())
237            }
238        }
239
240        impl Serialize for $name {
241            fn serialize<S: serde::Serializer>(
242                &self,
243                s: S,
244            ) -> Result<S::Ok, S::Error> {
245                s.serialize_str(&self.to_hex())
246            }
247        }
248
249        impl<'de> Deserialize<'de> for $name {
250            fn deserialize<D: serde::Deserializer<'de>>(
251                d: D,
252            ) -> Result<Self, D::Error> {
253                let s = String::deserialize(d)?;
254                s.parse().map_err(serde::de::Error::custom)
255            }
256        }
257
258        // Hand-written for the same reason as every id newtype: a derived
259        // schema becomes a `$ref` into `$defs`, which the Claude.ai MCP
260        // connector mangles (see CLAUDE.md in the agora repo).
261        #[cfg(feature = "schemars")]
262        impl schemars::JsonSchema for $name {
263            fn inline_schema() -> bool {
264                true
265            }
266
267            fn schema_name() -> std::borrow::Cow<'static, str> {
268                std::borrow::Cow::Borrowed(stringify!($name))
269            }
270
271            fn schema_id() -> std::borrow::Cow<'static, str> {
272                std::borrow::Cow::Borrowed(concat!(
273                    module_path!(),
274                    "::",
275                    stringify!($name)
276                ))
277            }
278
279            fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
280                schemars::json_schema!({
281                    "type": "string",
282                    "pattern": format!("^[0-9a-f]{{{}}}$", $len * 2),
283                    "description": format!("{} bytes, lowercase hex", $len),
284                })
285            }
286        }
287    };
288}
289
290hex_bytes!(
291    /// A SHA-256 digest, hex on the wire
292    Sha256Hex,
293    32
294);
295
296hex_bytes!(
297    /// An Ed25519 signature, hex on the wire
298    SignatureHex,
299    64
300);
301
302hex_bytes!(
303    /// An Ed25519 public key, hex on the wire
304    PublicKeyHex,
305    32
306);
307
308hex_bytes!(
309    /// A git commit id (SHA-1), hex on the wire: what a [`DataSet`] is
310    /// pinned to (0.91)
311    GitCommitId,
312    20
313);
314
315hex_bytes!(
316    /// A blinding value: 32 random bytes carried in a redactable entry's
317    /// `data` under [`BLIND_KEY`]. See [`blind_data`] for what it is for.
318    Blind,
319    32
320);
321
322hex_bytes!(
323    /// The salt of a [`TextCommitment`]: 32 random bytes kept beside the
324    /// text, and deleted with it
325    TextSalt,
326    32
327);
328
329impl Blind {
330    /// A fresh value from the operating system's random source
331    pub fn random() -> Self {
332        use rand::RngCore;
333        let mut bytes = [0u8; 32];
334        rand::rngs::OsRng.fill_bytes(&mut bytes);
335        Self(bytes)
336    }
337}
338
339impl TextSalt {
340    /// A fresh value from the operating system's random source
341    pub fn random() -> Self {
342        Self(*Blind::random().as_bytes())
343    }
344}
345
346impl From<Signature> for SignatureHex {
347    fn from(sig: Signature) -> Self {
348        Self(sig.to_bytes())
349    }
350}
351
352impl From<&SignatureHex> for Signature {
353    fn from(sig: &SignatureHex) -> Self {
354        Signature::from_bytes(&sig.0)
355    }
356}
357
358impl From<&VerifyingKey> for PublicKeyHex {
359    fn from(key: &VerifyingKey) -> Self {
360        Self(key.to_bytes())
361    }
362}
363
364impl PublicKeyHex {
365    /// The key, if the bytes are a valid curve point
366    pub fn to_verifying_key(
367        &self,
368    ) -> Result<VerifyingKey, ed25519_dalek::SignatureError> {
369        VerifyingKey::from_bytes(&self.0)
370    }
371}
372
373// ---------------------------------------------------------------------------
374// Canonical JSON and hashing
375// ---------------------------------------------------------------------------
376
377/// `value` as compact JSON with object keys sorted bytewise at every level.
378///
379/// `serde_json::to_vec` on a [`serde_json::Value`] is *not* canonical:
380/// with the `preserve_order` feature (on in every Agora workspace, off in
381/// this crate's own tests) objects serialize in insertion order, so the
382/// same value hashes differently depending on who built it. Strings and
383/// numbers use serde_json's own formatting, which is deterministic for a
384/// given value.
385pub fn canonical_json(value: &serde_json::Value) -> Vec<u8> {
386    let mut out = Vec::new();
387    write_canonical(value, &mut out);
388    out
389}
390
391fn write_canonical(value: &serde_json::Value, out: &mut Vec<u8>) {
392    use serde_json::Value;
393    match value {
394        Value::Null => out.extend_from_slice(b"null"),
395        Value::Bool(b) => {
396            out.extend_from_slice(if *b { b"true" } else { b"false" })
397        }
398        Value::Number(n) => serde_json::to_writer(&mut *out, n)
399            .expect("a number always serializes"),
400        Value::String(s) => serde_json::to_writer(&mut *out, s)
401            .expect("a string always serializes"),
402        Value::Array(items) => {
403            out.push(b'[');
404            for (i, item) in items.iter().enumerate() {
405                if i > 0 {
406                    out.push(b',');
407                }
408                write_canonical(item, out);
409            }
410            out.push(b']');
411        }
412        Value::Object(map) => {
413            let mut keys: Vec<&String> = map.keys().collect();
414            keys.sort_unstable();
415            out.push(b'{');
416            for (i, key) in keys.into_iter().enumerate() {
417                if i > 0 {
418                    out.push(b',');
419                }
420                serde_json::to_writer(&mut *out, key)
421                    .expect("a string always serializes");
422                out.push(b':');
423                write_canonical(&map[key], out);
424            }
425            out.push(b'}');
426        }
427    }
428}
429
430/// The RFC 6901 pointer to the first number in `data` that is not a 64-bit
431/// integer, if there is one.
432///
433/// Governance `data` never contains one. [`canonical_json`] writes a number
434/// the way `serde_json` does, and how that prints a float has changed
435/// between releases (`1e21` became `1e+21`); an integer past `u64` is a
436/// float to it as well, and its float parsing is not exactly rounded. A
437/// hash that is meant to be permanent cannot depend on any of that, so the
438/// writer refuses such `data` ([`blind_data`], [`redact_data`]) and a
439/// verifier reports it without hashing it. A fraction goes in a string.
440pub fn non_integer_number(data: &serde_json::Value) -> Option<String> {
441    fn find(value: &serde_json::Value, path: &mut String) -> bool {
442        use serde_json::Value::{Array, Number, Object};
443        let mark = path.len();
444        match value {
445            Number(n) => return n.is_f64(),
446            Array(items) => {
447                for (i, item) in items.iter().enumerate() {
448                    path.push_str(&format!("/{i}"));
449                    if find(item, path) {
450                        return true;
451                    }
452                    path.truncate(mark);
453                }
454            }
455            Object(map) => {
456                for (key, item) in map {
457                    path.push('/');
458                    path.push_str(&escape_pointer_token(key));
459                    if find(item, path) {
460                        return true;
461                    }
462                    path.truncate(mark);
463                }
464            }
465            _ => {}
466        }
467        false
468    }
469    let mut path = String::new();
470    find(data, &mut path).then_some(path)
471}
472
473/// One RFC 6901 reference token, escaped for a JSON Pointer (`~` → `~0`,
474/// `/` → `~1`). Redaction pointers are signed in amendments, so every
475/// writer escapes with this one function (0.74)
476pub fn escape_pointer_token(token: &str) -> String {
477    token.replace('~', "~0").replace('/', "~1")
478}
479
480/// One escaped JSON Pointer token, unescaped: the inverse of
481/// [`escape_pointer_token`] (0.74)
482pub fn unescape_pointer_token(token: &str) -> String {
483    token.replace("~1", "/").replace("~0", "~")
484}
485
486/// A JSON Pointer's reference tokens, unescaped. `""` (the whole
487/// document) has none (0.74)
488pub fn pointer_tokens(pointer: &str) -> Vec<String> {
489    pointer
490        .split('/')
491        .skip(1)
492        .map(unescape_pointer_token)
493        .collect()
494}
495
496/// SHA-256 over [`canonical_json`]
497pub fn data_hash(data: &serde_json::Value) -> Sha256Hex {
498    Sha256Hex(Sha256::digest(canonical_json(data)).into())
499}
500
501/// The fields an entry's hash commits to
502///
503/// A struct rather than a [`serde_json::Value`] so the preimage serializes
504/// in declaration order whatever `preserve_order` says.
505#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
506pub struct Envelope {
507    /// Always [`ENVELOPE_VERSION`]
508    pub agora_governance_log: u32,
509    pub id: GovernanceLogId,
510    pub entry_type: GovernanceLogEntryType,
511    /// Unix microseconds — the precision Postgres stores
512    pub created_at: i64,
513    pub prev_hash: Option<Sha256Hex>,
514    pub data_hash: Sha256Hex,
515}
516
517impl Envelope {
518    /// The version-1 envelope for these fields
519    pub fn new(
520        id: GovernanceLogId,
521        entry_type: GovernanceLogEntryType,
522        created_at: DateTime<Utc>,
523        prev_hash: Option<Sha256Hex>,
524        data_hash: Sha256Hex,
525    ) -> Self {
526        Self {
527            agora_governance_log: ENVELOPE_VERSION,
528            id,
529            entry_type,
530            created_at: created_at.timestamp_micros(),
531            prev_hash,
532            data_hash,
533        }
534    }
535
536    /// `created_at` as a timestamp again
537    pub fn created_at(&self) -> DateTime<Utc> {
538        DateTime::from_timestamp_micros(self.created_at)
539            .expect("an Envelope only ever holds an in-range timestamp")
540    }
541
542    /// The bytes that are hashed
543    pub fn preimage(&self) -> Vec<u8> {
544        serde_json::to_vec(self).expect("an Envelope always serializes")
545    }
546
547    /// SHA-256 over [`preimage`](Self::preimage)
548    pub fn entry_hash(&self) -> Sha256Hex {
549        Sha256Hex(Sha256::digest(self.preimage()).into())
550    }
551}
552
553/// `t` with anything below a microsecond dropped, so the value hashed is the
554/// value Postgres will store
555pub fn truncate_to_micros(t: DateTime<Utc>) -> DateTime<Utc> {
556    DateTime::from_timestamp_micros(t.timestamp_micros())
557        .expect("a timestamp that came from a DateTime is in range")
558}
559
560/// `t` with anything below a second dropped, so `signed_at` round-trips to
561/// the integer the signature covers
562pub fn truncate_to_seconds(t: DateTime<Utc>) -> DateTime<Utc> {
563    DateTime::from_timestamp(t.timestamp(), 0)
564        .expect("a timestamp that came from a DateTime is in range")
565}
566
567/// `true` when the attestation was signed more than [`RETROACTIVE_AFTER`]
568/// after the entry was recorded — history signed after the fact, which
569/// proves the key holder vouches for it now, not that it was signed then
570pub fn is_retroactive(
571    created_at: DateTime<Utc>,
572    signed_at: DateTime<Utc>,
573) -> bool {
574    signed_at - created_at > RETROACTIVE_AFTER
575}
576
577// ---------------------------------------------------------------------------
578// Wire types
579// ---------------------------------------------------------------------------
580
581/// What the server attests about one governance log entry
582#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
583#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
584#[cfg_attr(feature = "schemars", schemars(inline))]
585pub struct GovernanceAttestation {
586    /// Envelope version; see the module docs for what `1` commits to
587    pub envelope_version: u32,
588    /// Position in the chain, from 1. An index, not part of the envelope:
589    /// the `prev_hash` links are what prove order.
590    pub chain_seq: u64,
591    /// `entry_hash` of the previous entry; `null` only for the first
592    pub prev_hash: Option<Sha256Hex>,
593    /// SHA-256 of the entry's canonical `data`
594    pub data_hash: Sha256Hex,
595    /// SHA-256 of the envelope; what the signature covers
596    pub entry_hash: Sha256Hex,
597    /// Ed25519 over `entry_hash` and `signed_at`, by the platform's
598    /// governance signing key
599    pub signature: SignatureHex,
600    /// When the signature was made. Distinct from `created_at`: see
601    /// `retroactive`.
602    pub signed_at: DateTime<Utc>,
603    /// `true` when signed well after the entry was recorded — the entries
604    /// that predate signing were attested this way, which proves the
605    /// Steward vouches for them, not that they were signed at the time
606    pub retroactive: bool,
607}
608
609/// One link of the chain as `GET /api/governance/log/chain` returns it —
610/// everything needed to verify linkage and signatures, plus `data` for the
611/// entries a verifier has to read
612#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
613#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
614#[cfg_attr(feature = "schemars", schemars(inline))]
615pub struct GovernanceChainLink {
616    pub id: GovernanceLogId,
617    pub entry_type: GovernanceLogEntryType,
618    pub created_at: DateTime<Utc>,
619    pub attestation: GovernanceAttestation,
620    /// Present for `amendment` and `key_rotation` entries, whose content
621    /// is what the chain means and is small by construction. A Council
622    /// transcript is neither, and is read one entry at a time instead.
623    /// [`verify_chain`] hashes this raw value against `data_hash` before
624    /// reading it, so unknown future fields neither break an older
625    /// verifier nor escape the signature.
626    #[serde(default, skip_serializing_if = "Option::is_none")]
627    pub data: Option<serde_json::Value>,
628    /// The texts a version 2 [`Amendment`] commits to, as far as the
629    /// platform still holds them. Outside the envelope on purpose: a text
630    /// can be erased without the chain changing.
631    #[serde(
632        default,
633        skip_serializing_if = "Option::is_none",
634        deserialize_with = "read_as_written"
635    )]
636    pub texts: Option<AmendmentTexts>,
637}
638
639/// The platform's governance signing key, as `GET
640/// /api/governance/signing-key` publishes it
641#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
642#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
643#[cfg_attr(feature = "schemars", schemars(inline))]
644pub struct GovernanceSigningKey {
645    /// Always `"ed25519"`
646    pub algorithm: String,
647    pub public_key: PublicKeyHex,
648    /// The envelope version entries are currently signed under
649    pub envelope_version: u32,
650}
651
652impl GovernanceSigningKey {
653    /// The published form of `key`
654    pub fn new(key: &VerifyingKey) -> Self {
655        Self {
656            algorithm: "ed25519".to_string(),
657            public_key: key.into(),
658            envelope_version: ENVELOPE_VERSION,
659        }
660    }
661}
662
663// ---------------------------------------------------------------------------
664// Amendments
665// ---------------------------------------------------------------------------
666
667/// The [`Amendment`] payload version this module produces. Version 1,
668/// whose texts are in the signed `data`, still verifies: the platform has
669/// three, all reviewed to hold no personal data.
670pub const AMENDMENT_VERSION: u32 = 2;
671
672/// An amendment is malformed
673#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
674pub enum AmendmentError {
675    #[error("agora_governance_amendment is {0}, not 1 or {AMENDMENT_VERSION}")]
676    UnsupportedVersion(u32),
677    #[error(
678        "a version 1 amendment carries its texts and a version \
679         {AMENDMENT_VERSION} one commits to them; this does neither \
680         consistently"
681    )]
682    TextShape,
683    #[error("`{0}` beside the entry is not the text the entry committed to")]
684    TextMismatch(&'static str),
685    #[error("texts beside an entry that commits to none")]
686    UncommittedText,
687    #[error("kind `redaction` requires a `redaction`")]
688    MissingRedaction,
689    #[error("`redaction` is only valid on kind `redaction`")]
690    UnexpectedRedaction,
691    #[error("kind `revision` requires a `revision`")]
692    MissingRevision,
693    #[error("`revision` is only valid on kind `revision`")]
694    UnexpectedRevision,
695    #[error("the `revision` is malformed: {0}")]
696    RevisionShape(ReviseError),
697    #[error("kind `{0}` requires an `update`")]
698    MissingUpdate(AmendmentKind),
699    #[error("`update` is only valid on the obligation kinds")]
700    UnexpectedUpdate,
701    #[error("an `update` of kind `{update}` on an amendment of kind `{kind}`")]
702    UpdateMismatch {
703        kind: AmendmentKind,
704        update: AmendmentKind,
705    },
706    #[error(
707        "kind `{kind}` is only valid on an obligation, and {target} is not one"
708    )]
709    NotAnObligation {
710        kind: AmendmentKind,
711        target: GovernanceLogId,
712    },
713    #[error(
714        "kind `{0}` changes precedential force, which an obligation does not have"
715    )]
716    PrecedentialOnObligation(AmendmentKind),
717    #[error(
718        "a revision of an obligation may not touch `{0}`; its status and \
719         deadline change only through the obligation kinds"
720    )]
721    FixedObligationField(String),
722    #[error("amendment target {0} is not an entry of this chain")]
723    UnknownTarget(GovernanceLogId),
724    #[error("amendment target {0} is not an earlier entry")]
725    ForwardReference(GovernanceLogId),
726    #[error("target_entry_hash is not {0}'s entry_hash")]
727    WrongTargetHash(GovernanceLogId),
728}
729
730/// The `data` of an `amendment` entry: what an earlier entry now means.
731///
732/// The target is never edited — except its `data` under a [`Redaction`] —
733/// so the amendment is the whole record of the change, and both are
734/// signed links of the same chain.
735#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
736#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
737#[cfg_attr(feature = "schemars", schemars(inline))]
738pub struct Amendment {
739    /// Always [`AMENDMENT_VERSION`]
740    pub agora_governance_amendment: u32,
741    pub target: GovernanceLogId,
742    /// The target's `entry_hash` — binds this to one exact entry
743    pub target_entry_hash: Sha256Hex,
744    pub kind: AmendmentKind,
745    /// The governance entry that authorizes this, when one does
746    /// (e.g. `GOV-2026-0005`). `None` for a lawful-deletion redaction.
747    #[serde(default)]
748    pub authority: Option<GovernanceLogId>,
749    /// Section or legal basis, human-readable: `"§1 (Red Team Cases
750    /// Recharacterized)"`, `"GDPR Art. 17(1)(a)"`. Never personal data —
751    /// and erasable, for the day that rule is broken.
752    pub basis: AmendmentText,
753    /// The label readers and prompts show next to the target
754    pub note: AmendmentText,
755    /// Why, at length — `note` is the label, this is the reasoning.
756    #[serde(default, skip_serializing_if = "Option::is_none")]
757    pub rationale: Option<AmendmentText>,
758    /// Present iff `kind` is [`AmendmentKind::Redaction`]
759    #[serde(default, skip_serializing_if = "Option::is_none")]
760    pub redaction: Option<Redaction>,
761    /// Present iff `kind` is [`AmendmentKind::Revision`] (0.43)
762    #[serde(default, skip_serializing_if = "Option::is_none")]
763    pub revision: Option<Revision>,
764    /// Present iff `kind` is one of [`AmendmentKind::OBLIGATION`], and of
765    /// that kind (0.72)
766    #[serde(default, skip_serializing_if = "Option::is_none")]
767    pub update: Option<ObligationUpdate>,
768}
769
770/// What a [`AmendmentKind::Redaction`] removed, and what is left
771#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
772#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
773#[cfg_attr(feature = "schemars", schemars(inline))]
774pub struct Redaction {
775    /// RFC 6901 JSON pointers into the target's `data` whose values were
776    /// replaced. Paths only: never the removed content, never the subject.
777    pub fields: Vec<String>,
778    /// What the target's `data` hashes to after redaction, so the redacted
779    /// content is itself verifiable and cannot be altered again silently
780    pub resulting_data_hash: Sha256Hex,
781    /// What the target's [`latest`] version hashes to after redaction: its
782    /// [`Revision`]s rebased over the redacted data. `None` when it has
783    /// none. (0.43)
784    #[serde(default, skip_serializing_if = "Option::is_none")]
785    pub resulting_latest_hash: Option<Sha256Hex>,
786}
787
788/// A commit on a governance entry: an RFC 6902 patch from its previous
789/// version to the next.
790///
791/// Nothing is overwritten. The entry keeps the `data` it was signed with,
792/// and its [`latest`] version is derived by applying every revision's
793/// patch in chain order, as git derives a checkout from its commits. See
794/// [`AmendmentDraft::revision`].
795///
796/// A value a patch adds (`add`, `replace`, `test`) lives in the amendment,
797/// which redaction cannot reach yet; removals, moves and copies carry no
798/// content.
799#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
800pub struct Revision {
801    /// From the previous version to this one
802    pub patch: json_patch::Patch,
803    /// For each `remove` of a duplicate, the removed path and the path it
804    /// duplicated: what a later redaction of the one must also erase from
805    /// the stored original. Not `test` ops, which would carry the values.
806    #[serde(default, skip_serializing_if = "Vec::is_empty")]
807    pub duplicates: Vec<(String, String)>,
808    /// What the entry's `data` hashes to after `patch`
809    pub resulting_data_hash: Sha256Hex,
810}
811
812/// What a [`Revision`] changes, before it is applied: a patch, and the
813/// duplicates it removes, if that is what it does
814#[derive(Debug, Clone, PartialEq, Eq, Default)]
815pub struct Edit {
816    pub patch: json_patch::Patch,
817    /// See [`Revision::duplicates`]
818    pub duplicates: Vec<(String, String)>,
819}
820
821impl From<json_patch::Patch> for Edit {
822    fn from(patch: json_patch::Patch) -> Self {
823        Self {
824            patch,
825            duplicates: Vec::new(),
826        }
827    }
828}
829
830/// Written by hand: `json_patch` derives nothing inline, and a `$ref` in
831/// a schema is not an option (see the agora CLAUDE.md)
832#[cfg(feature = "schemars")]
833impl schemars::JsonSchema for Revision {
834    fn inline_schema() -> bool {
835        true
836    }
837
838    fn schema_name() -> std::borrow::Cow<'static, str> {
839        "Revision".into()
840    }
841
842    fn json_schema(
843        generator: &mut schemars::SchemaGenerator,
844    ) -> schemars::Schema {
845        let hash = generator.subschema_for::<Sha256Hex>();
846        schemars::json_schema!({
847            "type": "object",
848            "properties": {
849                "patch": {
850                    "description": "RFC 6902 JSON Patch from the previous version to this one",
851                    "type": "array",
852                    "items": {
853                        "type": "object",
854                        "properties": {
855                            "op": {
856                                "type": "string",
857                                "enum": ["add", "remove", "replace", "move", "copy", "test"]
858                            },
859                            "path": {"type": "string"},
860                            "from": {"type": "string"},
861                            "value": true
862                        },
863                        "required": ["op", "path"]
864                    }
865                },
866                "duplicates": {
867                    "description": "(removed path, the path it duplicated) for each duplicate removed",
868                    "type": "array",
869                    "items": {
870                        "type": "array",
871                        "items": {"type": "string"},
872                        "minItems": 2,
873                        "maxItems": 2
874                    }
875                },
876                "resulting_data_hash": hash
877            },
878            "required": ["patch", "resulting_data_hash"]
879        })
880    }
881}
882
883impl Revision {
884    /// Everything wrong with it that is checkable without `data`: an empty
885    /// patch, or a duplicate the patch does not remove
886    pub fn validate(&self) -> Result<(), ReviseError> {
887        if self.patch.is_empty() {
888            return Err(ReviseError::EmptyPatch);
889        }
890        for (path, _) in &self.duplicates {
891            let removed = self.patch.iter().any(|op| {
892                matches!(op, json_patch::PatchOperation::Remove(r) if r.path.as_str() == path)
893            });
894            if !removed {
895                return Err(ReviseError::NotRemoved(path.clone()));
896            }
897        }
898        Ok(())
899    }
900
901    /// An [`Edit`] removing each `(path, same_as)` pair's `path`, whose
902    /// value is byte-identical (as [`canonical_json`]) to `same_as`'s,
903    /// which it leaves in place
904    pub fn remove_duplicates(
905        data: &serde_json::Value,
906        pairs: &[(&str, &str)],
907    ) -> Result<Edit, ReviseError> {
908        let mut ops = Vec::with_capacity(pairs.len());
909        for (path, _) in pairs {
910            let path = json_patch::jsonptr::PointerBuf::parse(*path)
911                .map_err(|e| ReviseError::Patch(e.to_string()))?;
912            ops.push(json_patch::PatchOperation::Remove(
913                json_patch::RemoveOperation { path },
914            ));
915        }
916        let edit = Edit {
917            patch: json_patch::Patch(ops),
918            duplicates: pairs
919                .iter()
920                .map(|(p, s)| (p.to_string(), s.to_string()))
921                .collect(),
922        };
923        check_duplicates(data, &edit)?;
924        Ok(edit)
925    }
926}
927
928/// Each of `edit`'s duplicates is removed by its patch, was byte-identical
929/// to its `same_as`, and its `same_as` survives the patch unchanged
930fn check_duplicates(
931    data: &serde_json::Value,
932    edit: &Edit,
933) -> Result<(), ReviseError> {
934    for (path, same_as) in &edit.duplicates {
935        let resolve = |pointer: &str| {
936            data.pointer(pointer)
937                .ok_or_else(|| ReviseError::Unresolved(pointer.to_string()))
938        };
939        let (removed, kept) = (resolve(path)?, resolve(same_as)?);
940        if canonical_json(removed) != canonical_json(kept) {
941            return Err(ReviseError::NotIdentical {
942                path: path.clone(),
943                same_as: same_as.clone(),
944            });
945        }
946    }
947    let revised = apply_patch(data, &edit.patch)?;
948    for (path, same_as) in &edit.duplicates {
949        let kept = data.pointer(same_as);
950        let removes = |op: &json_patch::PatchOperation| matches!(op, json_patch::PatchOperation::Remove(r) if r.path.as_str() == path);
951        if !edit.patch.iter().any(removes) {
952            return Err(ReviseError::NotRemoved(path.clone()));
953        }
954        let survives = revised.pointer(same_as).is_some_and(|v| {
955            kept.is_some_and(|k| canonical_json(v) == canonical_json(k))
956        });
957        if !survives {
958            return Err(ReviseError::SourceRemoved {
959                path: path.clone(),
960                same_as: same_as.clone(),
961            });
962        }
963    }
964    Ok(())
965}
966
967/// An [`Amendment`] and the texts it commits to: what a writer appends,
968/// the first as the entry's `data` and the second beside it
969#[derive(Debug, Clone, PartialEq, Eq)]
970pub struct AmendmentDraft {
971    pub amendment: Amendment,
972    pub texts: AmendmentTexts,
973}
974
975impl AmendmentDraft {
976    /// An amendment of `kind` against `target`.
977    ///
978    /// Redactions and revisions go through [`redaction`](Self::redaction)
979    /// and [`revision`](Self::revision) instead, the only ways to get
980    /// a `resulting_data_hash` of data that actually exists. A `&str` or `String`
981    /// text gets a [random](TextSalt::random) salt.
982    pub fn new(
983        target: GovernanceLogId,
984        target_entry_hash: Sha256Hex,
985        kind: AmendmentKind,
986        basis: impl Into<CommittedText>,
987        note: impl Into<CommittedText>,
988    ) -> Result<Self, AmendmentError> {
989        match kind {
990            AmendmentKind::Redaction => {
991                return Err(AmendmentError::MissingRedaction);
992            }
993            AmendmentKind::Revision => {
994                return Err(AmendmentError::MissingRevision);
995            }
996            k if k.is_obligation() => {
997                return Err(AmendmentError::MissingUpdate(k));
998            }
999            _ => {}
1000        }
1001        let (basis, note) = (basis.into(), note.into());
1002        Ok(Self {
1003            amendment: Amendment {
1004                agora_governance_amendment: AMENDMENT_VERSION,
1005                target,
1006                target_entry_hash,
1007                kind,
1008                authority: None,
1009                basis: AmendmentText::Committed(basis.commitment()),
1010                note: AmendmentText::Committed(note.commitment()),
1011                rationale: None,
1012                redaction: None,
1013                revision: None,
1014                update: None,
1015            },
1016            texts: AmendmentTexts {
1017                basis: Some(basis),
1018                note: Some(note),
1019                rationale: None,
1020            },
1021        })
1022    }
1023
1024    /// A redaction of `fields` from the target's `data`, with the redacted
1025    /// data it commits to.
1026    ///
1027    /// `amendment_id` is the id this amendment will be appended under: the
1028    /// marker left behind names it, so the redaction says who ordered it.
1029    /// Append both together or neither — the returned `data` is what the
1030    /// target's row must hold for [`verify_chain`] to accept it. `blind`
1031    /// is the target's new [`Blind`]: [random](Blind::random), so a
1032    /// rehearsal's `resulting_data_hash` is not the real one's.
1033    ///
1034    /// `revisions` are the target's, in chain order. They are rebased over
1035    /// the redacted data, which must still take every patch, and a value
1036    /// they removed as a duplicate of a redacted one is redacted from the
1037    /// stored original too: `fields` is extended with it, or the erased
1038    /// text would still be readable as first signed.
1039    // TODO(docs/design-govlog-revisions.md): redact inside a revision's
1040    // patch values, before any revision adds content.
1041    #[allow(clippy::too_many_arguments)]
1042    pub fn redaction(
1043        amendment_id: &GovernanceLogId,
1044        target: GovernanceLogId,
1045        target_entry_hash: Sha256Hex,
1046        basis: impl Into<CommittedText>,
1047        note: impl Into<CommittedText>,
1048        mut fields: Vec<String>,
1049        data: &serde_json::Value,
1050        blind: Blind,
1051        revisions: &[&Revision],
1052    ) -> Result<(Self, serde_json::Value), RedactError> {
1053        for extra in duplicates_of(&fields, revisions) {
1054            let covered = fields.iter().any(|f| {
1055                extra.strip_prefix(f.as_str()).is_some_and(|rest| {
1056                    rest.is_empty() || rest.starts_with('/')
1057                })
1058            });
1059            if !covered && data.pointer(&extra).is_some() {
1060                fields.push(extra);
1061            }
1062        }
1063        let redacted = redact_data(data, &fields, amendment_id, blind)?;
1064        let resulting_latest_hash = match revisions {
1065            [] => None,
1066            _ => Some(data_hash(
1067                &latest(&redacted, revisions.iter().copied())
1068                    .map_err(|e| RedactError::Rebase(e.to_string()))?,
1069            )),
1070        };
1071        let (basis, note) = (basis.into(), note.into());
1072        Ok((
1073            Self {
1074                amendment: Amendment {
1075                    agora_governance_amendment: AMENDMENT_VERSION,
1076                    target,
1077                    target_entry_hash,
1078                    kind: AmendmentKind::Redaction,
1079                    authority: None,
1080                    basis: AmendmentText::Committed(basis.commitment()),
1081                    note: AmendmentText::Committed(note.commitment()),
1082                    rationale: None,
1083                    redaction: Some(Redaction {
1084                        fields,
1085                        resulting_data_hash: data_hash(&redacted),
1086                        resulting_latest_hash,
1087                    }),
1088                    revision: None,
1089                    update: None,
1090                },
1091                texts: AmendmentTexts {
1092                    basis: Some(basis),
1093                    note: Some(note),
1094                    rationale: None,
1095                },
1096            },
1097            redacted,
1098        ))
1099    }
1100
1101    /// A revision of the target by `edit` (a patch, or
1102    /// [`Revision::remove_duplicates`]), applied to its `latest` version
1103    /// (see [`latest`]); returns the next version it commits to.
1104    ///
1105    /// Nothing is written to the target. A patch that adds content (`add`
1106    /// or `replace`) to a target without a [`Blind`] adds one too, so the
1107    /// public `resulting_data_hash` cannot confirm a guess at text a later
1108    /// redaction removes; `copy` and `move` add nothing that was not
1109    /// already covered by the entry's own hash. A patch may not touch an
1110    /// existing blind.
1111    pub fn revision(
1112        target: GovernanceLogId,
1113        target_type: GovernanceLogEntryType,
1114        target_entry_hash: Sha256Hex,
1115        basis: impl Into<CommittedText>,
1116        note: impl Into<CommittedText>,
1117        latest: &serde_json::Value,
1118        edit: impl Into<Edit>,
1119    ) -> Result<(Self, serde_json::Value), ReviseError> {
1120        let Edit {
1121            mut patch,
1122            duplicates,
1123        } = edit.into();
1124        if !is_revisable(target_type) {
1125            return Err(ReviseError::NotRevisable(target_type));
1126        }
1127        if patch.is_empty() {
1128            return Err(ReviseError::EmptyPatch);
1129        }
1130        let blind_pointer = format!("/{BLIND_KEY}");
1131        for op in patch.iter() {
1132            let from = match op {
1133                json_patch::PatchOperation::Move(m) => Some(m.from.as_str()),
1134                json_patch::PatchOperation::Copy(c) => Some(c.from.as_str()),
1135                _ => None,
1136            };
1137            for pointer in
1138                [Some(op.path().as_str()), from].into_iter().flatten()
1139            {
1140                if overlaps(pointer, &blind_pointer) {
1141                    return Err(ReviseError::BlindPointer(pointer.to_string()));
1142                }
1143            }
1144        }
1145        let adds = patch.iter().any(|op| {
1146            matches!(
1147                op,
1148                json_patch::PatchOperation::Add(_)
1149                    | json_patch::PatchOperation::Replace(_)
1150            )
1151        });
1152        if adds && latest.is_object() && latest.get(BLIND_KEY).is_none() {
1153            patch.0.push(json_patch::PatchOperation::Add(
1154                json_patch::AddOperation {
1155                    path: json_patch::jsonptr::PointerBuf::from_tokens([
1156                        BLIND_KEY,
1157                    ]),
1158                    value: Blind::random().to_hex().into(),
1159                },
1160            ));
1161        }
1162        let edit = Edit { patch, duplicates };
1163        check_duplicates(latest, &edit)?;
1164        let Edit { patch, duplicates } = edit;
1165        let revised = apply_patch(latest, &patch)?;
1166        let (basis, note) = (basis.into(), note.into());
1167        Ok((
1168            Self {
1169                amendment: Amendment {
1170                    agora_governance_amendment: AMENDMENT_VERSION,
1171                    target,
1172                    target_entry_hash,
1173                    kind: AmendmentKind::Revision,
1174                    authority: None,
1175                    basis: AmendmentText::Committed(basis.commitment()),
1176                    note: AmendmentText::Committed(note.commitment()),
1177                    rationale: None,
1178                    redaction: None,
1179                    revision: Some(Revision {
1180                        patch,
1181                        duplicates,
1182                        resulting_data_hash: data_hash(&revised),
1183                    }),
1184                    update: None,
1185                },
1186                texts: AmendmentTexts {
1187                    basis: Some(basis),
1188                    note: Some(note),
1189                    rationale: None,
1190                },
1191            },
1192            revised,
1193        ))
1194    }
1195
1196    /// An amendment to an obligation, of the kind `update` is
1197    pub fn obligation(
1198        target: GovernanceLogId,
1199        target_entry_hash: Sha256Hex,
1200        update: ObligationUpdate,
1201        basis: impl Into<CommittedText>,
1202        note: impl Into<CommittedText>,
1203    ) -> Self {
1204        let (basis, note) = (basis.into(), note.into());
1205        Self {
1206            amendment: Amendment {
1207                agora_governance_amendment: AMENDMENT_VERSION,
1208                target,
1209                target_entry_hash,
1210                kind: update.kind(),
1211                authority: None,
1212                basis: AmendmentText::Committed(basis.commitment()),
1213                note: AmendmentText::Committed(note.commitment()),
1214                rationale: None,
1215                redaction: None,
1216                revision: None,
1217                update: Some(update),
1218            },
1219            texts: AmendmentTexts {
1220                basis: Some(basis),
1221                note: Some(note),
1222                rationale: None,
1223            },
1224        }
1225    }
1226
1227    /// The draft with the governance entry that authorizes it
1228    pub fn with_authority(mut self, authority: GovernanceLogId) -> Self {
1229        self.amendment.authority = Some(authority);
1230        self
1231    }
1232
1233    /// The draft with its [`rationale`](Amendment::rationale)
1234    pub fn with_rationale(
1235        mut self,
1236        rationale: impl Into<CommittedText>,
1237    ) -> Self {
1238        let rationale = rationale.into();
1239        self.amendment.rationale =
1240            Some(AmendmentText::Committed(rationale.commitment()));
1241        self.texts.rationale = Some(rationale);
1242        self
1243    }
1244}
1245
1246impl Amendment {
1247    /// Version, the shape of the texts for that version, and the
1248    /// redaction- and revision-shape invariants — everything checkable without the rest
1249    /// of the chain
1250    pub fn validate(&self) -> Result<(), AmendmentError> {
1251        let plain = match self.agora_governance_amendment {
1252            1 => true,
1253            AMENDMENT_VERSION => false,
1254            other => return Err(AmendmentError::UnsupportedVersion(other)),
1255        };
1256        let texts =
1257            [Some(&self.basis), Some(&self.note), self.rationale.as_ref()];
1258        if texts.into_iter().flatten().any(|t| t.is_plain() != plain) {
1259            return Err(AmendmentError::TextShape);
1260        }
1261        match (self.kind, &self.redaction) {
1262            (AmendmentKind::Redaction, None) => {
1263                return Err(AmendmentError::MissingRedaction);
1264            }
1265            (k, Some(_)) if k != AmendmentKind::Redaction => {
1266                return Err(AmendmentError::UnexpectedRedaction);
1267            }
1268            _ => {}
1269        }
1270        match (self.kind, &self.revision) {
1271            (AmendmentKind::Revision, None) => {
1272                return Err(AmendmentError::MissingRevision);
1273            }
1274            (AmendmentKind::Revision, Some(r)) => {
1275                r.validate().map_err(AmendmentError::RevisionShape)?;
1276            }
1277            (_, Some(_)) => return Err(AmendmentError::UnexpectedRevision),
1278            _ => {}
1279        }
1280        match (self.kind, &self.update) {
1281            (kind, None) if kind.is_obligation() => {
1282                Err(AmendmentError::MissingUpdate(kind))
1283            }
1284            (kind, Some(_)) if !kind.is_obligation() => {
1285                Err(AmendmentError::UnexpectedUpdate)
1286            }
1287            (kind, Some(update)) if update.kind() != kind => {
1288                Err(AmendmentError::UpdateMismatch {
1289                    kind,
1290                    update: update.kind(),
1291                })
1292            }
1293            _ => Ok(()),
1294        }
1295    }
1296
1297    /// Where each committed text stands given what is `beside` the entry;
1298    /// `None` for version 1, whose texts are in the signed `data`.
1299    ///
1300    /// A text that is beside the entry and is not the one committed to,
1301    /// or that the entry never committed to at all, is an error: someone
1302    /// put words next to a signed entry that the signer did not write.
1303    pub fn text_status(
1304        &self,
1305        beside: Option<&AmendmentTexts>,
1306    ) -> Result<Option<AmendmentTextStatus>, AmendmentError> {
1307        let empty = AmendmentTexts::default();
1308        let beside = beside.unwrap_or(&empty);
1309        let (Some(basis), Some(note)) = (
1310            self.basis.status(beside.basis.as_ref()),
1311            self.note.status(beside.note.as_ref()),
1312        ) else {
1313            return if beside.is_empty() {
1314                Ok(None)
1315            } else {
1316                Err(AmendmentError::UncommittedText)
1317            };
1318        };
1319        let rationale = match (&self.rationale, &beside.rationale) {
1320            (None, Some(_)) => return Err(AmendmentError::UncommittedText),
1321            (None, None) => None,
1322            (Some(text), beside) => text.status(beside.as_ref()),
1323        };
1324        for (name, status) in [
1325            ("basis", Some(basis)),
1326            ("note", Some(note)),
1327            ("rationale", rationale),
1328        ] {
1329            if status == Some(TextStatus::Mismatch) {
1330                return Err(AmendmentError::TextMismatch(name));
1331            }
1332        }
1333        Ok(Some(AmendmentTextStatus {
1334            basis,
1335            note,
1336            rationale,
1337        }))
1338    }
1339}
1340
1341/// What an amendment does to the [`Standing`] of the entry it names, when
1342/// it changes it at all
1343pub fn kind_standing(kind: AmendmentKind) -> Option<Standing> {
1344    match kind {
1345        AmendmentKind::NonPrecedential => Some(Standing::NonPrecedential),
1346        AmendmentKind::Overruled => Some(Standing::Overruled),
1347        AmendmentKind::Superseded => Some(Standing::Superseded),
1348        AmendmentKind::Reinstated => Some(Standing::InForce),
1349        AmendmentKind::Correction
1350        | AmendmentKind::Redaction
1351        | AmendmentKind::Reattested
1352        | AmendmentKind::Revision
1353        | AmendmentKind::Planned
1354        | AmendmentKind::Discharged
1355        | AmendmentKind::Lapsed
1356        | AmendmentKind::Rescheduled
1357        | AmendmentKind::Withdrawn => None,
1358    }
1359}
1360
1361/// The [`Standing`] conferred by `kinds` — the amendments naming one entry,
1362/// in chain order. The last one that changes standing wins.
1363pub fn standing(kinds: impl IntoIterator<Item = AmendmentKind>) -> Standing {
1364    kinds
1365        .into_iter()
1366        .filter_map(kind_standing)
1367        .last()
1368        .unwrap_or_default()
1369}
1370
1371/// The top-level key of a redactable entry's `data` that holds its
1372/// [`Blind`]
1373pub const BLIND_KEY: &str = "_blind";
1374
1375/// Whether entries of this type can be redacted, and so carry a [`Blind`].
1376/// Amendments and key rotations cannot: verifiers read their `data`, and a
1377/// chain whose own corrections can be edited proves nothing.
1378pub fn is_redactable(entry_type: GovernanceLogEntryType) -> bool {
1379    !matches!(
1380        entry_type,
1381        GovernanceLogEntryType::Amendment | GovernanceLogEntryType::KeyRotation
1382    )
1383}
1384
1385/// `data` cannot be blinded
1386#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1387pub enum BlindError {
1388    #[error("a redactable entry's data must be a JSON object")]
1389    NotAnObject,
1390    #[error(
1391        "data already has a {BLIND_KEY:?} key; the writer supplies it, not the caller"
1392    )]
1393    AlreadyBlinded,
1394    #[error(
1395        "{0:?} is a number that is not a 64-bit integer; governance data \
1396         never contains one (put a fraction in a string)"
1397    )]
1398    NonIntegerNumber(String),
1399}
1400
1401/// `data` with a [`Blind`] under [`BLIND_KEY`] — what a writer signs and
1402/// stores for every [redactable](is_redactable) entry.
1403///
1404/// An entry's `data_hash` is public and permanent: the chain cannot verify
1405/// without it. After a redaction everything in `data` *except* the removed
1406/// values is public too, so without a blind anyone could test a guess at a
1407/// removed value — a name, a handle — by putting it back and hashing. The
1408/// blind is 256 bits of the preimage that [`redact_data`] replaces along
1409/// with the values, so the old hash can no longer be reproduced by anyone
1410/// who did not already hold the unredacted entry. It is not a secret while
1411/// the entry is whole, and it is not part of the envelope: verifiers hash
1412/// `data` as they always did.
1413///
1414/// Entries written before blinding existed have none. Their first
1415/// redaction is only as safe as the removed values are hard to guess
1416/// (redact the enclosing value when in doubt); it leaves a blind behind,
1417/// so later ones are protected.
1418pub fn blind_data(
1419    data: &serde_json::Value,
1420    blind: Blind,
1421) -> Result<serde_json::Value, BlindError> {
1422    if let Some(pointer) = non_integer_number(data) {
1423        return Err(BlindError::NonIntegerNumber(pointer));
1424    }
1425    let mut out = data.clone();
1426    let object = out.as_object_mut().ok_or(BlindError::NotAnObject)?;
1427    if object.contains_key(BLIND_KEY) {
1428        return Err(BlindError::AlreadyBlinded);
1429    }
1430    object.insert(BLIND_KEY.to_string(), blind.to_hex().into());
1431    Ok(out)
1432}
1433
1434/// A [`Redaction`] cannot be applied as asked
1435#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1436pub enum RedactError {
1437    #[error("pointer {0:?} does not resolve in the entry's data")]
1438    Unresolved(String),
1439    #[error("the empty pointer would redact the whole entry")]
1440    WholeEntry,
1441    #[error("a redaction names at least one pointer")]
1442    NoFields,
1443    #[error("{0:?} is the entry's blind; every redaction replaces it already")]
1444    BlindPointer(String),
1445    #[error("a revision no longer applies to the redacted data: {0}")]
1446    Rebase(String),
1447    #[error(
1448        "{0:?} is a number that is not a 64-bit integer; governance data \
1449         never contains one"
1450    )]
1451    NonIntegerNumber(String),
1452}
1453
1454/// The marker a redaction leaves in place of a value
1455pub fn redaction_marker(amendment_id: &GovernanceLogId) -> String {
1456    format!("[redacted by {amendment_id}]")
1457}
1458
1459/// `data` with the value at each RFC 6901 pointer in `fields` replaced by
1460/// [`redaction_marker`].
1461///
1462/// Whole-value replacement only: a redaction tool that can write arbitrary
1463/// replacement prose is a rewrite tool. The server and every verifier share
1464/// this one definition, because what it returns is what the target's
1465/// `resulting_data_hash` covers.
1466///
1467/// The entry's [`Blind`] is replaced by `blind` — a fresh one, not a
1468/// marker. Destroying the old value is what stops a removed value being
1469/// confirmed against the entry's original `data_hash` (see [`blind_data`]);
1470/// leaving a *new* one is what protects the next redaction of the same
1471/// entry, whose removed values could otherwise be tested against this
1472/// one's public `resulting_data_hash`. An entry that predates blinding
1473/// gains one here. `blind` must be [random](Blind::random) outside tests.
1474pub fn redact_data(
1475    data: &serde_json::Value,
1476    fields: &[String],
1477    amendment_id: &GovernanceLogId,
1478    blind: Blind,
1479) -> Result<serde_json::Value, RedactError> {
1480    if fields.is_empty() {
1481        return Err(RedactError::NoFields);
1482    }
1483    if let Some(pointer) = non_integer_number(data) {
1484        return Err(RedactError::NonIntegerNumber(pointer));
1485    }
1486    let blind_pointer = format!("/{BLIND_KEY}");
1487    let marker = serde_json::Value::String(redaction_marker(amendment_id));
1488    let mut out = data.clone();
1489    for pointer in fields {
1490        if pointer.is_empty() {
1491            return Err(RedactError::WholeEntry);
1492        }
1493        if *pointer == blind_pointer {
1494            return Err(RedactError::BlindPointer(pointer.clone()));
1495        }
1496        let slot = out
1497            .pointer_mut(pointer)
1498            .ok_or_else(|| RedactError::Unresolved(pointer.clone()))?;
1499        *slot = marker.clone();
1500    }
1501    // Every entry a writer has produced is an object. Anything else has
1502    // nowhere to keep a blind, and staying redactable matters more.
1503    if let Some(object) = out.as_object_mut() {
1504        object.insert(BLIND_KEY.to_string(), blind.to_hex().into());
1505    }
1506    Ok(out)
1507}
1508
1509/// Whether entries of this type can be revised. Those that cannot be
1510/// [redacted](is_redactable) cannot, nor can a [`StewardRecord`]: it is
1511/// the disclosure of what was done to the others.
1512pub fn is_revisable(entry_type: GovernanceLogEntryType) -> bool {
1513    is_redactable(entry_type)
1514        && entry_type != GovernanceLogEntryType::StewardRecord
1515}
1516
1517/// A [`Revision`] cannot be made or applied as asked
1518#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1519pub enum ReviseError {
1520    #[error("a revision's patch has at least one op")]
1521    EmptyPatch,
1522    #[error("the patch does not apply: {0}")]
1523    Patch(String),
1524    #[error("{0:?} is the entry's blind, which a revision never touches")]
1525    BlindPointer(String),
1526    #[error("pointer {0:?} does not resolve")]
1527    Unresolved(String),
1528    #[error("{path:?} is not byte-identical to {same_as:?}")]
1529    NotIdentical { path: String, same_as: String },
1530    #[error("{same_as:?}, which {path:?} duplicates, has to survive the patch")]
1531    SourceRemoved { path: String, same_as: String },
1532    #[error("{0:?} is listed as a duplicate the patch does not remove")]
1533    NotRemoved(String),
1534    #[error("{0} entries are never revised")]
1535    NotRevisable(GovernanceLogEntryType),
1536    #[error(
1537        "{0:?} is a number that is not a 64-bit integer; governance data \
1538         never contains one"
1539    )]
1540    NonIntegerNumber(String),
1541}
1542
1543/// `a` and `b` name the same value, or one lies inside the other
1544fn overlaps(a: &str, b: &str) -> bool {
1545    let within = |inner: &str, outer: &str| {
1546        inner
1547            .strip_prefix(outer)
1548            .is_some_and(|rest| rest.is_empty() || rest.starts_with('/'))
1549    };
1550    within(a, b) || within(b, a)
1551}
1552
1553/// `data` with `patch` applied, as the `json-patch` crate applies RFC 6902
1554pub fn apply_patch(
1555    data: &serde_json::Value,
1556    patch: &json_patch::Patch,
1557) -> Result<serde_json::Value, ReviseError> {
1558    let mut out = data.clone();
1559    json_patch::patch(&mut out, patch)
1560        .map_err(|e| ReviseError::Patch(e.to_string()))?;
1561    if let Some(pointer) = non_integer_number(&out) {
1562        return Err(ReviseError::NonIntegerNumber(pointer));
1563    }
1564    Ok(out)
1565}
1566
1567/// An entry's latest version: its stored `data` with each revision's
1568/// patch applied in chain order. A redaction rebases them (see
1569/// [`AmendmentDraft::redaction`]), so an error means the history is
1570/// broken, not that a patch is out of date.
1571pub fn latest<'a>(
1572    data: &serde_json::Value,
1573    revisions: impl IntoIterator<Item = &'a Revision>,
1574) -> Result<serde_json::Value, ReviseError> {
1575    let mut current = data.clone();
1576    for revision in revisions {
1577        current = apply_patch(&current, &revision.patch)?;
1578    }
1579    Ok(current)
1580}
1581
1582/// The paths `revisions` removed as duplicates of a value in `fields` (or
1583/// of one inside it, or holding it): what a redaction of `fields` must
1584/// also erase from the stored original
1585fn duplicates_of(fields: &[String], revisions: &[&Revision]) -> Vec<String> {
1586    let mut extra = Vec::new();
1587    for (removed, same_as) in revisions.iter().flat_map(|r| &r.duplicates) {
1588        for field in fields {
1589            if let Some(rest) = field.strip_prefix(same_as.as_str())
1590                && (rest.is_empty() || rest.starts_with('/'))
1591            {
1592                // Part of the source: the same part of the copy.
1593                extra.push(format!("{removed}{rest}"));
1594            } else if overlaps(field, same_as) {
1595                // The whole source: the whole copy.
1596                extra.push(removed.clone());
1597            }
1598        }
1599    }
1600    extra
1601}
1602
1603/// The revisions of one entry, for
1604/// [`check_content`](GovernanceVerification::check_content)
1605#[derive(Debug, Clone, PartialEq, Eq, Default)]
1606pub struct RevisionHistory {
1607    /// In chain order
1608    pub revisions: Vec<(GovernanceLogId, Revision)>,
1609    /// How many of them came before the entry's last redaction, which
1610    /// rebased them: their own hashes describe unredacted history
1611    pub rebased: usize,
1612    /// What the last redaction says the rebased ones fold to
1613    pub rebased_hash: Option<Sha256Hex>,
1614}
1615
1616/// What a reader needs next to an amended entry
1617#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1618#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1619#[cfg_attr(feature = "schemars", schemars(inline))]
1620pub struct AmendmentNotice {
1621    pub id: GovernanceLogId,
1622    pub kind: AmendmentKind,
1623    #[serde(default)]
1624    pub authority: Option<GovernanceLogId>,
1625    pub basis: String,
1626    pub note: String,
1627    /// See [`Amendment::rationale`]
1628    #[serde(default, skip_serializing_if = "Option::is_none")]
1629    pub rationale: Option<String>,
1630    /// See [`Amendment::update`] (0.72)
1631    #[serde(default, skip_serializing_if = "Option::is_none")]
1632    pub update: Option<ObligationUpdate>,
1633    pub created_at: DateTime<Utc>,
1634}
1635
1636impl AmendmentNotice {
1637    /// The notice for `amendment`, appended as `id` at `created_at`
1638    /// A text no longer `beside` the entry reads [`WITHHELD_TEXT`]
1639    pub fn new(
1640        id: GovernanceLogId,
1641        created_at: DateTime<Utc>,
1642        amendment: &Amendment,
1643        beside: Option<&AmendmentTexts>,
1644    ) -> Self {
1645        let beside = beside.cloned().unwrap_or_default();
1646        Self {
1647            id,
1648            kind: amendment.kind,
1649            authority: amendment.authority.clone(),
1650            basis: amendment.basis.resolve(beside.basis.as_ref()).into(),
1651            note: amendment.note.resolve(beside.note.as_ref()).into(),
1652            rationale: amendment
1653                .rationale
1654                .as_ref()
1655                .map(|r| r.resolve(beside.rationale.as_ref()).into()),
1656            update: amendment.update.clone(),
1657            created_at,
1658        }
1659    }
1660}
1661
1662/// A later entry that names this one in its record, other than as an
1663/// amendment's target: a Steward's record about it, an amendment made under
1664/// its authority, a decision overruling it. What a reader of the entry
1665/// needs to know exists, so it is listed beside the entry rather than found
1666/// only by reading forward. (0.68)
1667#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1668#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1669#[cfg_attr(feature = "schemars", schemars(inline))]
1670pub struct GovernanceCitation {
1671    pub id: GovernanceLogId,
1672    pub entry_type: GovernanceLogEntryType,
1673    pub title: String,
1674    pub relation: CitationRelation,
1675    pub created_at: DateTime<Utc>,
1676}
1677
1678/// A citing field that does not parse: which relation it would have been
1679/// (0.87). Until 0.87 it named nothing, so the cited entry's `cited_by`
1680/// left the citing entry out without a word
1681#[derive(Debug, thiserror::Error)]
1682#[error("the record's {relation:?} field does not parse: {error}")]
1683pub struct MalformedCitation {
1684    /// The relation the field carries, for the caller that knows from
1685    /// elsewhere (a database match) that the entry does cite
1686    pub relation: CitationRelation,
1687    #[source]
1688    pub error: serde_json::Error,
1689}
1690
1691/// The entries `data` names, and how: the links a later entry of
1692/// `entry_type` makes to earlier ones, read from its record's own fields.
1693/// An amendment's `target` is left out (see [`CitationRelation`]). A field
1694/// that doesn't parse is a [`MalformedCitation`]. (0.68)
1695pub fn citations_in(
1696    entry_type: GovernanceLogEntryType,
1697    data: &serde_json::Value,
1698) -> Result<Vec<(GovernanceLogId, CitationRelation)>, MalformedCitation> {
1699    #[derive(Deserialize)]
1700    struct Concerns {
1701        #[serde(default)]
1702        concerns: Vec<GovernanceLogId>,
1703    }
1704    #[derive(Deserialize)]
1705    struct Authority {
1706        #[serde(default)]
1707        authority: Option<GovernanceLogId>,
1708    }
1709    #[derive(Deserialize)]
1710    struct Overrules {
1711        #[serde(default)]
1712        overrules: Vec<GovernanceLogId>,
1713    }
1714    fn each(
1715        ids: impl IntoIterator<Item = GovernanceLogId>,
1716        relation: CitationRelation,
1717    ) -> Vec<(GovernanceLogId, CitationRelation)> {
1718        ids.into_iter().map(|id| (id, relation)).collect()
1719    }
1720    let malformed =
1721        |relation| move |error| MalformedCitation { relation, error };
1722    match entry_type {
1723        GovernanceLogEntryType::StewardRecord => {
1724            serde_json::from_value::<Concerns>(data.clone())
1725                .map(|c| each(c.concerns, CitationRelation::Concerns))
1726                .map_err(malformed(CitationRelation::Concerns))
1727        }
1728        GovernanceLogEntryType::Amendment => {
1729            serde_json::from_value::<Authority>(data.clone())
1730                .map(|a| each(a.authority, CitationRelation::Authority))
1731                .map_err(malformed(CitationRelation::Authority))
1732        }
1733        GovernanceLogEntryType::CouncilDecision => {
1734            serde_json::from_value::<Overrules>(data.clone())
1735                .map(|o| each(o.overrules, CitationRelation::Overrules))
1736                .map_err(malformed(CitationRelation::Overrules))
1737        }
1738        _ => Ok(Vec::new()),
1739    }
1740}
1741
1742// ---------------------------------------------------------------------------
1743// Key rotation
1744// ---------------------------------------------------------------------------
1745
1746/// The [`KeyRotation`] payload version this module produces and verifies
1747pub const KEY_ROTATION_VERSION: u32 = 2;
1748
1749/// The key the chain started under, as this build of agentkit knows it.
1750///
1751/// Frozen. It predates the root keys, so until the chain's first rotation
1752/// carries its retroactive [`KeyCertificate`] this list is the only
1753/// second channel a verifier has for it; every later key is certified by
1754/// [`ROOT_KEYS`] instead and never appears here.
1755pub const PUBLISHED_KEYS: &[&str] =
1756    &["ebb3091dd328f1463362c171121921b2fe14628e3fc4c145deaccefb85c0e78a"];
1757
1758/// Why the key changed
1759#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1760#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1761#[cfg_attr(feature = "schemars", schemars(inline))]
1762#[serde(rename_all = "snake_case")]
1763pub enum RotationReason {
1764    // Scheduled or voluntary; the old key signed the rotation itself.
1765    Routine,
1766    // The old key is in someone else's hands; the new key signed the
1767    // rotation.
1768    Compromise,
1769}
1770
1771/// The last entry the compromised key is trusted for
1772#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1773#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1774#[cfg_attr(feature = "schemars", schemars(inline))]
1775#[serde(deny_unknown_fields)]
1776pub struct TrustedHead {
1777    pub id: GovernanceLogId,
1778    pub chain_seq: u64,
1779    pub entry_hash: Sha256Hex,
1780}
1781
1782/// A rotation is malformed, unauthenticated, or inconsistent with the chain
1783#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1784pub enum RotationError {
1785    #[error("agora_governance_key_rotation is {0}, not {KEY_ROTATION_VERSION}")]
1786    UnsupportedVersion(u32),
1787    #[error("new_key is not a valid Ed25519 public key")]
1788    BadNewKey,
1789    #[error(
1790        "the proof of possession does not verify for this rotation at this position"
1791    )]
1792    BadProof,
1793    #[error("a compromise certificate must name last_trusted")]
1794    MissingLastTrusted,
1795    #[error("certificate: {0}")]
1796    Certificate(#[from] CertificateError),
1797    #[error("outgoing_certificate: {0}")]
1798    OutgoingCertificate(CertificateError),
1799    #[error(
1800        "the chain's first rotation must carry the genesis key's \
1801         outgoing_certificate"
1802    )]
1803    MissingGenesisCertificate,
1804    #[error(
1805        "outgoing_certificate belongs on the chain's first rotation and \
1806         nowhere else"
1807    )]
1808    UnexpectedOutgoingCertificate,
1809    #[error("old_key is not the key that was in force")]
1810    WrongOldKey,
1811    #[error("last_trusted does not name an earlier entry of this chain")]
1812    UnknownLastTrusted,
1813    #[error("new_key has already held this chain; a key is never brought back")]
1814    ReusedKey,
1815    #[error("last_trusted names an entry an earlier compromise repudiated")]
1816    RepudiatedLastTrusted,
1817}
1818
1819/// The `data` of a `key_rotation` entry.
1820///
1821/// Build one with [`routine`](Self::routine) or
1822/// [`compromise`](Self::compromise): both compute the proof of possession.
1823/// The `certificate` is what authenticates the change; the proof only
1824/// shows the certified key is one somebody holds.
1825///
1826/// No free text, and unknown fields are refused: a rotation can never be
1827/// redacted, so it carries nothing anyone could need erased. Narrative
1828/// belongs in a separate, redactable entry.
1829#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1830#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1831#[cfg_attr(feature = "schemars", schemars(inline))]
1832#[serde(deny_unknown_fields)]
1833pub struct KeyRotation {
1834    /// Always [`KEY_ROTATION_VERSION`]
1835    pub agora_governance_key_rotation: u32,
1836    pub reason: RotationReason,
1837    pub old_key: PublicKeyHex,
1838    pub new_key: PublicKeyHex,
1839    /// `crypto::sign(new_key, ` [`RotationStatement::hash`] `,
1840    /// proof_signed_at)` — the new key signing for itself, at one position
1841    /// in one chain
1842    pub proof: SignatureHex,
1843    /// Unix seconds; what the proof signature covers
1844    pub proof_signed_at: i64,
1845    /// The root's word that `new_key` holds the chain from here. For a
1846    /// compromise its statement also names the last entry trusted under
1847    /// `old_key`.
1848    pub certificate: KeyCertificate,
1849    /// The [`CertPurpose::Genesis`] certificate for the key the chain
1850    /// started under, which predates the root: on the chain's first
1851    /// rotation, and only there.
1852    #[serde(default, skip_serializing_if = "Option::is_none")]
1853    pub outgoing_certificate: Option<KeyCertificate>,
1854}
1855
1856/// What the proof of possession signs
1857///
1858/// A struct rather than a [`serde_json::Value`] for the same reason as
1859/// [`Envelope`]: declaration-ordered serialization whatever `preserve_order`
1860/// says. `prev_hash` is the rotation entry's own, so a proof lifted out of
1861/// one chain position does not verify in another.
1862#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1863pub struct RotationStatement {
1864    /// Always [`KEY_ROTATION_VERSION`]
1865    pub agora_governance_key_rotation: u32,
1866    pub reason: RotationReason,
1867    pub old_key: PublicKeyHex,
1868    pub new_key: PublicKeyHex,
1869    pub prev_hash: Option<Sha256Hex>,
1870}
1871
1872impl RotationStatement {
1873    /// The statement for a rotation at the position named by `prev_hash`
1874    pub fn new(
1875        reason: RotationReason,
1876        old_key: PublicKeyHex,
1877        new_key: PublicKeyHex,
1878        prev_hash: Option<Sha256Hex>,
1879    ) -> Self {
1880        Self {
1881            agora_governance_key_rotation: KEY_ROTATION_VERSION,
1882            reason,
1883            old_key,
1884            new_key,
1885            prev_hash,
1886        }
1887    }
1888
1889    /// The bytes that are hashed
1890    pub fn preimage(&self) -> Vec<u8> {
1891        serde_json::to_vec(self).expect("a RotationStatement always serializes")
1892    }
1893
1894    /// SHA-256 over [`preimage`](Self::preimage) — what the proof covers
1895    pub fn hash(&self) -> Sha256Hex {
1896        Sha256Hex(Sha256::digest(self.preimage()).into())
1897    }
1898}
1899
1900impl KeyRotation {
1901    /// A scheduled rotation from `old_key` to `new_signing_key`.
1902    ///
1903    /// The entry itself is signed by the **old** key; entries after it
1904    /// verify under the new one. `prev_hash` is the rotation entry's own,
1905    /// and `certificate` is over [`KeyCertStatement::routine`] at it.
1906    pub fn routine(
1907        old_key: PublicKeyHex,
1908        new_signing_key: &SigningKey,
1909        prev_hash: Option<Sha256Hex>,
1910        now: DateTime<Utc>,
1911        certificate: KeyCertificate,
1912    ) -> Self {
1913        Self::build(
1914            RotationReason::Routine,
1915            old_key,
1916            new_signing_key,
1917            prev_hash,
1918            now,
1919            certificate,
1920        )
1921    }
1922
1923    /// A declaration that `old_key` is compromised, trusted only through
1924    /// the `last_trusted` its `certificate` names.
1925    ///
1926    /// The entry is signed by the **new** key — the old one proves nothing
1927    /// any more. `last_trusted` must name an entry from before any earlier
1928    /// compromise window; a reattestation inside one restores the entry,
1929    /// not the ability to anchor trust there.
1930    pub fn compromise(
1931        old_key: PublicKeyHex,
1932        new_signing_key: &SigningKey,
1933        prev_hash: Option<Sha256Hex>,
1934        now: DateTime<Utc>,
1935        certificate: KeyCertificate,
1936    ) -> Self {
1937        Self::build(
1938            RotationReason::Compromise,
1939            old_key,
1940            new_signing_key,
1941            prev_hash,
1942            now,
1943            certificate,
1944        )
1945    }
1946
1947    fn build(
1948        reason: RotationReason,
1949        old_key: PublicKeyHex,
1950        new_signing_key: &SigningKey,
1951        prev_hash: Option<Sha256Hex>,
1952        now: DateTime<Utc>,
1953        certificate: KeyCertificate,
1954    ) -> Self {
1955        let new_key = PublicKeyHex::from(&new_signing_key.verifying_key());
1956        let proof_signed_at = truncate_to_seconds(now).timestamp();
1957        let statement =
1958            RotationStatement::new(reason, old_key, new_key, prev_hash);
1959        let proof = crypto::sign(
1960            new_signing_key,
1961            statement.hash().as_bytes(),
1962            proof_signed_at,
1963        );
1964        Self {
1965            agora_governance_key_rotation: KEY_ROTATION_VERSION,
1966            reason,
1967            old_key,
1968            new_key,
1969            proof: proof.into(),
1970            proof_signed_at,
1971            certificate,
1972            outgoing_certificate: None,
1973        }
1974    }
1975
1976    /// This rotation, carrying the genesis key's retroactive certificate
1977    pub fn with_outgoing(mut self, certificate: KeyCertificate) -> Self {
1978        self.outgoing_certificate = Some(certificate);
1979        self
1980    }
1981
1982    /// The statement this rotation's proof covers, at `prev_hash`
1983    pub fn statement(&self, prev_hash: Option<Sha256Hex>) -> RotationStatement {
1984        RotationStatement::new(
1985            self.reason,
1986            self.old_key,
1987            self.new_key,
1988            prev_hash,
1989        )
1990    }
1991
1992    /// Compromise only: the last entry trusted under `old_key`, as the
1993    /// root certified it
1994    pub fn last_trusted(&self) -> Option<&TrustedHead> {
1995        self.certificate.statement.last_trusted.as_ref()
1996    }
1997
1998    /// What `certificate` must say for this rotation, appended at `seq`
1999    /// with `prev_hash`, to be authentic.
2000    ///
2001    /// Derived from the chain. Only `last_trusted` is taken from the
2002    /// certificate, because only the root can say it.
2003    pub fn expected_statement(
2004        &self,
2005        seq: u64,
2006        prev_hash: Option<Sha256Hex>,
2007    ) -> Result<KeyCertStatement, RotationError> {
2008        match self.reason {
2009            RotationReason::Routine => {
2010                Ok(KeyCertStatement::routine(self.new_key, seq, prev_hash))
2011            }
2012            RotationReason::Compromise => Ok(KeyCertStatement::compromise(
2013                self.new_key,
2014                seq,
2015                prev_hash,
2016                self.last_trusted()
2017                    .cloned()
2018                    .ok_or(RotationError::MissingLastTrusted)?,
2019            )),
2020        }
2021    }
2022
2023    /// Version and the proof of possession at the position `prev_hash`
2024    /// names
2025    pub fn verify_proof(
2026        &self,
2027        prev_hash: Option<Sha256Hex>,
2028    ) -> Result<(), RotationError> {
2029        if self.agora_governance_key_rotation != KEY_ROTATION_VERSION {
2030            return Err(RotationError::UnsupportedVersion(
2031                self.agora_governance_key_rotation,
2032            ));
2033        }
2034        let new_key = self
2035            .new_key
2036            .to_verifying_key()
2037            .map_err(|_| RotationError::BadNewKey)?;
2038        crypto::verify(
2039            &new_key,
2040            self.statement(prev_hash).hash().as_bytes(),
2041            self.proof_signed_at,
2042            &Signature::from(&self.proof),
2043        )
2044        .then_some(())
2045        .ok_or(RotationError::BadProof)
2046    }
2047
2048    /// [`verify_proof`](Self::verify_proof), and `certificate` is the
2049    /// root's for this key at this position — everything checkable
2050    /// without the rest of the chain
2051    pub fn verify_certified(
2052        &self,
2053        seq: u64,
2054        prev_hash: Option<Sha256Hex>,
2055        roots: &RootSet,
2056    ) -> Result<(), RotationError> {
2057        self.verify_proof(prev_hash)?;
2058        let expected = self.expected_statement(seq, prev_hash)?;
2059        Ok(self.certificate.verify_for(&expected, roots)?)
2060    }
2061}
2062
2063/// The genesis keys a verifier trusts out of band.
2064///
2065/// Only the key the chain started under needs one: every later key is
2066/// certified by the [`RootSet`]. [`published`](Self::published) is this
2067/// build's [`PUBLISHED_KEYS`]; [`pinned`](Self::pinned) is the key a
2068/// client saw first and kept.
2069#[derive(Debug, Clone, Default, PartialEq, Eq)]
2070pub struct KeyAnchor {
2071    keys: HashSet<PublicKeyHex>,
2072}
2073
2074impl KeyAnchor {
2075    /// The keys compiled into this build of agentkit
2076    pub fn published() -> Self {
2077        PUBLISHED_KEYS
2078            .iter()
2079            .map(|k| {
2080                k.parse()
2081                    .expect("PUBLISHED_KEYS are valid 32-byte hex keys")
2082            })
2083            .collect()
2084    }
2085
2086    /// Just the one key, as a client that pinned what it saw first trusts it
2087    pub fn pinned(key: PublicKeyHex) -> Self {
2088        std::iter::once(key).collect()
2089    }
2090
2091    /// This anchor, plus `key`
2092    pub fn with(mut self, key: PublicKeyHex) -> Self {
2093        self.keys.insert(key);
2094        self
2095    }
2096
2097    pub fn contains(&self, key: &PublicKeyHex) -> bool {
2098        self.keys.contains(key)
2099    }
2100
2101    pub fn is_empty(&self) -> bool {
2102        self.keys.is_empty()
2103    }
2104
2105    /// The anchored keys, in no particular order
2106    pub fn keys(&self) -> impl Iterator<Item = &PublicKeyHex> {
2107        self.keys.iter()
2108    }
2109}
2110
2111impl FromIterator<PublicKeyHex> for KeyAnchor {
2112    fn from_iter<I: IntoIterator<Item = PublicKeyHex>>(iter: I) -> Self {
2113        Self {
2114            keys: iter.into_iter().collect(),
2115        }
2116    }
2117}
2118
2119/// One key's span of the chain, as [`verify_chain`] derives it and
2120/// `GET /api/governance/signing-keys` publishes it
2121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2122#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2123#[cfg_attr(feature = "schemars", schemars(inline))]
2124pub struct GovernanceKeyRecord {
2125    pub public_key: PublicKeyHex,
2126    /// The first `chain_seq` this key signed
2127    pub from_seq: u64,
2128    /// The last `chain_seq` this key is trusted for; `null` while active.
2129    /// For a compromised key this is `last_trusted`, not the seq at which
2130    /// the compromise was declared.
2131    #[serde(default)]
2132    pub through_seq: Option<u64>,
2133    pub status: KeyStatus,
2134    /// The rotation entry that introduced this key; `null` for the genesis
2135    /// key, which predates the chain
2136    #[serde(default)]
2137    pub introduced_by: Option<GovernanceLogId>,
2138    /// The rotation entry that ended this key's span
2139    #[serde(default)]
2140    pub retired_by: Option<GovernanceLogId>,
2141    /// A [`KeyCertificate`] from the root vouches for this key. `false`
2142    /// only for a genesis key whose retroactive certificate the chain does
2143    /// not carry yet.
2144    #[serde(default)]
2145    pub certified: bool,
2146}
2147
2148/// The signing key history as `GET /api/governance/signing-keys` returns it
2149///
2150/// An object rather than a bare array: MCP structured content needs a
2151/// top-level object.
2152#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2153#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2154#[cfg_attr(feature = "schemars", schemars(inline))]
2155pub struct GovernanceSigningKeys {
2156    /// Oldest first
2157    pub keys: Vec<GovernanceKeyRecord>,
2158}
2159
2160/// The verdict on one entry
2161#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2162#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2163#[cfg_attr(feature = "schemars", schemars(inline))]
2164pub struct EntryVerdict {
2165    pub id: GovernanceLogId,
2166    pub chain_seq: u64,
2167    /// The signature verifies over `entry_hash` and `signed_at` under the
2168    /// published key
2169    pub signature_valid: bool,
2170    /// `entry_hash` recomputes from the envelope fields, `prev_hash` is the
2171    /// previous entry's `entry_hash`, and `chain_seq` is contiguous
2172    pub link_valid: bool,
2173    /// The entry's stored `data` hashes to the attested `data_hash` — or,
2174    /// when a redaction names the entry, to the redaction's
2175    /// `resulting_data_hash` — or `data` is the entry's
2176    /// [latest](Self::latest_data_hash) version. `null` when the
2177    /// verifier did not read `data`. `false` with a clean chain means the
2178    /// content was changed after attestation and no amendment says so.
2179    #[serde(default)]
2180    pub content_matches: Option<bool>,
2181    /// See [`GovernanceAttestation::retroactive`]
2182    pub retroactive: bool,
2183    /// `created_at` is earlier than the previous link's. Informational:
2184    /// chain order is what is attested, and a clock step does not break it.
2185    pub out_of_order: bool,
2186    /// Amendment entries that name this one
2187    #[serde(default)]
2188    pub amended_by: Vec<GovernanceLogId>,
2189    /// The key the signature was checked under — the one in force at this
2190    /// position, or for a compromise declaration the certified new key
2191    #[serde(default, skip_serializing_if = "Option::is_none")]
2192    pub signed_by: Option<PublicKeyHex>,
2193    /// A redaction amendment names this entry, so its `data` has lawfully
2194    /// changed since it was attested
2195    #[serde(default)]
2196    pub redacted: bool,
2197    /// What the entry's `data` must hash to now, when it has been redacted
2198    #[serde(default, skip_serializing_if = "Option::is_none")]
2199    pub redacted_data_hash: Option<Sha256Hex>,
2200    /// Revision amendments naming this entry, in chain order: its latest
2201    /// version is its stored `data` with each one's patch applied (0.43)
2202    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2203    pub revisions: Vec<GovernanceLogId>,
2204    /// What the entry's [`latest`] version hashes to: the last revision's
2205    /// `resulting_data_hash`, or a later redaction's
2206    /// `resulting_latest_hash` (0.43)
2207    #[serde(default, skip_serializing_if = "Option::is_none")]
2208    pub latest_data_hash: Option<Sha256Hex>,
2209    /// Revisions whose own `resulting_data_hash` a later redaction
2210    /// superseded: they describe the unredacted history, and the
2211    /// redaction's `resulting_latest_hash` is checked instead. Reported,
2212    /// not a failure. (0.43)
2213    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2214    pub superseded_revisions: Vec<GovernanceLogId>,
2215    /// What [`check_content`](GovernanceVerification::check_content)
2216    /// folds. Not on the wire: the amendments carry it.
2217    #[serde(skip)]
2218    pub history: RevisionHistory,
2219    /// Signed inside a compromise window and not reattested: the key
2220    /// holder of record disclaims it
2221    #[serde(default)]
2222    pub repudiated: bool,
2223    /// [`AmendmentKind::Reattested`] amendments vouching for this entry
2224    /// under a later, trusted key
2225    #[serde(default)]
2226    pub reattested_by: Vec<GovernanceLogId>,
2227    /// A version 2 amendment's texts: each beside the entry and matching
2228    /// what it committed to, or withheld. A text that does not match is a
2229    /// `problem`.
2230    #[serde(default, skip_serializing_if = "Option::is_none")]
2231    pub texts: Option<AmendmentTextStatus>,
2232    /// What failed, when something did
2233    #[serde(default, skip_serializing_if = "Option::is_none")]
2234    pub problem: Option<String>,
2235}
2236
2237impl EntryVerdict {
2238    /// Whether `data` is the stored content or the latest version. For the
2239    /// stored content, every revision is folded over it and checked; one
2240    /// that does not apply or produce its hash is a `problem`.
2241    fn content_check(
2242        &mut self,
2243        attested: Sha256Hex,
2244        data: &serde_json::Value,
2245    ) -> bool {
2246        // Never hashed: see `non_integer_number`.
2247        if non_integer_number(data).is_some() {
2248            return false;
2249        }
2250        let hash = data_hash(data);
2251        if hash == attested && self.redacted {
2252            // A copy from before the redaction: authentic, and nothing
2253            // later was built on it.
2254            return true;
2255        }
2256        if hash != attested && Some(hash) != self.redacted_data_hash {
2257            return Some(hash) == self.latest_data_hash;
2258        }
2259        let history = &self.history;
2260        let mut current = data.clone();
2261        let mut failures = Vec::new();
2262        for (i, (id, revision)) in history.revisions.iter().enumerate() {
2263            if i == history.rebased {
2264                break;
2265            }
2266            if let Some(failure) = false_duplicate(id, revision, &current) {
2267                failures.push(failure);
2268                break;
2269            }
2270            match apply_patch(&current, &revision.patch) {
2271                Ok(next) => current = next,
2272                Err(_) => {
2273                    failures.push(format!(
2274                        "revision {id} does not apply to the redacted data"
2275                    ));
2276                    break;
2277                }
2278            }
2279        }
2280        if failures.is_empty()
2281            && let Some(expected) = history.rebased_hash
2282            && data_hash(&current) != expected
2283        {
2284            failures.push(
2285                "the revisions rebased over the redacted data do not \
2286                 produce the redaction's resulting_latest_hash"
2287                    .to_string(),
2288            );
2289        }
2290        for (id, revision) in history.revisions.iter().skip(history.rebased) {
2291            if !failures.is_empty() {
2292                break;
2293            }
2294            if let Some(failure) = false_duplicate(id, revision, &current) {
2295                failures.push(failure);
2296                break;
2297            }
2298            match apply_patch(&current, &revision.patch) {
2299                Ok(next)
2300                    if data_hash(&next) == revision.resulting_data_hash =>
2301                {
2302                    current = next;
2303                }
2304                Ok(_) => failures.push(format!(
2305                    "revision {id} does not produce its resulting_data_hash"
2306                )),
2307                Err(_) => {
2308                    failures.push(format!("revision {id} does not apply"))
2309                }
2310            }
2311        }
2312        for failure in failures {
2313            add_problem(&mut self.problem, failure);
2314        }
2315        true
2316    }
2317}
2318
2319/// Append `problem` to `problems`, once
2320fn add_problem(problems: &mut Option<String>, problem: String) {
2321    *problems = Some(match problems.take() {
2322        Some(p) if p.contains(&problem) => p,
2323        Some(p) => format!("{p}; {problem}"),
2324        None => problem,
2325    });
2326}
2327
2328/// The first of `revision`'s duplicates that is not one in `current`, the
2329/// version it was applied to: what makes "nothing was lost" checkable
2330fn false_duplicate(
2331    id: &GovernanceLogId,
2332    revision: &Revision,
2333    current: &serde_json::Value,
2334) -> Option<String> {
2335    revision.duplicates.iter().find_map(|(path, same_as)| {
2336        let same = match (current.pointer(path), current.pointer(same_as)) {
2337            (Some(a), Some(b)) => canonical_json(a) == canonical_json(b),
2338            _ => false,
2339        };
2340        (!same).then(|| {
2341            format!(
2342                "revision {id} removed {path} as a duplicate of {same_as}, \
2343                 but they differ"
2344            )
2345        })
2346    })
2347}
2348
2349/// A verification of the whole chain
2350#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2351#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2352#[cfg_attr(feature = "schemars", schemars(inline))]
2353pub struct GovernanceVerification {
2354    /// The key in force for the next entry — the active end of `keys`
2355    pub public_key: PublicKeyHex,
2356    /// Every entry's signature and link verified under the key in force,
2357    /// no entry's content is known to differ from what was attested, and
2358    /// every amendment and rotation is well-formed. Repudiated entries do
2359    /// not clear this by themselves: repudiation is a declared state, not
2360    /// a defect, and `repudiated` is where to look for it.
2361    pub ok: bool,
2362    /// The last entry in the chain
2363    #[serde(default)]
2364    pub head: Option<GovernanceLogId>,
2365    /// In chain order
2366    pub entries: Vec<EntryVerdict>,
2367    /// The signing key history the chain itself declares, oldest first
2368    #[serde(default)]
2369    pub keys: Vec<GovernanceKeyRecord>,
2370    /// The genesis key, when neither this verifier's [`KeyAnchor`] nor a
2371    /// [`CertPurpose::Genesis`] certificate in the chain vouches for it.
2372    /// Not a failure, but a reference client says so loudly. Never a later
2373    /// key: those are certified or they do not hold the chain at all.
2374    #[serde(default)]
2375    pub unanchored_keys: Vec<PublicKeyHex>,
2376    /// Entries inside a compromise window that no reattestation restored
2377    #[serde(default)]
2378    pub repudiated: Vec<GovernanceLogId>,
2379}
2380
2381/// A [`GovernanceVerification`] without one verdict per entry: what
2382/// `verify_governance_log` returns unless asked for `full` (0.91.4). The
2383/// check behind it is the whole one; only the report is shorter.
2384#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2385#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2386#[cfg_attr(feature = "schemars", schemars(inline))]
2387pub struct GovernanceVerificationSummary {
2388    /// See [`GovernanceVerification::public_key`]
2389    pub public_key: PublicKeyHex,
2390    /// See [`GovernanceVerification::ok`]
2391    pub ok: bool,
2392    /// The last entry in the chain
2393    #[serde(default)]
2394    pub head: Option<GovernanceLogId>,
2395    /// Entries checked
2396    pub entries: u64,
2397    /// Entries whose signature did not verify
2398    pub signature_failures: u64,
2399    /// Entries whose hash link did not verify
2400    pub link_failures: u64,
2401    /// Entries whose content no longer hashes to what was attested
2402    pub content_mismatches: u64,
2403    /// Entries with a `problem`
2404    pub problems: u64,
2405    /// Entries signed well after they were recorded
2406    pub retroactive: u64,
2407    /// Entries a redaction names
2408    pub redacted: u64,
2409    /// Every entry that failed a check or has a `problem`, in chain order
2410    #[serde(default)]
2411    pub failed: Vec<GovernanceLogId>,
2412    /// The first of them, verdict in full
2413    #[serde(default, skip_serializing_if = "Option::is_none")]
2414    pub first_failure: Option<EntryVerdict>,
2415    /// See [`GovernanceVerification::keys`]
2416    #[serde(default)]
2417    pub keys: Vec<GovernanceKeyRecord>,
2418    /// See [`GovernanceVerification::unanchored_keys`]
2419    #[serde(default)]
2420    pub unanchored_keys: Vec<PublicKeyHex>,
2421    /// See [`GovernanceVerification::repudiated`]
2422    #[serde(default)]
2423    pub repudiated: Vec<GovernanceLogId>,
2424    pub how_to_read: String,
2425}
2426
2427impl EntryVerdict {
2428    /// Whether this entry fails what [`GovernanceVerification::settle`]
2429    /// requires of every entry
2430    pub fn failed(&self) -> bool {
2431        !self.signature_valid
2432            || !self.link_valid
2433            || self.content_matches == Some(false)
2434            || self.problem.is_some()
2435    }
2436}
2437
2438impl GovernanceVerification {
2439    /// The report without one verdict per entry (0.91.4)
2440    pub fn summary(&self) -> GovernanceVerificationSummary {
2441        let count = |f: &dyn Fn(&EntryVerdict) -> bool| -> u64 {
2442            self.entries.iter().filter(|e| f(e)).count() as u64
2443        };
2444        let failed: Vec<&EntryVerdict> =
2445            self.entries.iter().filter(|e| e.failed()).collect();
2446        GovernanceVerificationSummary {
2447            public_key: self.public_key,
2448            ok: self.ok,
2449            head: self.head.clone(),
2450            entries: self.entries.len() as u64,
2451            signature_failures: count(&|e| !e.signature_valid),
2452            link_failures: count(&|e| !e.link_valid),
2453            content_mismatches: count(&|e| e.content_matches == Some(false)),
2454            problems: count(&|e| e.problem.is_some()),
2455            retroactive: count(&|e| e.retroactive),
2456            redacted: count(&|e| e.redacted),
2457            failed: failed.iter().map(|e| e.id.clone()).collect(),
2458            first_failure: failed.first().map(|e| (*e).clone()),
2459            keys: self.keys.clone(),
2460            unanchored_keys: self.unanchored_keys.clone(),
2461            repudiated: self.repudiated.clone(),
2462            how_to_read: format!(
2463                "A summary of the whole check of {n} entries: verify_governance_log \
2464                 with full=true returns one verdict per entry (signature, link, \
2465                 content, redaction, revisions, signing key), about {kb} KB. \
2466                 Either way this is the server checking itself, not independent \
2467                 evidence; agora-agentkit's govlog::verify_chain is the \
2468                 independent check.",
2469                n = self.entries.len(),
2470                kb = serde_json::to_string(self).map_or(0, |s| s.len() / 1024),
2471            ),
2472        }
2473    }
2474
2475    /// Recompute `ok` from the entries
2476    pub fn settle(mut self) -> Self {
2477        self.ok = self.entries.iter().all(|e| !e.failed());
2478        self
2479    }
2480
2481    /// Record whether `data` is the content `link` attested — or what a
2482    /// redaction of it left behind, or its latest version. Folding its
2483    /// revisions over the stored content checks them too.
2484    ///
2485    /// The chain endpoint carries `data` only for amendments and
2486    /// rotations, so this is how a caller that read an entry in full folds
2487    /// that read into the report. `false` (and a `false`
2488    /// [`content_matches`](EntryVerdict::content_matches), which clears
2489    /// [`ok`](Self::ok) on the next [`settle`](Self::settle)) when the
2490    /// entry is not in this report at all.
2491    pub fn check_content(
2492        &mut self,
2493        link: &GovernanceChainLink,
2494        data: &serde_json::Value,
2495    ) -> bool {
2496        let Some(entry) = self.entries.iter_mut().find(|e| e.id == link.id)
2497        else {
2498            return false;
2499        };
2500        let ok = entry.content_check(link.attestation.data_hash, data);
2501        entry.content_matches = Some(ok);
2502        ok
2503    }
2504}
2505
2506// ---------------------------------------------------------------------------
2507// Signing
2508// ---------------------------------------------------------------------------
2509
2510/// Attest an entry: the one construction path for
2511/// [`GovernanceAttestation`], used by the server at insert and by tests
2512/// building fixtures.
2513///
2514/// `signed_at` is truncated to whole seconds, the precision the signature
2515/// covers; store the value the attestation carries, not the one passed in.
2516pub fn attest(
2517    key: &SigningKey,
2518    envelope: &Envelope,
2519    chain_seq: u64,
2520    signed_at: DateTime<Utc>,
2521) -> GovernanceAttestation {
2522    let signed_at = truncate_to_seconds(signed_at);
2523    let entry_hash = envelope.entry_hash();
2524    let signature =
2525        crypto::sign(key, entry_hash.as_bytes(), signed_at.timestamp());
2526    GovernanceAttestation {
2527        envelope_version: envelope.agora_governance_log,
2528        chain_seq,
2529        prev_hash: envelope.prev_hash,
2530        data_hash: envelope.data_hash,
2531        entry_hash,
2532        signature: signature.into(),
2533        signed_at,
2534        retroactive: is_retroactive(envelope.created_at(), signed_at),
2535    }
2536}
2537
2538// ---------------------------------------------------------------------------
2539// Verification
2540// ---------------------------------------------------------------------------
2541
2542/// Why one link failed on its own, before chain context
2543#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
2544pub enum LinkError {
2545    #[error(
2546        "envelope version {0} is not supported (this verifier knows {ENVELOPE_VERSION})"
2547    )]
2548    UnsupportedVersion(u32),
2549    #[error("entry_hash does not recompute from the envelope fields")]
2550    HashMismatch,
2551    #[error("signature does not verify under the published key")]
2552    BadSignature,
2553    /// The entry's type is newer than this verifier: its envelope cannot
2554    /// be recomputed (the type's name is part of it), so the entry is
2555    /// unverifiable here rather than tampered with. Upgrade agentkit (0.91)
2556    #[error(
2557        "this entry's type is newer than this verifier, which cannot check it; \
2558         upgrade agora-agentkit"
2559    )]
2560    UnknownEntryType,
2561}
2562
2563/// Recompute a link's `entry_hash` from its fields
2564pub fn recompute_entry_hash(link: &GovernanceChainLink) -> Sha256Hex {
2565    Envelope::new(
2566        link.id.clone(),
2567        link.entry_type,
2568        link.created_at,
2569        link.attestation.prev_hash,
2570        link.attestation.data_hash,
2571    )
2572    .entry_hash()
2573}
2574
2575/// Verify one link in isolation: version, hash recomputation, signature
2576pub fn verify_link(
2577    link: &GovernanceChainLink,
2578    key: &VerifyingKey,
2579) -> Result<(), LinkError> {
2580    let a = &link.attestation;
2581    if a.envelope_version != ENVELOPE_VERSION {
2582        return Err(LinkError::UnsupportedVersion(a.envelope_version));
2583    }
2584    if link.entry_type == GovernanceLogEntryType::Unknown {
2585        return Err(LinkError::UnknownEntryType);
2586    }
2587    if recompute_entry_hash(link) != a.entry_hash {
2588        return Err(LinkError::HashMismatch);
2589    }
2590    if !crypto::verify(
2591        key,
2592        a.entry_hash.as_bytes(),
2593        a.signed_at.timestamp(),
2594        &Signature::from(&a.signature),
2595    ) {
2596        return Err(LinkError::BadSignature);
2597    }
2598    Ok(())
2599}
2600
2601/// `true` when `data` is what `link` attested
2602pub fn verify_data(
2603    link: &GovernanceChainLink,
2604    data: &serde_json::Value,
2605) -> bool {
2606    data_hash(data) == link.attestation.data_hash
2607}
2608
2609/// Whether `read`, serialized again, has the shape `written` had: an
2610/// object wherever it has one, an array of the same length wherever it
2611/// has one. Missing and `null` are the same thing.
2612///
2613/// serde's derived structs also read positionally from an array, so
2614/// `[]` is a perfectly good struct of optional fields and `[2, "routine",
2615/// …]` a perfectly good rotation. No other implementation would agree,
2616/// and two verifiers that disagree about what is well-formed can be shown
2617/// two different chains.
2618fn same_shape(written: &serde_json::Value, read: &serde_json::Value) -> bool {
2619    use serde_json::Value::{Array, Null, Object};
2620    match (written, read) {
2621        (Object(w), Object(r)) => r.iter().all(|(k, r)| match w.get(k) {
2622            Some(w) => same_shape(w, r),
2623            None => r.is_null(),
2624        }),
2625        (Array(w), Array(r)) => {
2626            w.len() == r.len() && w.iter().zip(r).all(|(w, r)| same_shape(w, r))
2627        }
2628        (_, Object(_) | Array(_)) => false,
2629        (Object(_) | Array(_), Null) => false,
2630        _ => true,
2631    }
2632}
2633
2634/// `T` from the JSON it was written as, held to [`same_shape`]
2635fn read_strictly<T>(written: &serde_json::Value) -> Result<T, String>
2636where
2637    T: Serialize + serde::de::DeserializeOwned,
2638{
2639    let read: T =
2640        serde_json::from_value(written.clone()).map_err(|e| e.to_string())?;
2641    let again = serde_json::to_value(&read).map_err(|e| e.to_string())?;
2642    if same_shape(written, &again) {
2643        Ok(read)
2644    } else {
2645        Err("an array where an object belongs, or the reverse".into())
2646    }
2647}
2648
2649/// A chain from the JSON it was served as, held to [`same_shape`]: a
2650/// link is an object, and so is everything in it that should be.
2651///
2652/// Prefer this to deserializing [`GovernanceChainLink`]s directly, which
2653/// also accepts a link written as an array of its fields. Nothing serves
2654/// one; a verifier that would read it agrees with no other.
2655pub fn links_from_json(
2656    chain: &serde_json::Value,
2657) -> Result<Vec<GovernanceChainLink>, String> {
2658    chain
2659        .as_array()
2660        .ok_or("a chain is an array of links")?
2661        .iter()
2662        .map(read_strictly)
2663        .collect()
2664}
2665
2666/// [`read_strictly`] for a field that is outside any signed `data`
2667fn read_as_written<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
2668where
2669    D: serde::Deserializer<'de>,
2670    T: Serialize + serde::de::DeserializeOwned,
2671{
2672    let written = serde_json::Value::deserialize(deserializer)?;
2673    if written.is_null() {
2674        return Ok(None);
2675    }
2676    read_strictly(&written)
2677        .map(Some)
2678        .map_err(serde::de::Error::custom)
2679}
2680
2681/// The id series reserved for one entry type, if it has one. The other
2682/// types share `GOV-` and `APP-`, which the verifier does not tell apart.
2683fn reserved_prefix(
2684    entry_type: GovernanceLogEntryType,
2685) -> Option<GovernanceLogPrefix> {
2686    match entry_type {
2687        GovernanceLogEntryType::Amendment => Some(GovernanceLogPrefix::Amd),
2688        GovernanceLogEntryType::KeyRotation => Some(GovernanceLogPrefix::Key),
2689        GovernanceLogEntryType::StewardRecord => Some(GovernanceLogPrefix::Rec),
2690        GovernanceLogEntryType::Obligation => Some(GovernanceLogPrefix::Obl),
2691        GovernanceLogEntryType::DataSet => Some(GovernanceLogPrefix::Data),
2692        GovernanceLogEntryType::Test => Some(GovernanceLogPrefix::Test),
2693        GovernanceLogEntryType::CouncilDecision
2694        | GovernanceLogEntryType::AppealsCourtDecision
2695        | GovernanceLogEntryType::EmergencyAction
2696        | GovernanceLogEntryType::PolicyChange
2697        | GovernanceLogEntryType::StewardVeto
2698        | GovernanceLogEntryType::Unknown => None,
2699    }
2700}
2701
2702/// The entry type a reserved id series belongs to, if it is reserved
2703fn reserved_for(prefix: GovernanceLogPrefix) -> Option<GovernanceLogEntryType> {
2704    match prefix {
2705        GovernanceLogPrefix::Amd => Some(GovernanceLogEntryType::Amendment),
2706        GovernanceLogPrefix::Key => Some(GovernanceLogEntryType::KeyRotation),
2707        GovernanceLogPrefix::Rec => Some(GovernanceLogEntryType::StewardRecord),
2708        GovernanceLogPrefix::Obl => Some(GovernanceLogEntryType::Obligation),
2709        GovernanceLogPrefix::Data => Some(GovernanceLogEntryType::DataSet),
2710        GovernanceLogPrefix::Test => Some(GovernanceLogEntryType::Test),
2711        GovernanceLogPrefix::Gov
2712        | GovernanceLogPrefix::App
2713        | GovernanceLogPrefix::Unknown => None,
2714    }
2715}
2716
2717/// The id series an entry type must use, and must not
2718fn prefix_problem(link: &GovernanceChainLink) -> Option<String> {
2719    let prefix = link.id.prefix();
2720    match (reserved_prefix(link.entry_type), reserved_for(prefix)) {
2721        (Some(want), _) if prefix != want => Some(format!(
2722            "the id of a {} entry must be in the {want}- series, not {}",
2723            link.entry_type, link.id
2724        )),
2725        (None, Some(owner)) => Some(format!(
2726            "{prefix}- ids are reserved for {owner} entries, but {} is a {}",
2727            link.id, link.entry_type
2728        )),
2729        _ => None,
2730    }
2731}
2732
2733/// The fields of an [`ObligationData`] a [`Revision`] may not touch: what it
2734/// is, who owes it, and when, change only through the obligation kinds,
2735/// which [`Obligation::check_amendment`] holds to their authority rules
2736pub const OBLIGATION_FIXED: [&str; 5] = [
2737    "/until",
2738    "/owed_by",
2739    "/kind",
2740    "/source",
2741    "/agora_obligation",
2742];
2743
2744/// The first of `pointers` that `revision`'s patch touches: an op's `path`,
2745/// or a `move` or `copy`'s `from`, at it, above it or below it
2746pub fn revision_touches<'a>(
2747    revision: &Revision,
2748    pointers: &[&'a str],
2749) -> Option<&'a str> {
2750    revision.patch.iter().find_map(|op| {
2751        let from = match op {
2752            json_patch::PatchOperation::Move(m) => Some(m.from.as_str()),
2753            json_patch::PatchOperation::Copy(c) => Some(c.from.as_str()),
2754            _ => None,
2755        };
2756        [Some(op.path().as_str()), from]
2757            .into_iter()
2758            .flatten()
2759            .find_map(|p| pointers.iter().copied().find(|f| overlaps(p, f)))
2760    })
2761}
2762
2763/// Which earlier entry an amendment names, once it is known to be one
2764fn amendment_target(
2765    amendment: &Amendment,
2766    seq: u64,
2767    seq_of: &HashMap<&str, u64>,
2768    links: &[&GovernanceChainLink],
2769) -> Result<u64, AmendmentError> {
2770    amendment.validate()?;
2771    let target = *seq_of.get(amendment.target.as_str()).ok_or_else(|| {
2772        AmendmentError::UnknownTarget(amendment.target.clone())
2773    })?;
2774    if target >= seq {
2775        return Err(AmendmentError::ForwardReference(amendment.target.clone()));
2776    }
2777    let target_link = links[target as usize - 1];
2778    if target_link.attestation.entry_hash != amendment.target_entry_hash {
2779        return Err(AmendmentError::WrongTargetHash(amendment.target.clone()));
2780    }
2781    let on_obligation =
2782        target_link.entry_type == GovernanceLogEntryType::Obligation;
2783    if amendment.kind.is_obligation() && !on_obligation {
2784        return Err(AmendmentError::NotAnObligation {
2785            kind: amendment.kind,
2786            target: amendment.target.clone(),
2787        });
2788    }
2789    if on_obligation && kind_standing(amendment.kind).is_some() {
2790        return Err(AmendmentError::PrecedentialOnObligation(amendment.kind));
2791    }
2792    if on_obligation
2793        && let Some(revision) = &amendment.revision
2794        && let Some(path) = revision_touches(revision, &OBLIGATION_FIXED)
2795    {
2796        return Err(AmendmentError::FixedObligationField(path.to_string()));
2797    }
2798    Ok(target)
2799}
2800
2801/// The key history as the walk discovers it
2802struct KeyWalk {
2803    /// Oldest first; the last entry is the active key
2804    history: Vec<(GovernanceKeyRecord, VerifyingKey)>,
2805    unanchored: Vec<PublicKeyHex>,
2806    /// Every key that has held the chain, voided ones included. The anchor
2807    /// lists retired and compromised keys too, so without this a thief
2808    /// could declare a "compromise" that rotates back to the key they stole.
2809    seen: HashSet<PublicKeyHex>,
2810    /// `chain_seq`s inside a compromise window
2811    repudiated: HashSet<u64>,
2812    genesis: PublicKeyHex,
2813    /// A rotation has carried the genesis key's certificate
2814    genesis_certified: bool,
2815}
2816
2817impl KeyWalk {
2818    fn new(genesis: &VerifyingKey, anchor: &KeyAnchor) -> Self {
2819        let public_key = PublicKeyHex::from(genesis);
2820        Self {
2821            genesis: public_key,
2822            genesis_certified: false,
2823            history: vec![(
2824                GovernanceKeyRecord {
2825                    public_key,
2826                    from_seq: 1,
2827                    through_seq: None,
2828                    status: KeyStatus::Active,
2829                    introduced_by: None,
2830                    retired_by: None,
2831                    certified: false,
2832                },
2833                *genesis,
2834            )],
2835            unanchored: if anchor.contains(&public_key) {
2836                Vec::new()
2837            } else {
2838                vec![public_key]
2839            },
2840            seen: HashSet::from([public_key]),
2841            repudiated: HashSet::new(),
2842        }
2843    }
2844
2845    /// The key that signs entry `seq`
2846    fn in_force(&self, seq: u64) -> (PublicKeyHex, VerifyingKey) {
2847        let (record, key) = self
2848            .history
2849            .iter()
2850            .rev()
2851            .find(|(r, _)| r.from_seq <= seq)
2852            .unwrap_or(&self.history[0]);
2853        (record.public_key, *key)
2854    }
2855
2856    /// The key that signs whatever comes next
2857    fn active(&self) -> PublicKeyHex {
2858        self.history
2859            .last()
2860            .map(|(r, _)| r.public_key)
2861            .expect("the genesis key is always in the history")
2862    }
2863
2864    fn close(
2865        &mut self,
2866        through_seq: u64,
2867        status: KeyStatus,
2868        by: &GovernanceLogId,
2869    ) {
2870        if let Some((record, _)) = self.history.last_mut() {
2871            record.through_seq = Some(through_seq);
2872            record.status = status;
2873            record.retired_by = Some(by.clone());
2874        }
2875    }
2876
2877    fn open(
2878        &mut self,
2879        public_key: PublicKeyHex,
2880        key: VerifyingKey,
2881        from_seq: u64,
2882        by: &GovernanceLogId,
2883    ) {
2884        self.history.push((
2885            GovernanceKeyRecord {
2886                public_key,
2887                from_seq,
2888                through_seq: None,
2889                status: KeyStatus::Active,
2890                introduced_by: Some(by.clone()),
2891                retired_by: None,
2892                certified: true,
2893            },
2894            key,
2895        ));
2896    }
2897
2898    /// Follow `rotation`, appended as `id` at `seq`
2899    fn apply(
2900        &mut self,
2901        rotation: &KeyRotation,
2902        link: &GovernanceChainLink,
2903        seq: u64,
2904        links: &[&GovernanceChainLink],
2905        roots: &RootSet,
2906    ) -> Result<(), RotationError> {
2907        rotation.verify_certified(seq, link.attestation.prev_hash, roots)?;
2908        let new_key = rotation
2909            .new_key
2910            .to_verifying_key()
2911            .map_err(|_| RotationError::BadNewKey)?;
2912        if self.seen.contains(&rotation.new_key) {
2913            return Err(RotationError::ReusedKey);
2914        }
2915        // The genesis key predates the root, so the first rotation brings
2916        // its certificate along. Whether that rotation is later voided by
2917        // a compromise does not matter: the certificate is the root's
2918        // statement, not the entry's.
2919        match (&rotation.outgoing_certificate, self.genesis_certified) {
2920            (None, false) => {
2921                return Err(RotationError::MissingGenesisCertificate);
2922            }
2923            (Some(_), true) => {
2924                return Err(RotationError::UnexpectedOutgoingCertificate);
2925            }
2926            (Some(certificate), false) => certificate
2927                .verify_for(&KeyCertStatement::genesis(self.genesis), roots)
2928                .map_err(RotationError::OutgoingCertificate)?,
2929            (None, true) => {}
2930        }
2931        match rotation.reason {
2932            RotationReason::Routine => {
2933                if rotation.old_key != self.in_force(seq).0 {
2934                    return Err(RotationError::WrongOldKey);
2935                }
2936                self.close(seq, KeyStatus::Retired, &link.id);
2937                self.open(rotation.new_key, new_key, seq + 1, &link.id);
2938            }
2939            RotationReason::Compromise => {
2940                let head = rotation
2941                    .last_trusted()
2942                    .ok_or(RotationError::MissingLastTrusted)?;
2943                let trusted_seq = head.chain_seq;
2944                let names_an_earlier_entry = trusted_seq >= 1
2945                    && trusted_seq < seq
2946                    && links[trusted_seq as usize - 1].id == head.id
2947                    && links[trusted_seq as usize - 1].attestation.entry_hash
2948                        == head.entry_hash;
2949                if !names_an_earlier_entry {
2950                    return Err(RotationError::UnknownLastTrusted);
2951                }
2952                // Trust cannot be anchored inside a window nobody trusts,
2953                // reattested or not: name an entry from before it.
2954                if self.repudiated.contains(&trusted_seq) {
2955                    return Err(RotationError::RepudiatedLastTrusted);
2956                }
2957                // The key in force at the last trusted entry: a rotation
2958                // inside the window is void with the rest of it.
2959                if rotation.old_key != self.in_force(trusted_seq).0 {
2960                    return Err(RotationError::WrongOldKey);
2961                }
2962                self.history.retain(|(r, _)| r.from_seq <= trusted_seq);
2963                self.close(trusted_seq, KeyStatus::Compromised, &link.id);
2964                self.open(rotation.new_key, new_key, seq, &link.id);
2965                self.repudiated.extend(trusted_seq + 1..seq);
2966            }
2967        }
2968        self.seen.insert(rotation.new_key);
2969        if !self.genesis_certified {
2970            self.genesis_certified = true;
2971            self.history[0].0.certified = true;
2972            self.unanchored.clear();
2973        }
2974        Ok(())
2975    }
2976}
2977
2978/// Verify a whole chain from `genesis_key`, following the rotations that
2979/// `roots` certified and no others.
2980///
2981/// Links are sorted by `chain_seq` first, so the caller's order does not
2982/// matter. `retroactive` and `out_of_order` are recomputed from the
2983/// timestamps, not copied. `content_matches` is filled only for links that
2984/// carry `data` — for the rest, see
2985/// [`check_content`](GovernanceVerification::check_content).
2986///
2987/// `genesis_key` is the key the chain started under; it is not in the
2988/// chain, so a verifier has to be told. Until the chain's first rotation
2989/// certifies it, `anchor` is what vouches for it: if it is not there it
2990/// is reported in `unanchored_keys` rather than rejected — a client
2991/// pinning what it saw first passes `KeyAnchor::pinned(key)` and gets a
2992/// clean report. Pass [`RootSet::published`] for `roots` outside tests.
2993///
2994/// A rotation is authentic iff its [`KeyCertificate`] is valid for the
2995/// new key at that position; who signed the entry only follows from which
2996/// key *can* (the old one for a routine rotation, the new one once the
2997/// old is compromised). So a thief holding the online key can append
2998/// entries — which a compromise declaration then repudiates — but can
2999/// never move the chain.
3000///
3001/// The rules the report records that no type states on its own: an id
3002/// belongs to its entry type's series and appears once (`AMD-`, `KEY-`
3003/// and `REC-` are each reserved for one type); only an entry whose
3004/// own signature and linkage verify amends anything or moves the key, so
3005/// a forged entry cannot also describe the chain; and an amendment inside
3006/// a repudiated window has no effect unless a [`AmendmentKind::Reattested`]
3007/// vouches for its own entry first, resolved to a fixpoint.
3008pub fn verify_chain(
3009    links: &[GovernanceChainLink],
3010    genesis_key: &VerifyingKey,
3011    anchor: &KeyAnchor,
3012    roots: &RootSet,
3013) -> GovernanceVerification {
3014    let mut links: Vec<&GovernanceChainLink> = links.iter().collect();
3015    links.sort_by_key(|l| l.attestation.chain_seq);
3016
3017    let mut seq_of: HashMap<&str, u64> = HashMap::new();
3018    let mut duplicates: HashSet<&str> = HashSet::new();
3019    for (i, link) in links.iter().enumerate() {
3020        if seq_of.insert(link.id.as_str(), i as u64 + 1).is_some() {
3021            duplicates.insert(link.id.as_str());
3022        }
3023    }
3024
3025    let mut walk = KeyWalk::new(genesis_key, anchor);
3026    // (seq, amendment id, amendment, target seq), in chain order
3027    let mut amendments: Vec<(u64, GovernanceLogId, Amendment, u64)> =
3028        Vec::new();
3029    let mut entries: Vec<EntryVerdict> = Vec::with_capacity(links.len());
3030    let mut prev: Option<&GovernanceChainLink> = None;
3031
3032    for (i, link) in links.iter().enumerate() {
3033        let a = &link.attestation;
3034        let expected_seq = i as u64 + 1;
3035        let mut problems: Vec<String> = Vec::new();
3036
3037        if let Some(problem) = prefix_problem(link) {
3038            problems.push(problem);
3039        }
3040        if duplicates.contains(link.id.as_str()) {
3041            problems.push(format!("{} appears more than once", link.id));
3042        }
3043
3044        // The two entry types the verifier has to read. `data` is hashed
3045        // against the envelope before it is parsed, so what is read is
3046        // what was signed.
3047        let carries_meaning = matches!(
3048            link.entry_type,
3049            GovernanceLogEntryType::Amendment
3050                | GovernanceLogEntryType::KeyRotation
3051        );
3052        let mut amendment: Option<Amendment> = None;
3053        let mut rotation: Option<KeyRotation> = None;
3054        let mut content_matches: Option<bool> = None;
3055        match &link.data {
3056            Some(data) if non_integer_number(data).is_some() => {
3057                content_matches = Some(false);
3058                problems.push(format!(
3059                    "`data` has a number that is not a 64-bit integer at {:?}; \
3060                     governance data never contains one",
3061                    non_integer_number(data).unwrap_or_default()
3062                ));
3063            }
3064            Some(data) => {
3065                let matched = data_hash(data) == a.data_hash;
3066                content_matches = Some(matched);
3067                if !matched {
3068                    if carries_meaning {
3069                        problems.push(
3070                            "`data` does not hash to the attested data_hash"
3071                                .into(),
3072                        );
3073                    }
3074                } else {
3075                    match link.entry_type {
3076                        GovernanceLogEntryType::Amendment => {
3077                            match read_strictly(data) {
3078                                Ok(v) => amendment = Some(v),
3079                                Err(e) => problems.push(format!(
3080                                    "amendment `data` is malformed: {e}"
3081                                )),
3082                            }
3083                        }
3084                        GovernanceLogEntryType::KeyRotation => {
3085                            match read_strictly(data) {
3086                                Ok(v) => rotation = Some(v),
3087                                Err(e) => problems.push(format!(
3088                                    "key_rotation `data` is malformed: {e}"
3089                                )),
3090                            }
3091                        }
3092                        _ => {}
3093                    }
3094                }
3095            }
3096            None if carries_meaning => problems.push(format!(
3097                "a {} entry must carry its `data`",
3098                link.entry_type
3099            )),
3100            None => {}
3101        }
3102
3103        // A compromise declaration is signed by the new key, and is
3104        // taken at its word only if the root certified that key here.
3105        // Everything else is signed by the key in force.
3106        let declared = rotation
3107            .as_ref()
3108            .filter(|r| r.reason == RotationReason::Compromise)
3109            .map(|r| {
3110                if walk.seen.contains(&r.new_key) {
3111                    return Err(RotationError::ReusedKey);
3112                }
3113                r.verify_certified(expected_seq, a.prev_hash, roots)?;
3114                r.new_key
3115                    .to_verifying_key()
3116                    .map_err(|_| RotationError::BadNewKey)
3117            });
3118        let (key_hex, key) = match &declared {
3119            Some(Ok(k)) => (PublicKeyHex::from(k), *k),
3120            _ => walk.in_force(expected_seq),
3121        };
3122        // Say why a declaration was not taken at its word; the bad
3123        // signature that follows is the consequence, not the cause.
3124        if let Some(Err(e)) = &declared {
3125            problems.push(e.to_string());
3126        }
3127
3128        let (hash_ok, signature_valid) = match verify_link(link, &key) {
3129            Ok(()) => (true, true),
3130            Err(LinkError::BadSignature) => {
3131                problems.push(LinkError::BadSignature.to_string());
3132                (true, false)
3133            }
3134            Err(e) => {
3135                problems.push(e.to_string());
3136                (false, false)
3137            }
3138        };
3139
3140        let mut link_valid = hash_ok;
3141        if a.chain_seq != expected_seq {
3142            link_valid = false;
3143            problems.push(format!(
3144                "chain_seq {} where {expected_seq} was expected",
3145                a.chain_seq
3146            ));
3147        }
3148        let expected_prev = prev.map(|p| p.attestation.entry_hash);
3149        if a.prev_hash != expected_prev {
3150            link_valid = false;
3151            problems.push(match (a.prev_hash, expected_prev) {
3152                (Some(_), None) => "first entry names a predecessor".into(),
3153                (None, Some(_)) => "prev_hash is null mid-chain".into(),
3154                _ => "prev_hash is not the previous entry's entry_hash".into(),
3155            });
3156        }
3157        let out_of_order = prev.is_some_and(|p| link.created_at < p.created_at);
3158
3159        // Only an entry that is itself authentic moves the key or amends
3160        // anything: a forged one already fails the chain, and must not
3161        // also get to describe it.
3162        let authentic = signature_valid && link_valid;
3163        if let Some(rotation) = rotation.as_ref().filter(|_| authentic)
3164            && let Err(e) =
3165                walk.apply(rotation, link, expected_seq, &links, roots)
3166        {
3167            let problem = e.to_string();
3168            if !problems.contains(&problem) {
3169                problems.push(problem);
3170            }
3171        }
3172        // Texts are checked against what the entry signed whether or not
3173        // the amendment takes effect: a substituted text is a lie about
3174        // the record either way.
3175        let mut texts = None;
3176        if let Some(amendment) = amendment
3177            .as_ref()
3178            .filter(|a| authentic && a.validate().is_ok())
3179        {
3180            match amendment.text_status(link.texts.as_ref()) {
3181                Ok(status) => texts = status,
3182                Err(e) => problems.push(e.to_string()),
3183            }
3184        } else if link.texts.as_ref().is_some_and(|t| !t.is_empty()) {
3185            problems.push(AmendmentError::UncommittedText.to_string());
3186        }
3187        if let Some(amendment) = amendment.filter(|_| authentic) {
3188            match amendment_target(&amendment, expected_seq, &seq_of, &links) {
3189                Ok(target) => amendments.push((
3190                    expected_seq,
3191                    link.id.clone(),
3192                    amendment,
3193                    target,
3194                )),
3195                Err(e) => problems.push(e.to_string()),
3196            }
3197        }
3198
3199        entries.push(EntryVerdict {
3200            id: link.id.clone(),
3201            chain_seq: a.chain_seq,
3202            signature_valid,
3203            link_valid,
3204            content_matches,
3205            retroactive: is_retroactive(link.created_at, a.signed_at),
3206            out_of_order,
3207            amended_by: Vec::new(),
3208            signed_by: Some(key_hex),
3209            redacted: false,
3210            redacted_data_hash: None,
3211            revisions: Vec::new(),
3212            latest_data_hash: None,
3213            superseded_revisions: Vec::new(),
3214            history: RevisionHistory::default(),
3215            repudiated: false,
3216            reattested_by: Vec::new(),
3217            texts,
3218            problem: (!problems.is_empty()).then(|| problems.join("; ")),
3219        });
3220        prev = Some(link);
3221    }
3222
3223    // Reattestations first, and to a fixpoint: an amendment inside a
3224    // repudiated window has no effect unless something later vouches for
3225    // it, and that something can be another reattestation.
3226    let mut applied = vec![false; amendments.len()];
3227    loop {
3228        let mut changed = false;
3229        for (i, (seq, id, amendment, target)) in amendments.iter().enumerate() {
3230            if applied[i]
3231                || amendment.kind != AmendmentKind::Reattested
3232                || walk.repudiated.contains(seq)
3233            {
3234                continue;
3235            }
3236            applied[i] = true;
3237            entries[*target as usize - 1].reattested_by.push(id.clone());
3238            walk.repudiated.remove(target);
3239            changed = true;
3240        }
3241        if !changed {
3242            break;
3243        }
3244    }
3245
3246    for (seq, id, amendment, target) in &amendments {
3247        if walk.repudiated.contains(seq) {
3248            continue;
3249        }
3250        let entry = &mut entries[*target as usize - 1];
3251        entry.amended_by.push(id.clone());
3252        let mut unrebased = false;
3253        if let Some(redaction) = &amendment.redaction {
3254            // A redaction of a revised entry says what the rebase gives,
3255            // or the rebased history is checked against nothing.
3256            unrebased = !entry.revisions.is_empty()
3257                && redaction.resulting_latest_hash.is_none();
3258            entry.redacted = true;
3259            entry.redacted_data_hash = Some(redaction.resulting_data_hash);
3260            // The revisions so far are rebased over the redacted data.
3261            entry.history.rebased = entry.history.revisions.len();
3262            entry.history.rebased_hash = redaction.resulting_latest_hash;
3263            entry.latest_data_hash = redaction.resulting_latest_hash;
3264            entry.superseded_revisions = entry.revisions.clone();
3265        }
3266        if let Some(revision) = &amendment.revision {
3267            entry.revisions.push(id.clone());
3268            entry.latest_data_hash = Some(revision.resulting_data_hash);
3269            entry.history.revisions.push((id.clone(), revision.clone()));
3270        }
3271        if unrebased {
3272            let target = entry.id.clone();
3273            add_problem(
3274                &mut entries[*seq as usize - 1].problem,
3275                format!(
3276                    "the redaction of {target}, which has revisions, names no \
3277                     resulting_latest_hash"
3278                ),
3279            );
3280        }
3281    }
3282
3283    // A redacted entry's content is what the redaction left behind, and a
3284    // revised one's revisions are checked against it.
3285    for (i, link) in links.iter().enumerate() {
3286        let entry = &mut entries[i];
3287        if let Some(data) = &link.data
3288            && entry.content_matches.is_some()
3289            && (entry.redacted || !entry.history.revisions.is_empty())
3290        {
3291            entry.content_matches =
3292                Some(entry.content_check(link.attestation.data_hash, data));
3293        }
3294    }
3295
3296    let mut seen = HashSet::new();
3297    walk.unanchored.retain(|key| seen.insert(*key));
3298
3299    let mut repudiated = Vec::new();
3300    for (i, entry) in entries.iter_mut().enumerate() {
3301        if walk.repudiated.contains(&(i as u64 + 1)) {
3302            entry.repudiated = true;
3303            repudiated.push(entry.id.clone());
3304        }
3305    }
3306
3307    GovernanceVerification {
3308        public_key: walk.active(),
3309        ok: false,
3310        head: prev.map(|p| p.id.clone()),
3311        entries,
3312        keys: walk.history.into_iter().map(|(record, _)| record).collect(),
3313        unanchored_keys: walk.unanchored,
3314        repudiated,
3315    }
3316    .settle()
3317}
3318
3319#[cfg(test)]
3320mod tests {
3321    use super::*;
3322
3323    #[test]
3324    fn pointer_tokens_escape_and_unescape() {
3325        assert_eq!(escape_pointer_token("a/b~c"), "a~1b~0c");
3326        // `~01` is an escaped `~1`, not an escaped `/`
3327        assert_eq!(unescape_pointer_token("~01"), "~1");
3328        for token in ["", "plain", "a/b", "~", "~1", "/~0/"] {
3329            assert_eq!(
3330                unescape_pointer_token(&escape_pointer_token(token)),
3331                token
3332            );
3333        }
3334        assert_eq!(
3335            pointer_tokens("/rounds/0/a~1b~0c"),
3336            ["rounds", "0", "a/b~c"]
3337        );
3338        assert!(pointer_tokens("").is_empty());
3339    }
3340
3341    /// Each kind of record names what it cites in its own field; an entry
3342    /// type that cites nothing names nothing, and a malformed field is an
3343    /// error.
3344    #[test]
3345    fn citations_are_read_from_each_records_own_field() {
3346        use serde_json::json;
3347        let id = |s: &str| s.parse::<GovernanceLogId>().unwrap();
3348        assert_eq!(
3349            citations_in(
3350                GovernanceLogEntryType::StewardRecord,
3351                &json!({"title": "t", "concerns": ["GOV-2026-0011", "AMD-2026-0004"]}),
3352            )
3353            .unwrap(),
3354            vec![
3355                (id("GOV-2026-0011"), CitationRelation::Concerns),
3356                (id("AMD-2026-0004"), CitationRelation::Concerns),
3357            ]
3358        );
3359        assert_eq!(
3360            citations_in(
3361                GovernanceLogEntryType::Amendment,
3362                &json!({"target": "APP-2026-0001", "authority": "GOV-2026-0005"}),
3363            )
3364            .unwrap(),
3365            vec![(id("GOV-2026-0005"), CitationRelation::Authority)],
3366            "the target is not a citation: it is listed under `amendments`"
3367        );
3368        assert_eq!(
3369            citations_in(
3370                GovernanceLogEntryType::CouncilDecision,
3371                &json!({"overrules": ["APP-2026-0002"]}),
3372            )
3373            .unwrap(),
3374            vec![(id("APP-2026-0002"), CitationRelation::Overrules)]
3375        );
3376        assert!(
3377            citations_in(
3378                GovernanceLogEntryType::Amendment,
3379                &json!({"authority": null})
3380            )
3381            .unwrap()
3382            .is_empty()
3383        );
3384        // A malformed field is an error that says which relation it was,
3385        // never a silent "cites nothing"
3386        assert!(matches!(
3387            citations_in(
3388                GovernanceLogEntryType::StewardRecord,
3389                &json!({"concerns": "GOV-2026-0011"})
3390            ),
3391            Err(MalformedCitation {
3392                relation: CitationRelation::Concerns,
3393                ..
3394            })
3395        ));
3396        assert!(
3397            citations_in(
3398                GovernanceLogEntryType::AppealsCourtDecision,
3399                &json!({"concerns": ["GOV-2026-0011"]}),
3400            )
3401            .unwrap()
3402            .is_empty()
3403        );
3404    }
3405    use crate::crypto::generate_keypair;
3406    use serde_json::json;
3407
3408    pub(super) fn gov(n: u32) -> GovernanceLogId {
3409        format!("GOV-2026-{n:04}").parse().unwrap()
3410    }
3411
3412    pub(super) fn amd(n: u32) -> GovernanceLogId {
3413        format!("AMD-2026-{n:04}").parse().unwrap()
3414    }
3415
3416    pub(super) fn key_id(n: u32) -> GovernanceLogId {
3417        format!("KEY-2026-{n:04}").parse().unwrap()
3418    }
3419
3420    pub(super) fn rec(n: u32) -> GovernanceLogId {
3421        format!("REC-2026-{n:04}").parse().unwrap()
3422    }
3423
3424    pub(super) fn obl(n: u32) -> GovernanceLogId {
3425        format!("OBL-2026-{n:04}").parse().unwrap()
3426    }
3427
3428    pub(super) fn data_id(n: u32) -> GovernanceLogId {
3429        format!("DATA-2026-{n:04}").parse().unwrap()
3430    }
3431
3432    pub(super) fn test_id(n: u32) -> GovernanceLogId {
3433        format!("TEST-2026-{n:04}").parse().unwrap()
3434    }
3435
3436    pub(super) fn at(secs: i64) -> DateTime<Utc> {
3437        DateTime::from_timestamp(1_700_000_000 + secs, 123_456_789).unwrap()
3438    }
3439
3440    /// The anchor a client that pinned the key it first saw would hold
3441    fn anchored(key: &VerifyingKey) -> KeyAnchor {
3442        KeyAnchor::pinned(key.into())
3443    }
3444
3445    /// `draft` under salts fixed by its texts and `seq`, so that a chain
3446    /// built twice is the same bytes twice
3447    pub(super) fn resalted(draft: &AmendmentDraft, seq: u64) -> AmendmentDraft {
3448        let fix = |field: &str, t: &Option<CommittedText>| {
3449            t.as_ref().map(|t| {
3450                let salt = Sha256::digest(format!("{seq}/{field}/{}", t.text));
3451                CommittedText::with_salt(
3452                    TextSalt::from(<[u8; 32]>::from(salt)),
3453                    t.text.clone(),
3454                )
3455            })
3456        };
3457        let texts = AmendmentTexts {
3458            basis: fix("basis", &draft.texts.basis),
3459            note: fix("note", &draft.texts.note),
3460            rationale: fix("rationale", &draft.texts.rationale),
3461        };
3462        let commit = |t: &Option<CommittedText>| {
3463            t.as_ref().map(|t| AmendmentText::Committed(t.commitment()))
3464        };
3465        AmendmentDraft {
3466            amendment: Amendment {
3467                basis: commit(&texts.basis).unwrap(),
3468                note: commit(&texts.note).unwrap(),
3469                rationale: commit(&texts.rationale),
3470                ..draft.amendment.clone()
3471            },
3472            texts,
3473        }
3474    }
3475
3476    /// `draft` as version 1 wrote it: the texts in the signed `data`
3477    pub(super) fn v1(draft: AmendmentDraft) -> Amendment {
3478        let plain =
3479            |t: Option<CommittedText>| t.map(|t| AmendmentText::Plain(t.text));
3480        Amendment {
3481            agora_governance_amendment: 1,
3482            basis: plain(draft.texts.basis).unwrap(),
3483            note: plain(draft.texts.note).unwrap(),
3484            rationale: plain(draft.texts.rationale),
3485            ..draft.amendment
3486        }
3487    }
3488
3489    /// A throwaway root key. Fixed, so the vectors are byte-stable; the
3490    /// real ones live on hardware and sign nothing in a test.
3491    pub(super) fn root(n: u8) -> SigningKey {
3492        SigningKey::from_bytes(&[0xA0 + n; 32])
3493    }
3494
3495    /// `root(1)` and `root(2)`, either of which suffices
3496    pub(super) fn roots() -> RootSet {
3497        RootSet::new(
3498            [1, 2].map(|n| PublicKeyHex::from(&root(n).verifying_key())),
3499            1,
3500        )
3501    }
3502
3503    /// `statement`, signed by each of `signers` as a root would
3504    pub(super) fn certify(
3505        signers: &[&SigningKey],
3506        statement: KeyCertStatement,
3507    ) -> KeyCertificate {
3508        use ed25519_dalek::Signer;
3509        let message = statement.signed_bytes();
3510        signers.iter().fold(
3511            KeyCertificate::unsigned(statement),
3512            |certificate, signer| {
3513                certificate.with(RootSignature {
3514                    root_key: (&signer.verifying_key()).into(),
3515                    signature: signer.sign(&message).into(),
3516                })
3517            },
3518        )
3519    }
3520
3521    /// `root(1)`'s routine certificate for `key` at `c`'s next position
3522    fn for_new_at(c: &Chain, key: &VerifyingKey) -> KeyCertificate {
3523        certify(
3524            &[&root(1)],
3525            KeyCertStatement::routine(key.into(), c.next_seq(), c.prev_hash()),
3526        )
3527    }
3528
3529    pub(super) fn link(
3530        key: &SigningKey,
3531        n: u32,
3532        prev: Option<&GovernanceChainLink>,
3533        data: &serde_json::Value,
3534        signed_at: DateTime<Utc>,
3535    ) -> GovernanceChainLink {
3536        let created_at = truncate_to_micros(at(n as i64 * 10));
3537        let envelope = Envelope::new(
3538            gov(n),
3539            GovernanceLogEntryType::CouncilDecision,
3540            created_at,
3541            prev.map(|p| p.attestation.entry_hash),
3542            data_hash(data),
3543        );
3544        let attestation = attest(
3545            key,
3546            &envelope,
3547            prev.map_or(1, |p| p.attestation.chain_seq + 1),
3548            signed_at,
3549        );
3550        GovernanceChainLink {
3551            id: gov(n),
3552            entry_type: GovernanceLogEntryType::CouncilDecision,
3553            created_at,
3554            attestation,
3555            data: None,
3556            texts: None,
3557        }
3558    }
3559
3560    pub(super) fn chain(key: &SigningKey, n: u32) -> Vec<GovernanceChainLink> {
3561        let mut out: Vec<GovernanceChainLink> = Vec::new();
3562        for i in 1..=n {
3563            let data = json!({"title": format!("Decision {i}"), "outcome": "approved"});
3564            let l = link(key, i, out.last(), &data, at(i as i64 * 10 + 1));
3565            out.push(l);
3566        }
3567        out
3568    }
3569
3570    /// A chain under construction: one entry per `push`, each series
3571    /// numbered on its own, `data` carried for the entries a verifier
3572    /// reads.
3573    pub(super) struct Chain {
3574        pub(super) links: Vec<GovernanceChainLink>,
3575        gov: u32,
3576        amd: u32,
3577        key: u32,
3578        /// Whoever signed the first entry
3579        genesis: Option<PublicKeyHex>,
3580    }
3581
3582    impl Chain {
3583        pub(super) fn new() -> Self {
3584            Self {
3585                links: Vec::new(),
3586                gov: 0,
3587                amd: 0,
3588                key: 0,
3589                genesis: None,
3590            }
3591        }
3592
3593        pub(super) fn prev_hash(&self) -> Option<Sha256Hex> {
3594            self.links.last().map(|l| l.attestation.entry_hash)
3595        }
3596
3597        /// The `entry_hash` of the 1-indexed link `seq`
3598        pub(super) fn hash_at(&self, seq: usize) -> Sha256Hex {
3599            self.links[seq - 1].attestation.entry_hash
3600        }
3601
3602        /// The `chain_seq` the next entry gets
3603        pub(super) fn next_seq(&self) -> u64 {
3604            self.links.len() as u64 + 1
3605        }
3606
3607        /// The 1-indexed link `seq`, as a compromise names it
3608        pub(super) fn head(&self, seq: usize) -> TrustedHead {
3609            TrustedHead {
3610                id: self.links[seq - 1].id.clone(),
3611                chain_seq: seq as u64,
3612                entry_hash: self.hash_at(seq),
3613            }
3614        }
3615
3616        /// The genesis certificate, if the next rotation is the first
3617        fn outgoing(&self, rotation: KeyRotation) -> KeyRotation {
3618            match (self.key, self.genesis) {
3619                (0, Some(genesis)) => rotation.with_outgoing(certify(
3620                    &[&root(1)],
3621                    KeyCertStatement::genesis(genesis),
3622                )),
3623                _ => rotation,
3624            }
3625        }
3626
3627        /// A routine rotation to `new` at the next position, certified by
3628        /// `root(1)`
3629        pub(super) fn routine(
3630            &self,
3631            old: &VerifyingKey,
3632            new: &SigningKey,
3633        ) -> KeyRotation {
3634            let statement = KeyCertStatement::routine(
3635                (&new.verifying_key()).into(),
3636                self.next_seq(),
3637                self.prev_hash(),
3638            );
3639            self.outgoing(KeyRotation::routine(
3640                old.into(),
3641                new,
3642                self.prev_hash(),
3643                at(self.next_seq() as i64 * 10 + 5),
3644                certify(&[&root(1)], statement),
3645            ))
3646        }
3647
3648        /// A compromise declaration at the next position trusting `old`
3649        /// through the 1-indexed link `trusted`, certified by `root(1)`
3650        pub(super) fn compromise(
3651            &self,
3652            old: &VerifyingKey,
3653            new: &SigningKey,
3654            trusted: usize,
3655        ) -> KeyRotation {
3656            let statement = KeyCertStatement::compromise(
3657                (&new.verifying_key()).into(),
3658                self.next_seq(),
3659                self.prev_hash(),
3660                self.head(trusted),
3661            );
3662            self.outgoing(KeyRotation::compromise(
3663                old.into(),
3664                new,
3665                self.prev_hash(),
3666                at(self.next_seq() as i64 * 10 + 5),
3667                certify(&[&root(1)], statement),
3668            ))
3669        }
3670
3671        /// What [`Chain::amend`] will call the next amendment
3672        pub(super) fn next_amd(&self) -> GovernanceLogId {
3673            amd(self.amd + 1)
3674        }
3675
3676        pub(super) fn push(
3677            &mut self,
3678            signer: &SigningKey,
3679            id: GovernanceLogId,
3680            entry_type: GovernanceLogEntryType,
3681            data: serde_json::Value,
3682            carry: bool,
3683        ) -> GovernanceLogId {
3684            self.genesis
3685                .get_or_insert_with(|| (&signer.verifying_key()).into());
3686            let n = self.links.len() as i64 + 1;
3687            let created_at = truncate_to_micros(at(n * 10));
3688            let envelope = Envelope::new(
3689                id.clone(),
3690                entry_type,
3691                created_at,
3692                self.prev_hash(),
3693                data_hash(&data),
3694            );
3695            let attestation =
3696                attest(signer, &envelope, n as u64, at(n * 10 + 1));
3697            self.links.push(GovernanceChainLink {
3698                id: id.clone(),
3699                entry_type,
3700                created_at,
3701                attestation,
3702                data: carry.then_some(data),
3703                texts: None,
3704            });
3705            id
3706        }
3707
3708        /// A council decision carrying `data` (which the link does not,
3709        /// as the chain endpoint does not carry transcripts)
3710        pub(super) fn entry(
3711            &mut self,
3712            signer: &SigningKey,
3713            data: serde_json::Value,
3714        ) -> GovernanceLogId {
3715            self.gov += 1;
3716            let id = gov(self.gov);
3717            self.push(
3718                signer,
3719                id,
3720                GovernanceLogEntryType::CouncilDecision,
3721                data,
3722                false,
3723            )
3724        }
3725
3726        pub(super) fn decision(
3727            &mut self,
3728            signer: &SigningKey,
3729        ) -> GovernanceLogId {
3730            let data = json!({"title": format!("Decision {}", self.gov + 1)});
3731            self.entry(signer, data)
3732        }
3733
3734        /// `draft`'s amendment as the entry's `data`, its texts beside it
3735        pub(super) fn amend(
3736            &mut self,
3737            signer: &SigningKey,
3738            draft: &AmendmentDraft,
3739        ) -> GovernanceLogId {
3740            let draft = resalted(draft, self.next_seq());
3741            let id = self.amend_v1(signer, &draft.amendment);
3742            let link = self.links.last_mut().unwrap();
3743            link.texts = Some(draft.texts);
3744            id
3745        }
3746
3747        /// An amendment with nothing beside it: version 1, or a version 2
3748        /// whose texts have all been withheld
3749        pub(super) fn amend_v1(
3750            &mut self,
3751            signer: &SigningKey,
3752            amendment: &Amendment,
3753        ) -> GovernanceLogId {
3754            self.amd += 1;
3755            let id = amd(self.amd);
3756            self.push(
3757                signer,
3758                id,
3759                GovernanceLogEntryType::Amendment,
3760                serde_json::to_value(amendment).unwrap(),
3761                true,
3762            )
3763        }
3764
3765        pub(super) fn rotate(
3766            &mut self,
3767            signer: &SigningKey,
3768            rotation: &KeyRotation,
3769        ) -> GovernanceLogId {
3770            self.key += 1;
3771            let id = key_id(self.key);
3772            self.push(
3773                signer,
3774                id,
3775                GovernanceLogEntryType::KeyRotation,
3776                serde_json::to_value(rotation).unwrap(),
3777                true,
3778            )
3779        }
3780    }
3781
3782    // -- canonical JSON --
3783
3784    #[test]
3785    fn canonical_json_sorts_keys_at_every_level() {
3786        let v = json!({"b": {"z": 1, "a": [{"y": 2, "x": 3}]}, "a": null});
3787        assert_eq!(
3788            canonical_json(&v),
3789            br#"{"a":null,"b":{"a":[{"x":3,"y":2}],"z":1}}"#
3790        );
3791    }
3792
3793    #[test]
3794    fn canonical_json_is_compact_and_escapes_like_serde() {
3795        let v = json!({"s": "tab\there \"q\" ünïcode \u{1F600}", "n": [1, -2, 3.5, true, false]});
3796        let bytes = canonical_json(&v);
3797        let text = std::str::from_utf8(&bytes).unwrap();
3798        assert_eq!(
3799            text,
3800            r#"{"n":[1,-2,3.5,true,false],"s":"tab\there \"q\" ünïcode 😀"}"#
3801        );
3802    }
3803
3804    #[test]
3805    fn canonical_json_ignores_insertion_order() {
3806        let mut a = serde_json::Map::new();
3807        a.insert("z".into(), json!(1));
3808        a.insert("a".into(), json!(2));
3809        let mut b = serde_json::Map::new();
3810        b.insert("a".into(), json!(2));
3811        b.insert("z".into(), json!(1));
3812        assert_eq!(
3813            canonical_json(&serde_json::Value::Object(a)),
3814            canonical_json(&serde_json::Value::Object(b))
3815        );
3816    }
3817
3818    #[test]
3819    fn canonical_json_empty_containers() {
3820        assert_eq!(canonical_json(&json!({})), b"{}");
3821        assert_eq!(canonical_json(&json!([])), b"[]");
3822        assert_eq!(
3823            canonical_json(&json!({"a": {}, "b": []})),
3824            br#"{"a":{},"b":[]}"#
3825        );
3826    }
3827
3828    // -- envelope --
3829
3830    #[test]
3831    fn preimage_is_declaration_ordered_json() {
3832        let e = Envelope::new(
3833            gov(1),
3834            GovernanceLogEntryType::AppealsCourtDecision,
3835            at(0),
3836            None,
3837            data_hash(&json!({})),
3838        );
3839        let text = String::from_utf8(e.preimage()).unwrap();
3840        assert!(text.starts_with(r#"{"agora_governance_log":1,"id":"GOV-2026-0001","entry_type":"appeals_court_decision","created_at":1700000000123456,"prev_hash":null,"data_hash":""#), "{text}");
3841    }
3842
3843    #[test]
3844    fn every_envelope_field_changes_the_hash() {
3845        let base = Envelope::new(
3846            gov(1),
3847            GovernanceLogEntryType::CouncilDecision,
3848            at(0),
3849            None,
3850            data_hash(&json!({"a":1})),
3851        );
3852        let h = base.entry_hash();
3853        let mut e = base.clone();
3854        e.id = gov(2);
3855        assert_ne!(e.entry_hash(), h);
3856        let mut e = base.clone();
3857        e.entry_type = GovernanceLogEntryType::PolicyChange;
3858        assert_ne!(e.entry_hash(), h);
3859        let mut e = base.clone();
3860        e.created_at += 1;
3861        assert_ne!(e.entry_hash(), h);
3862        let mut e = base.clone();
3863        e.prev_hash = Some(h);
3864        assert_ne!(e.entry_hash(), h);
3865        let mut e = base.clone();
3866        e.data_hash = data_hash(&json!({"a":2}));
3867        assert_ne!(e.entry_hash(), h);
3868        assert_eq!(base.entry_hash(), h, "and it is deterministic");
3869    }
3870
3871    #[test]
3872    fn truncation_matches_what_the_envelope_carries() {
3873        let t = at(0);
3874        assert_eq!(truncate_to_micros(t).timestamp_subsec_nanos(), 123_456_000);
3875        assert_eq!(truncate_to_seconds(t).timestamp_subsec_nanos(), 0);
3876        assert_eq!(
3877            Envelope::new(
3878                gov(1),
3879                GovernanceLogEntryType::CouncilDecision,
3880                t,
3881                None,
3882                data_hash(&json!(null))
3883            )
3884            .created_at,
3885            truncate_to_micros(t).timestamp_micros()
3886        );
3887    }
3888
3889    // -- hex newtypes --
3890
3891    #[test]
3892    fn hex_newtypes_round_trip_and_reject_wrong_lengths() {
3893        let h = data_hash(&json!(1));
3894        let s = serde_json::to_string(&h).unwrap();
3895        assert_eq!(s.len(), 66);
3896        let back: Sha256Hex = serde_json::from_str(&s).unwrap();
3897        assert_eq!(back, h);
3898        assert!(serde_json::from_str::<Sha256Hex>("\"abcd\"").is_err());
3899        assert!("zz".repeat(32).parse::<Sha256Hex>().is_err());
3900        assert!(Sha256Hex::try_from(vec![0u8; 31]).is_err());
3901        assert_eq!(format!("{h:?}"), format!("Sha256Hex({h})"));
3902    }
3903
3904    // -- signing and verification --
3905
3906    #[test]
3907    fn attest_then_verify_link() {
3908        let (key, pk) = generate_keypair();
3909        let l = link(&key, 1, None, &json!({"a": 1}), at(5));
3910        assert_eq!(verify_link(&l, &pk), Ok(()));
3911        assert!(verify_data(&l, &json!({"a": 1})));
3912        assert!(!verify_data(&l, &json!({"a": 2})));
3913        assert!(!l.attestation.retroactive);
3914    }
3915
3916    #[test]
3917    fn wrong_key_fails_signature_only() {
3918        let (key, _) = generate_keypair();
3919        let (_, other) = generate_keypair();
3920        let l = link(&key, 1, None, &json!({}), at(5));
3921        assert_eq!(verify_link(&l, &other), Err(LinkError::BadSignature));
3922    }
3923
3924    #[test]
3925    fn tampering_with_any_attested_field_is_detected() {
3926        let (key, pk) = generate_keypair();
3927        let l = link(&key, 1, None, &json!({"a": 1}), at(5));
3928
3929        let mut t = l.clone();
3930        t.created_at += chrono::Duration::microseconds(1);
3931        assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
3932
3933        let mut t = l.clone();
3934        t.entry_type = GovernanceLogEntryType::StewardVeto;
3935        assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
3936
3937        let mut t = l.clone();
3938        t.attestation.data_hash = data_hash(&json!({"a": 2}));
3939        assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
3940
3941        // Back-dating the signature: the hash still recomputes, but the
3942        // signature covered the real signed_at.
3943        let mut t = l.clone();
3944        t.attestation.signed_at -= chrono::Duration::seconds(1);
3945        assert_eq!(verify_link(&t, &pk), Err(LinkError::BadSignature));
3946
3947        let mut t = l.clone();
3948        t.attestation.envelope_version = 2;
3949        assert_eq!(verify_link(&t, &pk), Err(LinkError::UnsupportedVersion(2)));
3950    }
3951
3952    #[test]
3953    fn a_good_chain_verifies_in_any_input_order() {
3954        let (key, pk) = generate_keypair();
3955        let mut c = chain(&key, 4);
3956        c.reverse();
3957        let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
3958        assert!(v.ok, "{v:#?}");
3959        assert_eq!(v.head, Some(gov(4)));
3960        assert_eq!(
3961            v.entries.iter().map(|e| e.chain_seq).collect::<Vec<_>>(),
3962            [1, 2, 3, 4]
3963        );
3964        assert!(v.entries.iter().all(|e| e.content_matches.is_none()
3965            && e.problem.is_none()
3966            && !e.out_of_order));
3967        assert_eq!(v.public_key, PublicKeyHex::from(&pk));
3968    }
3969
3970    #[test]
3971    fn empty_chain_is_ok_with_no_head() {
3972        let (_, pk) = generate_keypair();
3973        let v = verify_chain(&[], &pk, &anchored(&pk), &roots());
3974        assert!(v.ok);
3975        assert!(v.head.is_none());
3976        assert!(v.entries.is_empty());
3977    }
3978
3979    #[test]
3980    fn a_changed_entry_breaks_its_signature_and_the_next_link() {
3981        let (key, pk) = generate_keypair();
3982        let mut c = chain(&key, 3);
3983        // Re-attest entry 2 with different data but the same predecessor,
3984        // as a key holder rewriting history would.
3985        let rewritten = link(
3986            &key,
3987            2,
3988            Some(&c[0]),
3989            &json!({"title": "Decision 2", "outcome": "REJECTED"}),
3990            at(21),
3991        );
3992        c[1] = rewritten;
3993        let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
3994        assert!(!v.ok);
3995        assert!(
3996            v.entries[1].signature_valid && v.entries[1].link_valid,
3997            "the rewrite itself is well-formed: {:#?}",
3998            v.entries[1]
3999        );
4000        assert!(
4001            !v.entries[2].link_valid,
4002            "but entry 3 no longer points at it: {:#?}",
4003            v.entries[2]
4004        );
4005        assert!(
4006            v.entries[2]
4007                .problem
4008                .as_deref()
4009                .unwrap()
4010                .contains("prev_hash")
4011        );
4012    }
4013
4014    #[test]
4015    fn a_removed_entry_is_a_gap_and_a_broken_link() {
4016        let (key, pk) = generate_keypair();
4017        let mut c = chain(&key, 3);
4018        c.remove(1);
4019        let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
4020        assert!(!v.ok);
4021        assert!(v.entries[0].link_valid);
4022        let p = v.entries[1].problem.as_deref().unwrap();
4023        assert!(p.contains("chain_seq 3 where 2 was expected"), "{p}");
4024        assert!(p.contains("prev_hash"), "{p}");
4025    }
4026
4027    /// `verify_governance_log`'s default: counts, the failures by id and
4028    /// the first in full, never one row per entry (beta round, 2026-10-15)
4029    #[test]
4030    fn a_summary_counts_and_names_the_failures() {
4031        let (key, pk) = generate_keypair();
4032        let clean =
4033            verify_chain(&chain(&key, 3), &pk, &anchored(&pk), &roots());
4034        let summary = clean.summary();
4035        assert!(summary.ok);
4036        assert_eq!((summary.entries, summary.failed.len()), (3, 0));
4037        assert_eq!(summary.head, clean.head);
4038        assert!(summary.first_failure.is_none());
4039        assert!(summary.how_to_read.contains("full=true"));
4040
4041        let mut c = chain(&key, 3);
4042        c.remove(1);
4043        let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
4044        let summary = v.summary();
4045        assert!(!summary.ok);
4046        assert_eq!(summary.link_failures, 1);
4047        assert_eq!(summary.failed, vec![v.entries[1].id.clone()]);
4048        assert_eq!(summary.first_failure.as_ref(), Some(&v.entries[1]));
4049        let json = serde_json::to_value(&summary).unwrap();
4050        assert!(json["entries"].is_u64(), "a count, not the rows: {json}");
4051    }
4052
4053    #[test]
4054    fn a_second_genesis_is_rejected() {
4055        let (key, pk) = generate_keypair();
4056        let mut c = chain(&key, 2);
4057        let rogue = link(&key, 2, None, &json!({}), at(21));
4058        c[1] = rogue;
4059        let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
4060        assert!(!v.ok);
4061        assert!(
4062            v.entries[1]
4063                .problem
4064                .as_deref()
4065                .unwrap()
4066                .contains("null mid-chain")
4067        );
4068    }
4069
4070    #[test]
4071    fn retroactive_and_out_of_order_are_recomputed_not_copied() {
4072        let (key, pk) = generate_keypair();
4073        let first = link(&key, 1, None, &json!({}), at(10 + 3600));
4074        // Second entry recorded *before* the first by the clock, but after it
4075        // in the chain. Valid chain, flagged order.
4076        let created = truncate_to_micros(at(5));
4077        let envelope = Envelope::new(
4078            gov(2),
4079            GovernanceLogEntryType::CouncilDecision,
4080            created,
4081            Some(first.attestation.entry_hash),
4082            data_hash(&json!({})),
4083        );
4084        let attestation = attest(&key, &envelope, 2, at(6));
4085        let mut second = GovernanceChainLink {
4086            id: gov(2),
4087            entry_type: GovernanceLogEntryType::CouncilDecision,
4088            created_at: created,
4089            attestation,
4090            data: None,
4091            texts: None,
4092        };
4093        second.attestation.retroactive = true; // a lying flag on the wire
4094        let v = verify_chain(&[first, second], &pk, &anchored(&pk), &roots());
4095        assert!(v.ok, "{v:#?}");
4096        assert!(v.entries[0].retroactive);
4097        assert!(!v.entries[1].retroactive, "recomputed from timestamps");
4098        assert!(v.entries[1].out_of_order);
4099    }
4100
4101    #[test]
4102    fn content_mismatch_settles_to_not_ok() {
4103        let (key, pk) = generate_keypair();
4104        let c = chain(&key, 1);
4105        let mut v = verify_chain(&c, &pk, &anchored(&pk), &roots());
4106        v.entries[0].content_matches = Some(true);
4107        assert!(v.clone().settle().ok);
4108        v.entries[0].content_matches = Some(false);
4109        assert!(!v.settle().ok);
4110    }
4111
4112    // -- amendments --
4113
4114    #[test]
4115    fn an_amendment_fills_amended_by_on_its_target() {
4116        let (key, pk) = generate_keypair();
4117        let mut c = Chain::new();
4118        let target = c.decision(&key);
4119        c.decision(&key);
4120        let amendment = AmendmentDraft::new(
4121            target,
4122            c.hash_at(1),
4123            AmendmentKind::NonPrecedential,
4124            "§1 (Red Team Cases Recharacterized)",
4125            "diagnostic finding — not citable as moderation precedent",
4126        )
4127        .unwrap()
4128        .with_authority(gov(5))
4129        .with_rationale("§5 leaves the ruling itself standing");
4130        let id = c.amend(&key, &amendment);
4131
4132        let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4133        assert!(v.ok, "{v:#?}");
4134        assert_eq!(v.entries[0].amended_by, vec![id]);
4135        assert!(v.entries[1].amended_by.is_empty());
4136        assert!(!v.entries[0].redacted);
4137        assert_eq!(v.head, Some(amd(1)));
4138        // The amendment link carries `data`, so its own content is checked.
4139        assert_eq!(v.entries[2].content_matches, Some(true));
4140        assert_eq!(v.entries[0].content_matches, None);
4141        assert_eq!(
4142            standing([amendment.amendment.kind]),
4143            Standing::NonPrecedential
4144        );
4145    }
4146
4147    #[test]
4148    fn an_amendment_must_name_an_earlier_entry_by_its_exact_hash() {
4149        let (key, pk) = generate_keypair();
4150        let problem = |c: &Chain, at: usize| -> String {
4151            verify_chain(&c.links, &pk, &anchored(&pk), &roots()).entries[at]
4152                .problem
4153                .clone()
4154                .unwrap_or_default()
4155        };
4156
4157        let mut c = Chain::new();
4158        c.decision(&key);
4159        let unknown = AmendmentDraft::new(
4160            gov(99),
4161            c.hash_at(1),
4162            AmendmentKind::Overruled,
4163            "b",
4164            "n",
4165        )
4166        .unwrap();
4167        c.amend(&key, &unknown);
4168        assert!(
4169            problem(&c, 1).contains("is not an entry of this chain"),
4170            "{}",
4171            problem(&c, 1)
4172        );
4173        assert!(!verify_chain(&c.links, &pk, &anchored(&pk), &roots()).ok);
4174
4175        // Names an entry that does not exist yet.
4176        let mut c = Chain::new();
4177        c.decision(&key);
4178        let forward = AmendmentDraft::new(
4179            gov(2),
4180            c.hash_at(1),
4181            AmendmentKind::Overruled,
4182            "b",
4183            "n",
4184        )
4185        .unwrap();
4186        c.amend(&key, &forward);
4187        c.decision(&key);
4188        assert!(
4189            problem(&c, 1).contains("not an earlier entry"),
4190            "{}",
4191            problem(&c, 1)
4192        );
4193
4194        // Right id, wrong entry.
4195        let mut c = Chain::new();
4196        let target = c.decision(&key);
4197        let wrong_hash = AmendmentDraft::new(
4198            target,
4199            data_hash(&json!("some other entry")),
4200            AmendmentKind::Overruled,
4201            "b",
4202            "n",
4203        )
4204        .unwrap();
4205        c.amend(&key, &wrong_hash);
4206        assert!(problem(&c, 1).contains("entry_hash"), "{}", problem(&c, 1));
4207        assert!(
4208            verify_chain(&c.links, &pk, &anchored(&pk), &roots()).entries[0]
4209                .amended_by
4210                .is_empty()
4211        );
4212    }
4213
4214    #[test]
4215    fn redaction_shape_violations_fail_verification() {
4216        let (key, pk) = generate_keypair();
4217        let amend_with = |mutate: &dyn Fn(&mut Amendment)| -> String {
4218            let mut c = Chain::new();
4219            let target = c.decision(&key);
4220            let mut amendment = AmendmentDraft::new(
4221                target,
4222                c.hash_at(1),
4223                AmendmentKind::Correction,
4224                "b",
4225                "n",
4226            )
4227            .unwrap();
4228            mutate(&mut amendment.amendment);
4229            c.amend_v1(&key, &amendment.amendment);
4230            let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4231            assert!(!v.ok, "{v:#?}");
4232            v.entries[1].problem.clone().unwrap_or_default()
4233        };
4234
4235        // `redaction` is the only way to get a well-formed one, so a
4236        // redaction kind without a `Redaction` can only be hand-built.
4237        assert_eq!(
4238            AmendmentDraft::new(
4239                gov(1),
4240                data_hash(&json!(null)),
4241                AmendmentKind::Redaction,
4242                "b",
4243                "n"
4244            ),
4245            Err(AmendmentError::MissingRedaction)
4246        );
4247        let p = amend_with(&|a| a.kind = AmendmentKind::Redaction);
4248        assert!(p.contains("requires a `redaction`"), "{p}");
4249
4250        let p = amend_with(&|a| {
4251            a.redaction = Some(Redaction {
4252                fields: vec!["/x".into()],
4253                resulting_data_hash: data_hash(&json!({})),
4254                resulting_latest_hash: None,
4255            })
4256        });
4257        assert!(p.contains("only valid on kind"), "{p}");
4258
4259        let p = amend_with(&|a| a.agora_governance_amendment = 3);
4260        assert!(p.contains("agora_governance_amendment is 3"), "{p}");
4261
4262        // A version says where the texts are, and both ways round it is
4263        // held to it.
4264        let p = amend_with(&|a| a.agora_governance_amendment = 1);
4265        assert!(p.contains("does neither consistently"), "{p}");
4266        let p = amend_with(&|a| a.note = AmendmentText::Plain("n".into()));
4267        assert!(p.contains("does neither consistently"), "{p}");
4268    }
4269
4270    #[test]
4271    fn governance_data_holds_no_number_that_is_not_a_64_bit_integer() {
4272        let fine = json!({"a": [1, -2, u64::MAX, i64::MIN], "b": {"c": "0.5"}});
4273        assert_eq!(non_integer_number(&fine), None);
4274        assert!(blind_data(&fine, Blind::random()).is_ok());
4275
4276        for (text, pointer) in [
4277            (r#"{"a": {"b/c": [1, 0.5]}}"#, "/a/b~1c/1"),
4278            (r#"{"n": 1.0}"#, "/n"),
4279            (r#"{"n": 1e3}"#, "/n"),
4280            (r#"{"n": 18446744073709551616}"#, "/n"),
4281            (r#"{"n": -9223372036854775809}"#, "/n"),
4282        ] {
4283            let data: serde_json::Value = serde_json::from_str(text).unwrap();
4284            assert_eq!(non_integer_number(&data).as_deref(), Some(pointer));
4285            assert_eq!(
4286                blind_data(&data, Blind::random()),
4287                Err(BlindError::NonIntegerNumber(pointer.into())),
4288                "the writer refuses it"
4289            );
4290            assert_eq!(
4291                redact_data(&data, &["/n".into()], &amd(1), Blind::random()),
4292                Err(RedactError::NonIntegerNumber(pointer.into()))
4293            );
4294        }
4295    }
4296
4297    #[test]
4298    fn standing_is_the_last_amendment_that_changes_it() {
4299        use AmendmentKind::*;
4300        assert_eq!(standing([]), Standing::InForce);
4301        assert_eq!(standing([Correction, Redaction]), Standing::InForce);
4302        assert_eq!(
4303            standing([Correction, NonPrecedential, Redaction]),
4304            Standing::NonPrecedential
4305        );
4306        assert_eq!(standing([Overruled, Reinstated]), Standing::InForce);
4307        assert_eq!(standing([Reinstated, Superseded]), Standing::Superseded);
4308        assert_eq!(kind_standing(Reattested), None);
4309        assert_eq!(kind_standing(Reinstated), Some(Standing::InForce));
4310    }
4311
4312    #[test]
4313    fn a_redaction_verifies_against_the_amendment_and_nothing_else() {
4314        let (key, pk) = generate_keypair();
4315        let data = json!({
4316            "finding": "upheld",
4317            "subject": {"handle": "someone", "detail": "personal"},
4318        });
4319        let mut c = Chain::new();
4320        let target = c.entry(&key, data.clone());
4321        let amendment_id = c.next_amd();
4322        let (amendment, redacted) = AmendmentDraft::redaction(
4323            &amendment_id,
4324            target,
4325            c.hash_at(1),
4326            "GDPR Art. 17(1)(a)",
4327            "personal data removed on request",
4328            vec!["/subject/handle".into(), "/subject/detail".into()],
4329            &data,
4330            Blind::from([7; 32]),
4331            &[],
4332        )
4333        .unwrap();
4334        assert_eq!(c.amend(&key, &amendment), amendment_id);
4335        assert_eq!(redacted[BLIND_KEY], json!(Blind::from([7; 32])));
4336
4337        let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4338        assert!(v.ok, "{v:#?}");
4339        assert!(v.entries[0].redacted);
4340        assert_eq!(v.entries[0].redacted_data_hash, Some(data_hash(&redacted)));
4341        assert_eq!(
4342            redacted["subject"]["handle"],
4343            json!(format!("[redacted by {amendment_id}]"))
4344        );
4345        assert_eq!(redacted["finding"], json!("upheld"), "and nothing else");
4346
4347        // What the server now serves verifies.
4348        assert!(v.check_content(&c.links[0], &redacted));
4349        assert_eq!(v.entries[0].content_matches, Some(true));
4350        assert!(v.clone().settle().ok);
4351        // So does the original, for anyone who kept a copy.
4352        assert!(v.check_content(&c.links[0], &data));
4353
4354        // A second edit does not.
4355        let mut tampered = redacted.clone();
4356        tampered["finding"] = json!("overturned");
4357        assert!(!v.check_content(&c.links[0], &tampered));
4358        assert_eq!(v.entries[0].content_matches, Some(false));
4359        assert!(!v.settle().ok);
4360    }
4361
4362    /// A Council record with a seat whose `raw_text` repeats its
4363    /// `rationale`, and one whose does not
4364    fn seats() -> serde_json::Value {
4365        json!({
4366            "title": "A motion",
4367            "rounds": [{
4368                "number": 1,
4369                "responses": [
4370                    {"role": "lawyer", "rationale": "Because.", "raw_text": "Because.", "vote": "yes"},
4371                    {"role": "artist", "rationale": "Why not.", "raw_text": "Why not?", "vote": "no"},
4372                ],
4373            }],
4374            "subject": {"handle": "someone"},
4375            BLIND_KEY: Blind::from([3; 32]),
4376        })
4377    }
4378
4379    const LAWYER: &str = "/rounds/0/responses/0";
4380
4381    /// An RFC 6902 patch from its JSON
4382    fn patch(ops: serde_json::Value) -> json_patch::Patch {
4383        serde_json::from_value(ops).unwrap()
4384    }
4385
4386    /// The lawyer's `raw_text` removed as a duplicate of the rationale
4387    fn dedup(data: &serde_json::Value) -> Edit {
4388        let (raw, rationale) =
4389            (format!("{LAWYER}/raw_text"), format!("{LAWYER}/rationale"));
4390        Revision::remove_duplicates(data, &[(&raw, &rationale)]).unwrap()
4391    }
4392
4393    fn revise(
4394        c: &Chain,
4395        target: &GovernanceLogId,
4396        latest: &serde_json::Value,
4397        patch: impl Into<Edit>,
4398    ) -> (AmendmentDraft, serde_json::Value) {
4399        AmendmentDraft::revision(
4400            target.clone(),
4401            GovernanceLogEntryType::CouncilDecision,
4402            c.hash_at(1),
4403            "Steward's record REC-2026-0001",
4404            "raw_text identical to the rationale removed",
4405            latest,
4406            patch,
4407        )
4408        .unwrap()
4409    }
4410
4411    fn revision_of(draft: &AmendmentDraft) -> &Revision {
4412        draft.amendment.revision.as_ref().unwrap()
4413    }
4414
4415    #[test]
4416    fn a_revision_overwrites_nothing_and_folds_to_the_latest() {
4417        let (key, pk) = generate_keypair();
4418        let data = seats();
4419        let mut c = Chain::new();
4420        let target = c.entry(&key, data.clone());
4421        let (first, v1) = revise(&c, &target, &data, dedup(&data));
4422        c.amend(&key, &first);
4423        let (second, v2) = revise(
4424            &c,
4425            &target,
4426            &v1,
4427            patch(json!([{"op": "move", "from": "/title", "path": "/motion"}])),
4428        );
4429        c.amend(&key, &second);
4430
4431        let seat = &v1["rounds"][0]["responses"];
4432        assert!(seat[0].get("raw_text").is_none(), "removed, no marker");
4433        assert_eq!(seat[1]["raw_text"], json!("Why not?"), "the other stays");
4434        assert_eq!(v2["motion"], json!("A motion"));
4435        assert_eq!(v2[BLIND_KEY], data[BLIND_KEY], "blind kept");
4436        assert_eq!(
4437            latest(&data, [revision_of(&first), revision_of(&second)]).unwrap(),
4438            v2
4439        );
4440
4441        let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4442        assert!(v.ok, "{v:#?}");
4443        let entry = &v.entries[0];
4444        assert_eq!(entry.revisions, [amd(1), amd(2)]);
4445        assert_eq!(entry.latest_data_hash, Some(data_hash(&v2)));
4446        assert!(!entry.redacted && entry.redacted_data_hash.is_none());
4447
4448        // The stored data is what was signed, and the latest checks too.
4449        assert!(v.check_content(&c.links[0], &data));
4450        assert!(v.check_content(&c.links[0], &v2));
4451        assert!(!v.check_content(&c.links[0], &v1), "not the latest");
4452        v.check_content(&c.links[0], &data);
4453        assert!(v.clone().settle().ok);
4454
4455        // The restoration history is not on the wire, and a report from
4456        // before 0.43 still parses.
4457        let wire = serde_json::to_value(&v).unwrap();
4458        assert!(wire["entries"][0].get("history").is_none());
4459        let mut old = wire.clone();
4460        for field in ["revisions", "latest_data_hash", "superseded_revisions"] {
4461            old["entries"][0].as_object_mut().unwrap().remove(field);
4462        }
4463        let old: GovernanceVerification = serde_json::from_value(old).unwrap();
4464        assert!(old.entries[0].revisions.is_empty());
4465    }
4466
4467    /// A revision claiming a hash its patch does not produce
4468    #[test]
4469    fn a_revision_that_does_not_produce_its_hash_fails() {
4470        let (key, pk) = generate_keypair();
4471        let data = seats();
4472        let mut c = Chain::new();
4473        let target = c.entry(&key, data.clone());
4474        let (mut draft, _) = revise(&c, &target, &data, dedup(&data));
4475        draft
4476            .amendment
4477            .revision
4478            .as_mut()
4479            .unwrap()
4480            .resulting_data_hash = data_hash(&json!({"something": "else"}));
4481        c.amend(&key, &draft);
4482
4483        let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4484        assert!(v.ok, "the chain itself is sound: {v:#?}");
4485        assert!(v.check_content(&c.links[0], &data), "the stored data is");
4486        assert!(
4487            v.entries[0]
4488                .problem
4489                .as_deref()
4490                .is_some_and(|p| p.contains("does not produce")),
4491            "{:?}",
4492            v.entries[0].problem
4493        );
4494        assert!(!v.settle().ok);
4495
4496        // The same, when the link carries its data.
4497        c.links[0].data = Some(data);
4498        let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4499        assert!(!v.ok);
4500    }
4501
4502    fn redact(
4503        c: &Chain,
4504        target: &GovernanceLogId,
4505        fields: &[&str],
4506        data: &serde_json::Value,
4507        revisions: &[&Revision],
4508    ) -> Result<(AmendmentDraft, serde_json::Value), RedactError> {
4509        AmendmentDraft::redaction(
4510            &c.next_amd(),
4511            target.clone(),
4512            c.hash_at(1),
4513            "GDPR Art. 17(1)(a)",
4514            "removed on request",
4515            fields.iter().map(|f| f.to_string()).collect(),
4516            data,
4517            Blind::from([9; 32]),
4518            revisions,
4519        )
4520    }
4521
4522    /// A redaction after a revision rebases it over the redacted data, and
4523    /// erases the duplicate the revision removed along with its source
4524    #[test]
4525    fn a_revision_then_a_redaction() {
4526        let (key, pk) = generate_keypair();
4527        let data = seats();
4528        let mut c = Chain::new();
4529        let target = c.entry(&key, data.clone());
4530        let (revision, _) = revise(&c, &target, &data, dedup(&data));
4531        c.amend(&key, &revision);
4532        let rationale = format!("{LAWYER}/rationale");
4533        let (redaction, redacted) = redact(
4534            &c,
4535            &target,
4536            &[&rationale],
4537            &data,
4538            &[revision_of(&revision)],
4539        )
4540        .unwrap();
4541        c.amend(&key, &redaction);
4542
4543        // The copy the revision removed is erased from the original too.
4544        let fields = &redaction.amendment.redaction.as_ref().unwrap().fields;
4545        assert_eq!(fields, &[rationale, format!("{LAWYER}/raw_text")]);
4546        let seat = &redacted["rounds"][0]["responses"][0];
4547        assert_eq!(seat["raw_text"], seat["rationale"]);
4548        assert!(seat["raw_text"].as_str().unwrap().starts_with("[redacted"));
4549
4550        let rebased = latest(&redacted, [revision_of(&revision)]).unwrap();
4551        assert!(
4552            rebased["rounds"][0]["responses"][0]
4553                .get("raw_text")
4554                .is_none()
4555        );
4556        assert_eq!(
4557            redaction
4558                .amendment
4559                .redaction
4560                .as_ref()
4561                .unwrap()
4562                .resulting_latest_hash,
4563            Some(data_hash(&rebased))
4564        );
4565
4566        let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4567        assert!(v.ok, "{v:#?}");
4568        let entry = &v.entries[0];
4569        assert_eq!(entry.latest_data_hash, Some(data_hash(&rebased)));
4570        assert_eq!(entry.superseded_revisions, [amd(1)]);
4571        assert!(v.check_content(&c.links[0], &redacted));
4572        assert!(v.check_content(&c.links[0], &rebased));
4573        assert!(
4574            v.check_content(&c.links[0], &data),
4575            "for whoever kept a copy"
4576        );
4577        v.check_content(&c.links[0], &redacted);
4578        assert!(v.clone().settle().ok, "{v:#?}");
4579
4580        // A redaction that lies about the rebase is caught.
4581        let mut c2 = Chain::new();
4582        let target = c2.entry(&key, data.clone());
4583        c2.amend(&key, &revision);
4584        let (mut lying, _) = redact(
4585            &c2,
4586            &target,
4587            &["/subject/handle"],
4588            &data,
4589            &[revision_of(&revision)],
4590        )
4591        .unwrap();
4592        let (_, honest) = redact(
4593            &c2,
4594            &target,
4595            &["/subject/handle"],
4596            &data,
4597            &[revision_of(&revision)],
4598        )
4599        .unwrap();
4600        lying
4601            .amendment
4602            .redaction
4603            .as_mut()
4604            .unwrap()
4605            .resulting_latest_hash = Some(data_hash(&json!({})));
4606        c2.amend(&key, &lying);
4607        let mut v = verify_chain(&c2.links, &pk, &anchored(&pk), &roots());
4608        assert!(v.check_content(&c2.links[0], &honest));
4609        assert!(!v.settle().ok);
4610    }
4611
4612    /// A redaction of a revised entry that does not say what the rebase
4613    /// gives leaves the rebased history checked against nothing
4614    #[test]
4615    fn a_redaction_of_a_revised_entry_must_name_the_latest_hash() {
4616        let (key, pk) = generate_keypair();
4617        let data = seats();
4618        let mut c = Chain::new();
4619        let target = c.entry(&key, data.clone());
4620        let (revision, _) = revise(&c, &target, &data, dedup(&data));
4621        c.amend(&key, &revision);
4622        // What a caller passing no revisions produces.
4623        let (redaction, _) =
4624            redact(&c, &target, &["/subject/handle"], &data, &[]).unwrap();
4625        c.amend(&key, &redaction);
4626        let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4627        assert!(!v.ok);
4628        assert!(
4629            v.entries[2]
4630                .problem
4631                .as_deref()
4632                .is_some_and(|p| p.contains("names no resulting_latest_hash")),
4633            "{v:#?}"
4634        );
4635    }
4636
4637    /// A revision's duplicates are checked against the version it applied
4638    /// to: one that removed a value unlike its `same_as` lost something
4639    #[test]
4640    fn a_false_duplicate_fails() {
4641        let (key, pk) = generate_keypair();
4642        let data = seats();
4643        let mut c = Chain::new();
4644        let target = c.entry(&key, data.clone());
4645        let artist = "/rounds/0/responses/1";
4646        let (raw, rationale) =
4647            (format!("{artist}/raw_text"), format!("{artist}/rationale"));
4648        // Hand-built: the builder refuses it.
4649        let (mut lying, _) = revise(
4650            &c,
4651            &target,
4652            &data,
4653            patch(json!([{"op": "remove", "path": raw}])),
4654        );
4655        let revised = apply_patch(&data, &revision_of(&lying).patch).unwrap();
4656        let r = lying.amendment.revision.as_mut().unwrap();
4657        r.duplicates = vec![(raw, rationale)];
4658        r.resulting_data_hash = data_hash(&revised);
4659        c.amend(&key, &lying);
4660
4661        let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4662        assert!(v.ok, "unread, the claim is unchecked: {v:#?}");
4663        assert!(v.check_content(&c.links[0], &data));
4664        assert!(
4665            v.entries[0]
4666                .problem
4667                .as_deref()
4668                .is_some_and(|p| p.contains("but they differ")),
4669            "{v:#?}"
4670        );
4671        assert!(!v.settle().ok);
4672    }
4673
4674    /// A redaction the revisions could not be rebased over is refused
4675    #[test]
4676    fn a_redaction_that_breaks_a_revision_is_refused() {
4677        let (key, _) = generate_keypair();
4678        let data = seats();
4679        let mut c = Chain::new();
4680        let target = c.entry(&key, data.clone());
4681        let (moved, _) = revise(
4682            &c,
4683            &target,
4684            &data,
4685            patch(
4686                json!([{"op": "move", "from": "/subject/handle", "path": "/handle"}]),
4687            ),
4688        );
4689        c.amend(&key, &moved);
4690        let err =
4691            redact(&c, &target, &["/subject"], &data, &[revision_of(&moved)])
4692                .unwrap_err();
4693        assert!(matches!(err, RedactError::Rebase(_)), "{err}");
4694        // Redacting the moved value itself rebases fine.
4695        assert!(
4696            redact(
4697                &c,
4698                &target,
4699                &["/subject/handle"],
4700                &data,
4701                &[revision_of(&moved)]
4702            )
4703            .is_ok()
4704        );
4705    }
4706
4707    /// Part of a source, or a whole one inside a redacted value, is
4708    /// followed to the same part of the copy
4709    #[test]
4710    fn a_redaction_follows_every_duplicate_of_what_it_erases() {
4711        let revision = Revision {
4712            patch: patch(json!([{"op": "remove", "path": "/copy"}])),
4713            duplicates: vec![("/copy".into(), "/source/inner".into())],
4714            resulting_data_hash: data_hash(&json!(null)),
4715        };
4716        let extra = |fields: &[&str]| {
4717            let fields: Vec<String> =
4718                fields.iter().map(|f| f.to_string()).collect();
4719            duplicates_of(&fields, &[&revision])
4720        };
4721        assert_eq!(extra(&["/source/inner"]), ["/copy"]);
4722        assert_eq!(extra(&["/source/inner/name"]), ["/copy/name"]);
4723        assert_eq!(extra(&["/source"]), ["/copy"]);
4724        assert!(extra(&["/source/other"]).is_empty());
4725        assert!(extra(&["/source/inn"]).is_empty());
4726    }
4727
4728    /// A revision after a redaction applies to what the redaction left,
4729    /// and is checked against it
4730    #[test]
4731    fn a_redaction_then_a_revision() {
4732        let (key, pk) = generate_keypair();
4733        let data = seats();
4734        let mut c = Chain::new();
4735        let target = c.entry(&key, data.clone());
4736        let (redaction, redacted) =
4737            redact(&c, &target, &["/subject/handle"], &data, &[]).unwrap();
4738        assert_eq!(
4739            redaction
4740                .amendment
4741                .redaction
4742                .as_ref()
4743                .unwrap()
4744                .resulting_latest_hash,
4745            None,
4746            "nothing to rebase"
4747        );
4748        c.amend(&key, &redaction);
4749        let (revision, revised) =
4750            revise(&c, &target, &redacted, dedup(&redacted));
4751        c.amend(&key, &revision);
4752
4753        let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4754        assert!(v.ok, "{v:#?}");
4755        assert_eq!(v.entries[0].latest_data_hash, Some(data_hash(&revised)));
4756        assert!(v.entries[0].superseded_revisions.is_empty());
4757        assert!(v.check_content(&c.links[0], &redacted));
4758        assert!(v.check_content(&c.links[0], &revised));
4759        assert!(v.settle().ok);
4760    }
4761
4762    #[test]
4763    fn a_revision_refuses_what_it_should() {
4764        let data = seats();
4765        let raw = &format!("{LAWYER}/raw_text");
4766        let rationale = &format!("{LAWYER}/rationale");
4767        let artist = "/rounds/0/responses/1";
4768        let dedup =
4769            |pairs: &[(&str, &str)]| Revision::remove_duplicates(&data, pairs);
4770
4771        assert_eq!(
4772            dedup(&[(
4773                &format!("{artist}/raw_text"),
4774                &format!("{artist}/rationale")
4775            )]),
4776            Err(ReviseError::NotIdentical {
4777                path: format!("{artist}/raw_text"),
4778                same_as: format!("{artist}/rationale"),
4779            })
4780        );
4781        assert_eq!(
4782            dedup(&[(raw, rationale), (rationale, raw)]),
4783            Err(ReviseError::SourceRemoved {
4784                path: raw.clone(),
4785                same_as: rationale.clone(),
4786            }),
4787            "a same_as the patch removes"
4788        );
4789        assert_eq!(
4790            dedup(&[("/nope", rationale)]),
4791            Err(ReviseError::Unresolved("/nope".into()))
4792        );
4793
4794        let make = |target_type, p: Edit| {
4795            AmendmentDraft::revision(
4796                gov(1),
4797                target_type,
4798                data_hash(&json!(null)),
4799                "b",
4800                "n",
4801                &data,
4802                p,
4803            )
4804            .map(|(_, v)| v)
4805        };
4806        let council = GovernanceLogEntryType::CouncilDecision;
4807        assert_eq!(
4808            make(council, patch(json!([])).into()),
4809            Err(ReviseError::EmptyPatch)
4810        );
4811        assert!(matches!(
4812            make(
4813                council,
4814                patch(json!([{"op": "remove", "path": "/nope"}])).into()
4815            ),
4816            Err(ReviseError::Patch(_))
4817        ));
4818        assert_eq!(
4819            make(
4820                council,
4821                patch(json!([{"op": "remove", "path": "/_blind"}])).into()
4822            ),
4823            Err(ReviseError::BlindPointer("/_blind".into()))
4824        );
4825        assert_eq!(
4826            make(
4827                council,
4828                patch(json!([{"op": "copy", "from": "/_blind", "path": "/b"}]))
4829                    .into()
4830            ),
4831            Err(ReviseError::BlindPointer("/_blind".into()))
4832        );
4833        for entry_type in [
4834            GovernanceLogEntryType::Amendment,
4835            GovernanceLogEntryType::KeyRotation,
4836            GovernanceLogEntryType::StewardRecord,
4837        ] {
4838            assert!(!is_revisable(entry_type));
4839            assert_eq!(
4840                make(entry_type, dedup(&[(raw, rationale)]).unwrap()),
4841                Err(ReviseError::NotRevisable(entry_type))
4842            );
4843        }
4844        assert!(is_revisable(council));
4845    }
4846
4847    /// Added content blinds an unblinded target; nothing else does
4848    #[test]
4849    fn a_revision_that_adds_content_blinds_the_target() {
4850        let unblinded =
4851            json!({"title": "Old", "rationale": "r", "raw_text": "r"});
4852        let revise = |data: &serde_json::Value, p: serde_json::Value| {
4853            AmendmentDraft::revision(
4854                gov(1),
4855                GovernanceLogEntryType::CouncilDecision,
4856                data_hash(&json!(null)),
4857                "b",
4858                "n",
4859                data,
4860                patch(p),
4861            )
4862            .unwrap()
4863        };
4864        let add = json!([{"op": "add", "path": "/attachments", "value": []}]);
4865        let (draft, revised) = revise(&unblinded, add.clone());
4866        assert!(revised.get(BLIND_KEY).is_some());
4867        assert_eq!(revision_of(&draft).patch.len(), 2, "in the same patch");
4868        assert_eq!(
4869            latest(&unblinded, [revision_of(&draft)]).unwrap(),
4870            revised,
4871            "so the fold reproduces it"
4872        );
4873
4874        let (_, revised) =
4875            revise(&unblinded, json!([{"op": "remove", "path": "/raw_text"}]));
4876        assert!(revised.get(BLIND_KEY).is_none(), "a removal adds nothing");
4877
4878        let (draft, revised) = revise(&seats(), add);
4879        assert_eq!(revised[BLIND_KEY], seats()[BLIND_KEY], "never rotated");
4880        assert_eq!(revision_of(&draft).patch.len(), 1);
4881    }
4882
4883    #[test]
4884    fn revision_shape_violations_fail_verification() {
4885        let (key, pk) = generate_keypair();
4886        let amend_with = |mutate: &dyn Fn(&mut Amendment)| -> String {
4887            let mut c = Chain::new();
4888            let target = c.entry(&key, seats());
4889            let (mut draft, _) = revise(&c, &target, &seats(), dedup(&seats()));
4890            mutate(&mut draft.amendment);
4891            c.amend_v1(&key, &draft.amendment);
4892            let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4893            assert!(!v.ok, "{v:#?}");
4894            v.entries[1].problem.clone().unwrap_or_default()
4895        };
4896        assert_eq!(
4897            AmendmentDraft::new(
4898                gov(1),
4899                data_hash(&json!(null)),
4900                AmendmentKind::Revision,
4901                "b",
4902                "n"
4903            ),
4904            Err(AmendmentError::MissingRevision)
4905        );
4906        let p = amend_with(&|a| a.revision = None);
4907        assert!(p.contains("requires a `revision`"), "{p}");
4908        let p = amend_with(&|a| a.kind = AmendmentKind::Correction);
4909        assert!(p.contains("only valid on kind `revision`"), "{p}");
4910        let p = amend_with(&|a| a.revision.as_mut().unwrap().patch.0.clear());
4911        assert!(p.contains("at least one op"), "{p}");
4912    }
4913
4914    /// Hand-written, so pinned: `wire_schemas_are_ref_free` covers `$ref`
4915    #[cfg(feature = "schemars")]
4916    #[test]
4917    fn the_revision_schema_describes_a_patch() {
4918        let schema = crate::responses::inline_schema_for::<Amendment>();
4919        let revision = &schema["properties"]["revision"]["properties"];
4920        assert_eq!(revision["patch"]["type"], json!("array"), "{schema}");
4921        assert_eq!(
4922            revision["patch"]["items"]["properties"]["op"]["enum"],
4923            json!(["add", "remove", "replace", "move", "copy", "test"])
4924        );
4925        assert_eq!(revision["resulting_data_hash"]["type"], json!("string"));
4926    }
4927
4928    #[test]
4929    fn content_that_matches_nothing_fails_without_an_amendment() {
4930        let (key, pk) = generate_keypair();
4931        let mut c = Chain::new();
4932        c.entry(&key, json!({"a": 1}));
4933        let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4934        assert!(!v.check_content(&c.links[0], &json!({"a": 2})));
4935        assert!(!v.clone().settle().ok);
4936        // An entry that is not in the report at all is not a pass either.
4937        let other = link(&key, 9, None, &json!({}), at(9));
4938        assert!(!v.check_content(&other, &json!({})));
4939    }
4940
4941    #[test]
4942    fn tampering_with_an_amendments_data_is_caught_by_the_data_hash() {
4943        let (key, pk) = generate_keypair();
4944        let mut c = Chain::new();
4945        let target = c.decision(&key);
4946        let amendment = AmendmentDraft::new(
4947            target,
4948            c.hash_at(1),
4949            AmendmentKind::Overruled,
4950            "b",
4951            "overruled by a later decision",
4952        )
4953        .unwrap();
4954        c.amend(&key, &amendment);
4955        c.links[1].data.as_mut().unwrap()["note"] =
4956            json!("reinstated, actually");
4957
4958        let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4959        assert!(!v.ok, "{v:#?}");
4960        let p = v.entries[1].problem.as_deref().unwrap();
4961        assert!(p.contains("does not hash to the attested data_hash"), "{p}");
4962        assert!(
4963            v.entries[0].amended_by.is_empty(),
4964            "an unreadable amendment has no effect"
4965        );
4966        assert!(
4967            v.entries[1].signature_valid && v.entries[1].link_valid,
4968            "the envelope is untouched — only the content is not what it \
4969             committed to"
4970        );
4971    }
4972
4973    #[test]
4974    fn an_amendment_or_rotation_must_carry_its_data() {
4975        let (key, pk) = generate_keypair();
4976        let mut c = Chain::new();
4977        let target = c.decision(&key);
4978        let amendment = AmendmentDraft::new(
4979            target,
4980            c.hash_at(1),
4981            AmendmentKind::Correction,
4982            "b",
4983            "n",
4984        )
4985        .unwrap();
4986        c.amend(&key, &amendment);
4987        let rotation = c.routine(&pk, &generate_keypair().0);
4988        c.rotate(&key, &rotation);
4989        c.links[1].data = None;
4990        c.links[2].data = None;
4991
4992        let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4993        assert!(!v.ok, "{v:#?}");
4994        for (i, entry_type) in [(1, "amendment"), (2, "key_rotation")] {
4995            let p = v.entries[i].problem.as_deref().unwrap();
4996            assert!(p.contains(&format!("a {entry_type} entry")), "{p}");
4997            assert!(p.contains("must carry its `data`"), "{p}");
4998        }
4999    }
5000
5001    #[test]
5002    fn the_id_series_must_match_the_entry_type() {
5003        let (key, pk) = generate_keypair();
5004        let mut c = Chain::new();
5005        let target = c.decision(&key);
5006        let amendment = AmendmentDraft::new(
5007            target,
5008            c.hash_at(1),
5009            AmendmentKind::Correction,
5010            "b",
5011            "n",
5012        )
5013        .unwrap();
5014        c.push(
5015            &key,
5016            gov(7),
5017            GovernanceLogEntryType::Amendment,
5018            serde_json::to_value(&amendment.amendment).unwrap(),
5019            true,
5020        );
5021        let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
5022        assert!(!v.ok, "{v:#?}");
5023        let p = v.entries[1].problem.as_deref().unwrap();
5024        assert!(p.contains("must be in the AMD- series"), "{p}");
5025
5026        let mut c = Chain::new();
5027        c.push(
5028            &key,
5029            key_id(1),
5030            GovernanceLogEntryType::CouncilDecision,
5031            json!({}),
5032            false,
5033        );
5034        let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
5035        let p = v.entries[0].problem.as_deref().unwrap();
5036        assert!(p.contains("reserved"), "{p}");
5037    }
5038
5039    #[test]
5040    fn redact_data_replaces_whole_values_and_refuses_the_rest() {
5041        let id = amd(3);
5042        let blind = Blind::from([9; 32]);
5043        let data = json!({"a": {"b": [1, {"c": "secret"}]}, "d/e": "slash"});
5044        let out = redact_data(
5045            &data,
5046            &["/a/b/1/c".into(), "/d~1e".into()],
5047            &id,
5048            blind,
5049        )
5050        .unwrap();
5051        assert_eq!(out["a"]["b"][1]["c"], json!(redaction_marker(&id)));
5052        assert_eq!(out["d/e"], json!(redaction_marker(&id)));
5053        assert_eq!(out["a"]["b"][0], json!(1), "untouched");
5054        assert_eq!(out[BLIND_KEY], json!(blind), "a legacy entry gains one");
5055
5056        assert_eq!(
5057            redact_data(&data, &["/a/nope".into()], &id, blind),
5058            Err(RedactError::Unresolved("/a/nope".into()))
5059        );
5060        assert_eq!(
5061            redact_data(&data, &["".into()], &id, blind),
5062            Err(RedactError::WholeEntry)
5063        );
5064        assert_eq!(
5065            redact_data(&data, &[], &id, blind),
5066            Err(RedactError::NoFields)
5067        );
5068        assert_eq!(
5069            redact_data(&data, &["/_blind".into()], &id, blind),
5070            Err(RedactError::BlindPointer("/_blind".into()))
5071        );
5072    }
5073
5074    #[test]
5075    fn blind_data_is_for_objects_and_is_the_writers_to_supply() {
5076        let blind = Blind::from([1; 32]);
5077        let out = blind_data(&json!({"finding": "upheld"}), blind).unwrap();
5078        assert_eq!(out, json!({"finding": "upheld", "_blind": blind}));
5079        assert_eq!(
5080            blind_data(&json!([1]), blind),
5081            Err(BlindError::NotAnObject)
5082        );
5083        assert_eq!(blind_data(&out, blind), Err(BlindError::AlreadyBlinded));
5084        assert_ne!(Blind::random(), Blind::random());
5085
5086        use GovernanceLogEntryType::*;
5087        assert!(!is_redactable(Amendment) && !is_redactable(KeyRotation));
5088        assert!(
5089            is_redactable(CouncilDecision) && is_redactable(EmergencyAction)
5090        );
5091    }
5092
5093    /// The attack blinding exists for, performed. Everything the attacker
5094    /// uses is public after a redaction: the redacted data, the entry's
5095    /// original `data_hash`, and each redaction's `resulting_data_hash`.
5096    #[test]
5097    fn a_removed_value_cannot_be_confirmed_by_guessing_it() {
5098        // Put a guess back where a marker is and see if a public hash agrees
5099        fn confirms(
5100            public: &serde_json::Value,
5101            pointer: &str,
5102            guess: &str,
5103            hash: Sha256Hex,
5104        ) -> bool {
5105            let mut attempt = public.clone();
5106            *attempt.pointer_mut(pointer).unwrap() = json!(guess);
5107            data_hash(&attempt) == hash
5108        }
5109        let legacy = json!({"finding": "upheld", "handle": "someone", "city": "Utrecht"});
5110
5111        // Blinded when written: the right guess confirms nothing.
5112        let written = blind_data(&legacy, Blind::random()).unwrap();
5113        let first = redact_data(
5114            &written,
5115            &["/handle".into()],
5116            &amd(1),
5117            Blind::random(),
5118        )
5119        .unwrap();
5120        assert!(!confirms(&first, "/handle", "someone", data_hash(&written)));
5121
5122        // A second redaction of the same entry: the first one's
5123        // `resulting_data_hash` is public too, and covers the city.
5124        let second =
5125            redact_data(&first, &["/city".into()], &amd(2), Blind::random())
5126                .unwrap();
5127        assert!(!confirms(&second, "/city", "Utrecht", data_hash(&first)));
5128
5129        // An entry from before blinding has nothing to destroy, and gains a
5130        // key it never had: strip it and the right guess does confirm. This
5131        // is the residual exposure of the entries that predate 0.27...
5132        let mut stripped =
5133            redact_data(&legacy, &["/handle".into()], &amd(3), Blind::random())
5134                .unwrap();
5135        let legacy_first = stripped.clone();
5136        stripped.as_object_mut().unwrap().remove(BLIND_KEY);
5137        assert!(confirms(
5138            &stripped,
5139            "/handle",
5140            "someone",
5141            data_hash(&legacy)
5142        ));
5143        // ...and it ends at the first redaction, which left a blind behind.
5144        let legacy_second = redact_data(
5145            &legacy_first,
5146            &["/city".into()],
5147            &amd(4),
5148            Blind::random(),
5149        )
5150        .unwrap();
5151        assert!(!confirms(
5152            &legacy_second,
5153            "/city",
5154            "Utrecht",
5155            data_hash(&legacy_first)
5156        ));
5157    }
5158
5159    // -- key rotation --
5160
5161    #[test]
5162    fn a_routine_rotation_moves_the_chain_to_the_new_key() {
5163        let (old, old_pk) = generate_keypair();
5164        let (new, new_pk) = generate_keypair();
5165        let mut c = Chain::new();
5166        c.decision(&old);
5167        let rotation = c.routine(&old_pk, &new);
5168        let rotation_id = c.rotate(&old, &rotation);
5169        c.decision(&new);
5170
5171        // Nothing but the root vouches for the new key, and nothing else
5172        // has to.
5173        let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5174        assert!(v.ok, "{v:#?}");
5175        assert_eq!(v.public_key, (&new_pk).into());
5176        assert_eq!(
5177            v.entries[1].signed_by,
5178            Some((&old_pk).into()),
5179            "the rotation itself is signed by the old key"
5180        );
5181        assert_eq!(v.entries[2].signed_by, Some((&new_pk).into()));
5182        assert!(v.unanchored_keys.is_empty());
5183        assert!(v.repudiated.is_empty());
5184        assert_eq!(v.keys.len(), 2);
5185        assert_eq!(v.keys[0].public_key, (&old_pk).into());
5186        assert_eq!(v.keys[0].from_seq, 1);
5187        assert_eq!(v.keys[0].through_seq, Some(2));
5188        assert_eq!(v.keys[0].status, KeyStatus::Retired);
5189        assert_eq!(v.keys[0].introduced_by, None);
5190        assert_eq!(v.keys[0].retired_by.as_ref(), Some(&rotation_id));
5191        assert!(v.keys[0].certified, "retroactively, by the rotation");
5192        assert_eq!(v.keys[1].from_seq, 3);
5193        assert_eq!(v.keys[1].through_seq, None);
5194        assert_eq!(v.keys[1].status, KeyStatus::Active);
5195        assert_eq!(v.keys[1].introduced_by.as_ref(), Some(&rotation_id));
5196        assert!(v.keys[1].certified);
5197    }
5198
5199    #[test]
5200    fn the_old_key_cannot_sign_after_a_routine_rotation() {
5201        let (old, old_pk) = generate_keypair();
5202        let (new, _) = generate_keypair();
5203        let mut c = Chain::new();
5204        c.decision(&old);
5205        let rotation = c.routine(&old_pk, &new);
5206        c.rotate(&old, &rotation);
5207        c.decision(&old);
5208
5209        let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5210        assert!(!v.ok, "{v:#?}");
5211        assert!(!v.entries[2].signature_valid);
5212        assert!(
5213            v.entries[2].link_valid,
5214            "the linkage is fine; the key is not"
5215        );
5216    }
5217
5218    /// The scenario the root exists for: the online key alone moves
5219    /// nothing, however well-formed the rotation it signs.
5220    #[test]
5221    fn the_online_key_cannot_certify_its_own_successor() {
5222        let (old, old_pk) = generate_keypair();
5223        let (thief, _) = generate_keypair();
5224        let mut c = Chain::new();
5225        c.decision(&old);
5226        let mut rotation = c.routine(&old_pk, &thief);
5227        // Signed by the stolen online key instead of a root.
5228        let statement = rotation.certificate.statement.clone();
5229        rotation.certificate = certify(&[&old], statement);
5230        c.rotate(&old, &rotation);
5231        c.decision(&thief);
5232
5233        let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5234        assert!(!v.ok, "{v:#?}");
5235        let p = v.entries[1].problem.as_deref().unwrap();
5236        assert!(p.contains("0 valid root signature"), "{p}");
5237        assert_eq!(v.public_key, (&old_pk).into(), "the chain does not move");
5238        assert!(!v.entries[2].signature_valid);
5239        assert_eq!(v.keys.len(), 1);
5240    }
5241
5242    #[test]
5243    fn a_certificate_counts_distinct_known_roots_only() {
5244        let (old, old_pk) = generate_keypair();
5245        let (new, _) = generate_keypair();
5246        let (stranger, _) = generate_keypair();
5247        let two_of_two = RootSet::new(
5248            [
5249                (&root(1).verifying_key()).into(),
5250                (&root(2).verifying_key()).into(),
5251            ],
5252            2,
5253        );
5254        let verdict = |signers: &[&SigningKey], roots: &RootSet| {
5255            let mut c = Chain::new();
5256            c.decision(&old);
5257            let mut rotation = c.routine(&old_pk, &new);
5258            let statement = rotation.certificate.statement.clone();
5259            rotation.certificate = certify(signers, statement);
5260            // The genesis certificate is held to the same threshold.
5261            let genesis = KeyCertStatement::genesis((&old_pk).into());
5262            rotation.outgoing_certificate =
5263                Some(certify(&[&root(1), &root(2)], genesis));
5264            c.rotate(&old, &rotation);
5265            verify_chain(&c.links, &old_pk, &anchored(&old_pk), roots)
5266        };
5267
5268        assert!(verdict(&[&root(1), &root(2)], &two_of_two).ok);
5269        assert!(verdict(&[&root(2)], &roots()).ok, "either root, 1-of-2");
5270
5271        for (signers, why) in [
5272            (vec![&root(1)], "below the threshold"),
5273            (vec![&root(1), &root(1)], "one root twice is one root"),
5274            (vec![&root(1), &stranger], "an unknown root is nobody"),
5275            (vec![], "unsigned"),
5276        ] {
5277            let v = verdict(&signers, &two_of_two);
5278            assert!(!v.ok, "{why}: {v:#?}");
5279            let p = v.entries[1].problem.as_deref().unwrap();
5280            assert!(p.contains("where 2 are needed"), "{why}: {p}");
5281        }
5282
5283        // An unknown signer beside a sufficient set is not an error.
5284        assert!(verdict(&[&stranger, &root(1)], &roots()).ok);
5285    }
5286
5287    #[test]
5288    fn a_certificate_is_good_for_one_statement_at_one_position() {
5289        let (old, old_pk) = generate_keypair();
5290        let (new, new_pk) = generate_keypair();
5291        let (other, other_pk) = generate_keypair();
5292        let anchor = anchored(&old_pk);
5293
5294        // Certified for the position right after entry 1, appended one
5295        // entry later. The proof of possession is rebuilt for the new
5296        // position; the certificate cannot be.
5297        let mut c = Chain::new();
5298        c.decision(&old);
5299        let early = c.routine(&old_pk, &new);
5300        c.decision(&old);
5301        let mut rotation = c.routine(&old_pk, &new);
5302        rotation.certificate = early.certificate;
5303        c.rotate(&old, &rotation);
5304        let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5305        assert!(!v.ok, "{v:#?}");
5306        let p = v.entries[2].problem.as_deref().unwrap();
5307        assert!(
5308            p.contains("different key, purpose or chain position"),
5309            "{p}"
5310        );
5311        assert_eq!(v.public_key, (&old_pk).into());
5312
5313        // Certified for one key, presented for another.
5314        let mut c = Chain::new();
5315        c.decision(&old);
5316        let for_new = c.routine(&old_pk, &new);
5317        let mut rotation = c.routine(&old_pk, &other);
5318        rotation.certificate = for_new.certificate;
5319        c.rotate(&old, &rotation);
5320        let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5321        let p = v.entries[1].problem.as_deref().unwrap();
5322        assert!(
5323            p.contains("different key, purpose or chain position"),
5324            "{p}"
5325        );
5326
5327        // The statement altered after signing, to match.
5328        let mut c = Chain::new();
5329        c.decision(&old);
5330        let mut rotation = c.routine(&old_pk, &other);
5331        rotation.certificate = for_new_at(&c, &new_pk);
5332        rotation.certificate.statement.key = (&other_pk).into();
5333        c.rotate(&old, &rotation);
5334        let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5335        let p = v.entries[1].problem.as_deref().unwrap();
5336        assert!(p.contains("0 valid root signature"), "{p}");
5337
5338        // A routine certificate does not authorize a compromise.
5339        let mut c = Chain::new();
5340        c.decision(&old);
5341        c.decision(&old);
5342        let mut rotation = c.compromise(&old_pk, &new, 1);
5343        rotation.certificate = for_new_at(&c, &new_pk);
5344        c.rotate(&new, &rotation);
5345        let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5346        assert!(!v.ok);
5347        assert!(v.repudiated.is_empty(), "{v:#?}");
5348        assert_eq!(v.public_key, (&old_pk).into());
5349    }
5350
5351    /// The same signature over the same JSON without the domain prefix —
5352    /// what a root key tricked into signing "just some JSON" would produce
5353    #[test]
5354    fn a_root_signature_without_the_domain_prefix_certifies_nothing() {
5355        use ed25519_dalek::Signer;
5356        let (old, old_pk) = generate_keypair();
5357        let (new, _) = generate_keypair();
5358        let mut c = Chain::new();
5359        c.decision(&old);
5360        let mut rotation = c.routine(&old_pk, &new);
5361        let statement = rotation.certificate.statement.clone();
5362        let bare = canonical_json(&serde_json::to_value(&statement).unwrap());
5363        assert_eq!(
5364            statement.signed_bytes(),
5365            [ROOT_DOMAIN, bare.as_slice()].concat()
5366        );
5367        rotation.certificate =
5368            KeyCertificate::unsigned(statement).with(RootSignature {
5369                root_key: (&root(1).verifying_key()).into(),
5370                signature: root(1).sign(&bare).into(),
5371            });
5372        c.rotate(&old, &rotation);
5373        let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5374        assert!(!v.ok, "{v:#?}");
5375        assert_eq!(v.public_key, (&old_pk).into());
5376    }
5377
5378    #[test]
5379    fn the_first_rotation_carries_the_genesis_certificate_and_only_it_does() {
5380        let (k1, k1_pk) = generate_keypair();
5381        let (k2, k2_pk) = generate_keypair();
5382        let (k3, _) = generate_keypair();
5383        let genesis =
5384            || certify(&[&root(1)], KeyCertStatement::genesis((&k1_pk).into()));
5385
5386        // Missing.
5387        let mut c = Chain::new();
5388        c.decision(&k1);
5389        let mut rotation = c.routine(&k1_pk, &k2);
5390        rotation.outgoing_certificate = None;
5391        c.rotate(&k1, &rotation);
5392        let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5393        assert!(!v.ok);
5394        let p = v.entries[1].problem.as_deref().unwrap();
5395        assert!(p.contains("genesis key's outgoing_certificate"), "{p}");
5396        assert_eq!(v.public_key, (&k1_pk).into());
5397
5398        // For some other key.
5399        let mut c = Chain::new();
5400        c.decision(&k1);
5401        let rotation = c.routine(&k1_pk, &k2).with_outgoing(certify(
5402            &[&root(1)],
5403            KeyCertStatement::genesis((&k2_pk).into()),
5404        ));
5405        c.rotate(&k1, &rotation);
5406        let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5407        let p = v.entries[1].problem.as_deref().unwrap();
5408        assert!(p.starts_with("outgoing_certificate:"), "{p}");
5409
5410        // Present, and it is what vouches for a genesis key no anchor
5411        // knows.
5412        let mut c = Chain::new();
5413        c.decision(&k1);
5414        let rotation = c.routine(&k1_pk, &k2);
5415        assert_eq!(rotation.outgoing_certificate, Some(genesis()));
5416        c.rotate(&k1, &rotation);
5417        let v = verify_chain(&c.links, &k1_pk, &KeyAnchor::default(), &roots());
5418        assert!(v.ok, "{v:#?}");
5419        assert!(v.unanchored_keys.is_empty(), "{v:#?}");
5420
5421        // A second one, later, is refused.
5422        let again = c.routine(&k2_pk, &k3).with_outgoing(genesis());
5423        c.rotate(&k2, &again);
5424        let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5425        assert!(!v.ok);
5426        let p = v.entries[2].problem.as_deref().unwrap();
5427        assert!(p.contains("nowhere else"), "{p}");
5428    }
5429
5430    #[test]
5431    fn a_forged_proof_of_possession_is_rejected() {
5432        let (old, old_pk) = generate_keypair();
5433        let (_, new_pk) = generate_keypair();
5434        let (thief, _) = generate_keypair();
5435        let mut c = Chain::new();
5436        c.decision(&old);
5437        // A rotation to a key nobody holds, certified in good faith: the
5438        // proof is signed by the old key instead of by `new_key` itself.
5439        let mut rotation = c.routine(&old_pk, &thief);
5440        rotation.new_key = (&new_pk).into();
5441        rotation.certificate = for_new_at(&c, &new_pk);
5442        let statement = rotation.statement(c.prev_hash());
5443        rotation.proof = crypto::sign(
5444            &old,
5445            statement.hash().as_bytes(),
5446            rotation.proof_signed_at,
5447        )
5448        .into();
5449        c.rotate(&old, &rotation);
5450
5451        assert_eq!(
5452            rotation.verify_proof(c.links[1].attestation.prev_hash),
5453            Err(RotationError::BadProof)
5454        );
5455        let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5456        assert!(!v.ok, "{v:#?}");
5457        assert!(
5458            v.entries[1]
5459                .problem
5460                .as_deref()
5461                .unwrap()
5462                .contains("proof of possession")
5463        );
5464        assert_eq!(v.public_key, (&old_pk).into(), "the chain does not move");
5465    }
5466
5467    #[test]
5468    fn a_compromise_repudiates_the_window_and_a_reattestation_restores_one() {
5469        let (old, old_pk) = generate_keypair();
5470        let (new, new_pk) = generate_keypair();
5471        let mut c = Chain::new();
5472        c.decision(&old); // 1 — the last entry anyone trusts
5473        c.decision(&old); // 2 — inside the window
5474        let reattested = c.decision(&old); // 3 — inside, later vouched for
5475        let rotation = c.compromise(&old_pk, &new, 1);
5476        let rotation_id = c.rotate(&new, &rotation); // 4 — signed by the NEW key
5477        let vouch = AmendmentDraft::new(
5478            reattested,
5479            c.hash_at(3),
5480            AmendmentKind::Reattested,
5481            "Art. VII",
5482            "independently verified; the Steward vouches for it",
5483        )
5484        .unwrap();
5485        let vouch_id = c.amend(&new, &vouch); // 5
5486
5487        let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5488        assert!(
5489            v.ok,
5490            "repudiation is a declared state, not a defect: {v:#?}"
5491        );
5492        assert_eq!(v.repudiated, vec![gov(2)]);
5493        assert!(v.entries[1].repudiated);
5494        assert!(!v.entries[2].repudiated);
5495        assert_eq!(v.entries[2].reattested_by, vec![vouch_id.clone()]);
5496        assert_eq!(v.entries[2].amended_by, vec![vouch_id]);
5497        assert_eq!(v.entries[3].signed_by, Some((&new_pk).into()));
5498        assert_eq!(v.public_key, (&new_pk).into());
5499        assert_eq!(v.keys.len(), 2);
5500        assert_eq!(v.keys[0].status, KeyStatus::Compromised);
5501        assert_eq!(
5502            v.keys[0].through_seq,
5503            Some(1),
5504            "trusted through the last trusted entry, not through the \
5505             declaration"
5506        );
5507        assert_eq!(v.keys[0].retired_by.as_ref(), Some(&rotation_id));
5508        assert_eq!(v.keys[1].from_seq, 4, "the declaration is its own first");
5509    }
5510
5511    /// The root says where the window opens. A declaration cannot trust
5512    /// the old key one entry further than its certificate does.
5513    #[test]
5514    fn last_trusted_is_the_roots_to_say() {
5515        let (old, old_pk) = generate_keypair();
5516        let (new, _) = generate_keypair();
5517        let mut c = Chain::new();
5518        c.decision(&old); // 1
5519        c.decision(&old); // 2
5520        let mut rotation = c.compromise(&old_pk, &new, 1);
5521        rotation.certificate.statement.last_trusted = Some(c.head(2));
5522        c.rotate(&new, &rotation);
5523        let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5524        assert!(!v.ok, "{v:#?}");
5525        assert!(v.repudiated.is_empty());
5526        assert_eq!(v.public_key, (&old_pk).into());
5527    }
5528
5529    #[test]
5530    fn a_stolen_key_cannot_be_rotated_back_in() {
5531        // K1 is compromised and replaced by K2. The thief, still holding
5532        // K1, declares a "compromise" of K2 naming K1 as the new key — and
5533        // even a root certificate would not bring a key back.
5534        let (k1, k1_pk) = generate_keypair();
5535        let (k2, k2_pk) = generate_keypair();
5536        let mut c = Chain::new();
5537        c.decision(&k1); // 1
5538        let real = c.compromise(&k1_pk, &k2, 1);
5539        c.rotate(&k2, &real); // 2
5540        c.decision(&k2); // 3
5541        let honest =
5542            verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5543        assert!(honest.ok, "{honest:#?}");
5544
5545        let hijack = c.compromise(&k2_pk, &k1, 3);
5546        c.rotate(&k1, &hijack); // 4 — signed by the stolen key
5547        c.decision(&k1); // 5
5548
5549        let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5550        assert!(!v.ok);
5551        assert_eq!(v.public_key, (&k2_pk).into(), "the chain stays with K2");
5552        let p = v.entries[3].problem.as_deref().unwrap();
5553        assert!(p.contains("never brought back"), "{p}");
5554        assert!(!v.entries[3].signature_valid, "{:#?}", v.entries[3]);
5555        assert!(!v.entries[4].signature_valid, "K1 signs nothing again");
5556        assert_eq!(v.keys.len(), 2);
5557        assert_eq!(v.keys[1].status, KeyStatus::Active);
5558    }
5559
5560    #[test]
5561    fn a_routine_rotation_cannot_reuse_a_key_either() {
5562        let (k1, k1_pk) = generate_keypair();
5563        let (k2, k2_pk) = generate_keypair();
5564        let mut c = Chain::new();
5565        c.decision(&k1);
5566        let out = c.routine(&k1_pk, &k2);
5567        c.rotate(&k1, &out);
5568        let back = c.routine(&k2_pk, &k1);
5569        c.rotate(&k2, &back);
5570        let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5571        assert!(!v.ok);
5572        assert!(
5573            v.entries[2]
5574                .problem
5575                .as_deref()
5576                .unwrap()
5577                .contains("never brought back"),
5578            "{:#?}",
5579            v.entries[2]
5580        );
5581        assert_eq!(v.public_key, (&k2_pk).into());
5582    }
5583
5584    #[test]
5585    fn a_forged_entry_has_no_effects() {
5586        let (steward, steward_pk) = generate_keypair();
5587        let (forger, _) = generate_keypair();
5588        let anchor = anchored(&steward_pk);
5589        let mut c = Chain::new();
5590        let target = c.decision(&steward); // 1
5591        let fake = AmendmentDraft::new(
5592            target,
5593            c.hash_at(1),
5594            AmendmentKind::Overruled,
5595            "none",
5596            "overruled, says nobody with the key",
5597        )
5598        .unwrap();
5599        c.amend(&forger, &fake); // 2 — not signed by the key in force
5600        // Certified, even: a certificate is not a licence to skip the old
5601        // key's signature on a routine rotation.
5602        let grab = c.routine(&steward_pk, &forger);
5603        c.rotate(&forger, &grab); // 3 — likewise
5604
5605        let v = verify_chain(&c.links, &steward_pk, &anchor, &roots());
5606        assert!(!v.ok);
5607        assert!(v.entries[0].amended_by.is_empty(), "{:#?}", v.entries[0]);
5608        assert_eq!(v.public_key, (&steward_pk).into());
5609        assert_eq!(v.keys.len(), 1);
5610        assert!(v.unanchored_keys.is_empty());
5611    }
5612
5613    #[test]
5614    fn a_repeated_id_is_a_problem() {
5615        let (key, pk) = generate_keypair();
5616        let mut c = Chain::new();
5617        c.decision(&key);
5618        c.gov = 0;
5619        c.decision(&key); // GOV-2026-0001 again
5620        let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
5621        assert!(!v.ok);
5622        assert!(v.entries.iter().all(|e| {
5623            e.problem
5624                .as_deref()
5625                .is_some_and(|p| p.contains("more than once"))
5626        }));
5627    }
5628
5629    #[test]
5630    fn a_second_compromise_cannot_anchor_inside_the_first_window() {
5631        let (k1, k1_pk) = generate_keypair();
5632        let (k2, _) = generate_keypair();
5633        let (k3, _) = generate_keypair();
5634        let mut c = Chain::new();
5635        c.decision(&k1); // 1 — trusted
5636        c.decision(&k1); // 2 — inside the first window
5637        let first = c.compromise(&k1_pk, &k2, 1);
5638        c.rotate(&k2, &first); // 3
5639        let second = c.compromise(&k1_pk, &k3, 2);
5640        c.rotate(&k3, &second); // 4
5641
5642        let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5643        assert!(!v.ok);
5644        let p = v.entries[3].problem.as_deref().unwrap();
5645        assert!(p.contains("repudiated"), "{p}");
5646        assert_eq!(v.keys.len(), 2, "K1 and K2; K3 never took the chain");
5647        assert_eq!(v.keys[0].through_seq, Some(1));
5648    }
5649
5650    #[test]
5651    fn an_uncertified_compromise_fails_closed() {
5652        let (old, old_pk) = generate_keypair();
5653        let (new, new_pk) = generate_keypair();
5654        let mut c = Chain::new();
5655        c.decision(&old);
5656        c.decision(&old);
5657        let mut rotation = c.compromise(&old_pk, &new, 1);
5658        let statement = rotation.certificate.statement.clone();
5659        rotation.certificate = certify(&[&new], statement);
5660        c.rotate(&new, &rotation);
5661
5662        // Being in an anchor does not help: only the root moves the chain.
5663        let anchor = anchored(&old_pk).with((&new_pk).into());
5664        let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5665        assert!(!v.ok, "{v:#?}");
5666        let p = v.entries[2].problem.as_deref().unwrap();
5667        assert!(p.contains("0 valid root signature"), "{p}");
5668        assert!(!v.entries[2].signature_valid, "checked under the old key");
5669        assert!(v.repudiated.is_empty(), "and nothing is repudiated");
5670        assert_eq!(v.public_key, (&old_pk).into());
5671    }
5672
5673    /// A key stolen before anyone knew: the Steward rotates routinely,
5674    /// then learns the old key was already out, and names a head from
5675    /// before the rotation. The rotation is void with the rest of the
5676    /// window.
5677    #[test]
5678    fn a_rotation_inside_the_window_is_void_with_it() {
5679        let (steward, steward_pk) = generate_keypair();
5680        let (successor, _) = generate_keypair();
5681        let (recovery, recovery_pk) = generate_keypair();
5682
5683        let mut c = Chain::new();
5684        c.decision(&steward); // 1 — the last entry anyone trusts
5685        c.decision(&steward); // 2 — the thief's, as it turns out
5686        let routine = c.routine(&steward_pk, &successor);
5687        c.rotate(&steward, &routine); // 3
5688        c.decision(&successor); // 4
5689
5690        let declaration = c.compromise(&steward_pk, &recovery, 1);
5691        c.rotate(&recovery, &declaration); // 5
5692
5693        let v = verify_chain(
5694            &c.links,
5695            &steward_pk,
5696            &anchored(&steward_pk),
5697            &roots(),
5698        );
5699        assert!(v.ok, "{v:#?}");
5700        assert_eq!(v.repudiated, vec![gov(2), key_id(1), gov(3)]);
5701        assert_eq!(v.public_key, (&recovery_pk).into());
5702        assert_eq!(v.keys.len(), 2, "the successor is not part of history");
5703        assert_eq!(v.keys[0].public_key, (&steward_pk).into());
5704        assert_eq!(v.keys[0].status, KeyStatus::Compromised);
5705        assert!(
5706            v.keys[0].certified,
5707            "the genesis certificate rode in on the voided rotation and \
5708             is the root's word all the same"
5709        );
5710        assert_eq!(v.keys[1].public_key, (&recovery_pk).into());
5711    }
5712
5713    #[test]
5714    fn a_compromise_must_name_a_real_head_and_the_key_that_held_it() {
5715        let (old, old_pk) = generate_keypair();
5716        let (new, new_pk) = generate_keypair();
5717        let anchor = anchored(&old_pk);
5718
5719        // A head whose hash is not that entry's — certified, so the only
5720        // thing wrong is what the chain says about it.
5721        let mut c = Chain::new();
5722        c.decision(&old);
5723        let mut trusted = c.head(1);
5724        trusted.entry_hash = data_hash(&json!("nope"));
5725        let statement = KeyCertStatement::compromise(
5726            (&new_pk).into(),
5727            2,
5728            c.prev_hash(),
5729            trusted,
5730        );
5731        let mut rotation = c.compromise(&old_pk, &new, 1);
5732        rotation.certificate = certify(&[&root(1)], statement);
5733        c.rotate(&new, &rotation);
5734        let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5735        let p = v.entries[1].problem.as_deref().unwrap();
5736        assert!(p.contains("last_trusted"), "{p}");
5737
5738        // An old_key that was never in force.
5739        let (_, other_pk) = generate_keypair();
5740        let mut c = Chain::new();
5741        c.decision(&old);
5742        let rotation = c.compromise(&other_pk, &new, 1);
5743        c.rotate(&new, &rotation);
5744        let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5745        let p = v.entries[1].problem.as_deref().unwrap();
5746        assert!(p.contains("old_key is not the key"), "{p}");
5747
5748        // A compromise whose certificate names no head at all.
5749        let mut c = Chain::new();
5750        c.decision(&old);
5751        let mut rotation = c.compromise(&old_pk, &new, 1);
5752        rotation.certificate.statement.last_trusted = None;
5753        c.rotate(&new, &rotation);
5754        let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5755        let p = v.entries[1].problem.as_deref().unwrap();
5756        assert!(p.contains("must name last_trusted"), "{p}");
5757    }
5758
5759    #[test]
5760    fn a_rotation_carries_no_free_text() {
5761        let (old, old_pk) = generate_keypair();
5762        let (new, _) = generate_keypair();
5763        let mut c = Chain::new();
5764        c.decision(&old);
5765        let rotation = c.routine(&old_pk, &new);
5766        let mut value = serde_json::to_value(&rotation).unwrap();
5767        assert!(serde_json::from_value::<KeyRotation>(value.clone()).is_ok());
5768        value["note"] = json!("at the request of …");
5769        assert!(serde_json::from_value::<KeyRotation>(value).is_err());
5770
5771        let mut head = serde_json::to_value(c.head(1)).unwrap();
5772        head["comment"] = json!("the last one I remember signing");
5773        assert!(serde_json::from_value::<TrustedHead>(head).is_err());
5774
5775        let mut statement =
5776            serde_json::to_value(&rotation.certificate.statement).unwrap();
5777        statement["comment"] = json!("signed in the kitchen");
5778        assert!(serde_json::from_value::<KeyCertStatement>(statement).is_err());
5779    }
5780
5781    #[test]
5782    fn the_root_statement_bytes_are_pinned() {
5783        let key: PublicKeyHex = PUBLISHED_KEYS[0].parse().unwrap();
5784        assert_eq!(
5785            String::from_utf8(KeyCertStatement::genesis(key).signed_bytes())
5786                .unwrap(),
5787            "agora-governance-root-v1\n\
5788             {\"agora_governance_key_cert\":1,\"from_seq\":1,\
5789             \"key\":\"ebb3091dd328f1463362c171121921b2fe14628e3fc4c145deaccefb85c0e78a\",\
5790             \"last_trusted\":null,\"prev_hash\":null,\"purpose\":\"genesis\"}"
5791        );
5792
5793        // The same statement `governance/root/root_sign.py --self-test`
5794        // pins in the agora repository: the tool that signs and the
5795        // verifiers that check must mean the same bytes.
5796        let statement = KeyCertStatement::compromise(
5797            Sha256Hex::from([0xab; 32]).to_string().parse().unwrap(),
5798            15,
5799            Some([0xcd; 32].into()),
5800            TrustedHead {
5801                id: gov(10),
5802                chain_seq: 11,
5803                entry_hash: [0xef; 32].into(),
5804            },
5805        );
5806        assert_eq!(
5807            Sha256Hex::from(<[u8; 32]>::from(Sha256::digest(
5808                statement.signed_bytes()
5809            )))
5810            .to_string(),
5811            "633685771e08be126fd12ca4eb98c77120e5868236ff541ecba85ce6a21e6b68"
5812        );
5813    }
5814
5815    #[test]
5816    fn root_keys_are_curve_points_and_make_a_root_set() {
5817        let roots = RootSet::published();
5818        assert_eq!(roots.keys().count(), ROOT_KEYS.len());
5819        assert_eq!(roots.threshold(), ROOT_THRESHOLD);
5820        assert!(ROOT_THRESHOLD >= 1 && ROOT_THRESHOLD <= ROOT_KEYS.len());
5821        for root in ROOT_KEYS {
5822            let key: PublicKeyHex = root.parse().unwrap();
5823            assert!(roots.contains(&key));
5824            assert!(
5825                key.to_verifying_key().is_ok(),
5826                "{root} is not a valid Ed25519 public key"
5827            );
5828            assert!(
5829                !PUBLISHED_KEYS.contains(root),
5830                "a root key never signs entries"
5831            );
5832        }
5833        assert_eq!(RootSet::new([], 0).threshold(), 1);
5834    }
5835
5836    #[test]
5837    fn a_genesis_key_outside_the_anchor_is_reported_not_rejected() {
5838        let (key, pk) = generate_keypair();
5839        let c = chain(&key, 2);
5840        let v = verify_chain(&c, &pk, &KeyAnchor::default(), &roots());
5841        assert!(v.ok, "{v:#?}");
5842        assert_eq!(v.unanchored_keys, vec![PublicKeyHex::from(&pk)]);
5843        assert_eq!(v.keys.len(), 1);
5844        assert_eq!(v.keys[0].status, KeyStatus::Active);
5845        assert_eq!(v.keys[0].from_seq, 1);
5846        assert_eq!(v.keys[0].through_seq, None);
5847    }
5848
5849    #[test]
5850    fn published_keys_are_curve_points_and_make_an_anchor() {
5851        let anchor = KeyAnchor::published();
5852        assert!(!anchor.is_empty());
5853        assert_eq!(anchor.keys().count(), PUBLISHED_KEYS.len());
5854        for published in PUBLISHED_KEYS {
5855            let key: PublicKeyHex = published.parse().unwrap();
5856            assert!(anchor.contains(&key));
5857            assert!(
5858                key.to_verifying_key().is_ok(),
5859                "{published} is not a valid Ed25519 public key"
5860            );
5861        }
5862        let (_, other) = generate_keypair();
5863        assert!(!anchor.contains(&(&other).into()));
5864        assert!(
5865            anchor
5866                .clone()
5867                .with((&other).into())
5868                .contains(&(&other).into())
5869        );
5870    }
5871
5872    // -- the wire --
5873
5874    #[test]
5875    fn amendments_and_rotations_round_trip_as_entry_data() {
5876        let (key, pk) = generate_keypair();
5877        let amendment = AmendmentDraft::new(
5878            gov(1),
5879            data_hash(&json!("x")),
5880            AmendmentKind::Superseded,
5881            "Art. VI § 2",
5882            "superseded by GOV-2026-0009",
5883        )
5884        .unwrap()
5885        .with_authority(gov(9))
5886        .with_rationale("the later decision covers the same subject");
5887        let AmendmentDraft { amendment, texts } = amendment;
5888        let value = serde_json::to_value(&amendment).unwrap();
5889        assert_eq!(value["kind"], "superseded");
5890        assert_eq!(value["agora_governance_amendment"], 2);
5891        assert!(value.get("redaction").is_none(), "{value}");
5892        let text = value.to_string();
5893        assert!(!text.contains("Art. VI"), "no text in signed data: {text}");
5894        assert!(!text.contains("salt"), "and no salt: {text}");
5895        assert_eq!(
5896            value["note"]["commitment"],
5897            texts
5898                .note
5899                .as_ref()
5900                .unwrap()
5901                .commitment()
5902                .commitment
5903                .to_string()
5904        );
5905        assert_eq!(
5906            serde_json::from_value::<Amendment>(value).unwrap(),
5907            amendment
5908        );
5909        let beside = serde_json::to_value(&texts).unwrap();
5910        assert_eq!(beside["basis"]["text"], "Art. VI § 2");
5911        assert_eq!(
5912            serde_json::from_value::<AmendmentTexts>(beside).unwrap(),
5913            texts
5914        );
5915
5916        // Version 1, as the platform's three are, still reads.
5917        let legacy = json!({
5918            "agora_governance_amendment": 1,
5919            "target": "GOV-2026-0001",
5920            "target_entry_hash": data_hash(&json!("x")),
5921            "kind": "non_precedential",
5922            "authority": "GOV-2026-0005",
5923            "basis": "§1",
5924            "note": "diagnostic finding",
5925        });
5926        let legacy: Amendment = serde_json::from_value(legacy).unwrap();
5927        assert_eq!(legacy.validate(), Ok(()));
5928        assert_eq!(legacy.basis, AmendmentText::Plain("§1".into()));
5929
5930        let mut c = Chain::new();
5931        c.decision(&key);
5932        let rotation = c.compromise(&pk, &generate_keypair().0, 1);
5933        let value = serde_json::to_value(&rotation).unwrap();
5934        assert_eq!(value["agora_governance_key_rotation"], 2);
5935        assert_eq!(value["reason"], "compromise");
5936        let statement = &value["certificate"]["statement"];
5937        assert_eq!(statement["purpose"], "compromise");
5938        assert_eq!(statement["last_trusted"]["chain_seq"], 1);
5939        assert_eq!(
5940            value["outgoing_certificate"]["statement"]["purpose"],
5941            "genesis"
5942        );
5943        assert!(value.get("note").is_none(), "{value}");
5944        assert_eq!(
5945            serde_json::from_value::<KeyRotation>(value).unwrap(),
5946            rotation
5947        );
5948
5949        let notice =
5950            AmendmentNotice::new(amd(1), at(0), &amendment, Some(&texts));
5951        let value = serde_json::to_value(&notice).unwrap();
5952        assert_eq!(value["id"], "AMD-2026-0001");
5953        assert_eq!(value["note"], "superseded by GOV-2026-0009");
5954        assert_eq!(
5955            serde_json::from_value::<AmendmentNotice>(value).unwrap(),
5956            notice
5957        );
5958
5959        // The rationale erased: the label stays, and nothing else moves.
5960        let erased = AmendmentTexts {
5961            rationale: None,
5962            ..texts.clone()
5963        };
5964        let notice =
5965            AmendmentNotice::new(amd(1), at(0), &amendment, Some(&erased));
5966        assert_eq!(notice.note, "superseded by GOV-2026-0009");
5967        assert_eq!(notice.rationale.as_deref(), Some(WITHHELD_TEXT));
5968        let notice = AmendmentNotice::new(amd(1), at(0), &amendment, None);
5969        assert_eq!(notice.basis, WITHHELD_TEXT);
5970        let notice = AmendmentNotice::new(amd(1), at(0), &legacy, None);
5971        assert_eq!(notice.note, "diagnostic finding");
5972    }
5973
5974    /// The verifier hashes the raw `data` value, so a field it has never
5975    /// heard of neither breaks it nor escapes the signature — and an
5976    /// amendment written without one verifies just the same.
5977    #[test]
5978    fn an_amendment_verifies_with_or_without_its_optional_fields() {
5979        let (key, pk) = generate_keypair();
5980        let mut c = Chain::new();
5981        let target = c.decision(&key);
5982        let bare = AmendmentDraft::new(
5983            target.clone(),
5984            c.hash_at(1),
5985            AmendmentKind::Correction,
5986            "clerical",
5987            "typo in the citation",
5988        )
5989        .unwrap();
5990        assert!(
5991            serde_json::to_value(&bare.amendment)
5992                .unwrap()
5993                .get("rationale")
5994                .is_none()
5995        );
5996        c.amend(&key, &bare);
5997        let full = bare.clone().with_rationale("at length: …");
5998        c.amend(&key, &full);
5999        // And a field from a future version of the shape.
6000        let mut future = serde_json::to_value(&full.amendment).unwrap();
6001        future["superseded_by_something_new"] = json!(["later"]);
6002        c.push(
6003            &key,
6004            amd(3),
6005            GovernanceLogEntryType::Amendment,
6006            future,
6007            true,
6008        );
6009
6010        let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
6011        assert!(v.ok, "{v:#?}");
6012        assert_eq!(
6013            v.entries[0].amended_by,
6014            vec![amd(1), amd(2), amd(3)],
6015            "all three name the target"
6016        );
6017    }
6018
6019    /// Pre-0.26 JSON — no `data`, no `signed_by`, no repudiation — still
6020    /// parses, because every field added since is `#[serde(default)]`.
6021    #[test]
6022    fn pre_0_26_wire_still_deserializes() {
6023        let link: GovernanceChainLink = serde_json::from_value(json!({
6024            "id": "GOV-2026-0001",
6025            "entry_type": "council_decision",
6026            "created_at": "2023-11-14T22:13:20.123456Z",
6027            "attestation": {
6028                "envelope_version": 1,
6029                "chain_seq": 1,
6030                "prev_hash": null,
6031                "data_hash": "00".repeat(32),
6032                "entry_hash": "11".repeat(32),
6033                "signature": "22".repeat(64),
6034                "signed_at": "2023-11-14T22:13:21Z",
6035                "retroactive": false,
6036            },
6037        }))
6038        .unwrap();
6039        assert!(link.data.is_none());
6040        assert!(
6041            serde_json::to_value(&link).unwrap().get("data").is_none(),
6042            "and a link without data does not grow a null field"
6043        );
6044
6045        let verdict: EntryVerdict = serde_json::from_value(json!({
6046            "id": "GOV-2026-0001",
6047            "chain_seq": 1,
6048            "signature_valid": true,
6049            "link_valid": true,
6050            "content_matches": null,
6051            "retroactive": false,
6052            "out_of_order": false,
6053            "amended_by": [],
6054        }))
6055        .unwrap();
6056        assert!(!verdict.repudiated && !verdict.redacted);
6057        assert!(verdict.signed_by.is_none());
6058        assert!(verdict.reattested_by.is_empty());
6059
6060        let verification: GovernanceVerification =
6061            serde_json::from_value(json!({
6062                "public_key": "33".repeat(32),
6063                "ok": true,
6064                "head": "GOV-2026-0001",
6065                "entries": [],
6066            }))
6067            .unwrap();
6068        assert!(verification.keys.is_empty());
6069        assert!(verification.unanchored_keys.is_empty());
6070        assert!(verification.repudiated.is_empty());
6071    }
6072
6073    #[test]
6074    fn the_report_round_trips() {
6075        let (key, pk) = generate_keypair();
6076        let mut c = Chain::new();
6077        let target = c.decision(&key);
6078        let amendment = AmendmentDraft::new(
6079            target,
6080            c.hash_at(1),
6081            AmendmentKind::Overruled,
6082            "b",
6083            "n",
6084        )
6085        .unwrap();
6086        c.amend(&key, &amendment);
6087        let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
6088        let text = serde_json::to_string(&v).unwrap();
6089        assert_eq!(
6090            serde_json::from_str::<GovernanceVerification>(&text).unwrap(),
6091            v
6092        );
6093
6094        let keys = GovernanceSigningKeys { keys: v.keys };
6095        let value = serde_json::to_value(&keys).unwrap();
6096        assert!(value["keys"].is_array(), "an object, not a bare array");
6097        assert_eq!(value["keys"][0]["status"], "active");
6098        assert_eq!(
6099            serde_json::from_value::<GovernanceSigningKeys>(value).unwrap(),
6100            keys
6101        );
6102    }
6103
6104    /// The envelope is version 1 and there is a live chain signed under it:
6105    /// these bytes and this hash are load-bearing, not a snapshot to
6106    /// re-bless when something changes them.
6107    #[test]
6108    fn envelope_v1_preimage_and_hash_are_pinned() {
6109        let envelope = Envelope::new(
6110            gov(6),
6111            GovernanceLogEntryType::CouncilDecision,
6112            at(0),
6113            Some(Sha256Hex::from([0x11; 32])),
6114            data_hash(&json!({"outcome": "approved", "title": "Ratification"})),
6115        );
6116        assert_eq!(
6117            String::from_utf8(envelope.preimage()).unwrap(),
6118            "{\"agora_governance_log\":1,\"id\":\"GOV-2026-0006\",\
6119             \"entry_type\":\"council_decision\",\
6120             \"created_at\":1700000000123456,\
6121             \"prev_hash\":\"1111111111111111111111111111111111111111111111111111111111111111\",\
6122             \"data_hash\":\"a4adf645ae3f60c56484d01aea87d6d490321d7fc66b1607df14b023fe567c7b\"}"
6123        );
6124        assert_eq!(
6125            envelope.entry_hash().to_hex(),
6126            "ba27577432f81e415f1c01cc4cfabab6070e3ac50fd468fffe195ef19c0e9464"
6127        );
6128    }
6129
6130    /// The parity check the Steward's second channel exists for: the live
6131    /// chain verifies under what this build has compiled in — the genesis
6132    /// key in [`PUBLISHED_KEYS`] and the roots in [`ROOT_KEYS`] — and the
6133    /// key the platform serves is the one that walk ends on.
6134    ///
6135    /// Before the first rotation that is the genesis key itself; after it,
6136    /// a key this crate has never heard of and does not need to, because
6137    /// the root certified it. A served key the roots did not certify, or a
6138    /// chain that no longer verifies, fails here within the hour.
6139    ///
6140    /// Networked, so it is `#[ignore]`d and CI runs it as its own job —
6141    /// a 5G blip should not read as a code failure. `just
6142    /// check-published-keys`.
6143    #[cfg(feature = "agora-client")]
6144    #[tokio::test]
6145    #[ignore = "networked: hits the live platform"]
6146    async fn the_published_key_is_the_one_the_platform_serves() {
6147        let client = crate::client::Client::new(
6148            url::Url::parse("https://subliminal.technology").unwrap(),
6149        )
6150        .unwrap();
6151        let served = client.get_governance_signing_key().await.unwrap();
6152        assert_eq!(served.algorithm, "ed25519");
6153
6154        let genesis: PublicKeyHex = PUBLISHED_KEYS
6155            .first()
6156            .expect("PUBLISHED_KEYS is never empty")
6157            .parse()
6158            .unwrap();
6159        let links = client.get_governance_chain().await.unwrap();
6160        let report = verify_chain(
6161            &links,
6162            &genesis.to_verifying_key().unwrap(),
6163            &KeyAnchor::published(),
6164            &RootSet::published(),
6165        );
6166        let problems: Vec<_> = report
6167            .entries
6168            .iter()
6169            .filter_map(|e| {
6170                e.problem.as_ref().map(|p| (e.id.clone(), p.clone()))
6171            })
6172            .collect();
6173        assert!(
6174            report.ok,
6175            "the live chain does not verify under this build's genesis key \
6176             and roots: {problems:#?}"
6177        );
6178        assert!(report.unanchored_keys.is_empty(), "{report:#?}");
6179        assert_eq!(
6180            served.public_key, report.public_key,
6181            "the platform serves {} but the chain, followed under the \
6182             published roots, is held by {}",
6183            served.public_key, report.public_key
6184        );
6185    }
6186
6187    #[cfg(feature = "schemars")]
6188    #[test]
6189    fn wire_schemas_are_ref_free() {
6190        use crate::responses::inline_schema_for;
6191        for (name, schema) in [
6192            (
6193                "GovernanceAttestation",
6194                inline_schema_for::<GovernanceAttestation>(),
6195            ),
6196            (
6197                "GovernanceChainLink",
6198                inline_schema_for::<GovernanceChainLink>(),
6199            ),
6200            (
6201                "GovernanceSigningKey",
6202                inline_schema_for::<GovernanceSigningKey>(),
6203            ),
6204            (
6205                "GovernanceVerification",
6206                inline_schema_for::<GovernanceVerification>(),
6207            ),
6208            (
6209                "Vec<GovernanceChainLink>",
6210                inline_schema_for::<Vec<GovernanceChainLink>>(),
6211            ),
6212            ("Amendment", inline_schema_for::<Amendment>()),
6213            ("Redaction", inline_schema_for::<Redaction>()),
6214            ("Revision", inline_schema_for::<Revision>()),
6215            ("AmendmentNotice", inline_schema_for::<AmendmentNotice>()),
6216            ("KeyRotation", inline_schema_for::<KeyRotation>()),
6217            ("TrustedHead", inline_schema_for::<TrustedHead>()),
6218            (
6219                "GovernanceKeyRecord",
6220                inline_schema_for::<GovernanceKeyRecord>(),
6221            ),
6222            (
6223                "GovernanceSigningKeys",
6224                inline_schema_for::<GovernanceSigningKeys>(),
6225            ),
6226            ("AmendmentKind", inline_schema_for::<AmendmentKind>()),
6227            ("Standing", inline_schema_for::<Standing>()),
6228            ("KeyStatus", inline_schema_for::<KeyStatus>()),
6229            ("RotationReason", inline_schema_for::<RotationReason>()),
6230        ] {
6231            let text = serde_json::to_string(&schema).unwrap();
6232            assert!(!text.contains("$ref"), "{name} must be $ref-free: {text}");
6233            assert!(
6234                !text.contains("$defs"),
6235                "{name} must be $defs-free: {text}"
6236            );
6237        }
6238        let text =
6239            serde_json::to_string(&inline_schema_for::<Sha256Hex>()).unwrap();
6240        assert!(text.contains("^[0-9a-f]{64}$"), "{text}");
6241        let text = serde_json::to_string(&inline_schema_for::<SignatureHex>())
6242            .unwrap();
6243        assert!(text.contains("^[0-9a-f]{128}$"), "{text}");
6244    }
6245}