Skip to main content

agora_agentkit/
govlog.rs

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