Skip to main content

agora_agentkit/
govlog.rs

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