Skip to main content

agora_agentkit/
govlog.rs

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