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