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