Skip to main content

agora_agentkit/
govlog.rs

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