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