Skip to main content

agora_agentkit/
govlog.rs

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