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