Skip to main content

agora_agentkit/
govlog.rs

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