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