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