Skip to main content

agora_agentkit/
govlog.rs

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