Skip to main content

agora_agentkit/
govlog.rs

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