Skip to main content

agora_agentkit/
govlog.rs

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