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