1use crate::crypto::{self, Signature, SigningKey, VerifyingKey};
64use crate::enums::GovernanceLogEntryType;
65use crate::ids::{GovernanceLogId, GovernanceLogPrefix};
66use chrono::{DateTime, Utc};
67use serde::{Deserialize, Serialize};
68use sha2::{Digest, Sha256};
69use std::collections::{HashMap, HashSet};
70
71pub use crate::enums::{AmendmentKind, CitationRelation, KeyStatus, Standing};
72
73mod texts;
74pub use texts::{
75 AmendmentText, AmendmentTextStatus, AmendmentTexts, CommittedText,
76 TextCommitment, TextStatus, WITHHELD_TEXT,
77};
78
79mod council;
80pub use council::{
81 Abstention, AgendaRanking, Ballot, BlankStatement, CouncilAttachment,
82 CouncilDecisionRecord, CouncilMember, CouncilRound, CouncilSeat,
83 CouncilVote, Decided, DecisionCategory, FinalRoundError, FinalVotes,
84 NotASeatChoice, PlacedProposal, RECUSED_DEFERRED_TAG, Rationale,
85 RecordRuleError, RecusalVote, STEWARD_RECUSED_TAG, SeatChoice, SeatModels,
86 SeatRanking, SeatResponse, SeatVote, SeatVotes, Statement,
87 StewardEmergency, StewardRecusal, StewardVote, Vote, Votes,
88};
89
90mod redactable;
91pub use redactable::{REDACTION_MARKER_PATTERN, Redactable};
92
93pub mod reading;
94
95pub use json_patch;
98
99mod obligation;
100pub use obligation::request;
101pub mod quote;
102pub use obligation::{
103 Evidence, KnownEntry, OBLIGATION_VERSION, Obligation, ObligationData,
104 ObligationError, ObligationRuleError, ObligationState, ObligationUpdate,
105 Until,
106};
107
108mod data_set;
109pub use data_set::{
110 DATA_SET_VERSION, DataFile, DataFileMismatch, DataFilePath,
111 DataFilePathError, DataSet, DataSetLookupError, DataSetLookupFailure,
112 DataSetTarget, MAX_DATA_FILE_BYTES,
113};
114
115mod record;
116pub use record::{
117 RecordAttachment, RecordParticipant, STEWARD_RECORD_VERSION, StewardRecord,
118};
119
120mod root;
121pub use root::{
122 CertPurpose, CertificateError, KEY_CERT_VERSION, KeyCertStatement,
123 KeyCertificate, ROOT_DOMAIN, ROOT_KEYS, ROOT_THRESHOLD, RootSet,
124 RootSignature,
125};
126
127#[cfg(test)]
129mod vectors;
130
131pub const ENVELOPE_VERSION: u32 = 1;
133
134pub const RETROACTIVE_AFTER: chrono::Duration = chrono::Duration::seconds(60);
137
138#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
145#[error("{type_name}: expected {expected} bytes of hex, got {got:?}")]
146pub struct HexLengthError {
147 pub type_name: &'static str,
148 pub expected: usize,
149 pub got: String,
150}
151
152macro_rules! hex_bytes {
153 ($(#[$meta:meta])* $name:ident, $len:expr) => {
154 $(#[$meta])*
155 #[derive(Clone, Copy, PartialEq, Eq, Hash)]
156 #[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
157 #[cfg_attr(feature = "sqlx", sqlx(transparent))]
158 pub struct $name([u8; $len]);
159
160 impl $name {
161 pub fn as_bytes(&self) -> &[u8; $len] {
163 &self.0
164 }
165
166 pub fn to_hex(&self) -> String {
168 hex::encode(self.0)
169 }
170 }
171
172 impl From<[u8; $len]> for $name {
173 fn from(bytes: [u8; $len]) -> Self {
174 Self(bytes)
175 }
176 }
177
178 impl TryFrom<&[u8]> for $name {
179 type Error = HexLengthError;
180
181 fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
182 <[u8; $len]>::try_from(bytes).map(Self).map_err(|_| {
183 HexLengthError {
184 type_name: stringify!($name),
185 expected: $len,
186 got: hex::encode(bytes),
187 }
188 })
189 }
190 }
191
192 impl TryFrom<Vec<u8>> for $name {
193 type Error = HexLengthError;
194
195 fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
196 Self::try_from(bytes.as_slice())
197 }
198 }
199
200 impl std::str::FromStr for $name {
201 type Err = HexLengthError;
202
203 fn from_str(s: &str) -> Result<Self, Self::Err> {
204 let bytes = hex::decode(s.trim()).map_err(|_| HexLengthError {
205 type_name: stringify!($name),
206 expected: $len,
207 got: s.to_string(),
208 })?;
209 Self::try_from(bytes.as_slice())
210 }
211 }
212
213 impl std::fmt::Display for $name {
214 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215 f.write_str(&self.to_hex())
216 }
217 }
218
219 impl std::fmt::Debug for $name {
220 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
221 write!(f, "{}({})", stringify!($name), self.to_hex())
222 }
223 }
224
225 impl Serialize for $name {
226 fn serialize<S: serde::Serializer>(
227 &self,
228 s: S,
229 ) -> Result<S::Ok, S::Error> {
230 s.serialize_str(&self.to_hex())
231 }
232 }
233
234 impl<'de> Deserialize<'de> for $name {
235 fn deserialize<D: serde::Deserializer<'de>>(
236 d: D,
237 ) -> Result<Self, D::Error> {
238 let s = String::deserialize(d)?;
239 s.parse().map_err(serde::de::Error::custom)
240 }
241 }
242
243 #[cfg(feature = "schemars")]
247 impl schemars::JsonSchema for $name {
248 fn inline_schema() -> bool {
249 true
250 }
251
252 fn schema_name() -> std::borrow::Cow<'static, str> {
253 std::borrow::Cow::Borrowed(stringify!($name))
254 }
255
256 fn schema_id() -> std::borrow::Cow<'static, str> {
257 std::borrow::Cow::Borrowed(concat!(
258 module_path!(),
259 "::",
260 stringify!($name)
261 ))
262 }
263
264 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
265 schemars::json_schema!({
266 "type": "string",
267 "pattern": format!("^[0-9a-f]{{{}}}$", $len * 2),
268 "description": format!("{} bytes, lowercase hex", $len),
269 })
270 }
271 }
272 };
273}
274
275hex_bytes!(
276 Sha256Hex,
278 32
279);
280
281hex_bytes!(
282 SignatureHex,
284 64
285);
286
287hex_bytes!(
288 PublicKeyHex,
290 32
291);
292
293hex_bytes!(
294 GitCommitId,
297 20
298);
299
300hex_bytes!(
301 Blind,
304 32
305);
306
307hex_bytes!(
308 TextSalt,
311 32
312);
313
314impl Blind {
315 pub fn random() -> Self {
317 use rand::RngCore;
318 let mut bytes = [0u8; 32];
319 rand::rngs::OsRng.fill_bytes(&mut bytes);
320 Self(bytes)
321 }
322}
323
324impl TextSalt {
325 pub fn random() -> Self {
327 Self(*Blind::random().as_bytes())
328 }
329}
330
331impl From<Signature> for SignatureHex {
332 fn from(sig: Signature) -> Self {
333 Self(sig.to_bytes())
334 }
335}
336
337impl From<&SignatureHex> for Signature {
338 fn from(sig: &SignatureHex) -> Self {
339 Signature::from_bytes(&sig.0)
340 }
341}
342
343impl From<&VerifyingKey> for PublicKeyHex {
344 fn from(key: &VerifyingKey) -> Self {
345 Self(key.to_bytes())
346 }
347}
348
349impl PublicKeyHex {
350 pub fn to_verifying_key(
352 &self,
353 ) -> Result<VerifyingKey, ed25519_dalek::SignatureError> {
354 VerifyingKey::from_bytes(&self.0)
355 }
356}
357
358pub fn canonical_json(value: &serde_json::Value) -> Vec<u8> {
371 let mut out = Vec::new();
372 write_canonical(value, &mut out);
373 out
374}
375
376fn write_canonical(value: &serde_json::Value, out: &mut Vec<u8>) {
377 use serde_json::Value;
378 match value {
379 Value::Null => out.extend_from_slice(b"null"),
380 Value::Bool(b) => {
381 out.extend_from_slice(if *b { b"true" } else { b"false" })
382 }
383 Value::Number(n) => serde_json::to_writer(&mut *out, n)
384 .expect("a number always serializes"),
385 Value::String(s) => serde_json::to_writer(&mut *out, s)
386 .expect("a string always serializes"),
387 Value::Array(items) => {
388 out.push(b'[');
389 for (i, item) in items.iter().enumerate() {
390 if i > 0 {
391 out.push(b',');
392 }
393 write_canonical(item, out);
394 }
395 out.push(b']');
396 }
397 Value::Object(map) => {
398 let mut keys: Vec<&String> = map.keys().collect();
399 keys.sort_unstable();
400 out.push(b'{');
401 for (i, key) in keys.into_iter().enumerate() {
402 if i > 0 {
403 out.push(b',');
404 }
405 serde_json::to_writer(&mut *out, key)
406 .expect("a string always serializes");
407 out.push(b':');
408 write_canonical(&map[key], out);
409 }
410 out.push(b'}');
411 }
412 }
413}
414
415pub fn non_integer_number(data: &serde_json::Value) -> Option<String> {
426 fn find(value: &serde_json::Value, path: &mut String) -> bool {
427 use serde_json::Value::{Array, Number, Object};
428 let mark = path.len();
429 match value {
430 Number(n) => return n.is_f64(),
431 Array(items) => {
432 for (i, item) in items.iter().enumerate() {
433 path.push_str(&format!("/{i}"));
434 if find(item, path) {
435 return true;
436 }
437 path.truncate(mark);
438 }
439 }
440 Object(map) => {
441 for (key, item) in map {
442 path.push('/');
443 path.push_str(&escape_pointer_token(key));
444 if find(item, path) {
445 return true;
446 }
447 path.truncate(mark);
448 }
449 }
450 _ => {}
451 }
452 false
453 }
454 let mut path = String::new();
455 find(data, &mut path).then_some(path)
456}
457
458pub fn escape_pointer_token(token: &str) -> String {
462 token.replace('~', "~0").replace('/', "~1")
463}
464
465pub fn unescape_pointer_token(token: &str) -> String {
468 token.replace("~1", "/").replace("~0", "~")
469}
470
471pub fn pointer_tokens(pointer: &str) -> Vec<String> {
474 pointer
475 .split('/')
476 .skip(1)
477 .map(unescape_pointer_token)
478 .collect()
479}
480
481pub fn data_hash(data: &serde_json::Value) -> Sha256Hex {
483 Sha256Hex(Sha256::digest(canonical_json(data)).into())
484}
485
486#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
491pub struct Envelope {
492 pub agora_governance_log: u32,
494 pub id: GovernanceLogId,
495 pub entry_type: GovernanceLogEntryType,
496 pub created_at: i64,
498 pub prev_hash: Option<Sha256Hex>,
499 pub data_hash: Sha256Hex,
500}
501
502impl Envelope {
503 pub fn new(
505 id: GovernanceLogId,
506 entry_type: GovernanceLogEntryType,
507 created_at: DateTime<Utc>,
508 prev_hash: Option<Sha256Hex>,
509 data_hash: Sha256Hex,
510 ) -> Self {
511 Self {
512 agora_governance_log: ENVELOPE_VERSION,
513 id,
514 entry_type,
515 created_at: created_at.timestamp_micros(),
516 prev_hash,
517 data_hash,
518 }
519 }
520
521 pub fn created_at(&self) -> DateTime<Utc> {
523 DateTime::from_timestamp_micros(self.created_at)
524 .expect("an Envelope only ever holds an in-range timestamp")
525 }
526
527 pub fn preimage(&self) -> Vec<u8> {
529 serde_json::to_vec(self).expect("an Envelope always serializes")
530 }
531
532 pub fn entry_hash(&self) -> Sha256Hex {
534 Sha256Hex(Sha256::digest(self.preimage()).into())
535 }
536}
537
538pub fn truncate_to_micros(t: DateTime<Utc>) -> DateTime<Utc> {
541 DateTime::from_timestamp_micros(t.timestamp_micros())
542 .expect("a timestamp that came from a DateTime is in range")
543}
544
545pub fn truncate_to_seconds(t: DateTime<Utc>) -> DateTime<Utc> {
548 DateTime::from_timestamp(t.timestamp(), 0)
549 .expect("a timestamp that came from a DateTime is in range")
550}
551
552pub fn is_retroactive(
556 created_at: DateTime<Utc>,
557 signed_at: DateTime<Utc>,
558) -> bool {
559 signed_at - created_at > RETROACTIVE_AFTER
560}
561
562#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
568#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
569#[cfg_attr(feature = "schemars", schemars(inline))]
570pub struct GovernanceAttestation {
571 pub envelope_version: u32,
573 pub chain_seq: u64,
576 pub prev_hash: Option<Sha256Hex>,
578 pub data_hash: Sha256Hex,
580 pub entry_hash: Sha256Hex,
582 pub signature: SignatureHex,
585 pub signed_at: DateTime<Utc>,
588 pub retroactive: bool,
592}
593
594#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
598#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
599#[cfg_attr(feature = "schemars", schemars(inline))]
600pub struct GovernanceChainLink {
601 pub id: GovernanceLogId,
602 pub entry_type: GovernanceLogEntryType,
603 pub created_at: DateTime<Utc>,
604 pub attestation: GovernanceAttestation,
605 #[serde(default, skip_serializing_if = "Option::is_none")]
612 pub data: Option<serde_json::Value>,
613 #[serde(
617 default,
618 skip_serializing_if = "Option::is_none",
619 deserialize_with = "read_as_written"
620 )]
621 pub texts: Option<AmendmentTexts>,
622}
623
624#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
627#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
628#[cfg_attr(feature = "schemars", schemars(inline))]
629pub struct GovernanceSigningKey {
630 pub algorithm: String,
632 pub public_key: PublicKeyHex,
633 pub envelope_version: u32,
635}
636
637impl GovernanceSigningKey {
638 pub fn new(key: &VerifyingKey) -> Self {
640 Self {
641 algorithm: "ed25519".to_string(),
642 public_key: key.into(),
643 envelope_version: ENVELOPE_VERSION,
644 }
645 }
646}
647
648pub const AMENDMENT_VERSION: u32 = 2;
656
657#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
659pub enum AmendmentError {
660 #[error("agora_governance_amendment is {0}, not 1 or {AMENDMENT_VERSION}")]
661 UnsupportedVersion(u32),
662 #[error(
663 "a version 1 amendment carries its texts and a version \
664 {AMENDMENT_VERSION} one commits to them; this does neither \
665 consistently"
666 )]
667 TextShape,
668 #[error("`{0}` beside the entry is not the text the entry committed to")]
669 TextMismatch(&'static str),
670 #[error("texts beside an entry that commits to none")]
671 UncommittedText,
672 #[error("kind `redaction` requires a `redaction`")]
673 MissingRedaction,
674 #[error("`redaction` is only valid on kind `redaction`")]
675 UnexpectedRedaction,
676 #[error("kind `revision` requires a `revision`")]
677 MissingRevision,
678 #[error("`revision` is only valid on kind `revision`")]
679 UnexpectedRevision,
680 #[error("the `revision` is malformed: {0}")]
681 RevisionShape(ReviseError),
682 #[error("kind `{0}` requires an `update`")]
683 MissingUpdate(AmendmentKind),
684 #[error("`update` is only valid on the obligation kinds")]
685 UnexpectedUpdate,
686 #[error("an `update` of kind `{update}` on an amendment of kind `{kind}`")]
687 UpdateMismatch {
688 kind: AmendmentKind,
689 update: AmendmentKind,
690 },
691 #[error(
692 "kind `{kind}` is only valid on an obligation, and {target} is not one"
693 )]
694 NotAnObligation {
695 kind: AmendmentKind,
696 target: GovernanceLogId,
697 },
698 #[error(
699 "kind `{0}` changes precedential force, which an obligation does not have"
700 )]
701 PrecedentialOnObligation(AmendmentKind),
702 #[error(
703 "a revision of an obligation may not touch `{0}`; its status and \
704 deadline change only through the obligation kinds"
705 )]
706 FixedObligationField(String),
707 #[error("amendment target {0} is not an entry of this chain")]
708 UnknownTarget(GovernanceLogId),
709 #[error("amendment target {0} is not an earlier entry")]
710 ForwardReference(GovernanceLogId),
711 #[error("target_entry_hash is not {0}'s entry_hash")]
712 WrongTargetHash(GovernanceLogId),
713}
714
715#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
721#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
722#[cfg_attr(feature = "schemars", schemars(inline))]
723pub struct Amendment {
724 pub agora_governance_amendment: u32,
726 pub target: GovernanceLogId,
727 pub target_entry_hash: Sha256Hex,
729 pub kind: AmendmentKind,
730 #[serde(default)]
733 pub authority: Option<GovernanceLogId>,
734 pub basis: AmendmentText,
738 pub note: AmendmentText,
740 #[serde(default, skip_serializing_if = "Option::is_none")]
742 pub rationale: Option<AmendmentText>,
743 #[serde(default, skip_serializing_if = "Option::is_none")]
745 pub redaction: Option<Redaction>,
746 #[serde(default, skip_serializing_if = "Option::is_none")]
748 pub revision: Option<Revision>,
749 #[serde(default, skip_serializing_if = "Option::is_none")]
752 pub update: Option<ObligationUpdate>,
753}
754
755#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
757#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
758#[cfg_attr(feature = "schemars", schemars(inline))]
759pub struct Redaction {
760 pub fields: Vec<String>,
763 pub resulting_data_hash: Sha256Hex,
766 #[serde(default, skip_serializing_if = "Option::is_none")]
770 pub resulting_latest_hash: Option<Sha256Hex>,
771}
772
773#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
785pub struct Revision {
786 pub patch: json_patch::Patch,
788 #[serde(default, skip_serializing_if = "Vec::is_empty")]
792 pub duplicates: Vec<(String, String)>,
793 pub resulting_data_hash: Sha256Hex,
795}
796
797#[derive(Debug, Clone, PartialEq, Eq, Default)]
800pub struct Edit {
801 pub patch: json_patch::Patch,
802 pub duplicates: Vec<(String, String)>,
804}
805
806impl From<json_patch::Patch> for Edit {
807 fn from(patch: json_patch::Patch) -> Self {
808 Self {
809 patch,
810 duplicates: Vec::new(),
811 }
812 }
813}
814
815#[cfg(feature = "schemars")]
818impl schemars::JsonSchema for Revision {
819 fn inline_schema() -> bool {
820 true
821 }
822
823 fn schema_name() -> std::borrow::Cow<'static, str> {
824 "Revision".into()
825 }
826
827 fn json_schema(
828 generator: &mut schemars::SchemaGenerator,
829 ) -> schemars::Schema {
830 let hash = generator.subschema_for::<Sha256Hex>();
831 schemars::json_schema!({
832 "type": "object",
833 "properties": {
834 "patch": {
835 "description": "RFC 6902 JSON Patch from the previous version to this one",
836 "type": "array",
837 "items": {
838 "type": "object",
839 "properties": {
840 "op": {
841 "type": "string",
842 "enum": ["add", "remove", "replace", "move", "copy", "test"]
843 },
844 "path": {"type": "string"},
845 "from": {"type": "string"},
846 "value": true
847 },
848 "required": ["op", "path"]
849 }
850 },
851 "duplicates": {
852 "description": "(removed path, the path it duplicated) for each duplicate removed",
853 "type": "array",
854 "items": {
855 "type": "array",
856 "items": {"type": "string"},
857 "minItems": 2,
858 "maxItems": 2
859 }
860 },
861 "resulting_data_hash": hash
862 },
863 "required": ["patch", "resulting_data_hash"]
864 })
865 }
866}
867
868impl Revision {
869 pub fn validate(&self) -> Result<(), ReviseError> {
872 if self.patch.is_empty() {
873 return Err(ReviseError::EmptyPatch);
874 }
875 for (path, _) in &self.duplicates {
876 let removed = self.patch.iter().any(|op| {
877 matches!(op, json_patch::PatchOperation::Remove(r) if r.path.as_str() == path)
878 });
879 if !removed {
880 return Err(ReviseError::NotRemoved(path.clone()));
881 }
882 }
883 Ok(())
884 }
885
886 pub fn remove_duplicates(
890 data: &serde_json::Value,
891 pairs: &[(&str, &str)],
892 ) -> Result<Edit, ReviseError> {
893 let mut ops = Vec::with_capacity(pairs.len());
894 for (path, _) in pairs {
895 let path = json_patch::jsonptr::PointerBuf::parse(*path)
896 .map_err(|e| ReviseError::Patch(e.to_string()))?;
897 ops.push(json_patch::PatchOperation::Remove(
898 json_patch::RemoveOperation { path },
899 ));
900 }
901 let edit = Edit {
902 patch: json_patch::Patch(ops),
903 duplicates: pairs
904 .iter()
905 .map(|(p, s)| (p.to_string(), s.to_string()))
906 .collect(),
907 };
908 check_duplicates(data, &edit)?;
909 Ok(edit)
910 }
911}
912
913fn check_duplicates(
916 data: &serde_json::Value,
917 edit: &Edit,
918) -> Result<(), ReviseError> {
919 for (path, same_as) in &edit.duplicates {
920 let resolve = |pointer: &str| {
921 data.pointer(pointer)
922 .ok_or_else(|| ReviseError::Unresolved(pointer.to_string()))
923 };
924 let (removed, kept) = (resolve(path)?, resolve(same_as)?);
925 if canonical_json(removed) != canonical_json(kept) {
926 return Err(ReviseError::NotIdentical {
927 path: path.clone(),
928 same_as: same_as.clone(),
929 });
930 }
931 }
932 let revised = apply_patch(data, &edit.patch)?;
933 for (path, same_as) in &edit.duplicates {
934 let kept = data.pointer(same_as);
935 let removes = |op: &json_patch::PatchOperation| matches!(op, json_patch::PatchOperation::Remove(r) if r.path.as_str() == path);
936 if !edit.patch.iter().any(removes) {
937 return Err(ReviseError::NotRemoved(path.clone()));
938 }
939 let survives = revised.pointer(same_as).is_some_and(|v| {
940 kept.is_some_and(|k| canonical_json(v) == canonical_json(k))
941 });
942 if !survives {
943 return Err(ReviseError::SourceRemoved {
944 path: path.clone(),
945 same_as: same_as.clone(),
946 });
947 }
948 }
949 Ok(())
950}
951
952#[derive(Debug, Clone, PartialEq, Eq)]
955pub struct AmendmentDraft {
956 pub amendment: Amendment,
957 pub texts: AmendmentTexts,
958}
959
960impl AmendmentDraft {
961 pub fn new(
968 target: GovernanceLogId,
969 target_entry_hash: Sha256Hex,
970 kind: AmendmentKind,
971 basis: impl Into<CommittedText>,
972 note: impl Into<CommittedText>,
973 ) -> Result<Self, AmendmentError> {
974 match kind {
975 AmendmentKind::Redaction => {
976 return Err(AmendmentError::MissingRedaction);
977 }
978 AmendmentKind::Revision => {
979 return Err(AmendmentError::MissingRevision);
980 }
981 k if k.is_obligation() => {
982 return Err(AmendmentError::MissingUpdate(k));
983 }
984 _ => {}
985 }
986 let (basis, note) = (basis.into(), note.into());
987 Ok(Self {
988 amendment: Amendment {
989 agora_governance_amendment: AMENDMENT_VERSION,
990 target,
991 target_entry_hash,
992 kind,
993 authority: None,
994 basis: AmendmentText::Committed(basis.commitment()),
995 note: AmendmentText::Committed(note.commitment()),
996 rationale: None,
997 redaction: None,
998 revision: None,
999 update: None,
1000 },
1001 texts: AmendmentTexts {
1002 basis: Some(basis),
1003 note: Some(note),
1004 rationale: None,
1005 },
1006 })
1007 }
1008
1009 #[allow(clippy::too_many_arguments)]
1027 pub fn redaction(
1028 amendment_id: &GovernanceLogId,
1029 target: GovernanceLogId,
1030 target_entry_hash: Sha256Hex,
1031 basis: impl Into<CommittedText>,
1032 note: impl Into<CommittedText>,
1033 mut fields: Vec<String>,
1034 data: &serde_json::Value,
1035 blind: Blind,
1036 revisions: &[&Revision],
1037 ) -> Result<(Self, serde_json::Value), RedactError> {
1038 for extra in duplicates_of(&fields, revisions) {
1039 let covered = fields.iter().any(|f| {
1040 extra.strip_prefix(f.as_str()).is_some_and(|rest| {
1041 rest.is_empty() || rest.starts_with('/')
1042 })
1043 });
1044 if !covered && data.pointer(&extra).is_some() {
1045 fields.push(extra);
1046 }
1047 }
1048 let redacted = redact_data(data, &fields, amendment_id, blind)?;
1049 let resulting_latest_hash = match revisions {
1050 [] => None,
1051 _ => Some(data_hash(
1052 &latest(&redacted, revisions.iter().copied())
1053 .map_err(|e| RedactError::Rebase(e.to_string()))?,
1054 )),
1055 };
1056 let (basis, note) = (basis.into(), note.into());
1057 Ok((
1058 Self {
1059 amendment: Amendment {
1060 agora_governance_amendment: AMENDMENT_VERSION,
1061 target,
1062 target_entry_hash,
1063 kind: AmendmentKind::Redaction,
1064 authority: None,
1065 basis: AmendmentText::Committed(basis.commitment()),
1066 note: AmendmentText::Committed(note.commitment()),
1067 rationale: None,
1068 redaction: Some(Redaction {
1069 fields,
1070 resulting_data_hash: data_hash(&redacted),
1071 resulting_latest_hash,
1072 }),
1073 revision: None,
1074 update: None,
1075 },
1076 texts: AmendmentTexts {
1077 basis: Some(basis),
1078 note: Some(note),
1079 rationale: None,
1080 },
1081 },
1082 redacted,
1083 ))
1084 }
1085
1086 pub fn revision(
1097 target: GovernanceLogId,
1098 target_type: GovernanceLogEntryType,
1099 target_entry_hash: Sha256Hex,
1100 basis: impl Into<CommittedText>,
1101 note: impl Into<CommittedText>,
1102 latest: &serde_json::Value,
1103 edit: impl Into<Edit>,
1104 ) -> Result<(Self, serde_json::Value), ReviseError> {
1105 let Edit {
1106 mut patch,
1107 duplicates,
1108 } = edit.into();
1109 if !is_revisable(target_type) {
1110 return Err(ReviseError::NotRevisable(target_type));
1111 }
1112 if patch.is_empty() {
1113 return Err(ReviseError::EmptyPatch);
1114 }
1115 let blind_pointer = format!("/{BLIND_KEY}");
1116 for op in patch.iter() {
1117 let from = match op {
1118 json_patch::PatchOperation::Move(m) => Some(m.from.as_str()),
1119 json_patch::PatchOperation::Copy(c) => Some(c.from.as_str()),
1120 _ => None,
1121 };
1122 for pointer in
1123 [Some(op.path().as_str()), from].into_iter().flatten()
1124 {
1125 if overlaps(pointer, &blind_pointer) {
1126 return Err(ReviseError::BlindPointer(pointer.to_string()));
1127 }
1128 }
1129 }
1130 let adds = patch.iter().any(|op| {
1131 matches!(
1132 op,
1133 json_patch::PatchOperation::Add(_)
1134 | json_patch::PatchOperation::Replace(_)
1135 )
1136 });
1137 if adds && latest.is_object() && latest.get(BLIND_KEY).is_none() {
1138 patch.0.push(json_patch::PatchOperation::Add(
1139 json_patch::AddOperation {
1140 path: json_patch::jsonptr::PointerBuf::from_tokens([
1141 BLIND_KEY,
1142 ]),
1143 value: Blind::random().to_hex().into(),
1144 },
1145 ));
1146 }
1147 let edit = Edit { patch, duplicates };
1148 check_duplicates(latest, &edit)?;
1149 let Edit { patch, duplicates } = edit;
1150 let revised = apply_patch(latest, &patch)?;
1151 let (basis, note) = (basis.into(), note.into());
1152 Ok((
1153 Self {
1154 amendment: Amendment {
1155 agora_governance_amendment: AMENDMENT_VERSION,
1156 target,
1157 target_entry_hash,
1158 kind: AmendmentKind::Revision,
1159 authority: None,
1160 basis: AmendmentText::Committed(basis.commitment()),
1161 note: AmendmentText::Committed(note.commitment()),
1162 rationale: None,
1163 redaction: None,
1164 revision: Some(Revision {
1165 patch,
1166 duplicates,
1167 resulting_data_hash: data_hash(&revised),
1168 }),
1169 update: None,
1170 },
1171 texts: AmendmentTexts {
1172 basis: Some(basis),
1173 note: Some(note),
1174 rationale: None,
1175 },
1176 },
1177 revised,
1178 ))
1179 }
1180
1181 pub fn obligation(
1183 target: GovernanceLogId,
1184 target_entry_hash: Sha256Hex,
1185 update: ObligationUpdate,
1186 basis: impl Into<CommittedText>,
1187 note: impl Into<CommittedText>,
1188 ) -> Self {
1189 let (basis, note) = (basis.into(), note.into());
1190 Self {
1191 amendment: Amendment {
1192 agora_governance_amendment: AMENDMENT_VERSION,
1193 target,
1194 target_entry_hash,
1195 kind: update.kind(),
1196 authority: None,
1197 basis: AmendmentText::Committed(basis.commitment()),
1198 note: AmendmentText::Committed(note.commitment()),
1199 rationale: None,
1200 redaction: None,
1201 revision: None,
1202 update: Some(update),
1203 },
1204 texts: AmendmentTexts {
1205 basis: Some(basis),
1206 note: Some(note),
1207 rationale: None,
1208 },
1209 }
1210 }
1211
1212 pub fn with_authority(mut self, authority: GovernanceLogId) -> Self {
1214 self.amendment.authority = Some(authority);
1215 self
1216 }
1217
1218 pub fn with_rationale(
1220 mut self,
1221 rationale: impl Into<CommittedText>,
1222 ) -> Self {
1223 let rationale = rationale.into();
1224 self.amendment.rationale =
1225 Some(AmendmentText::Committed(rationale.commitment()));
1226 self.texts.rationale = Some(rationale);
1227 self
1228 }
1229}
1230
1231impl Amendment {
1232 pub fn validate(&self) -> Result<(), AmendmentError> {
1236 let plain = match self.agora_governance_amendment {
1237 1 => true,
1238 AMENDMENT_VERSION => false,
1239 other => return Err(AmendmentError::UnsupportedVersion(other)),
1240 };
1241 let texts =
1242 [Some(&self.basis), Some(&self.note), self.rationale.as_ref()];
1243 if texts.into_iter().flatten().any(|t| t.is_plain() != plain) {
1244 return Err(AmendmentError::TextShape);
1245 }
1246 match (self.kind, &self.redaction) {
1247 (AmendmentKind::Redaction, None) => {
1248 return Err(AmendmentError::MissingRedaction);
1249 }
1250 (k, Some(_)) if k != AmendmentKind::Redaction => {
1251 return Err(AmendmentError::UnexpectedRedaction);
1252 }
1253 _ => {}
1254 }
1255 match (self.kind, &self.revision) {
1256 (AmendmentKind::Revision, None) => {
1257 return Err(AmendmentError::MissingRevision);
1258 }
1259 (AmendmentKind::Revision, Some(r)) => {
1260 r.validate().map_err(AmendmentError::RevisionShape)?;
1261 }
1262 (_, Some(_)) => return Err(AmendmentError::UnexpectedRevision),
1263 _ => {}
1264 }
1265 match (self.kind, &self.update) {
1266 (kind, None) if kind.is_obligation() => {
1267 Err(AmendmentError::MissingUpdate(kind))
1268 }
1269 (kind, Some(_)) if !kind.is_obligation() => {
1270 Err(AmendmentError::UnexpectedUpdate)
1271 }
1272 (kind, Some(update)) if update.kind() != kind => {
1273 Err(AmendmentError::UpdateMismatch {
1274 kind,
1275 update: update.kind(),
1276 })
1277 }
1278 _ => Ok(()),
1279 }
1280 }
1281
1282 pub fn text_status(
1289 &self,
1290 beside: Option<&AmendmentTexts>,
1291 ) -> Result<Option<AmendmentTextStatus>, AmendmentError> {
1292 let empty = AmendmentTexts::default();
1293 let beside = beside.unwrap_or(&empty);
1294 let (Some(basis), Some(note)) = (
1295 self.basis.status(beside.basis.as_ref()),
1296 self.note.status(beside.note.as_ref()),
1297 ) else {
1298 return if beside.is_empty() {
1299 Ok(None)
1300 } else {
1301 Err(AmendmentError::UncommittedText)
1302 };
1303 };
1304 let rationale = match (&self.rationale, &beside.rationale) {
1305 (None, Some(_)) => return Err(AmendmentError::UncommittedText),
1306 (None, None) => None,
1307 (Some(text), beside) => text.status(beside.as_ref()),
1308 };
1309 for (name, status) in [
1310 ("basis", Some(basis)),
1311 ("note", Some(note)),
1312 ("rationale", rationale),
1313 ] {
1314 if status == Some(TextStatus::Mismatch) {
1315 return Err(AmendmentError::TextMismatch(name));
1316 }
1317 }
1318 Ok(Some(AmendmentTextStatus {
1319 basis,
1320 note,
1321 rationale,
1322 }))
1323 }
1324}
1325
1326pub fn kind_standing(kind: AmendmentKind) -> Option<Standing> {
1329 match kind {
1330 AmendmentKind::NonPrecedential => Some(Standing::NonPrecedential),
1331 AmendmentKind::Overruled => Some(Standing::Overruled),
1332 AmendmentKind::Superseded => Some(Standing::Superseded),
1333 AmendmentKind::Reinstated => Some(Standing::InForce),
1334 AmendmentKind::Correction
1335 | AmendmentKind::Redaction
1336 | AmendmentKind::Reattested
1337 | AmendmentKind::Revision
1338 | AmendmentKind::Planned
1339 | AmendmentKind::Discharged
1340 | AmendmentKind::Lapsed
1341 | AmendmentKind::Rescheduled
1342 | AmendmentKind::Withdrawn => None,
1343 }
1344}
1345
1346pub fn standing(kinds: impl IntoIterator<Item = AmendmentKind>) -> Standing {
1349 kinds
1350 .into_iter()
1351 .filter_map(kind_standing)
1352 .last()
1353 .unwrap_or_default()
1354}
1355
1356pub const BLIND_KEY: &str = "_blind";
1359
1360pub fn is_redactable(entry_type: GovernanceLogEntryType) -> bool {
1364 !matches!(
1365 entry_type,
1366 GovernanceLogEntryType::Amendment | GovernanceLogEntryType::KeyRotation
1367 )
1368}
1369
1370#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1372pub enum BlindError {
1373 #[error("a redactable entry's data must be a JSON object")]
1374 NotAnObject,
1375 #[error(
1376 "data already has a {BLIND_KEY:?} key; the writer supplies it, not the caller"
1377 )]
1378 AlreadyBlinded,
1379 #[error(
1380 "{0:?} is a number that is not a 64-bit integer; governance data \
1381 never contains one (put a fraction in a string)"
1382 )]
1383 NonIntegerNumber(String),
1384}
1385
1386pub fn blind_data(
1404 data: &serde_json::Value,
1405 blind: Blind,
1406) -> Result<serde_json::Value, BlindError> {
1407 if let Some(pointer) = non_integer_number(data) {
1408 return Err(BlindError::NonIntegerNumber(pointer));
1409 }
1410 let mut out = data.clone();
1411 let object = out.as_object_mut().ok_or(BlindError::NotAnObject)?;
1412 if object.contains_key(BLIND_KEY) {
1413 return Err(BlindError::AlreadyBlinded);
1414 }
1415 object.insert(BLIND_KEY.to_string(), blind.to_hex().into());
1416 Ok(out)
1417}
1418
1419#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1421pub enum RedactError {
1422 #[error("pointer {0:?} does not resolve in the entry's data")]
1423 Unresolved(String),
1424 #[error("the empty pointer would redact the whole entry")]
1425 WholeEntry,
1426 #[error("a redaction names at least one pointer")]
1427 NoFields,
1428 #[error("{0:?} is the entry's blind; every redaction replaces it already")]
1429 BlindPointer(String),
1430 #[error("a revision no longer applies to the redacted data: {0}")]
1431 Rebase(String),
1432 #[error(
1433 "{0:?} is a number that is not a 64-bit integer; governance data \
1434 never contains one"
1435 )]
1436 NonIntegerNumber(String),
1437}
1438
1439pub fn redaction_marker(amendment_id: &GovernanceLogId) -> String {
1441 format!("[redacted by {amendment_id}]")
1442}
1443
1444pub fn redact_data(
1460 data: &serde_json::Value,
1461 fields: &[String],
1462 amendment_id: &GovernanceLogId,
1463 blind: Blind,
1464) -> Result<serde_json::Value, RedactError> {
1465 if fields.is_empty() {
1466 return Err(RedactError::NoFields);
1467 }
1468 if let Some(pointer) = non_integer_number(data) {
1469 return Err(RedactError::NonIntegerNumber(pointer));
1470 }
1471 let blind_pointer = format!("/{BLIND_KEY}");
1472 let marker = serde_json::Value::String(redaction_marker(amendment_id));
1473 let mut out = data.clone();
1474 for pointer in fields {
1475 if pointer.is_empty() {
1476 return Err(RedactError::WholeEntry);
1477 }
1478 if *pointer == blind_pointer {
1479 return Err(RedactError::BlindPointer(pointer.clone()));
1480 }
1481 let slot = out
1482 .pointer_mut(pointer)
1483 .ok_or_else(|| RedactError::Unresolved(pointer.clone()))?;
1484 *slot = marker.clone();
1485 }
1486 if let Some(object) = out.as_object_mut() {
1489 object.insert(BLIND_KEY.to_string(), blind.to_hex().into());
1490 }
1491 Ok(out)
1492}
1493
1494pub fn is_revisable(entry_type: GovernanceLogEntryType) -> bool {
1498 is_redactable(entry_type)
1499 && entry_type != GovernanceLogEntryType::StewardRecord
1500}
1501
1502#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1504pub enum ReviseError {
1505 #[error("a revision's patch has at least one op")]
1506 EmptyPatch,
1507 #[error("the patch does not apply: {0}")]
1508 Patch(String),
1509 #[error("{0:?} is the entry's blind, which a revision never touches")]
1510 BlindPointer(String),
1511 #[error("pointer {0:?} does not resolve")]
1512 Unresolved(String),
1513 #[error("{path:?} is not byte-identical to {same_as:?}")]
1514 NotIdentical { path: String, same_as: String },
1515 #[error("{same_as:?}, which {path:?} duplicates, has to survive the patch")]
1516 SourceRemoved { path: String, same_as: String },
1517 #[error("{0:?} is listed as a duplicate the patch does not remove")]
1518 NotRemoved(String),
1519 #[error("{0} entries are never revised")]
1520 NotRevisable(GovernanceLogEntryType),
1521 #[error(
1522 "{0:?} is a number that is not a 64-bit integer; governance data \
1523 never contains one"
1524 )]
1525 NonIntegerNumber(String),
1526}
1527
1528fn overlaps(a: &str, b: &str) -> bool {
1530 let within = |inner: &str, outer: &str| {
1531 inner
1532 .strip_prefix(outer)
1533 .is_some_and(|rest| rest.is_empty() || rest.starts_with('/'))
1534 };
1535 within(a, b) || within(b, a)
1536}
1537
1538pub fn apply_patch(
1540 data: &serde_json::Value,
1541 patch: &json_patch::Patch,
1542) -> Result<serde_json::Value, ReviseError> {
1543 let mut out = data.clone();
1544 json_patch::patch(&mut out, patch)
1545 .map_err(|e| ReviseError::Patch(e.to_string()))?;
1546 if let Some(pointer) = non_integer_number(&out) {
1547 return Err(ReviseError::NonIntegerNumber(pointer));
1548 }
1549 Ok(out)
1550}
1551
1552pub fn latest<'a>(
1557 data: &serde_json::Value,
1558 revisions: impl IntoIterator<Item = &'a Revision>,
1559) -> Result<serde_json::Value, ReviseError> {
1560 let mut current = data.clone();
1561 for revision in revisions {
1562 current = apply_patch(¤t, &revision.patch)?;
1563 }
1564 Ok(current)
1565}
1566
1567fn duplicates_of(fields: &[String], revisions: &[&Revision]) -> Vec<String> {
1571 let mut extra = Vec::new();
1572 for (removed, same_as) in revisions.iter().flat_map(|r| &r.duplicates) {
1573 for field in fields {
1574 if let Some(rest) = field.strip_prefix(same_as.as_str())
1575 && (rest.is_empty() || rest.starts_with('/'))
1576 {
1577 extra.push(format!("{removed}{rest}"));
1579 } else if overlaps(field, same_as) {
1580 extra.push(removed.clone());
1582 }
1583 }
1584 }
1585 extra
1586}
1587
1588#[derive(Debug, Clone, PartialEq, Eq, Default)]
1591pub struct RevisionHistory {
1592 pub revisions: Vec<(GovernanceLogId, Revision)>,
1594 pub rebased: usize,
1597 pub rebased_hash: Option<Sha256Hex>,
1599}
1600
1601#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1603#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1604#[cfg_attr(feature = "schemars", schemars(inline))]
1605pub struct AmendmentNotice {
1606 pub id: GovernanceLogId,
1607 pub kind: AmendmentKind,
1608 #[serde(default)]
1609 pub authority: Option<GovernanceLogId>,
1610 pub basis: String,
1611 pub note: String,
1612 #[serde(default, skip_serializing_if = "Option::is_none")]
1614 pub rationale: Option<String>,
1615 #[serde(default, skip_serializing_if = "Option::is_none")]
1617 pub update: Option<ObligationUpdate>,
1618 pub created_at: DateTime<Utc>,
1619}
1620
1621impl AmendmentNotice {
1622 pub fn new(
1625 id: GovernanceLogId,
1626 created_at: DateTime<Utc>,
1627 amendment: &Amendment,
1628 beside: Option<&AmendmentTexts>,
1629 ) -> Self {
1630 let beside = beside.cloned().unwrap_or_default();
1631 Self {
1632 id,
1633 kind: amendment.kind,
1634 authority: amendment.authority.clone(),
1635 basis: amendment.basis.resolve(beside.basis.as_ref()).into(),
1636 note: amendment.note.resolve(beside.note.as_ref()).into(),
1637 rationale: amendment
1638 .rationale
1639 .as_ref()
1640 .map(|r| r.resolve(beside.rationale.as_ref()).into()),
1641 update: amendment.update.clone(),
1642 created_at,
1643 }
1644 }
1645}
1646
1647#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1653#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1654#[cfg_attr(feature = "schemars", schemars(inline))]
1655pub struct GovernanceCitation {
1656 pub id: GovernanceLogId,
1657 pub entry_type: GovernanceLogEntryType,
1658 pub title: String,
1659 pub relation: CitationRelation,
1660 pub created_at: DateTime<Utc>,
1661}
1662
1663#[derive(Debug, thiserror::Error)]
1667#[error("the record's {relation:?} field does not parse: {error}")]
1668pub struct MalformedCitation {
1669 pub relation: CitationRelation,
1672 #[source]
1673 pub error: serde_json::Error,
1674}
1675
1676pub fn citations_in(
1681 entry_type: GovernanceLogEntryType,
1682 data: &serde_json::Value,
1683) -> Result<Vec<(GovernanceLogId, CitationRelation)>, MalformedCitation> {
1684 #[derive(Deserialize)]
1685 struct Concerns {
1686 #[serde(default)]
1687 concerns: Vec<GovernanceLogId>,
1688 }
1689 #[derive(Deserialize)]
1690 struct Authority {
1691 #[serde(default)]
1692 authority: Option<GovernanceLogId>,
1693 }
1694 #[derive(Deserialize)]
1695 struct Overrules {
1696 #[serde(default)]
1697 overrules: Vec<GovernanceLogId>,
1698 }
1699 fn each(
1700 ids: impl IntoIterator<Item = GovernanceLogId>,
1701 relation: CitationRelation,
1702 ) -> Vec<(GovernanceLogId, CitationRelation)> {
1703 ids.into_iter().map(|id| (id, relation)).collect()
1704 }
1705 let malformed =
1706 |relation| move |error| MalformedCitation { relation, error };
1707 match entry_type {
1708 GovernanceLogEntryType::StewardRecord => {
1709 serde_json::from_value::<Concerns>(data.clone())
1710 .map(|c| each(c.concerns, CitationRelation::Concerns))
1711 .map_err(malformed(CitationRelation::Concerns))
1712 }
1713 GovernanceLogEntryType::Amendment => {
1714 serde_json::from_value::<Authority>(data.clone())
1715 .map(|a| each(a.authority, CitationRelation::Authority))
1716 .map_err(malformed(CitationRelation::Authority))
1717 }
1718 GovernanceLogEntryType::CouncilDecision => {
1719 serde_json::from_value::<Overrules>(data.clone())
1720 .map(|o| each(o.overrules, CitationRelation::Overrules))
1721 .map_err(malformed(CitationRelation::Overrules))
1722 }
1723 _ => Ok(Vec::new()),
1724 }
1725}
1726
1727pub const KEY_ROTATION_VERSION: u32 = 2;
1733
1734pub const PUBLISHED_KEYS: &[&str] =
1741 &["ebb3091dd328f1463362c171121921b2fe14628e3fc4c145deaccefb85c0e78a"];
1742
1743#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1745#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1746#[cfg_attr(feature = "schemars", schemars(inline))]
1747#[serde(rename_all = "snake_case")]
1748pub enum RotationReason {
1749 Routine,
1751 Compromise,
1754}
1755
1756#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1758#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1759#[cfg_attr(feature = "schemars", schemars(inline))]
1760#[serde(deny_unknown_fields)]
1761pub struct TrustedHead {
1762 pub id: GovernanceLogId,
1763 pub chain_seq: u64,
1764 pub entry_hash: Sha256Hex,
1765}
1766
1767#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1769pub enum RotationError {
1770 #[error("agora_governance_key_rotation is {0}, not {KEY_ROTATION_VERSION}")]
1771 UnsupportedVersion(u32),
1772 #[error("new_key is not a valid Ed25519 public key")]
1773 BadNewKey,
1774 #[error(
1775 "the proof of possession does not verify for this rotation at this position"
1776 )]
1777 BadProof,
1778 #[error("a compromise certificate must name last_trusted")]
1779 MissingLastTrusted,
1780 #[error("certificate: {0}")]
1781 Certificate(#[from] CertificateError),
1782 #[error("outgoing_certificate: {0}")]
1783 OutgoingCertificate(CertificateError),
1784 #[error(
1785 "the chain's first rotation must carry the genesis key's \
1786 outgoing_certificate"
1787 )]
1788 MissingGenesisCertificate,
1789 #[error(
1790 "outgoing_certificate belongs on the chain's first rotation and \
1791 nowhere else"
1792 )]
1793 UnexpectedOutgoingCertificate,
1794 #[error("old_key is not the key that was in force")]
1795 WrongOldKey,
1796 #[error("last_trusted does not name an earlier entry of this chain")]
1797 UnknownLastTrusted,
1798 #[error("new_key has already held this chain; a key is never brought back")]
1799 ReusedKey,
1800 #[error("last_trusted names an entry an earlier compromise repudiated")]
1801 RepudiatedLastTrusted,
1802}
1803
1804#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1815#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1816#[cfg_attr(feature = "schemars", schemars(inline))]
1817#[serde(deny_unknown_fields)]
1818pub struct KeyRotation {
1819 pub agora_governance_key_rotation: u32,
1821 pub reason: RotationReason,
1822 pub old_key: PublicKeyHex,
1823 pub new_key: PublicKeyHex,
1824 pub proof: SignatureHex,
1828 pub proof_signed_at: i64,
1830 pub certificate: KeyCertificate,
1834 #[serde(default, skip_serializing_if = "Option::is_none")]
1838 pub outgoing_certificate: Option<KeyCertificate>,
1839}
1840
1841#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1848pub struct RotationStatement {
1849 pub agora_governance_key_rotation: u32,
1851 pub reason: RotationReason,
1852 pub old_key: PublicKeyHex,
1853 pub new_key: PublicKeyHex,
1854 pub prev_hash: Option<Sha256Hex>,
1855}
1856
1857impl RotationStatement {
1858 pub fn new(
1860 reason: RotationReason,
1861 old_key: PublicKeyHex,
1862 new_key: PublicKeyHex,
1863 prev_hash: Option<Sha256Hex>,
1864 ) -> Self {
1865 Self {
1866 agora_governance_key_rotation: KEY_ROTATION_VERSION,
1867 reason,
1868 old_key,
1869 new_key,
1870 prev_hash,
1871 }
1872 }
1873
1874 pub fn preimage(&self) -> Vec<u8> {
1876 serde_json::to_vec(self).expect("a RotationStatement always serializes")
1877 }
1878
1879 pub fn hash(&self) -> Sha256Hex {
1881 Sha256Hex(Sha256::digest(self.preimage()).into())
1882 }
1883}
1884
1885impl KeyRotation {
1886 pub fn routine(
1892 old_key: PublicKeyHex,
1893 new_signing_key: &SigningKey,
1894 prev_hash: Option<Sha256Hex>,
1895 now: DateTime<Utc>,
1896 certificate: KeyCertificate,
1897 ) -> Self {
1898 Self::build(
1899 RotationReason::Routine,
1900 old_key,
1901 new_signing_key,
1902 prev_hash,
1903 now,
1904 certificate,
1905 )
1906 }
1907
1908 pub fn compromise(
1916 old_key: PublicKeyHex,
1917 new_signing_key: &SigningKey,
1918 prev_hash: Option<Sha256Hex>,
1919 now: DateTime<Utc>,
1920 certificate: KeyCertificate,
1921 ) -> Self {
1922 Self::build(
1923 RotationReason::Compromise,
1924 old_key,
1925 new_signing_key,
1926 prev_hash,
1927 now,
1928 certificate,
1929 )
1930 }
1931
1932 fn build(
1933 reason: RotationReason,
1934 old_key: PublicKeyHex,
1935 new_signing_key: &SigningKey,
1936 prev_hash: Option<Sha256Hex>,
1937 now: DateTime<Utc>,
1938 certificate: KeyCertificate,
1939 ) -> Self {
1940 let new_key = PublicKeyHex::from(&new_signing_key.verifying_key());
1941 let proof_signed_at = truncate_to_seconds(now).timestamp();
1942 let statement =
1943 RotationStatement::new(reason, old_key, new_key, prev_hash);
1944 let proof = crypto::sign(
1945 new_signing_key,
1946 statement.hash().as_bytes(),
1947 proof_signed_at,
1948 );
1949 Self {
1950 agora_governance_key_rotation: KEY_ROTATION_VERSION,
1951 reason,
1952 old_key,
1953 new_key,
1954 proof: proof.into(),
1955 proof_signed_at,
1956 certificate,
1957 outgoing_certificate: None,
1958 }
1959 }
1960
1961 pub fn with_outgoing(mut self, certificate: KeyCertificate) -> Self {
1963 self.outgoing_certificate = Some(certificate);
1964 self
1965 }
1966
1967 pub fn statement(&self, prev_hash: Option<Sha256Hex>) -> RotationStatement {
1969 RotationStatement::new(
1970 self.reason,
1971 self.old_key,
1972 self.new_key,
1973 prev_hash,
1974 )
1975 }
1976
1977 pub fn last_trusted(&self) -> Option<&TrustedHead> {
1980 self.certificate.statement.last_trusted.as_ref()
1981 }
1982
1983 pub fn expected_statement(
1989 &self,
1990 seq: u64,
1991 prev_hash: Option<Sha256Hex>,
1992 ) -> Result<KeyCertStatement, RotationError> {
1993 match self.reason {
1994 RotationReason::Routine => {
1995 Ok(KeyCertStatement::routine(self.new_key, seq, prev_hash))
1996 }
1997 RotationReason::Compromise => Ok(KeyCertStatement::compromise(
1998 self.new_key,
1999 seq,
2000 prev_hash,
2001 self.last_trusted()
2002 .cloned()
2003 .ok_or(RotationError::MissingLastTrusted)?,
2004 )),
2005 }
2006 }
2007
2008 pub fn verify_proof(
2011 &self,
2012 prev_hash: Option<Sha256Hex>,
2013 ) -> Result<(), RotationError> {
2014 if self.agora_governance_key_rotation != KEY_ROTATION_VERSION {
2015 return Err(RotationError::UnsupportedVersion(
2016 self.agora_governance_key_rotation,
2017 ));
2018 }
2019 let new_key = self
2020 .new_key
2021 .to_verifying_key()
2022 .map_err(|_| RotationError::BadNewKey)?;
2023 crypto::verify(
2024 &new_key,
2025 self.statement(prev_hash).hash().as_bytes(),
2026 self.proof_signed_at,
2027 &Signature::from(&self.proof),
2028 )
2029 .then_some(())
2030 .ok_or(RotationError::BadProof)
2031 }
2032
2033 pub fn verify_certified(
2037 &self,
2038 seq: u64,
2039 prev_hash: Option<Sha256Hex>,
2040 roots: &RootSet,
2041 ) -> Result<(), RotationError> {
2042 self.verify_proof(prev_hash)?;
2043 let expected = self.expected_statement(seq, prev_hash)?;
2044 Ok(self.certificate.verify_for(&expected, roots)?)
2045 }
2046}
2047
2048#[derive(Debug, Clone, Default, PartialEq, Eq)]
2055pub struct KeyAnchor {
2056 keys: HashSet<PublicKeyHex>,
2057}
2058
2059impl KeyAnchor {
2060 pub fn published() -> Self {
2062 PUBLISHED_KEYS
2063 .iter()
2064 .map(|k| {
2065 k.parse()
2066 .expect("PUBLISHED_KEYS are valid 32-byte hex keys")
2067 })
2068 .collect()
2069 }
2070
2071 pub fn pinned(key: PublicKeyHex) -> Self {
2073 std::iter::once(key).collect()
2074 }
2075
2076 pub fn with(mut self, key: PublicKeyHex) -> Self {
2078 self.keys.insert(key);
2079 self
2080 }
2081
2082 pub fn contains(&self, key: &PublicKeyHex) -> bool {
2083 self.keys.contains(key)
2084 }
2085
2086 pub fn is_empty(&self) -> bool {
2087 self.keys.is_empty()
2088 }
2089
2090 pub fn keys(&self) -> impl Iterator<Item = &PublicKeyHex> {
2092 self.keys.iter()
2093 }
2094}
2095
2096impl FromIterator<PublicKeyHex> for KeyAnchor {
2097 fn from_iter<I: IntoIterator<Item = PublicKeyHex>>(iter: I) -> Self {
2098 Self {
2099 keys: iter.into_iter().collect(),
2100 }
2101 }
2102}
2103
2104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2107#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2108#[cfg_attr(feature = "schemars", schemars(inline))]
2109pub struct GovernanceKeyRecord {
2110 pub public_key: PublicKeyHex,
2111 pub from_seq: u64,
2113 #[serde(default)]
2117 pub through_seq: Option<u64>,
2118 pub status: KeyStatus,
2119 #[serde(default)]
2122 pub introduced_by: Option<GovernanceLogId>,
2123 #[serde(default)]
2125 pub retired_by: Option<GovernanceLogId>,
2126 #[serde(default)]
2130 pub certified: bool,
2131}
2132
2133#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2138#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2139#[cfg_attr(feature = "schemars", schemars(inline))]
2140pub struct GovernanceSigningKeys {
2141 pub keys: Vec<GovernanceKeyRecord>,
2143}
2144
2145#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2147#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2148#[cfg_attr(feature = "schemars", schemars(inline))]
2149pub struct EntryVerdict {
2150 pub id: GovernanceLogId,
2151 pub chain_seq: u64,
2152 pub signature_valid: bool,
2155 pub link_valid: bool,
2158 #[serde(default)]
2165 pub content_matches: Option<bool>,
2166 pub retroactive: bool,
2168 pub out_of_order: bool,
2171 #[serde(default)]
2173 pub amended_by: Vec<GovernanceLogId>,
2174 #[serde(default, skip_serializing_if = "Option::is_none")]
2177 pub signed_by: Option<PublicKeyHex>,
2178 #[serde(default)]
2181 pub redacted: bool,
2182 #[serde(default, skip_serializing_if = "Option::is_none")]
2184 pub redacted_data_hash: Option<Sha256Hex>,
2185 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2188 pub revisions: Vec<GovernanceLogId>,
2189 #[serde(default, skip_serializing_if = "Option::is_none")]
2193 pub latest_data_hash: Option<Sha256Hex>,
2194 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2199 pub superseded_revisions: Vec<GovernanceLogId>,
2200 #[serde(skip)]
2203 pub history: RevisionHistory,
2204 #[serde(default)]
2207 pub repudiated: bool,
2208 #[serde(default)]
2211 pub reattested_by: Vec<GovernanceLogId>,
2212 #[serde(default, skip_serializing_if = "Option::is_none")]
2216 pub texts: Option<AmendmentTextStatus>,
2217 #[serde(default, skip_serializing_if = "Option::is_none")]
2219 pub problem: Option<String>,
2220}
2221
2222impl EntryVerdict {
2223 fn content_check(
2227 &mut self,
2228 attested: Sha256Hex,
2229 data: &serde_json::Value,
2230 ) -> bool {
2231 if non_integer_number(data).is_some() {
2233 return false;
2234 }
2235 let hash = data_hash(data);
2236 if hash == attested && self.redacted {
2237 return true;
2240 }
2241 if hash != attested && Some(hash) != self.redacted_data_hash {
2242 return Some(hash) == self.latest_data_hash;
2243 }
2244 let history = &self.history;
2245 let mut current = data.clone();
2246 let mut failures = Vec::new();
2247 for (i, (id, revision)) in history.revisions.iter().enumerate() {
2248 if i == history.rebased {
2249 break;
2250 }
2251 if let Some(failure) = false_duplicate(id, revision, ¤t) {
2252 failures.push(failure);
2253 break;
2254 }
2255 match apply_patch(¤t, &revision.patch) {
2256 Ok(next) => current = next,
2257 Err(_) => {
2258 failures.push(format!(
2259 "revision {id} does not apply to the redacted data"
2260 ));
2261 break;
2262 }
2263 }
2264 }
2265 if failures.is_empty()
2266 && let Some(expected) = history.rebased_hash
2267 && data_hash(¤t) != expected
2268 {
2269 failures.push(
2270 "the revisions rebased over the redacted data do not \
2271 produce the redaction's resulting_latest_hash"
2272 .to_string(),
2273 );
2274 }
2275 for (id, revision) in history.revisions.iter().skip(history.rebased) {
2276 if !failures.is_empty() {
2277 break;
2278 }
2279 if let Some(failure) = false_duplicate(id, revision, ¤t) {
2280 failures.push(failure);
2281 break;
2282 }
2283 match apply_patch(¤t, &revision.patch) {
2284 Ok(next)
2285 if data_hash(&next) == revision.resulting_data_hash =>
2286 {
2287 current = next;
2288 }
2289 Ok(_) => failures.push(format!(
2290 "revision {id} does not produce its resulting_data_hash"
2291 )),
2292 Err(_) => {
2293 failures.push(format!("revision {id} does not apply"))
2294 }
2295 }
2296 }
2297 for failure in failures {
2298 add_problem(&mut self.problem, failure);
2299 }
2300 true
2301 }
2302}
2303
2304fn add_problem(problems: &mut Option<String>, problem: String) {
2306 *problems = Some(match problems.take() {
2307 Some(p) if p.contains(&problem) => p,
2308 Some(p) => format!("{p}; {problem}"),
2309 None => problem,
2310 });
2311}
2312
2313fn false_duplicate(
2316 id: &GovernanceLogId,
2317 revision: &Revision,
2318 current: &serde_json::Value,
2319) -> Option<String> {
2320 revision.duplicates.iter().find_map(|(path, same_as)| {
2321 let same = match (current.pointer(path), current.pointer(same_as)) {
2322 (Some(a), Some(b)) => canonical_json(a) == canonical_json(b),
2323 _ => false,
2324 };
2325 (!same).then(|| {
2326 format!(
2327 "revision {id} removed {path} as a duplicate of {same_as}, \
2328 but they differ"
2329 )
2330 })
2331 })
2332}
2333
2334#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2336#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2337#[cfg_attr(feature = "schemars", schemars(inline))]
2338pub struct GovernanceVerification {
2339 pub public_key: PublicKeyHex,
2341 pub ok: bool,
2347 #[serde(default)]
2349 pub head: Option<GovernanceLogId>,
2350 pub entries: Vec<EntryVerdict>,
2352 #[serde(default)]
2354 pub keys: Vec<GovernanceKeyRecord>,
2355 #[serde(default)]
2360 pub unanchored_keys: Vec<PublicKeyHex>,
2361 #[serde(default)]
2363 pub repudiated: Vec<GovernanceLogId>,
2364}
2365
2366#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2370#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2371#[cfg_attr(feature = "schemars", schemars(inline))]
2372pub struct GovernanceVerificationSummary {
2373 pub public_key: PublicKeyHex,
2375 pub ok: bool,
2377 #[serde(default)]
2379 pub head: Option<GovernanceLogId>,
2380 pub entries: u64,
2382 pub signature_failures: u64,
2384 pub link_failures: u64,
2386 pub content_mismatches: u64,
2388 pub problems: u64,
2390 pub retroactive: u64,
2392 pub redacted: u64,
2394 #[serde(default)]
2396 pub failed: Vec<GovernanceLogId>,
2397 #[serde(default, skip_serializing_if = "Option::is_none")]
2399 pub first_failure: Option<EntryVerdict>,
2400 #[serde(default)]
2402 pub keys: Vec<GovernanceKeyRecord>,
2403 #[serde(default)]
2405 pub unanchored_keys: Vec<PublicKeyHex>,
2406 #[serde(default)]
2408 pub repudiated: Vec<GovernanceLogId>,
2409 pub how_to_read: String,
2410}
2411
2412impl EntryVerdict {
2413 pub fn failed(&self) -> bool {
2416 !self.signature_valid
2417 || !self.link_valid
2418 || self.content_matches == Some(false)
2419 || self.problem.is_some()
2420 }
2421}
2422
2423impl GovernanceVerification {
2424 pub fn summary(&self) -> GovernanceVerificationSummary {
2426 let count = |f: &dyn Fn(&EntryVerdict) -> bool| -> u64 {
2427 self.entries.iter().filter(|e| f(e)).count() as u64
2428 };
2429 let failed: Vec<&EntryVerdict> =
2430 self.entries.iter().filter(|e| e.failed()).collect();
2431 GovernanceVerificationSummary {
2432 public_key: self.public_key,
2433 ok: self.ok,
2434 head: self.head.clone(),
2435 entries: self.entries.len() as u64,
2436 signature_failures: count(&|e| !e.signature_valid),
2437 link_failures: count(&|e| !e.link_valid),
2438 content_mismatches: count(&|e| e.content_matches == Some(false)),
2439 problems: count(&|e| e.problem.is_some()),
2440 retroactive: count(&|e| e.retroactive),
2441 redacted: count(&|e| e.redacted),
2442 failed: failed.iter().map(|e| e.id.clone()).collect(),
2443 first_failure: failed.first().map(|e| (*e).clone()),
2444 keys: self.keys.clone(),
2445 unanchored_keys: self.unanchored_keys.clone(),
2446 repudiated: self.repudiated.clone(),
2447 how_to_read: format!(
2448 "A summary of the whole check of {n} entries: verify_governance_log \
2449 with full=true returns one verdict per entry (signature, link, \
2450 content, redaction, revisions, signing key), about {kb} KB. \
2451 Either way this is the server checking itself, not independent \
2452 evidence; agora-agentkit's govlog::verify_chain is the \
2453 independent check.",
2454 n = self.entries.len(),
2455 kb = serde_json::to_string(self).map_or(0, |s| s.len() / 1024),
2456 ),
2457 }
2458 }
2459
2460 pub fn settle(mut self) -> Self {
2462 self.ok = self.entries.iter().all(|e| !e.failed());
2463 self
2464 }
2465
2466 pub fn check_content(
2477 &mut self,
2478 link: &GovernanceChainLink,
2479 data: &serde_json::Value,
2480 ) -> bool {
2481 let Some(entry) = self.entries.iter_mut().find(|e| e.id == link.id)
2482 else {
2483 return false;
2484 };
2485 let ok = entry.content_check(link.attestation.data_hash, data);
2486 entry.content_matches = Some(ok);
2487 ok
2488 }
2489}
2490
2491pub fn attest(
2502 key: &SigningKey,
2503 envelope: &Envelope,
2504 chain_seq: u64,
2505 signed_at: DateTime<Utc>,
2506) -> GovernanceAttestation {
2507 let signed_at = truncate_to_seconds(signed_at);
2508 let entry_hash = envelope.entry_hash();
2509 let signature =
2510 crypto::sign(key, entry_hash.as_bytes(), signed_at.timestamp());
2511 GovernanceAttestation {
2512 envelope_version: envelope.agora_governance_log,
2513 chain_seq,
2514 prev_hash: envelope.prev_hash,
2515 data_hash: envelope.data_hash,
2516 entry_hash,
2517 signature: signature.into(),
2518 signed_at,
2519 retroactive: is_retroactive(envelope.created_at(), signed_at),
2520 }
2521}
2522
2523#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
2529pub enum LinkError {
2530 #[error(
2531 "envelope version {0} is not supported (this verifier knows {ENVELOPE_VERSION})"
2532 )]
2533 UnsupportedVersion(u32),
2534 #[error("entry_hash does not recompute from the envelope fields")]
2535 HashMismatch,
2536 #[error("signature does not verify under the published key")]
2537 BadSignature,
2538 #[error(
2542 "this entry's type is newer than this verifier, which cannot check it; \
2543 upgrade agora-agentkit"
2544 )]
2545 UnknownEntryType,
2546}
2547
2548pub fn recompute_entry_hash(link: &GovernanceChainLink) -> Sha256Hex {
2550 Envelope::new(
2551 link.id.clone(),
2552 link.entry_type,
2553 link.created_at,
2554 link.attestation.prev_hash,
2555 link.attestation.data_hash,
2556 )
2557 .entry_hash()
2558}
2559
2560pub fn verify_link(
2562 link: &GovernanceChainLink,
2563 key: &VerifyingKey,
2564) -> Result<(), LinkError> {
2565 let a = &link.attestation;
2566 if a.envelope_version != ENVELOPE_VERSION {
2567 return Err(LinkError::UnsupportedVersion(a.envelope_version));
2568 }
2569 if link.entry_type == GovernanceLogEntryType::Unknown {
2570 return Err(LinkError::UnknownEntryType);
2571 }
2572 if recompute_entry_hash(link) != a.entry_hash {
2573 return Err(LinkError::HashMismatch);
2574 }
2575 if !crypto::verify(
2576 key,
2577 a.entry_hash.as_bytes(),
2578 a.signed_at.timestamp(),
2579 &Signature::from(&a.signature),
2580 ) {
2581 return Err(LinkError::BadSignature);
2582 }
2583 Ok(())
2584}
2585
2586pub fn verify_data(
2588 link: &GovernanceChainLink,
2589 data: &serde_json::Value,
2590) -> bool {
2591 data_hash(data) == link.attestation.data_hash
2592}
2593
2594fn same_shape(written: &serde_json::Value, read: &serde_json::Value) -> bool {
2604 use serde_json::Value::{Array, Null, Object};
2605 match (written, read) {
2606 (Object(w), Object(r)) => r.iter().all(|(k, r)| match w.get(k) {
2607 Some(w) => same_shape(w, r),
2608 None => r.is_null(),
2609 }),
2610 (Array(w), Array(r)) => {
2611 w.len() == r.len() && w.iter().zip(r).all(|(w, r)| same_shape(w, r))
2612 }
2613 (_, Object(_) | Array(_)) => false,
2614 (Object(_) | Array(_), Null) => false,
2615 _ => true,
2616 }
2617}
2618
2619fn read_strictly<T>(written: &serde_json::Value) -> Result<T, String>
2621where
2622 T: Serialize + serde::de::DeserializeOwned,
2623{
2624 let read: T =
2625 serde_json::from_value(written.clone()).map_err(|e| e.to_string())?;
2626 let again = serde_json::to_value(&read).map_err(|e| e.to_string())?;
2627 if same_shape(written, &again) {
2628 Ok(read)
2629 } else {
2630 Err("an array where an object belongs, or the reverse".into())
2631 }
2632}
2633
2634pub fn links_from_json(
2641 chain: &serde_json::Value,
2642) -> Result<Vec<GovernanceChainLink>, String> {
2643 chain
2644 .as_array()
2645 .ok_or("a chain is an array of links")?
2646 .iter()
2647 .map(read_strictly)
2648 .collect()
2649}
2650
2651fn read_as_written<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
2653where
2654 D: serde::Deserializer<'de>,
2655 T: Serialize + serde::de::DeserializeOwned,
2656{
2657 let written = serde_json::Value::deserialize(deserializer)?;
2658 if written.is_null() {
2659 return Ok(None);
2660 }
2661 read_strictly(&written)
2662 .map(Some)
2663 .map_err(serde::de::Error::custom)
2664}
2665
2666fn reserved_prefix(
2669 entry_type: GovernanceLogEntryType,
2670) -> Option<GovernanceLogPrefix> {
2671 match entry_type {
2672 GovernanceLogEntryType::Amendment => Some(GovernanceLogPrefix::Amd),
2673 GovernanceLogEntryType::KeyRotation => Some(GovernanceLogPrefix::Key),
2674 GovernanceLogEntryType::StewardRecord => Some(GovernanceLogPrefix::Rec),
2675 GovernanceLogEntryType::Obligation => Some(GovernanceLogPrefix::Obl),
2676 GovernanceLogEntryType::DataSet => Some(GovernanceLogPrefix::Data),
2677 GovernanceLogEntryType::CouncilDecision
2678 | GovernanceLogEntryType::AppealsCourtDecision
2679 | GovernanceLogEntryType::EmergencyAction
2680 | GovernanceLogEntryType::PolicyChange
2681 | GovernanceLogEntryType::StewardVeto
2682 | GovernanceLogEntryType::Unknown => None,
2683 }
2684}
2685
2686fn reserved_for(prefix: GovernanceLogPrefix) -> Option<GovernanceLogEntryType> {
2688 match prefix {
2689 GovernanceLogPrefix::Amd => Some(GovernanceLogEntryType::Amendment),
2690 GovernanceLogPrefix::Key => Some(GovernanceLogEntryType::KeyRotation),
2691 GovernanceLogPrefix::Rec => Some(GovernanceLogEntryType::StewardRecord),
2692 GovernanceLogPrefix::Obl => Some(GovernanceLogEntryType::Obligation),
2693 GovernanceLogPrefix::Data => Some(GovernanceLogEntryType::DataSet),
2694 GovernanceLogPrefix::Gov
2695 | GovernanceLogPrefix::App
2696 | GovernanceLogPrefix::Unknown => None,
2697 }
2698}
2699
2700fn prefix_problem(link: &GovernanceChainLink) -> Option<String> {
2702 let prefix = link.id.prefix();
2703 match (reserved_prefix(link.entry_type), reserved_for(prefix)) {
2704 (Some(want), _) if prefix != want => Some(format!(
2705 "the id of a {} entry must be in the {want}- series, not {}",
2706 link.entry_type, link.id
2707 )),
2708 (None, Some(owner)) => Some(format!(
2709 "{prefix}- ids are reserved for {owner} entries, but {} is a {}",
2710 link.id, link.entry_type
2711 )),
2712 _ => None,
2713 }
2714}
2715
2716pub const OBLIGATION_FIXED: [&str; 5] = [
2720 "/until",
2721 "/owed_by",
2722 "/kind",
2723 "/source",
2724 "/agora_obligation",
2725];
2726
2727pub fn revision_touches<'a>(
2730 revision: &Revision,
2731 pointers: &[&'a str],
2732) -> Option<&'a str> {
2733 revision.patch.iter().find_map(|op| {
2734 let from = match op {
2735 json_patch::PatchOperation::Move(m) => Some(m.from.as_str()),
2736 json_patch::PatchOperation::Copy(c) => Some(c.from.as_str()),
2737 _ => None,
2738 };
2739 [Some(op.path().as_str()), from]
2740 .into_iter()
2741 .flatten()
2742 .find_map(|p| pointers.iter().copied().find(|f| overlaps(p, f)))
2743 })
2744}
2745
2746fn amendment_target(
2748 amendment: &Amendment,
2749 seq: u64,
2750 seq_of: &HashMap<&str, u64>,
2751 links: &[&GovernanceChainLink],
2752) -> Result<u64, AmendmentError> {
2753 amendment.validate()?;
2754 let target = *seq_of.get(amendment.target.as_str()).ok_or_else(|| {
2755 AmendmentError::UnknownTarget(amendment.target.clone())
2756 })?;
2757 if target >= seq {
2758 return Err(AmendmentError::ForwardReference(amendment.target.clone()));
2759 }
2760 let target_link = links[target as usize - 1];
2761 if target_link.attestation.entry_hash != amendment.target_entry_hash {
2762 return Err(AmendmentError::WrongTargetHash(amendment.target.clone()));
2763 }
2764 let on_obligation =
2765 target_link.entry_type == GovernanceLogEntryType::Obligation;
2766 if amendment.kind.is_obligation() && !on_obligation {
2767 return Err(AmendmentError::NotAnObligation {
2768 kind: amendment.kind,
2769 target: amendment.target.clone(),
2770 });
2771 }
2772 if on_obligation && kind_standing(amendment.kind).is_some() {
2773 return Err(AmendmentError::PrecedentialOnObligation(amendment.kind));
2774 }
2775 if on_obligation
2776 && let Some(revision) = &amendment.revision
2777 && let Some(path) = revision_touches(revision, &OBLIGATION_FIXED)
2778 {
2779 return Err(AmendmentError::FixedObligationField(path.to_string()));
2780 }
2781 Ok(target)
2782}
2783
2784struct KeyWalk {
2786 history: Vec<(GovernanceKeyRecord, VerifyingKey)>,
2788 unanchored: Vec<PublicKeyHex>,
2789 seen: HashSet<PublicKeyHex>,
2793 repudiated: HashSet<u64>,
2795 genesis: PublicKeyHex,
2796 genesis_certified: bool,
2798}
2799
2800impl KeyWalk {
2801 fn new(genesis: &VerifyingKey, anchor: &KeyAnchor) -> Self {
2802 let public_key = PublicKeyHex::from(genesis);
2803 Self {
2804 genesis: public_key,
2805 genesis_certified: false,
2806 history: vec![(
2807 GovernanceKeyRecord {
2808 public_key,
2809 from_seq: 1,
2810 through_seq: None,
2811 status: KeyStatus::Active,
2812 introduced_by: None,
2813 retired_by: None,
2814 certified: false,
2815 },
2816 *genesis,
2817 )],
2818 unanchored: if anchor.contains(&public_key) {
2819 Vec::new()
2820 } else {
2821 vec![public_key]
2822 },
2823 seen: HashSet::from([public_key]),
2824 repudiated: HashSet::new(),
2825 }
2826 }
2827
2828 fn in_force(&self, seq: u64) -> (PublicKeyHex, VerifyingKey) {
2830 let (record, key) = self
2831 .history
2832 .iter()
2833 .rev()
2834 .find(|(r, _)| r.from_seq <= seq)
2835 .unwrap_or(&self.history[0]);
2836 (record.public_key, *key)
2837 }
2838
2839 fn active(&self) -> PublicKeyHex {
2841 self.history
2842 .last()
2843 .map(|(r, _)| r.public_key)
2844 .expect("the genesis key is always in the history")
2845 }
2846
2847 fn close(
2848 &mut self,
2849 through_seq: u64,
2850 status: KeyStatus,
2851 by: &GovernanceLogId,
2852 ) {
2853 if let Some((record, _)) = self.history.last_mut() {
2854 record.through_seq = Some(through_seq);
2855 record.status = status;
2856 record.retired_by = Some(by.clone());
2857 }
2858 }
2859
2860 fn open(
2861 &mut self,
2862 public_key: PublicKeyHex,
2863 key: VerifyingKey,
2864 from_seq: u64,
2865 by: &GovernanceLogId,
2866 ) {
2867 self.history.push((
2868 GovernanceKeyRecord {
2869 public_key,
2870 from_seq,
2871 through_seq: None,
2872 status: KeyStatus::Active,
2873 introduced_by: Some(by.clone()),
2874 retired_by: None,
2875 certified: true,
2876 },
2877 key,
2878 ));
2879 }
2880
2881 fn apply(
2883 &mut self,
2884 rotation: &KeyRotation,
2885 link: &GovernanceChainLink,
2886 seq: u64,
2887 links: &[&GovernanceChainLink],
2888 roots: &RootSet,
2889 ) -> Result<(), RotationError> {
2890 rotation.verify_certified(seq, link.attestation.prev_hash, roots)?;
2891 let new_key = rotation
2892 .new_key
2893 .to_verifying_key()
2894 .map_err(|_| RotationError::BadNewKey)?;
2895 if self.seen.contains(&rotation.new_key) {
2896 return Err(RotationError::ReusedKey);
2897 }
2898 match (&rotation.outgoing_certificate, self.genesis_certified) {
2903 (None, false) => {
2904 return Err(RotationError::MissingGenesisCertificate);
2905 }
2906 (Some(_), true) => {
2907 return Err(RotationError::UnexpectedOutgoingCertificate);
2908 }
2909 (Some(certificate), false) => certificate
2910 .verify_for(&KeyCertStatement::genesis(self.genesis), roots)
2911 .map_err(RotationError::OutgoingCertificate)?,
2912 (None, true) => {}
2913 }
2914 match rotation.reason {
2915 RotationReason::Routine => {
2916 if rotation.old_key != self.in_force(seq).0 {
2917 return Err(RotationError::WrongOldKey);
2918 }
2919 self.close(seq, KeyStatus::Retired, &link.id);
2920 self.open(rotation.new_key, new_key, seq + 1, &link.id);
2921 }
2922 RotationReason::Compromise => {
2923 let head = rotation
2924 .last_trusted()
2925 .ok_or(RotationError::MissingLastTrusted)?;
2926 let trusted_seq = head.chain_seq;
2927 let names_an_earlier_entry = trusted_seq >= 1
2928 && trusted_seq < seq
2929 && links[trusted_seq as usize - 1].id == head.id
2930 && links[trusted_seq as usize - 1].attestation.entry_hash
2931 == head.entry_hash;
2932 if !names_an_earlier_entry {
2933 return Err(RotationError::UnknownLastTrusted);
2934 }
2935 if self.repudiated.contains(&trusted_seq) {
2938 return Err(RotationError::RepudiatedLastTrusted);
2939 }
2940 if rotation.old_key != self.in_force(trusted_seq).0 {
2943 return Err(RotationError::WrongOldKey);
2944 }
2945 self.history.retain(|(r, _)| r.from_seq <= trusted_seq);
2946 self.close(trusted_seq, KeyStatus::Compromised, &link.id);
2947 self.open(rotation.new_key, new_key, seq, &link.id);
2948 self.repudiated.extend(trusted_seq + 1..seq);
2949 }
2950 }
2951 self.seen.insert(rotation.new_key);
2952 if !self.genesis_certified {
2953 self.genesis_certified = true;
2954 self.history[0].0.certified = true;
2955 self.unanchored.clear();
2956 }
2957 Ok(())
2958 }
2959}
2960
2961pub fn verify_chain(
2992 links: &[GovernanceChainLink],
2993 genesis_key: &VerifyingKey,
2994 anchor: &KeyAnchor,
2995 roots: &RootSet,
2996) -> GovernanceVerification {
2997 let mut links: Vec<&GovernanceChainLink> = links.iter().collect();
2998 links.sort_by_key(|l| l.attestation.chain_seq);
2999
3000 let mut seq_of: HashMap<&str, u64> = HashMap::new();
3001 let mut duplicates: HashSet<&str> = HashSet::new();
3002 for (i, link) in links.iter().enumerate() {
3003 if seq_of.insert(link.id.as_str(), i as u64 + 1).is_some() {
3004 duplicates.insert(link.id.as_str());
3005 }
3006 }
3007
3008 let mut walk = KeyWalk::new(genesis_key, anchor);
3009 let mut amendments: Vec<(u64, GovernanceLogId, Amendment, u64)> =
3011 Vec::new();
3012 let mut entries: Vec<EntryVerdict> = Vec::with_capacity(links.len());
3013 let mut prev: Option<&GovernanceChainLink> = None;
3014
3015 for (i, link) in links.iter().enumerate() {
3016 let a = &link.attestation;
3017 let expected_seq = i as u64 + 1;
3018 let mut problems: Vec<String> = Vec::new();
3019
3020 if let Some(problem) = prefix_problem(link) {
3021 problems.push(problem);
3022 }
3023 if duplicates.contains(link.id.as_str()) {
3024 problems.push(format!("{} appears more than once", link.id));
3025 }
3026
3027 let carries_meaning = matches!(
3031 link.entry_type,
3032 GovernanceLogEntryType::Amendment
3033 | GovernanceLogEntryType::KeyRotation
3034 );
3035 let mut amendment: Option<Amendment> = None;
3036 let mut rotation: Option<KeyRotation> = None;
3037 let mut content_matches: Option<bool> = None;
3038 match &link.data {
3039 Some(data) if non_integer_number(data).is_some() => {
3040 content_matches = Some(false);
3041 problems.push(format!(
3042 "`data` has a number that is not a 64-bit integer at {:?}; \
3043 governance data never contains one",
3044 non_integer_number(data).unwrap_or_default()
3045 ));
3046 }
3047 Some(data) => {
3048 let matched = data_hash(data) == a.data_hash;
3049 content_matches = Some(matched);
3050 if !matched {
3051 if carries_meaning {
3052 problems.push(
3053 "`data` does not hash to the attested data_hash"
3054 .into(),
3055 );
3056 }
3057 } else {
3058 match link.entry_type {
3059 GovernanceLogEntryType::Amendment => {
3060 match read_strictly(data) {
3061 Ok(v) => amendment = Some(v),
3062 Err(e) => problems.push(format!(
3063 "amendment `data` is malformed: {e}"
3064 )),
3065 }
3066 }
3067 GovernanceLogEntryType::KeyRotation => {
3068 match read_strictly(data) {
3069 Ok(v) => rotation = Some(v),
3070 Err(e) => problems.push(format!(
3071 "key_rotation `data` is malformed: {e}"
3072 )),
3073 }
3074 }
3075 _ => {}
3076 }
3077 }
3078 }
3079 None if carries_meaning => problems.push(format!(
3080 "a {} entry must carry its `data`",
3081 link.entry_type
3082 )),
3083 None => {}
3084 }
3085
3086 let declared = rotation
3090 .as_ref()
3091 .filter(|r| r.reason == RotationReason::Compromise)
3092 .map(|r| {
3093 if walk.seen.contains(&r.new_key) {
3094 return Err(RotationError::ReusedKey);
3095 }
3096 r.verify_certified(expected_seq, a.prev_hash, roots)?;
3097 r.new_key
3098 .to_verifying_key()
3099 .map_err(|_| RotationError::BadNewKey)
3100 });
3101 let (key_hex, key) = match &declared {
3102 Some(Ok(k)) => (PublicKeyHex::from(k), *k),
3103 _ => walk.in_force(expected_seq),
3104 };
3105 if let Some(Err(e)) = &declared {
3108 problems.push(e.to_string());
3109 }
3110
3111 let (hash_ok, signature_valid) = match verify_link(link, &key) {
3112 Ok(()) => (true, true),
3113 Err(LinkError::BadSignature) => {
3114 problems.push(LinkError::BadSignature.to_string());
3115 (true, false)
3116 }
3117 Err(e) => {
3118 problems.push(e.to_string());
3119 (false, false)
3120 }
3121 };
3122
3123 let mut link_valid = hash_ok;
3124 if a.chain_seq != expected_seq {
3125 link_valid = false;
3126 problems.push(format!(
3127 "chain_seq {} where {expected_seq} was expected",
3128 a.chain_seq
3129 ));
3130 }
3131 let expected_prev = prev.map(|p| p.attestation.entry_hash);
3132 if a.prev_hash != expected_prev {
3133 link_valid = false;
3134 problems.push(match (a.prev_hash, expected_prev) {
3135 (Some(_), None) => "first entry names a predecessor".into(),
3136 (None, Some(_)) => "prev_hash is null mid-chain".into(),
3137 _ => "prev_hash is not the previous entry's entry_hash".into(),
3138 });
3139 }
3140 let out_of_order = prev.is_some_and(|p| link.created_at < p.created_at);
3141
3142 let authentic = signature_valid && link_valid;
3146 if let Some(rotation) = rotation.as_ref().filter(|_| authentic)
3147 && let Err(e) =
3148 walk.apply(rotation, link, expected_seq, &links, roots)
3149 {
3150 let problem = e.to_string();
3151 if !problems.contains(&problem) {
3152 problems.push(problem);
3153 }
3154 }
3155 let mut texts = None;
3159 if let Some(amendment) = amendment
3160 .as_ref()
3161 .filter(|a| authentic && a.validate().is_ok())
3162 {
3163 match amendment.text_status(link.texts.as_ref()) {
3164 Ok(status) => texts = status,
3165 Err(e) => problems.push(e.to_string()),
3166 }
3167 } else if link.texts.as_ref().is_some_and(|t| !t.is_empty()) {
3168 problems.push(AmendmentError::UncommittedText.to_string());
3169 }
3170 if let Some(amendment) = amendment.filter(|_| authentic) {
3171 match amendment_target(&amendment, expected_seq, &seq_of, &links) {
3172 Ok(target) => amendments.push((
3173 expected_seq,
3174 link.id.clone(),
3175 amendment,
3176 target,
3177 )),
3178 Err(e) => problems.push(e.to_string()),
3179 }
3180 }
3181
3182 entries.push(EntryVerdict {
3183 id: link.id.clone(),
3184 chain_seq: a.chain_seq,
3185 signature_valid,
3186 link_valid,
3187 content_matches,
3188 retroactive: is_retroactive(link.created_at, a.signed_at),
3189 out_of_order,
3190 amended_by: Vec::new(),
3191 signed_by: Some(key_hex),
3192 redacted: false,
3193 redacted_data_hash: None,
3194 revisions: Vec::new(),
3195 latest_data_hash: None,
3196 superseded_revisions: Vec::new(),
3197 history: RevisionHistory::default(),
3198 repudiated: false,
3199 reattested_by: Vec::new(),
3200 texts,
3201 problem: (!problems.is_empty()).then(|| problems.join("; ")),
3202 });
3203 prev = Some(link);
3204 }
3205
3206 let mut applied = vec![false; amendments.len()];
3210 loop {
3211 let mut changed = false;
3212 for (i, (seq, id, amendment, target)) in amendments.iter().enumerate() {
3213 if applied[i]
3214 || amendment.kind != AmendmentKind::Reattested
3215 || walk.repudiated.contains(seq)
3216 {
3217 continue;
3218 }
3219 applied[i] = true;
3220 entries[*target as usize - 1].reattested_by.push(id.clone());
3221 walk.repudiated.remove(target);
3222 changed = true;
3223 }
3224 if !changed {
3225 break;
3226 }
3227 }
3228
3229 for (seq, id, amendment, target) in &amendments {
3230 if walk.repudiated.contains(seq) {
3231 continue;
3232 }
3233 let entry = &mut entries[*target as usize - 1];
3234 entry.amended_by.push(id.clone());
3235 let mut unrebased = false;
3236 if let Some(redaction) = &amendment.redaction {
3237 unrebased = !entry.revisions.is_empty()
3240 && redaction.resulting_latest_hash.is_none();
3241 entry.redacted = true;
3242 entry.redacted_data_hash = Some(redaction.resulting_data_hash);
3243 entry.history.rebased = entry.history.revisions.len();
3245 entry.history.rebased_hash = redaction.resulting_latest_hash;
3246 entry.latest_data_hash = redaction.resulting_latest_hash;
3247 entry.superseded_revisions = entry.revisions.clone();
3248 }
3249 if let Some(revision) = &amendment.revision {
3250 entry.revisions.push(id.clone());
3251 entry.latest_data_hash = Some(revision.resulting_data_hash);
3252 entry.history.revisions.push((id.clone(), revision.clone()));
3253 }
3254 if unrebased {
3255 let target = entry.id.clone();
3256 add_problem(
3257 &mut entries[*seq as usize - 1].problem,
3258 format!(
3259 "the redaction of {target}, which has revisions, names no \
3260 resulting_latest_hash"
3261 ),
3262 );
3263 }
3264 }
3265
3266 for (i, link) in links.iter().enumerate() {
3269 let entry = &mut entries[i];
3270 if let Some(data) = &link.data
3271 && entry.content_matches.is_some()
3272 && (entry.redacted || !entry.history.revisions.is_empty())
3273 {
3274 entry.content_matches =
3275 Some(entry.content_check(link.attestation.data_hash, data));
3276 }
3277 }
3278
3279 let mut seen = HashSet::new();
3280 walk.unanchored.retain(|key| seen.insert(*key));
3281
3282 let mut repudiated = Vec::new();
3283 for (i, entry) in entries.iter_mut().enumerate() {
3284 if walk.repudiated.contains(&(i as u64 + 1)) {
3285 entry.repudiated = true;
3286 repudiated.push(entry.id.clone());
3287 }
3288 }
3289
3290 GovernanceVerification {
3291 public_key: walk.active(),
3292 ok: false,
3293 head: prev.map(|p| p.id.clone()),
3294 entries,
3295 keys: walk.history.into_iter().map(|(record, _)| record).collect(),
3296 unanchored_keys: walk.unanchored,
3297 repudiated,
3298 }
3299 .settle()
3300}
3301
3302#[cfg(test)]
3303mod tests {
3304 use super::*;
3305
3306 #[test]
3307 fn pointer_tokens_escape_and_unescape() {
3308 assert_eq!(escape_pointer_token("a/b~c"), "a~1b~0c");
3309 assert_eq!(unescape_pointer_token("~01"), "~1");
3311 for token in ["", "plain", "a/b", "~", "~1", "/~0/"] {
3312 assert_eq!(
3313 unescape_pointer_token(&escape_pointer_token(token)),
3314 token
3315 );
3316 }
3317 assert_eq!(
3318 pointer_tokens("/rounds/0/a~1b~0c"),
3319 ["rounds", "0", "a/b~c"]
3320 );
3321 assert!(pointer_tokens("").is_empty());
3322 }
3323
3324 #[test]
3328 fn citations_are_read_from_each_records_own_field() {
3329 use serde_json::json;
3330 let id = |s: &str| s.parse::<GovernanceLogId>().unwrap();
3331 assert_eq!(
3332 citations_in(
3333 GovernanceLogEntryType::StewardRecord,
3334 &json!({"title": "t", "concerns": ["GOV-2026-0011", "AMD-2026-0004"]}),
3335 )
3336 .unwrap(),
3337 vec![
3338 (id("GOV-2026-0011"), CitationRelation::Concerns),
3339 (id("AMD-2026-0004"), CitationRelation::Concerns),
3340 ]
3341 );
3342 assert_eq!(
3343 citations_in(
3344 GovernanceLogEntryType::Amendment,
3345 &json!({"target": "APP-2026-0001", "authority": "GOV-2026-0005"}),
3346 )
3347 .unwrap(),
3348 vec![(id("GOV-2026-0005"), CitationRelation::Authority)],
3349 "the target is not a citation: it is listed under `amendments`"
3350 );
3351 assert_eq!(
3352 citations_in(
3353 GovernanceLogEntryType::CouncilDecision,
3354 &json!({"overrules": ["APP-2026-0002"]}),
3355 )
3356 .unwrap(),
3357 vec![(id("APP-2026-0002"), CitationRelation::Overrules)]
3358 );
3359 assert!(
3360 citations_in(
3361 GovernanceLogEntryType::Amendment,
3362 &json!({"authority": null})
3363 )
3364 .unwrap()
3365 .is_empty()
3366 );
3367 assert!(matches!(
3370 citations_in(
3371 GovernanceLogEntryType::StewardRecord,
3372 &json!({"concerns": "GOV-2026-0011"})
3373 ),
3374 Err(MalformedCitation {
3375 relation: CitationRelation::Concerns,
3376 ..
3377 })
3378 ));
3379 assert!(
3380 citations_in(
3381 GovernanceLogEntryType::AppealsCourtDecision,
3382 &json!({"concerns": ["GOV-2026-0011"]}),
3383 )
3384 .unwrap()
3385 .is_empty()
3386 );
3387 }
3388 use crate::crypto::generate_keypair;
3389 use serde_json::json;
3390
3391 pub(super) fn gov(n: u32) -> GovernanceLogId {
3392 format!("GOV-2026-{n:04}").parse().unwrap()
3393 }
3394
3395 pub(super) fn amd(n: u32) -> GovernanceLogId {
3396 format!("AMD-2026-{n:04}").parse().unwrap()
3397 }
3398
3399 pub(super) fn key_id(n: u32) -> GovernanceLogId {
3400 format!("KEY-2026-{n:04}").parse().unwrap()
3401 }
3402
3403 pub(super) fn rec(n: u32) -> GovernanceLogId {
3404 format!("REC-2026-{n:04}").parse().unwrap()
3405 }
3406
3407 pub(super) fn obl(n: u32) -> GovernanceLogId {
3408 format!("OBL-2026-{n:04}").parse().unwrap()
3409 }
3410
3411 pub(super) fn data_id(n: u32) -> GovernanceLogId {
3412 format!("DATA-2026-{n:04}").parse().unwrap()
3413 }
3414
3415 pub(super) fn at(secs: i64) -> DateTime<Utc> {
3416 DateTime::from_timestamp(1_700_000_000 + secs, 123_456_789).unwrap()
3417 }
3418
3419 fn anchored(key: &VerifyingKey) -> KeyAnchor {
3421 KeyAnchor::pinned(key.into())
3422 }
3423
3424 pub(super) fn resalted(draft: &AmendmentDraft, seq: u64) -> AmendmentDraft {
3427 let fix = |field: &str, t: &Option<CommittedText>| {
3428 t.as_ref().map(|t| {
3429 let salt = Sha256::digest(format!("{seq}/{field}/{}", t.text));
3430 CommittedText::with_salt(
3431 TextSalt::from(<[u8; 32]>::from(salt)),
3432 t.text.clone(),
3433 )
3434 })
3435 };
3436 let texts = AmendmentTexts {
3437 basis: fix("basis", &draft.texts.basis),
3438 note: fix("note", &draft.texts.note),
3439 rationale: fix("rationale", &draft.texts.rationale),
3440 };
3441 let commit = |t: &Option<CommittedText>| {
3442 t.as_ref().map(|t| AmendmentText::Committed(t.commitment()))
3443 };
3444 AmendmentDraft {
3445 amendment: Amendment {
3446 basis: commit(&texts.basis).unwrap(),
3447 note: commit(&texts.note).unwrap(),
3448 rationale: commit(&texts.rationale),
3449 ..draft.amendment.clone()
3450 },
3451 texts,
3452 }
3453 }
3454
3455 pub(super) fn v1(draft: AmendmentDraft) -> Amendment {
3457 let plain =
3458 |t: Option<CommittedText>| t.map(|t| AmendmentText::Plain(t.text));
3459 Amendment {
3460 agora_governance_amendment: 1,
3461 basis: plain(draft.texts.basis).unwrap(),
3462 note: plain(draft.texts.note).unwrap(),
3463 rationale: plain(draft.texts.rationale),
3464 ..draft.amendment
3465 }
3466 }
3467
3468 pub(super) fn root(n: u8) -> SigningKey {
3471 SigningKey::from_bytes(&[0xA0 + n; 32])
3472 }
3473
3474 pub(super) fn roots() -> RootSet {
3476 RootSet::new(
3477 [1, 2].map(|n| PublicKeyHex::from(&root(n).verifying_key())),
3478 1,
3479 )
3480 }
3481
3482 pub(super) fn certify(
3484 signers: &[&SigningKey],
3485 statement: KeyCertStatement,
3486 ) -> KeyCertificate {
3487 use ed25519_dalek::Signer;
3488 let message = statement.signed_bytes();
3489 signers.iter().fold(
3490 KeyCertificate::unsigned(statement),
3491 |certificate, signer| {
3492 certificate.with(RootSignature {
3493 root_key: (&signer.verifying_key()).into(),
3494 signature: signer.sign(&message).into(),
3495 })
3496 },
3497 )
3498 }
3499
3500 fn for_new_at(c: &Chain, key: &VerifyingKey) -> KeyCertificate {
3502 certify(
3503 &[&root(1)],
3504 KeyCertStatement::routine(key.into(), c.next_seq(), c.prev_hash()),
3505 )
3506 }
3507
3508 pub(super) fn link(
3509 key: &SigningKey,
3510 n: u32,
3511 prev: Option<&GovernanceChainLink>,
3512 data: &serde_json::Value,
3513 signed_at: DateTime<Utc>,
3514 ) -> GovernanceChainLink {
3515 let created_at = truncate_to_micros(at(n as i64 * 10));
3516 let envelope = Envelope::new(
3517 gov(n),
3518 GovernanceLogEntryType::CouncilDecision,
3519 created_at,
3520 prev.map(|p| p.attestation.entry_hash),
3521 data_hash(data),
3522 );
3523 let attestation = attest(
3524 key,
3525 &envelope,
3526 prev.map_or(1, |p| p.attestation.chain_seq + 1),
3527 signed_at,
3528 );
3529 GovernanceChainLink {
3530 id: gov(n),
3531 entry_type: GovernanceLogEntryType::CouncilDecision,
3532 created_at,
3533 attestation,
3534 data: None,
3535 texts: None,
3536 }
3537 }
3538
3539 pub(super) fn chain(key: &SigningKey, n: u32) -> Vec<GovernanceChainLink> {
3540 let mut out: Vec<GovernanceChainLink> = Vec::new();
3541 for i in 1..=n {
3542 let data = json!({"title": format!("Decision {i}"), "outcome": "approved"});
3543 let l = link(key, i, out.last(), &data, at(i as i64 * 10 + 1));
3544 out.push(l);
3545 }
3546 out
3547 }
3548
3549 pub(super) struct Chain {
3553 pub(super) links: Vec<GovernanceChainLink>,
3554 gov: u32,
3555 amd: u32,
3556 key: u32,
3557 genesis: Option<PublicKeyHex>,
3559 }
3560
3561 impl Chain {
3562 pub(super) fn new() -> Self {
3563 Self {
3564 links: Vec::new(),
3565 gov: 0,
3566 amd: 0,
3567 key: 0,
3568 genesis: None,
3569 }
3570 }
3571
3572 pub(super) fn prev_hash(&self) -> Option<Sha256Hex> {
3573 self.links.last().map(|l| l.attestation.entry_hash)
3574 }
3575
3576 pub(super) fn hash_at(&self, seq: usize) -> Sha256Hex {
3578 self.links[seq - 1].attestation.entry_hash
3579 }
3580
3581 pub(super) fn next_seq(&self) -> u64 {
3583 self.links.len() as u64 + 1
3584 }
3585
3586 pub(super) fn head(&self, seq: usize) -> TrustedHead {
3588 TrustedHead {
3589 id: self.links[seq - 1].id.clone(),
3590 chain_seq: seq as u64,
3591 entry_hash: self.hash_at(seq),
3592 }
3593 }
3594
3595 fn outgoing(&self, rotation: KeyRotation) -> KeyRotation {
3597 match (self.key, self.genesis) {
3598 (0, Some(genesis)) => rotation.with_outgoing(certify(
3599 &[&root(1)],
3600 KeyCertStatement::genesis(genesis),
3601 )),
3602 _ => rotation,
3603 }
3604 }
3605
3606 pub(super) fn routine(
3609 &self,
3610 old: &VerifyingKey,
3611 new: &SigningKey,
3612 ) -> KeyRotation {
3613 let statement = KeyCertStatement::routine(
3614 (&new.verifying_key()).into(),
3615 self.next_seq(),
3616 self.prev_hash(),
3617 );
3618 self.outgoing(KeyRotation::routine(
3619 old.into(),
3620 new,
3621 self.prev_hash(),
3622 at(self.next_seq() as i64 * 10 + 5),
3623 certify(&[&root(1)], statement),
3624 ))
3625 }
3626
3627 pub(super) fn compromise(
3630 &self,
3631 old: &VerifyingKey,
3632 new: &SigningKey,
3633 trusted: usize,
3634 ) -> KeyRotation {
3635 let statement = KeyCertStatement::compromise(
3636 (&new.verifying_key()).into(),
3637 self.next_seq(),
3638 self.prev_hash(),
3639 self.head(trusted),
3640 );
3641 self.outgoing(KeyRotation::compromise(
3642 old.into(),
3643 new,
3644 self.prev_hash(),
3645 at(self.next_seq() as i64 * 10 + 5),
3646 certify(&[&root(1)], statement),
3647 ))
3648 }
3649
3650 pub(super) fn next_amd(&self) -> GovernanceLogId {
3652 amd(self.amd + 1)
3653 }
3654
3655 pub(super) fn push(
3656 &mut self,
3657 signer: &SigningKey,
3658 id: GovernanceLogId,
3659 entry_type: GovernanceLogEntryType,
3660 data: serde_json::Value,
3661 carry: bool,
3662 ) -> GovernanceLogId {
3663 self.genesis
3664 .get_or_insert_with(|| (&signer.verifying_key()).into());
3665 let n = self.links.len() as i64 + 1;
3666 let created_at = truncate_to_micros(at(n * 10));
3667 let envelope = Envelope::new(
3668 id.clone(),
3669 entry_type,
3670 created_at,
3671 self.prev_hash(),
3672 data_hash(&data),
3673 );
3674 let attestation =
3675 attest(signer, &envelope, n as u64, at(n * 10 + 1));
3676 self.links.push(GovernanceChainLink {
3677 id: id.clone(),
3678 entry_type,
3679 created_at,
3680 attestation,
3681 data: carry.then_some(data),
3682 texts: None,
3683 });
3684 id
3685 }
3686
3687 pub(super) fn entry(
3690 &mut self,
3691 signer: &SigningKey,
3692 data: serde_json::Value,
3693 ) -> GovernanceLogId {
3694 self.gov += 1;
3695 let id = gov(self.gov);
3696 self.push(
3697 signer,
3698 id,
3699 GovernanceLogEntryType::CouncilDecision,
3700 data,
3701 false,
3702 )
3703 }
3704
3705 pub(super) fn decision(
3706 &mut self,
3707 signer: &SigningKey,
3708 ) -> GovernanceLogId {
3709 let data = json!({"title": format!("Decision {}", self.gov + 1)});
3710 self.entry(signer, data)
3711 }
3712
3713 pub(super) fn amend(
3715 &mut self,
3716 signer: &SigningKey,
3717 draft: &AmendmentDraft,
3718 ) -> GovernanceLogId {
3719 let draft = resalted(draft, self.next_seq());
3720 let id = self.amend_v1(signer, &draft.amendment);
3721 let link = self.links.last_mut().unwrap();
3722 link.texts = Some(draft.texts);
3723 id
3724 }
3725
3726 pub(super) fn amend_v1(
3729 &mut self,
3730 signer: &SigningKey,
3731 amendment: &Amendment,
3732 ) -> GovernanceLogId {
3733 self.amd += 1;
3734 let id = amd(self.amd);
3735 self.push(
3736 signer,
3737 id,
3738 GovernanceLogEntryType::Amendment,
3739 serde_json::to_value(amendment).unwrap(),
3740 true,
3741 )
3742 }
3743
3744 pub(super) fn rotate(
3745 &mut self,
3746 signer: &SigningKey,
3747 rotation: &KeyRotation,
3748 ) -> GovernanceLogId {
3749 self.key += 1;
3750 let id = key_id(self.key);
3751 self.push(
3752 signer,
3753 id,
3754 GovernanceLogEntryType::KeyRotation,
3755 serde_json::to_value(rotation).unwrap(),
3756 true,
3757 )
3758 }
3759 }
3760
3761 #[test]
3764 fn canonical_json_sorts_keys_at_every_level() {
3765 let v = json!({"b": {"z": 1, "a": [{"y": 2, "x": 3}]}, "a": null});
3766 assert_eq!(
3767 canonical_json(&v),
3768 br#"{"a":null,"b":{"a":[{"x":3,"y":2}],"z":1}}"#
3769 );
3770 }
3771
3772 #[test]
3773 fn canonical_json_is_compact_and_escapes_like_serde() {
3774 let v = json!({"s": "tab\there \"q\" ünïcode \u{1F600}", "n": [1, -2, 3.5, true, false]});
3775 let bytes = canonical_json(&v);
3776 let text = std::str::from_utf8(&bytes).unwrap();
3777 assert_eq!(
3778 text,
3779 r#"{"n":[1,-2,3.5,true,false],"s":"tab\there \"q\" ünïcode 😀"}"#
3780 );
3781 }
3782
3783 #[test]
3784 fn canonical_json_ignores_insertion_order() {
3785 let mut a = serde_json::Map::new();
3786 a.insert("z".into(), json!(1));
3787 a.insert("a".into(), json!(2));
3788 let mut b = serde_json::Map::new();
3789 b.insert("a".into(), json!(2));
3790 b.insert("z".into(), json!(1));
3791 assert_eq!(
3792 canonical_json(&serde_json::Value::Object(a)),
3793 canonical_json(&serde_json::Value::Object(b))
3794 );
3795 }
3796
3797 #[test]
3798 fn canonical_json_empty_containers() {
3799 assert_eq!(canonical_json(&json!({})), b"{}");
3800 assert_eq!(canonical_json(&json!([])), b"[]");
3801 assert_eq!(
3802 canonical_json(&json!({"a": {}, "b": []})),
3803 br#"{"a":{},"b":[]}"#
3804 );
3805 }
3806
3807 #[test]
3810 fn preimage_is_declaration_ordered_json() {
3811 let e = Envelope::new(
3812 gov(1),
3813 GovernanceLogEntryType::AppealsCourtDecision,
3814 at(0),
3815 None,
3816 data_hash(&json!({})),
3817 );
3818 let text = String::from_utf8(e.preimage()).unwrap();
3819 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}");
3820 }
3821
3822 #[test]
3823 fn every_envelope_field_changes_the_hash() {
3824 let base = Envelope::new(
3825 gov(1),
3826 GovernanceLogEntryType::CouncilDecision,
3827 at(0),
3828 None,
3829 data_hash(&json!({"a":1})),
3830 );
3831 let h = base.entry_hash();
3832 let mut e = base.clone();
3833 e.id = gov(2);
3834 assert_ne!(e.entry_hash(), h);
3835 let mut e = base.clone();
3836 e.entry_type = GovernanceLogEntryType::PolicyChange;
3837 assert_ne!(e.entry_hash(), h);
3838 let mut e = base.clone();
3839 e.created_at += 1;
3840 assert_ne!(e.entry_hash(), h);
3841 let mut e = base.clone();
3842 e.prev_hash = Some(h);
3843 assert_ne!(e.entry_hash(), h);
3844 let mut e = base.clone();
3845 e.data_hash = data_hash(&json!({"a":2}));
3846 assert_ne!(e.entry_hash(), h);
3847 assert_eq!(base.entry_hash(), h, "and it is deterministic");
3848 }
3849
3850 #[test]
3851 fn truncation_matches_what_the_envelope_carries() {
3852 let t = at(0);
3853 assert_eq!(truncate_to_micros(t).timestamp_subsec_nanos(), 123_456_000);
3854 assert_eq!(truncate_to_seconds(t).timestamp_subsec_nanos(), 0);
3855 assert_eq!(
3856 Envelope::new(
3857 gov(1),
3858 GovernanceLogEntryType::CouncilDecision,
3859 t,
3860 None,
3861 data_hash(&json!(null))
3862 )
3863 .created_at,
3864 truncate_to_micros(t).timestamp_micros()
3865 );
3866 }
3867
3868 #[test]
3871 fn hex_newtypes_round_trip_and_reject_wrong_lengths() {
3872 let h = data_hash(&json!(1));
3873 let s = serde_json::to_string(&h).unwrap();
3874 assert_eq!(s.len(), 66);
3875 let back: Sha256Hex = serde_json::from_str(&s).unwrap();
3876 assert_eq!(back, h);
3877 assert!(serde_json::from_str::<Sha256Hex>("\"abcd\"").is_err());
3878 assert!("zz".repeat(32).parse::<Sha256Hex>().is_err());
3879 assert!(Sha256Hex::try_from(vec![0u8; 31]).is_err());
3880 assert_eq!(format!("{h:?}"), format!("Sha256Hex({h})"));
3881 }
3882
3883 #[test]
3886 fn attest_then_verify_link() {
3887 let (key, pk) = generate_keypair();
3888 let l = link(&key, 1, None, &json!({"a": 1}), at(5));
3889 assert_eq!(verify_link(&l, &pk), Ok(()));
3890 assert!(verify_data(&l, &json!({"a": 1})));
3891 assert!(!verify_data(&l, &json!({"a": 2})));
3892 assert!(!l.attestation.retroactive);
3893 }
3894
3895 #[test]
3896 fn wrong_key_fails_signature_only() {
3897 let (key, _) = generate_keypair();
3898 let (_, other) = generate_keypair();
3899 let l = link(&key, 1, None, &json!({}), at(5));
3900 assert_eq!(verify_link(&l, &other), Err(LinkError::BadSignature));
3901 }
3902
3903 #[test]
3904 fn tampering_with_any_attested_field_is_detected() {
3905 let (key, pk) = generate_keypair();
3906 let l = link(&key, 1, None, &json!({"a": 1}), at(5));
3907
3908 let mut t = l.clone();
3909 t.created_at += chrono::Duration::microseconds(1);
3910 assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
3911
3912 let mut t = l.clone();
3913 t.entry_type = GovernanceLogEntryType::StewardVeto;
3914 assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
3915
3916 let mut t = l.clone();
3917 t.attestation.data_hash = data_hash(&json!({"a": 2}));
3918 assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
3919
3920 let mut t = l.clone();
3923 t.attestation.signed_at -= chrono::Duration::seconds(1);
3924 assert_eq!(verify_link(&t, &pk), Err(LinkError::BadSignature));
3925
3926 let mut t = l.clone();
3927 t.attestation.envelope_version = 2;
3928 assert_eq!(verify_link(&t, &pk), Err(LinkError::UnsupportedVersion(2)));
3929 }
3930
3931 #[test]
3932 fn a_good_chain_verifies_in_any_input_order() {
3933 let (key, pk) = generate_keypair();
3934 let mut c = chain(&key, 4);
3935 c.reverse();
3936 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
3937 assert!(v.ok, "{v:#?}");
3938 assert_eq!(v.head, Some(gov(4)));
3939 assert_eq!(
3940 v.entries.iter().map(|e| e.chain_seq).collect::<Vec<_>>(),
3941 [1, 2, 3, 4]
3942 );
3943 assert!(v.entries.iter().all(|e| e.content_matches.is_none()
3944 && e.problem.is_none()
3945 && !e.out_of_order));
3946 assert_eq!(v.public_key, PublicKeyHex::from(&pk));
3947 }
3948
3949 #[test]
3950 fn empty_chain_is_ok_with_no_head() {
3951 let (_, pk) = generate_keypair();
3952 let v = verify_chain(&[], &pk, &anchored(&pk), &roots());
3953 assert!(v.ok);
3954 assert!(v.head.is_none());
3955 assert!(v.entries.is_empty());
3956 }
3957
3958 #[test]
3959 fn a_changed_entry_breaks_its_signature_and_the_next_link() {
3960 let (key, pk) = generate_keypair();
3961 let mut c = chain(&key, 3);
3962 let rewritten = link(
3965 &key,
3966 2,
3967 Some(&c[0]),
3968 &json!({"title": "Decision 2", "outcome": "REJECTED"}),
3969 at(21),
3970 );
3971 c[1] = rewritten;
3972 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
3973 assert!(!v.ok);
3974 assert!(
3975 v.entries[1].signature_valid && v.entries[1].link_valid,
3976 "the rewrite itself is well-formed: {:#?}",
3977 v.entries[1]
3978 );
3979 assert!(
3980 !v.entries[2].link_valid,
3981 "but entry 3 no longer points at it: {:#?}",
3982 v.entries[2]
3983 );
3984 assert!(
3985 v.entries[2]
3986 .problem
3987 .as_deref()
3988 .unwrap()
3989 .contains("prev_hash")
3990 );
3991 }
3992
3993 #[test]
3994 fn a_removed_entry_is_a_gap_and_a_broken_link() {
3995 let (key, pk) = generate_keypair();
3996 let mut c = chain(&key, 3);
3997 c.remove(1);
3998 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
3999 assert!(!v.ok);
4000 assert!(v.entries[0].link_valid);
4001 let p = v.entries[1].problem.as_deref().unwrap();
4002 assert!(p.contains("chain_seq 3 where 2 was expected"), "{p}");
4003 assert!(p.contains("prev_hash"), "{p}");
4004 }
4005
4006 #[test]
4009 fn a_summary_counts_and_names_the_failures() {
4010 let (key, pk) = generate_keypair();
4011 let clean =
4012 verify_chain(&chain(&key, 3), &pk, &anchored(&pk), &roots());
4013 let summary = clean.summary();
4014 assert!(summary.ok);
4015 assert_eq!((summary.entries, summary.failed.len()), (3, 0));
4016 assert_eq!(summary.head, clean.head);
4017 assert!(summary.first_failure.is_none());
4018 assert!(summary.how_to_read.contains("full=true"));
4019
4020 let mut c = chain(&key, 3);
4021 c.remove(1);
4022 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
4023 let summary = v.summary();
4024 assert!(!summary.ok);
4025 assert_eq!(summary.link_failures, 1);
4026 assert_eq!(summary.failed, vec![v.entries[1].id.clone()]);
4027 assert_eq!(summary.first_failure.as_ref(), Some(&v.entries[1]));
4028 let json = serde_json::to_value(&summary).unwrap();
4029 assert!(json["entries"].is_u64(), "a count, not the rows: {json}");
4030 }
4031
4032 #[test]
4033 fn a_second_genesis_is_rejected() {
4034 let (key, pk) = generate_keypair();
4035 let mut c = chain(&key, 2);
4036 let rogue = link(&key, 2, None, &json!({}), at(21));
4037 c[1] = rogue;
4038 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
4039 assert!(!v.ok);
4040 assert!(
4041 v.entries[1]
4042 .problem
4043 .as_deref()
4044 .unwrap()
4045 .contains("null mid-chain")
4046 );
4047 }
4048
4049 #[test]
4050 fn retroactive_and_out_of_order_are_recomputed_not_copied() {
4051 let (key, pk) = generate_keypair();
4052 let first = link(&key, 1, None, &json!({}), at(10 + 3600));
4053 let created = truncate_to_micros(at(5));
4056 let envelope = Envelope::new(
4057 gov(2),
4058 GovernanceLogEntryType::CouncilDecision,
4059 created,
4060 Some(first.attestation.entry_hash),
4061 data_hash(&json!({})),
4062 );
4063 let attestation = attest(&key, &envelope, 2, at(6));
4064 let mut second = GovernanceChainLink {
4065 id: gov(2),
4066 entry_type: GovernanceLogEntryType::CouncilDecision,
4067 created_at: created,
4068 attestation,
4069 data: None,
4070 texts: None,
4071 };
4072 second.attestation.retroactive = true; let v = verify_chain(&[first, second], &pk, &anchored(&pk), &roots());
4074 assert!(v.ok, "{v:#?}");
4075 assert!(v.entries[0].retroactive);
4076 assert!(!v.entries[1].retroactive, "recomputed from timestamps");
4077 assert!(v.entries[1].out_of_order);
4078 }
4079
4080 #[test]
4081 fn content_mismatch_settles_to_not_ok() {
4082 let (key, pk) = generate_keypair();
4083 let c = chain(&key, 1);
4084 let mut v = verify_chain(&c, &pk, &anchored(&pk), &roots());
4085 v.entries[0].content_matches = Some(true);
4086 assert!(v.clone().settle().ok);
4087 v.entries[0].content_matches = Some(false);
4088 assert!(!v.settle().ok);
4089 }
4090
4091 #[test]
4094 fn an_amendment_fills_amended_by_on_its_target() {
4095 let (key, pk) = generate_keypair();
4096 let mut c = Chain::new();
4097 let target = c.decision(&key);
4098 c.decision(&key);
4099 let amendment = AmendmentDraft::new(
4100 target,
4101 c.hash_at(1),
4102 AmendmentKind::NonPrecedential,
4103 "§1 (Red Team Cases Recharacterized)",
4104 "diagnostic finding — not citable as moderation precedent",
4105 )
4106 .unwrap()
4107 .with_authority(gov(5))
4108 .with_rationale("§5 leaves the ruling itself standing");
4109 let id = c.amend(&key, &amendment);
4110
4111 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4112 assert!(v.ok, "{v:#?}");
4113 assert_eq!(v.entries[0].amended_by, vec![id]);
4114 assert!(v.entries[1].amended_by.is_empty());
4115 assert!(!v.entries[0].redacted);
4116 assert_eq!(v.head, Some(amd(1)));
4117 assert_eq!(v.entries[2].content_matches, Some(true));
4119 assert_eq!(v.entries[0].content_matches, None);
4120 assert_eq!(
4121 standing([amendment.amendment.kind]),
4122 Standing::NonPrecedential
4123 );
4124 }
4125
4126 #[test]
4127 fn an_amendment_must_name_an_earlier_entry_by_its_exact_hash() {
4128 let (key, pk) = generate_keypair();
4129 let problem = |c: &Chain, at: usize| -> String {
4130 verify_chain(&c.links, &pk, &anchored(&pk), &roots()).entries[at]
4131 .problem
4132 .clone()
4133 .unwrap_or_default()
4134 };
4135
4136 let mut c = Chain::new();
4137 c.decision(&key);
4138 let unknown = AmendmentDraft::new(
4139 gov(99),
4140 c.hash_at(1),
4141 AmendmentKind::Overruled,
4142 "b",
4143 "n",
4144 )
4145 .unwrap();
4146 c.amend(&key, &unknown);
4147 assert!(
4148 problem(&c, 1).contains("is not an entry of this chain"),
4149 "{}",
4150 problem(&c, 1)
4151 );
4152 assert!(!verify_chain(&c.links, &pk, &anchored(&pk), &roots()).ok);
4153
4154 let mut c = Chain::new();
4156 c.decision(&key);
4157 let forward = AmendmentDraft::new(
4158 gov(2),
4159 c.hash_at(1),
4160 AmendmentKind::Overruled,
4161 "b",
4162 "n",
4163 )
4164 .unwrap();
4165 c.amend(&key, &forward);
4166 c.decision(&key);
4167 assert!(
4168 problem(&c, 1).contains("not an earlier entry"),
4169 "{}",
4170 problem(&c, 1)
4171 );
4172
4173 let mut c = Chain::new();
4175 let target = c.decision(&key);
4176 let wrong_hash = AmendmentDraft::new(
4177 target,
4178 data_hash(&json!("some other entry")),
4179 AmendmentKind::Overruled,
4180 "b",
4181 "n",
4182 )
4183 .unwrap();
4184 c.amend(&key, &wrong_hash);
4185 assert!(problem(&c, 1).contains("entry_hash"), "{}", problem(&c, 1));
4186 assert!(
4187 verify_chain(&c.links, &pk, &anchored(&pk), &roots()).entries[0]
4188 .amended_by
4189 .is_empty()
4190 );
4191 }
4192
4193 #[test]
4194 fn redaction_shape_violations_fail_verification() {
4195 let (key, pk) = generate_keypair();
4196 let amend_with = |mutate: &dyn Fn(&mut Amendment)| -> String {
4197 let mut c = Chain::new();
4198 let target = c.decision(&key);
4199 let mut amendment = AmendmentDraft::new(
4200 target,
4201 c.hash_at(1),
4202 AmendmentKind::Correction,
4203 "b",
4204 "n",
4205 )
4206 .unwrap();
4207 mutate(&mut amendment.amendment);
4208 c.amend_v1(&key, &amendment.amendment);
4209 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4210 assert!(!v.ok, "{v:#?}");
4211 v.entries[1].problem.clone().unwrap_or_default()
4212 };
4213
4214 assert_eq!(
4217 AmendmentDraft::new(
4218 gov(1),
4219 data_hash(&json!(null)),
4220 AmendmentKind::Redaction,
4221 "b",
4222 "n"
4223 ),
4224 Err(AmendmentError::MissingRedaction)
4225 );
4226 let p = amend_with(&|a| a.kind = AmendmentKind::Redaction);
4227 assert!(p.contains("requires a `redaction`"), "{p}");
4228
4229 let p = amend_with(&|a| {
4230 a.redaction = Some(Redaction {
4231 fields: vec!["/x".into()],
4232 resulting_data_hash: data_hash(&json!({})),
4233 resulting_latest_hash: None,
4234 })
4235 });
4236 assert!(p.contains("only valid on kind"), "{p}");
4237
4238 let p = amend_with(&|a| a.agora_governance_amendment = 3);
4239 assert!(p.contains("agora_governance_amendment is 3"), "{p}");
4240
4241 let p = amend_with(&|a| a.agora_governance_amendment = 1);
4244 assert!(p.contains("does neither consistently"), "{p}");
4245 let p = amend_with(&|a| a.note = AmendmentText::Plain("n".into()));
4246 assert!(p.contains("does neither consistently"), "{p}");
4247 }
4248
4249 #[test]
4250 fn governance_data_holds_no_number_that_is_not_a_64_bit_integer() {
4251 let fine = json!({"a": [1, -2, u64::MAX, i64::MIN], "b": {"c": "0.5"}});
4252 assert_eq!(non_integer_number(&fine), None);
4253 assert!(blind_data(&fine, Blind::random()).is_ok());
4254
4255 for (text, pointer) in [
4256 (r#"{"a": {"b/c": [1, 0.5]}}"#, "/a/b~1c/1"),
4257 (r#"{"n": 1.0}"#, "/n"),
4258 (r#"{"n": 1e3}"#, "/n"),
4259 (r#"{"n": 18446744073709551616}"#, "/n"),
4260 (r#"{"n": -9223372036854775809}"#, "/n"),
4261 ] {
4262 let data: serde_json::Value = serde_json::from_str(text).unwrap();
4263 assert_eq!(non_integer_number(&data).as_deref(), Some(pointer));
4264 assert_eq!(
4265 blind_data(&data, Blind::random()),
4266 Err(BlindError::NonIntegerNumber(pointer.into())),
4267 "the writer refuses it"
4268 );
4269 assert_eq!(
4270 redact_data(&data, &["/n".into()], &amd(1), Blind::random()),
4271 Err(RedactError::NonIntegerNumber(pointer.into()))
4272 );
4273 }
4274 }
4275
4276 #[test]
4277 fn standing_is_the_last_amendment_that_changes_it() {
4278 use AmendmentKind::*;
4279 assert_eq!(standing([]), Standing::InForce);
4280 assert_eq!(standing([Correction, Redaction]), Standing::InForce);
4281 assert_eq!(
4282 standing([Correction, NonPrecedential, Redaction]),
4283 Standing::NonPrecedential
4284 );
4285 assert_eq!(standing([Overruled, Reinstated]), Standing::InForce);
4286 assert_eq!(standing([Reinstated, Superseded]), Standing::Superseded);
4287 assert_eq!(kind_standing(Reattested), None);
4288 assert_eq!(kind_standing(Reinstated), Some(Standing::InForce));
4289 }
4290
4291 #[test]
4292 fn a_redaction_verifies_against_the_amendment_and_nothing_else() {
4293 let (key, pk) = generate_keypair();
4294 let data = json!({
4295 "finding": "upheld",
4296 "subject": {"handle": "someone", "detail": "personal"},
4297 });
4298 let mut c = Chain::new();
4299 let target = c.entry(&key, data.clone());
4300 let amendment_id = c.next_amd();
4301 let (amendment, redacted) = AmendmentDraft::redaction(
4302 &amendment_id,
4303 target,
4304 c.hash_at(1),
4305 "GDPR Art. 17(1)(a)",
4306 "personal data removed on request",
4307 vec!["/subject/handle".into(), "/subject/detail".into()],
4308 &data,
4309 Blind::from([7; 32]),
4310 &[],
4311 )
4312 .unwrap();
4313 assert_eq!(c.amend(&key, &amendment), amendment_id);
4314 assert_eq!(redacted[BLIND_KEY], json!(Blind::from([7; 32])));
4315
4316 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4317 assert!(v.ok, "{v:#?}");
4318 assert!(v.entries[0].redacted);
4319 assert_eq!(v.entries[0].redacted_data_hash, Some(data_hash(&redacted)));
4320 assert_eq!(
4321 redacted["subject"]["handle"],
4322 json!(format!("[redacted by {amendment_id}]"))
4323 );
4324 assert_eq!(redacted["finding"], json!("upheld"), "and nothing else");
4325
4326 assert!(v.check_content(&c.links[0], &redacted));
4328 assert_eq!(v.entries[0].content_matches, Some(true));
4329 assert!(v.clone().settle().ok);
4330 assert!(v.check_content(&c.links[0], &data));
4332
4333 let mut tampered = redacted.clone();
4335 tampered["finding"] = json!("overturned");
4336 assert!(!v.check_content(&c.links[0], &tampered));
4337 assert_eq!(v.entries[0].content_matches, Some(false));
4338 assert!(!v.settle().ok);
4339 }
4340
4341 fn seats() -> serde_json::Value {
4344 json!({
4345 "title": "A motion",
4346 "rounds": [{
4347 "number": 1,
4348 "responses": [
4349 {"role": "lawyer", "rationale": "Because.", "raw_text": "Because.", "vote": "yes"},
4350 {"role": "artist", "rationale": "Why not.", "raw_text": "Why not?", "vote": "no"},
4351 ],
4352 }],
4353 "subject": {"handle": "someone"},
4354 BLIND_KEY: Blind::from([3; 32]),
4355 })
4356 }
4357
4358 const LAWYER: &str = "/rounds/0/responses/0";
4359
4360 fn patch(ops: serde_json::Value) -> json_patch::Patch {
4362 serde_json::from_value(ops).unwrap()
4363 }
4364
4365 fn dedup(data: &serde_json::Value) -> Edit {
4367 let (raw, rationale) =
4368 (format!("{LAWYER}/raw_text"), format!("{LAWYER}/rationale"));
4369 Revision::remove_duplicates(data, &[(&raw, &rationale)]).unwrap()
4370 }
4371
4372 fn revise(
4373 c: &Chain,
4374 target: &GovernanceLogId,
4375 latest: &serde_json::Value,
4376 patch: impl Into<Edit>,
4377 ) -> (AmendmentDraft, serde_json::Value) {
4378 AmendmentDraft::revision(
4379 target.clone(),
4380 GovernanceLogEntryType::CouncilDecision,
4381 c.hash_at(1),
4382 "Steward's record REC-2026-0001",
4383 "raw_text identical to the rationale removed",
4384 latest,
4385 patch,
4386 )
4387 .unwrap()
4388 }
4389
4390 fn revision_of(draft: &AmendmentDraft) -> &Revision {
4391 draft.amendment.revision.as_ref().unwrap()
4392 }
4393
4394 #[test]
4395 fn a_revision_overwrites_nothing_and_folds_to_the_latest() {
4396 let (key, pk) = generate_keypair();
4397 let data = seats();
4398 let mut c = Chain::new();
4399 let target = c.entry(&key, data.clone());
4400 let (first, v1) = revise(&c, &target, &data, dedup(&data));
4401 c.amend(&key, &first);
4402 let (second, v2) = revise(
4403 &c,
4404 &target,
4405 &v1,
4406 patch(json!([{"op": "move", "from": "/title", "path": "/motion"}])),
4407 );
4408 c.amend(&key, &second);
4409
4410 let seat = &v1["rounds"][0]["responses"];
4411 assert!(seat[0].get("raw_text").is_none(), "removed, no marker");
4412 assert_eq!(seat[1]["raw_text"], json!("Why not?"), "the other stays");
4413 assert_eq!(v2["motion"], json!("A motion"));
4414 assert_eq!(v2[BLIND_KEY], data[BLIND_KEY], "blind kept");
4415 assert_eq!(
4416 latest(&data, [revision_of(&first), revision_of(&second)]).unwrap(),
4417 v2
4418 );
4419
4420 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4421 assert!(v.ok, "{v:#?}");
4422 let entry = &v.entries[0];
4423 assert_eq!(entry.revisions, [amd(1), amd(2)]);
4424 assert_eq!(entry.latest_data_hash, Some(data_hash(&v2)));
4425 assert!(!entry.redacted && entry.redacted_data_hash.is_none());
4426
4427 assert!(v.check_content(&c.links[0], &data));
4429 assert!(v.check_content(&c.links[0], &v2));
4430 assert!(!v.check_content(&c.links[0], &v1), "not the latest");
4431 v.check_content(&c.links[0], &data);
4432 assert!(v.clone().settle().ok);
4433
4434 let wire = serde_json::to_value(&v).unwrap();
4437 assert!(wire["entries"][0].get("history").is_none());
4438 let mut old = wire.clone();
4439 for field in ["revisions", "latest_data_hash", "superseded_revisions"] {
4440 old["entries"][0].as_object_mut().unwrap().remove(field);
4441 }
4442 let old: GovernanceVerification = serde_json::from_value(old).unwrap();
4443 assert!(old.entries[0].revisions.is_empty());
4444 }
4445
4446 #[test]
4448 fn a_revision_that_does_not_produce_its_hash_fails() {
4449 let (key, pk) = generate_keypair();
4450 let data = seats();
4451 let mut c = Chain::new();
4452 let target = c.entry(&key, data.clone());
4453 let (mut draft, _) = revise(&c, &target, &data, dedup(&data));
4454 draft
4455 .amendment
4456 .revision
4457 .as_mut()
4458 .unwrap()
4459 .resulting_data_hash = data_hash(&json!({"something": "else"}));
4460 c.amend(&key, &draft);
4461
4462 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4463 assert!(v.ok, "the chain itself is sound: {v:#?}");
4464 assert!(v.check_content(&c.links[0], &data), "the stored data is");
4465 assert!(
4466 v.entries[0]
4467 .problem
4468 .as_deref()
4469 .is_some_and(|p| p.contains("does not produce")),
4470 "{:?}",
4471 v.entries[0].problem
4472 );
4473 assert!(!v.settle().ok);
4474
4475 c.links[0].data = Some(data);
4477 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4478 assert!(!v.ok);
4479 }
4480
4481 fn redact(
4482 c: &Chain,
4483 target: &GovernanceLogId,
4484 fields: &[&str],
4485 data: &serde_json::Value,
4486 revisions: &[&Revision],
4487 ) -> Result<(AmendmentDraft, serde_json::Value), RedactError> {
4488 AmendmentDraft::redaction(
4489 &c.next_amd(),
4490 target.clone(),
4491 c.hash_at(1),
4492 "GDPR Art. 17(1)(a)",
4493 "removed on request",
4494 fields.iter().map(|f| f.to_string()).collect(),
4495 data,
4496 Blind::from([9; 32]),
4497 revisions,
4498 )
4499 }
4500
4501 #[test]
4504 fn a_revision_then_a_redaction() {
4505 let (key, pk) = generate_keypair();
4506 let data = seats();
4507 let mut c = Chain::new();
4508 let target = c.entry(&key, data.clone());
4509 let (revision, _) = revise(&c, &target, &data, dedup(&data));
4510 c.amend(&key, &revision);
4511 let rationale = format!("{LAWYER}/rationale");
4512 let (redaction, redacted) = redact(
4513 &c,
4514 &target,
4515 &[&rationale],
4516 &data,
4517 &[revision_of(&revision)],
4518 )
4519 .unwrap();
4520 c.amend(&key, &redaction);
4521
4522 let fields = &redaction.amendment.redaction.as_ref().unwrap().fields;
4524 assert_eq!(fields, &[rationale, format!("{LAWYER}/raw_text")]);
4525 let seat = &redacted["rounds"][0]["responses"][0];
4526 assert_eq!(seat["raw_text"], seat["rationale"]);
4527 assert!(seat["raw_text"].as_str().unwrap().starts_with("[redacted"));
4528
4529 let rebased = latest(&redacted, [revision_of(&revision)]).unwrap();
4530 assert!(
4531 rebased["rounds"][0]["responses"][0]
4532 .get("raw_text")
4533 .is_none()
4534 );
4535 assert_eq!(
4536 redaction
4537 .amendment
4538 .redaction
4539 .as_ref()
4540 .unwrap()
4541 .resulting_latest_hash,
4542 Some(data_hash(&rebased))
4543 );
4544
4545 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4546 assert!(v.ok, "{v:#?}");
4547 let entry = &v.entries[0];
4548 assert_eq!(entry.latest_data_hash, Some(data_hash(&rebased)));
4549 assert_eq!(entry.superseded_revisions, [amd(1)]);
4550 assert!(v.check_content(&c.links[0], &redacted));
4551 assert!(v.check_content(&c.links[0], &rebased));
4552 assert!(
4553 v.check_content(&c.links[0], &data),
4554 "for whoever kept a copy"
4555 );
4556 v.check_content(&c.links[0], &redacted);
4557 assert!(v.clone().settle().ok, "{v:#?}");
4558
4559 let mut c2 = Chain::new();
4561 let target = c2.entry(&key, data.clone());
4562 c2.amend(&key, &revision);
4563 let (mut lying, _) = redact(
4564 &c2,
4565 &target,
4566 &["/subject/handle"],
4567 &data,
4568 &[revision_of(&revision)],
4569 )
4570 .unwrap();
4571 let (_, honest) = redact(
4572 &c2,
4573 &target,
4574 &["/subject/handle"],
4575 &data,
4576 &[revision_of(&revision)],
4577 )
4578 .unwrap();
4579 lying
4580 .amendment
4581 .redaction
4582 .as_mut()
4583 .unwrap()
4584 .resulting_latest_hash = Some(data_hash(&json!({})));
4585 c2.amend(&key, &lying);
4586 let mut v = verify_chain(&c2.links, &pk, &anchored(&pk), &roots());
4587 assert!(v.check_content(&c2.links[0], &honest));
4588 assert!(!v.settle().ok);
4589 }
4590
4591 #[test]
4594 fn a_redaction_of_a_revised_entry_must_name_the_latest_hash() {
4595 let (key, pk) = generate_keypair();
4596 let data = seats();
4597 let mut c = Chain::new();
4598 let target = c.entry(&key, data.clone());
4599 let (revision, _) = revise(&c, &target, &data, dedup(&data));
4600 c.amend(&key, &revision);
4601 let (redaction, _) =
4603 redact(&c, &target, &["/subject/handle"], &data, &[]).unwrap();
4604 c.amend(&key, &redaction);
4605 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4606 assert!(!v.ok);
4607 assert!(
4608 v.entries[2]
4609 .problem
4610 .as_deref()
4611 .is_some_and(|p| p.contains("names no resulting_latest_hash")),
4612 "{v:#?}"
4613 );
4614 }
4615
4616 #[test]
4619 fn a_false_duplicate_fails() {
4620 let (key, pk) = generate_keypair();
4621 let data = seats();
4622 let mut c = Chain::new();
4623 let target = c.entry(&key, data.clone());
4624 let artist = "/rounds/0/responses/1";
4625 let (raw, rationale) =
4626 (format!("{artist}/raw_text"), format!("{artist}/rationale"));
4627 let (mut lying, _) = revise(
4629 &c,
4630 &target,
4631 &data,
4632 patch(json!([{"op": "remove", "path": raw}])),
4633 );
4634 let revised = apply_patch(&data, &revision_of(&lying).patch).unwrap();
4635 let r = lying.amendment.revision.as_mut().unwrap();
4636 r.duplicates = vec![(raw, rationale)];
4637 r.resulting_data_hash = data_hash(&revised);
4638 c.amend(&key, &lying);
4639
4640 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4641 assert!(v.ok, "unread, the claim is unchecked: {v:#?}");
4642 assert!(v.check_content(&c.links[0], &data));
4643 assert!(
4644 v.entries[0]
4645 .problem
4646 .as_deref()
4647 .is_some_and(|p| p.contains("but they differ")),
4648 "{v:#?}"
4649 );
4650 assert!(!v.settle().ok);
4651 }
4652
4653 #[test]
4655 fn a_redaction_that_breaks_a_revision_is_refused() {
4656 let (key, _) = generate_keypair();
4657 let data = seats();
4658 let mut c = Chain::new();
4659 let target = c.entry(&key, data.clone());
4660 let (moved, _) = revise(
4661 &c,
4662 &target,
4663 &data,
4664 patch(
4665 json!([{"op": "move", "from": "/subject/handle", "path": "/handle"}]),
4666 ),
4667 );
4668 c.amend(&key, &moved);
4669 let err =
4670 redact(&c, &target, &["/subject"], &data, &[revision_of(&moved)])
4671 .unwrap_err();
4672 assert!(matches!(err, RedactError::Rebase(_)), "{err}");
4673 assert!(
4675 redact(
4676 &c,
4677 &target,
4678 &["/subject/handle"],
4679 &data,
4680 &[revision_of(&moved)]
4681 )
4682 .is_ok()
4683 );
4684 }
4685
4686 #[test]
4689 fn a_redaction_follows_every_duplicate_of_what_it_erases() {
4690 let revision = Revision {
4691 patch: patch(json!([{"op": "remove", "path": "/copy"}])),
4692 duplicates: vec![("/copy".into(), "/source/inner".into())],
4693 resulting_data_hash: data_hash(&json!(null)),
4694 };
4695 let extra = |fields: &[&str]| {
4696 let fields: Vec<String> =
4697 fields.iter().map(|f| f.to_string()).collect();
4698 duplicates_of(&fields, &[&revision])
4699 };
4700 assert_eq!(extra(&["/source/inner"]), ["/copy"]);
4701 assert_eq!(extra(&["/source/inner/name"]), ["/copy/name"]);
4702 assert_eq!(extra(&["/source"]), ["/copy"]);
4703 assert!(extra(&["/source/other"]).is_empty());
4704 assert!(extra(&["/source/inn"]).is_empty());
4705 }
4706
4707 #[test]
4710 fn a_redaction_then_a_revision() {
4711 let (key, pk) = generate_keypair();
4712 let data = seats();
4713 let mut c = Chain::new();
4714 let target = c.entry(&key, data.clone());
4715 let (redaction, redacted) =
4716 redact(&c, &target, &["/subject/handle"], &data, &[]).unwrap();
4717 assert_eq!(
4718 redaction
4719 .amendment
4720 .redaction
4721 .as_ref()
4722 .unwrap()
4723 .resulting_latest_hash,
4724 None,
4725 "nothing to rebase"
4726 );
4727 c.amend(&key, &redaction);
4728 let (revision, revised) =
4729 revise(&c, &target, &redacted, dedup(&redacted));
4730 c.amend(&key, &revision);
4731
4732 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4733 assert!(v.ok, "{v:#?}");
4734 assert_eq!(v.entries[0].latest_data_hash, Some(data_hash(&revised)));
4735 assert!(v.entries[0].superseded_revisions.is_empty());
4736 assert!(v.check_content(&c.links[0], &redacted));
4737 assert!(v.check_content(&c.links[0], &revised));
4738 assert!(v.settle().ok);
4739 }
4740
4741 #[test]
4742 fn a_revision_refuses_what_it_should() {
4743 let data = seats();
4744 let raw = &format!("{LAWYER}/raw_text");
4745 let rationale = &format!("{LAWYER}/rationale");
4746 let artist = "/rounds/0/responses/1";
4747 let dedup =
4748 |pairs: &[(&str, &str)]| Revision::remove_duplicates(&data, pairs);
4749
4750 assert_eq!(
4751 dedup(&[(
4752 &format!("{artist}/raw_text"),
4753 &format!("{artist}/rationale")
4754 )]),
4755 Err(ReviseError::NotIdentical {
4756 path: format!("{artist}/raw_text"),
4757 same_as: format!("{artist}/rationale"),
4758 })
4759 );
4760 assert_eq!(
4761 dedup(&[(raw, rationale), (rationale, raw)]),
4762 Err(ReviseError::SourceRemoved {
4763 path: raw.clone(),
4764 same_as: rationale.clone(),
4765 }),
4766 "a same_as the patch removes"
4767 );
4768 assert_eq!(
4769 dedup(&[("/nope", rationale)]),
4770 Err(ReviseError::Unresolved("/nope".into()))
4771 );
4772
4773 let make = |target_type, p: Edit| {
4774 AmendmentDraft::revision(
4775 gov(1),
4776 target_type,
4777 data_hash(&json!(null)),
4778 "b",
4779 "n",
4780 &data,
4781 p,
4782 )
4783 .map(|(_, v)| v)
4784 };
4785 let council = GovernanceLogEntryType::CouncilDecision;
4786 assert_eq!(
4787 make(council, patch(json!([])).into()),
4788 Err(ReviseError::EmptyPatch)
4789 );
4790 assert!(matches!(
4791 make(
4792 council,
4793 patch(json!([{"op": "remove", "path": "/nope"}])).into()
4794 ),
4795 Err(ReviseError::Patch(_))
4796 ));
4797 assert_eq!(
4798 make(
4799 council,
4800 patch(json!([{"op": "remove", "path": "/_blind"}])).into()
4801 ),
4802 Err(ReviseError::BlindPointer("/_blind".into()))
4803 );
4804 assert_eq!(
4805 make(
4806 council,
4807 patch(json!([{"op": "copy", "from": "/_blind", "path": "/b"}]))
4808 .into()
4809 ),
4810 Err(ReviseError::BlindPointer("/_blind".into()))
4811 );
4812 for entry_type in [
4813 GovernanceLogEntryType::Amendment,
4814 GovernanceLogEntryType::KeyRotation,
4815 GovernanceLogEntryType::StewardRecord,
4816 ] {
4817 assert!(!is_revisable(entry_type));
4818 assert_eq!(
4819 make(entry_type, dedup(&[(raw, rationale)]).unwrap()),
4820 Err(ReviseError::NotRevisable(entry_type))
4821 );
4822 }
4823 assert!(is_revisable(council));
4824 }
4825
4826 #[test]
4828 fn a_revision_that_adds_content_blinds_the_target() {
4829 let unblinded =
4830 json!({"title": "Old", "rationale": "r", "raw_text": "r"});
4831 let revise = |data: &serde_json::Value, p: serde_json::Value| {
4832 AmendmentDraft::revision(
4833 gov(1),
4834 GovernanceLogEntryType::CouncilDecision,
4835 data_hash(&json!(null)),
4836 "b",
4837 "n",
4838 data,
4839 patch(p),
4840 )
4841 .unwrap()
4842 };
4843 let add = json!([{"op": "add", "path": "/attachments", "value": []}]);
4844 let (draft, revised) = revise(&unblinded, add.clone());
4845 assert!(revised.get(BLIND_KEY).is_some());
4846 assert_eq!(revision_of(&draft).patch.len(), 2, "in the same patch");
4847 assert_eq!(
4848 latest(&unblinded, [revision_of(&draft)]).unwrap(),
4849 revised,
4850 "so the fold reproduces it"
4851 );
4852
4853 let (_, revised) =
4854 revise(&unblinded, json!([{"op": "remove", "path": "/raw_text"}]));
4855 assert!(revised.get(BLIND_KEY).is_none(), "a removal adds nothing");
4856
4857 let (draft, revised) = revise(&seats(), add);
4858 assert_eq!(revised[BLIND_KEY], seats()[BLIND_KEY], "never rotated");
4859 assert_eq!(revision_of(&draft).patch.len(), 1);
4860 }
4861
4862 #[test]
4863 fn revision_shape_violations_fail_verification() {
4864 let (key, pk) = generate_keypair();
4865 let amend_with = |mutate: &dyn Fn(&mut Amendment)| -> String {
4866 let mut c = Chain::new();
4867 let target = c.entry(&key, seats());
4868 let (mut draft, _) = revise(&c, &target, &seats(), dedup(&seats()));
4869 mutate(&mut draft.amendment);
4870 c.amend_v1(&key, &draft.amendment);
4871 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4872 assert!(!v.ok, "{v:#?}");
4873 v.entries[1].problem.clone().unwrap_or_default()
4874 };
4875 assert_eq!(
4876 AmendmentDraft::new(
4877 gov(1),
4878 data_hash(&json!(null)),
4879 AmendmentKind::Revision,
4880 "b",
4881 "n"
4882 ),
4883 Err(AmendmentError::MissingRevision)
4884 );
4885 let p = amend_with(&|a| a.revision = None);
4886 assert!(p.contains("requires a `revision`"), "{p}");
4887 let p = amend_with(&|a| a.kind = AmendmentKind::Correction);
4888 assert!(p.contains("only valid on kind `revision`"), "{p}");
4889 let p = amend_with(&|a| a.revision.as_mut().unwrap().patch.0.clear());
4890 assert!(p.contains("at least one op"), "{p}");
4891 }
4892
4893 #[cfg(feature = "schemars")]
4895 #[test]
4896 fn the_revision_schema_describes_a_patch() {
4897 let schema = crate::responses::inline_schema_for::<Amendment>();
4898 let revision = &schema["properties"]["revision"]["properties"];
4899 assert_eq!(revision["patch"]["type"], json!("array"), "{schema}");
4900 assert_eq!(
4901 revision["patch"]["items"]["properties"]["op"]["enum"],
4902 json!(["add", "remove", "replace", "move", "copy", "test"])
4903 );
4904 assert_eq!(revision["resulting_data_hash"]["type"], json!("string"));
4905 }
4906
4907 #[test]
4908 fn content_that_matches_nothing_fails_without_an_amendment() {
4909 let (key, pk) = generate_keypair();
4910 let mut c = Chain::new();
4911 c.entry(&key, json!({"a": 1}));
4912 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4913 assert!(!v.check_content(&c.links[0], &json!({"a": 2})));
4914 assert!(!v.clone().settle().ok);
4915 let other = link(&key, 9, None, &json!({}), at(9));
4917 assert!(!v.check_content(&other, &json!({})));
4918 }
4919
4920 #[test]
4921 fn tampering_with_an_amendments_data_is_caught_by_the_data_hash() {
4922 let (key, pk) = generate_keypair();
4923 let mut c = Chain::new();
4924 let target = c.decision(&key);
4925 let amendment = AmendmentDraft::new(
4926 target,
4927 c.hash_at(1),
4928 AmendmentKind::Overruled,
4929 "b",
4930 "overruled by a later decision",
4931 )
4932 .unwrap();
4933 c.amend(&key, &amendment);
4934 c.links[1].data.as_mut().unwrap()["note"] =
4935 json!("reinstated, actually");
4936
4937 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4938 assert!(!v.ok, "{v:#?}");
4939 let p = v.entries[1].problem.as_deref().unwrap();
4940 assert!(p.contains("does not hash to the attested data_hash"), "{p}");
4941 assert!(
4942 v.entries[0].amended_by.is_empty(),
4943 "an unreadable amendment has no effect"
4944 );
4945 assert!(
4946 v.entries[1].signature_valid && v.entries[1].link_valid,
4947 "the envelope is untouched — only the content is not what it \
4948 committed to"
4949 );
4950 }
4951
4952 #[test]
4953 fn an_amendment_or_rotation_must_carry_its_data() {
4954 let (key, pk) = generate_keypair();
4955 let mut c = Chain::new();
4956 let target = c.decision(&key);
4957 let amendment = AmendmentDraft::new(
4958 target,
4959 c.hash_at(1),
4960 AmendmentKind::Correction,
4961 "b",
4962 "n",
4963 )
4964 .unwrap();
4965 c.amend(&key, &amendment);
4966 let rotation = c.routine(&pk, &generate_keypair().0);
4967 c.rotate(&key, &rotation);
4968 c.links[1].data = None;
4969 c.links[2].data = None;
4970
4971 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4972 assert!(!v.ok, "{v:#?}");
4973 for (i, entry_type) in [(1, "amendment"), (2, "key_rotation")] {
4974 let p = v.entries[i].problem.as_deref().unwrap();
4975 assert!(p.contains(&format!("a {entry_type} entry")), "{p}");
4976 assert!(p.contains("must carry its `data`"), "{p}");
4977 }
4978 }
4979
4980 #[test]
4981 fn the_id_series_must_match_the_entry_type() {
4982 let (key, pk) = generate_keypair();
4983 let mut c = Chain::new();
4984 let target = c.decision(&key);
4985 let amendment = AmendmentDraft::new(
4986 target,
4987 c.hash_at(1),
4988 AmendmentKind::Correction,
4989 "b",
4990 "n",
4991 )
4992 .unwrap();
4993 c.push(
4994 &key,
4995 gov(7),
4996 GovernanceLogEntryType::Amendment,
4997 serde_json::to_value(&amendment.amendment).unwrap(),
4998 true,
4999 );
5000 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
5001 assert!(!v.ok, "{v:#?}");
5002 let p = v.entries[1].problem.as_deref().unwrap();
5003 assert!(p.contains("must be in the AMD- series"), "{p}");
5004
5005 let mut c = Chain::new();
5006 c.push(
5007 &key,
5008 key_id(1),
5009 GovernanceLogEntryType::CouncilDecision,
5010 json!({}),
5011 false,
5012 );
5013 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
5014 let p = v.entries[0].problem.as_deref().unwrap();
5015 assert!(p.contains("reserved"), "{p}");
5016 }
5017
5018 #[test]
5019 fn redact_data_replaces_whole_values_and_refuses_the_rest() {
5020 let id = amd(3);
5021 let blind = Blind::from([9; 32]);
5022 let data = json!({"a": {"b": [1, {"c": "secret"}]}, "d/e": "slash"});
5023 let out = redact_data(
5024 &data,
5025 &["/a/b/1/c".into(), "/d~1e".into()],
5026 &id,
5027 blind,
5028 )
5029 .unwrap();
5030 assert_eq!(out["a"]["b"][1]["c"], json!(redaction_marker(&id)));
5031 assert_eq!(out["d/e"], json!(redaction_marker(&id)));
5032 assert_eq!(out["a"]["b"][0], json!(1), "untouched");
5033 assert_eq!(out[BLIND_KEY], json!(blind), "a legacy entry gains one");
5034
5035 assert_eq!(
5036 redact_data(&data, &["/a/nope".into()], &id, blind),
5037 Err(RedactError::Unresolved("/a/nope".into()))
5038 );
5039 assert_eq!(
5040 redact_data(&data, &["".into()], &id, blind),
5041 Err(RedactError::WholeEntry)
5042 );
5043 assert_eq!(
5044 redact_data(&data, &[], &id, blind),
5045 Err(RedactError::NoFields)
5046 );
5047 assert_eq!(
5048 redact_data(&data, &["/_blind".into()], &id, blind),
5049 Err(RedactError::BlindPointer("/_blind".into()))
5050 );
5051 }
5052
5053 #[test]
5054 fn blind_data_is_for_objects_and_is_the_writers_to_supply() {
5055 let blind = Blind::from([1; 32]);
5056 let out = blind_data(&json!({"finding": "upheld"}), blind).unwrap();
5057 assert_eq!(out, json!({"finding": "upheld", "_blind": blind}));
5058 assert_eq!(
5059 blind_data(&json!([1]), blind),
5060 Err(BlindError::NotAnObject)
5061 );
5062 assert_eq!(blind_data(&out, blind), Err(BlindError::AlreadyBlinded));
5063 assert_ne!(Blind::random(), Blind::random());
5064
5065 use GovernanceLogEntryType::*;
5066 assert!(!is_redactable(Amendment) && !is_redactable(KeyRotation));
5067 assert!(
5068 is_redactable(CouncilDecision) && is_redactable(EmergencyAction)
5069 );
5070 }
5071
5072 #[test]
5076 fn a_removed_value_cannot_be_confirmed_by_guessing_it() {
5077 fn confirms(
5079 public: &serde_json::Value,
5080 pointer: &str,
5081 guess: &str,
5082 hash: Sha256Hex,
5083 ) -> bool {
5084 let mut attempt = public.clone();
5085 *attempt.pointer_mut(pointer).unwrap() = json!(guess);
5086 data_hash(&attempt) == hash
5087 }
5088 let legacy = json!({"finding": "upheld", "handle": "someone", "city": "Utrecht"});
5089
5090 let written = blind_data(&legacy, Blind::random()).unwrap();
5092 let first = redact_data(
5093 &written,
5094 &["/handle".into()],
5095 &amd(1),
5096 Blind::random(),
5097 )
5098 .unwrap();
5099 assert!(!confirms(&first, "/handle", "someone", data_hash(&written)));
5100
5101 let second =
5104 redact_data(&first, &["/city".into()], &amd(2), Blind::random())
5105 .unwrap();
5106 assert!(!confirms(&second, "/city", "Utrecht", data_hash(&first)));
5107
5108 let mut stripped =
5112 redact_data(&legacy, &["/handle".into()], &amd(3), Blind::random())
5113 .unwrap();
5114 let legacy_first = stripped.clone();
5115 stripped.as_object_mut().unwrap().remove(BLIND_KEY);
5116 assert!(confirms(
5117 &stripped,
5118 "/handle",
5119 "someone",
5120 data_hash(&legacy)
5121 ));
5122 let legacy_second = redact_data(
5124 &legacy_first,
5125 &["/city".into()],
5126 &amd(4),
5127 Blind::random(),
5128 )
5129 .unwrap();
5130 assert!(!confirms(
5131 &legacy_second,
5132 "/city",
5133 "Utrecht",
5134 data_hash(&legacy_first)
5135 ));
5136 }
5137
5138 #[test]
5141 fn a_routine_rotation_moves_the_chain_to_the_new_key() {
5142 let (old, old_pk) = generate_keypair();
5143 let (new, new_pk) = generate_keypair();
5144 let mut c = Chain::new();
5145 c.decision(&old);
5146 let rotation = c.routine(&old_pk, &new);
5147 let rotation_id = c.rotate(&old, &rotation);
5148 c.decision(&new);
5149
5150 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5153 assert!(v.ok, "{v:#?}");
5154 assert_eq!(v.public_key, (&new_pk).into());
5155 assert_eq!(
5156 v.entries[1].signed_by,
5157 Some((&old_pk).into()),
5158 "the rotation itself is signed by the old key"
5159 );
5160 assert_eq!(v.entries[2].signed_by, Some((&new_pk).into()));
5161 assert!(v.unanchored_keys.is_empty());
5162 assert!(v.repudiated.is_empty());
5163 assert_eq!(v.keys.len(), 2);
5164 assert_eq!(v.keys[0].public_key, (&old_pk).into());
5165 assert_eq!(v.keys[0].from_seq, 1);
5166 assert_eq!(v.keys[0].through_seq, Some(2));
5167 assert_eq!(v.keys[0].status, KeyStatus::Retired);
5168 assert_eq!(v.keys[0].introduced_by, None);
5169 assert_eq!(v.keys[0].retired_by.as_ref(), Some(&rotation_id));
5170 assert!(v.keys[0].certified, "retroactively, by the rotation");
5171 assert_eq!(v.keys[1].from_seq, 3);
5172 assert_eq!(v.keys[1].through_seq, None);
5173 assert_eq!(v.keys[1].status, KeyStatus::Active);
5174 assert_eq!(v.keys[1].introduced_by.as_ref(), Some(&rotation_id));
5175 assert!(v.keys[1].certified);
5176 }
5177
5178 #[test]
5179 fn the_old_key_cannot_sign_after_a_routine_rotation() {
5180 let (old, old_pk) = generate_keypair();
5181 let (new, _) = generate_keypair();
5182 let mut c = Chain::new();
5183 c.decision(&old);
5184 let rotation = c.routine(&old_pk, &new);
5185 c.rotate(&old, &rotation);
5186 c.decision(&old);
5187
5188 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5189 assert!(!v.ok, "{v:#?}");
5190 assert!(!v.entries[2].signature_valid);
5191 assert!(
5192 v.entries[2].link_valid,
5193 "the linkage is fine; the key is not"
5194 );
5195 }
5196
5197 #[test]
5200 fn the_online_key_cannot_certify_its_own_successor() {
5201 let (old, old_pk) = generate_keypair();
5202 let (thief, _) = generate_keypair();
5203 let mut c = Chain::new();
5204 c.decision(&old);
5205 let mut rotation = c.routine(&old_pk, &thief);
5206 let statement = rotation.certificate.statement.clone();
5208 rotation.certificate = certify(&[&old], statement);
5209 c.rotate(&old, &rotation);
5210 c.decision(&thief);
5211
5212 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5213 assert!(!v.ok, "{v:#?}");
5214 let p = v.entries[1].problem.as_deref().unwrap();
5215 assert!(p.contains("0 valid root signature"), "{p}");
5216 assert_eq!(v.public_key, (&old_pk).into(), "the chain does not move");
5217 assert!(!v.entries[2].signature_valid);
5218 assert_eq!(v.keys.len(), 1);
5219 }
5220
5221 #[test]
5222 fn a_certificate_counts_distinct_known_roots_only() {
5223 let (old, old_pk) = generate_keypair();
5224 let (new, _) = generate_keypair();
5225 let (stranger, _) = generate_keypair();
5226 let two_of_two = RootSet::new(
5227 [
5228 (&root(1).verifying_key()).into(),
5229 (&root(2).verifying_key()).into(),
5230 ],
5231 2,
5232 );
5233 let verdict = |signers: &[&SigningKey], roots: &RootSet| {
5234 let mut c = Chain::new();
5235 c.decision(&old);
5236 let mut rotation = c.routine(&old_pk, &new);
5237 let statement = rotation.certificate.statement.clone();
5238 rotation.certificate = certify(signers, statement);
5239 let genesis = KeyCertStatement::genesis((&old_pk).into());
5241 rotation.outgoing_certificate =
5242 Some(certify(&[&root(1), &root(2)], genesis));
5243 c.rotate(&old, &rotation);
5244 verify_chain(&c.links, &old_pk, &anchored(&old_pk), roots)
5245 };
5246
5247 assert!(verdict(&[&root(1), &root(2)], &two_of_two).ok);
5248 assert!(verdict(&[&root(2)], &roots()).ok, "either root, 1-of-2");
5249
5250 for (signers, why) in [
5251 (vec![&root(1)], "below the threshold"),
5252 (vec![&root(1), &root(1)], "one root twice is one root"),
5253 (vec![&root(1), &stranger], "an unknown root is nobody"),
5254 (vec![], "unsigned"),
5255 ] {
5256 let v = verdict(&signers, &two_of_two);
5257 assert!(!v.ok, "{why}: {v:#?}");
5258 let p = v.entries[1].problem.as_deref().unwrap();
5259 assert!(p.contains("where 2 are needed"), "{why}: {p}");
5260 }
5261
5262 assert!(verdict(&[&stranger, &root(1)], &roots()).ok);
5264 }
5265
5266 #[test]
5267 fn a_certificate_is_good_for_one_statement_at_one_position() {
5268 let (old, old_pk) = generate_keypair();
5269 let (new, new_pk) = generate_keypair();
5270 let (other, other_pk) = generate_keypair();
5271 let anchor = anchored(&old_pk);
5272
5273 let mut c = Chain::new();
5277 c.decision(&old);
5278 let early = c.routine(&old_pk, &new);
5279 c.decision(&old);
5280 let mut rotation = c.routine(&old_pk, &new);
5281 rotation.certificate = early.certificate;
5282 c.rotate(&old, &rotation);
5283 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5284 assert!(!v.ok, "{v:#?}");
5285 let p = v.entries[2].problem.as_deref().unwrap();
5286 assert!(
5287 p.contains("different key, purpose or chain position"),
5288 "{p}"
5289 );
5290 assert_eq!(v.public_key, (&old_pk).into());
5291
5292 let mut c = Chain::new();
5294 c.decision(&old);
5295 let for_new = c.routine(&old_pk, &new);
5296 let mut rotation = c.routine(&old_pk, &other);
5297 rotation.certificate = for_new.certificate;
5298 c.rotate(&old, &rotation);
5299 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5300 let p = v.entries[1].problem.as_deref().unwrap();
5301 assert!(
5302 p.contains("different key, purpose or chain position"),
5303 "{p}"
5304 );
5305
5306 let mut c = Chain::new();
5308 c.decision(&old);
5309 let mut rotation = c.routine(&old_pk, &other);
5310 rotation.certificate = for_new_at(&c, &new_pk);
5311 rotation.certificate.statement.key = (&other_pk).into();
5312 c.rotate(&old, &rotation);
5313 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5314 let p = v.entries[1].problem.as_deref().unwrap();
5315 assert!(p.contains("0 valid root signature"), "{p}");
5316
5317 let mut c = Chain::new();
5319 c.decision(&old);
5320 c.decision(&old);
5321 let mut rotation = c.compromise(&old_pk, &new, 1);
5322 rotation.certificate = for_new_at(&c, &new_pk);
5323 c.rotate(&new, &rotation);
5324 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5325 assert!(!v.ok);
5326 assert!(v.repudiated.is_empty(), "{v:#?}");
5327 assert_eq!(v.public_key, (&old_pk).into());
5328 }
5329
5330 #[test]
5333 fn a_root_signature_without_the_domain_prefix_certifies_nothing() {
5334 use ed25519_dalek::Signer;
5335 let (old, old_pk) = generate_keypair();
5336 let (new, _) = generate_keypair();
5337 let mut c = Chain::new();
5338 c.decision(&old);
5339 let mut rotation = c.routine(&old_pk, &new);
5340 let statement = rotation.certificate.statement.clone();
5341 let bare = canonical_json(&serde_json::to_value(&statement).unwrap());
5342 assert_eq!(
5343 statement.signed_bytes(),
5344 [ROOT_DOMAIN, bare.as_slice()].concat()
5345 );
5346 rotation.certificate =
5347 KeyCertificate::unsigned(statement).with(RootSignature {
5348 root_key: (&root(1).verifying_key()).into(),
5349 signature: root(1).sign(&bare).into(),
5350 });
5351 c.rotate(&old, &rotation);
5352 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5353 assert!(!v.ok, "{v:#?}");
5354 assert_eq!(v.public_key, (&old_pk).into());
5355 }
5356
5357 #[test]
5358 fn the_first_rotation_carries_the_genesis_certificate_and_only_it_does() {
5359 let (k1, k1_pk) = generate_keypair();
5360 let (k2, k2_pk) = generate_keypair();
5361 let (k3, _) = generate_keypair();
5362 let genesis =
5363 || certify(&[&root(1)], KeyCertStatement::genesis((&k1_pk).into()));
5364
5365 let mut c = Chain::new();
5367 c.decision(&k1);
5368 let mut rotation = c.routine(&k1_pk, &k2);
5369 rotation.outgoing_certificate = None;
5370 c.rotate(&k1, &rotation);
5371 let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5372 assert!(!v.ok);
5373 let p = v.entries[1].problem.as_deref().unwrap();
5374 assert!(p.contains("genesis key's outgoing_certificate"), "{p}");
5375 assert_eq!(v.public_key, (&k1_pk).into());
5376
5377 let mut c = Chain::new();
5379 c.decision(&k1);
5380 let rotation = c.routine(&k1_pk, &k2).with_outgoing(certify(
5381 &[&root(1)],
5382 KeyCertStatement::genesis((&k2_pk).into()),
5383 ));
5384 c.rotate(&k1, &rotation);
5385 let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5386 let p = v.entries[1].problem.as_deref().unwrap();
5387 assert!(p.starts_with("outgoing_certificate:"), "{p}");
5388
5389 let mut c = Chain::new();
5392 c.decision(&k1);
5393 let rotation = c.routine(&k1_pk, &k2);
5394 assert_eq!(rotation.outgoing_certificate, Some(genesis()));
5395 c.rotate(&k1, &rotation);
5396 let v = verify_chain(&c.links, &k1_pk, &KeyAnchor::default(), &roots());
5397 assert!(v.ok, "{v:#?}");
5398 assert!(v.unanchored_keys.is_empty(), "{v:#?}");
5399
5400 let again = c.routine(&k2_pk, &k3).with_outgoing(genesis());
5402 c.rotate(&k2, &again);
5403 let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5404 assert!(!v.ok);
5405 let p = v.entries[2].problem.as_deref().unwrap();
5406 assert!(p.contains("nowhere else"), "{p}");
5407 }
5408
5409 #[test]
5410 fn a_forged_proof_of_possession_is_rejected() {
5411 let (old, old_pk) = generate_keypair();
5412 let (_, new_pk) = generate_keypair();
5413 let (thief, _) = generate_keypair();
5414 let mut c = Chain::new();
5415 c.decision(&old);
5416 let mut rotation = c.routine(&old_pk, &thief);
5419 rotation.new_key = (&new_pk).into();
5420 rotation.certificate = for_new_at(&c, &new_pk);
5421 let statement = rotation.statement(c.prev_hash());
5422 rotation.proof = crypto::sign(
5423 &old,
5424 statement.hash().as_bytes(),
5425 rotation.proof_signed_at,
5426 )
5427 .into();
5428 c.rotate(&old, &rotation);
5429
5430 assert_eq!(
5431 rotation.verify_proof(c.links[1].attestation.prev_hash),
5432 Err(RotationError::BadProof)
5433 );
5434 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5435 assert!(!v.ok, "{v:#?}");
5436 assert!(
5437 v.entries[1]
5438 .problem
5439 .as_deref()
5440 .unwrap()
5441 .contains("proof of possession")
5442 );
5443 assert_eq!(v.public_key, (&old_pk).into(), "the chain does not move");
5444 }
5445
5446 #[test]
5447 fn a_compromise_repudiates_the_window_and_a_reattestation_restores_one() {
5448 let (old, old_pk) = generate_keypair();
5449 let (new, new_pk) = generate_keypair();
5450 let mut c = Chain::new();
5451 c.decision(&old); c.decision(&old); let reattested = c.decision(&old); let rotation = c.compromise(&old_pk, &new, 1);
5455 let rotation_id = c.rotate(&new, &rotation); let vouch = AmendmentDraft::new(
5457 reattested,
5458 c.hash_at(3),
5459 AmendmentKind::Reattested,
5460 "Art. VII",
5461 "independently verified; the Steward vouches for it",
5462 )
5463 .unwrap();
5464 let vouch_id = c.amend(&new, &vouch); let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5467 assert!(
5468 v.ok,
5469 "repudiation is a declared state, not a defect: {v:#?}"
5470 );
5471 assert_eq!(v.repudiated, vec![gov(2)]);
5472 assert!(v.entries[1].repudiated);
5473 assert!(!v.entries[2].repudiated);
5474 assert_eq!(v.entries[2].reattested_by, vec![vouch_id.clone()]);
5475 assert_eq!(v.entries[2].amended_by, vec![vouch_id]);
5476 assert_eq!(v.entries[3].signed_by, Some((&new_pk).into()));
5477 assert_eq!(v.public_key, (&new_pk).into());
5478 assert_eq!(v.keys.len(), 2);
5479 assert_eq!(v.keys[0].status, KeyStatus::Compromised);
5480 assert_eq!(
5481 v.keys[0].through_seq,
5482 Some(1),
5483 "trusted through the last trusted entry, not through the \
5484 declaration"
5485 );
5486 assert_eq!(v.keys[0].retired_by.as_ref(), Some(&rotation_id));
5487 assert_eq!(v.keys[1].from_seq, 4, "the declaration is its own first");
5488 }
5489
5490 #[test]
5493 fn last_trusted_is_the_roots_to_say() {
5494 let (old, old_pk) = generate_keypair();
5495 let (new, _) = generate_keypair();
5496 let mut c = Chain::new();
5497 c.decision(&old); c.decision(&old); let mut rotation = c.compromise(&old_pk, &new, 1);
5500 rotation.certificate.statement.last_trusted = Some(c.head(2));
5501 c.rotate(&new, &rotation);
5502 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5503 assert!(!v.ok, "{v:#?}");
5504 assert!(v.repudiated.is_empty());
5505 assert_eq!(v.public_key, (&old_pk).into());
5506 }
5507
5508 #[test]
5509 fn a_stolen_key_cannot_be_rotated_back_in() {
5510 let (k1, k1_pk) = generate_keypair();
5514 let (k2, k2_pk) = generate_keypair();
5515 let mut c = Chain::new();
5516 c.decision(&k1); let real = c.compromise(&k1_pk, &k2, 1);
5518 c.rotate(&k2, &real); c.decision(&k2); let honest =
5521 verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5522 assert!(honest.ok, "{honest:#?}");
5523
5524 let hijack = c.compromise(&k2_pk, &k1, 3);
5525 c.rotate(&k1, &hijack); c.decision(&k1); let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5529 assert!(!v.ok);
5530 assert_eq!(v.public_key, (&k2_pk).into(), "the chain stays with K2");
5531 let p = v.entries[3].problem.as_deref().unwrap();
5532 assert!(p.contains("never brought back"), "{p}");
5533 assert!(!v.entries[3].signature_valid, "{:#?}", v.entries[3]);
5534 assert!(!v.entries[4].signature_valid, "K1 signs nothing again");
5535 assert_eq!(v.keys.len(), 2);
5536 assert_eq!(v.keys[1].status, KeyStatus::Active);
5537 }
5538
5539 #[test]
5540 fn a_routine_rotation_cannot_reuse_a_key_either() {
5541 let (k1, k1_pk) = generate_keypair();
5542 let (k2, k2_pk) = generate_keypair();
5543 let mut c = Chain::new();
5544 c.decision(&k1);
5545 let out = c.routine(&k1_pk, &k2);
5546 c.rotate(&k1, &out);
5547 let back = c.routine(&k2_pk, &k1);
5548 c.rotate(&k2, &back);
5549 let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5550 assert!(!v.ok);
5551 assert!(
5552 v.entries[2]
5553 .problem
5554 .as_deref()
5555 .unwrap()
5556 .contains("never brought back"),
5557 "{:#?}",
5558 v.entries[2]
5559 );
5560 assert_eq!(v.public_key, (&k2_pk).into());
5561 }
5562
5563 #[test]
5564 fn a_forged_entry_has_no_effects() {
5565 let (steward, steward_pk) = generate_keypair();
5566 let (forger, _) = generate_keypair();
5567 let anchor = anchored(&steward_pk);
5568 let mut c = Chain::new();
5569 let target = c.decision(&steward); let fake = AmendmentDraft::new(
5571 target,
5572 c.hash_at(1),
5573 AmendmentKind::Overruled,
5574 "none",
5575 "overruled, says nobody with the key",
5576 )
5577 .unwrap();
5578 c.amend(&forger, &fake); let grab = c.routine(&steward_pk, &forger);
5582 c.rotate(&forger, &grab); let v = verify_chain(&c.links, &steward_pk, &anchor, &roots());
5585 assert!(!v.ok);
5586 assert!(v.entries[0].amended_by.is_empty(), "{:#?}", v.entries[0]);
5587 assert_eq!(v.public_key, (&steward_pk).into());
5588 assert_eq!(v.keys.len(), 1);
5589 assert!(v.unanchored_keys.is_empty());
5590 }
5591
5592 #[test]
5593 fn a_repeated_id_is_a_problem() {
5594 let (key, pk) = generate_keypair();
5595 let mut c = Chain::new();
5596 c.decision(&key);
5597 c.gov = 0;
5598 c.decision(&key); let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
5600 assert!(!v.ok);
5601 assert!(v.entries.iter().all(|e| {
5602 e.problem
5603 .as_deref()
5604 .is_some_and(|p| p.contains("more than once"))
5605 }));
5606 }
5607
5608 #[test]
5609 fn a_second_compromise_cannot_anchor_inside_the_first_window() {
5610 let (k1, k1_pk) = generate_keypair();
5611 let (k2, _) = generate_keypair();
5612 let (k3, _) = generate_keypair();
5613 let mut c = Chain::new();
5614 c.decision(&k1); c.decision(&k1); let first = c.compromise(&k1_pk, &k2, 1);
5617 c.rotate(&k2, &first); let second = c.compromise(&k1_pk, &k3, 2);
5619 c.rotate(&k3, &second); let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5622 assert!(!v.ok);
5623 let p = v.entries[3].problem.as_deref().unwrap();
5624 assert!(p.contains("repudiated"), "{p}");
5625 assert_eq!(v.keys.len(), 2, "K1 and K2; K3 never took the chain");
5626 assert_eq!(v.keys[0].through_seq, Some(1));
5627 }
5628
5629 #[test]
5630 fn an_uncertified_compromise_fails_closed() {
5631 let (old, old_pk) = generate_keypair();
5632 let (new, new_pk) = generate_keypair();
5633 let mut c = Chain::new();
5634 c.decision(&old);
5635 c.decision(&old);
5636 let mut rotation = c.compromise(&old_pk, &new, 1);
5637 let statement = rotation.certificate.statement.clone();
5638 rotation.certificate = certify(&[&new], statement);
5639 c.rotate(&new, &rotation);
5640
5641 let anchor = anchored(&old_pk).with((&new_pk).into());
5643 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5644 assert!(!v.ok, "{v:#?}");
5645 let p = v.entries[2].problem.as_deref().unwrap();
5646 assert!(p.contains("0 valid root signature"), "{p}");
5647 assert!(!v.entries[2].signature_valid, "checked under the old key");
5648 assert!(v.repudiated.is_empty(), "and nothing is repudiated");
5649 assert_eq!(v.public_key, (&old_pk).into());
5650 }
5651
5652 #[test]
5657 fn a_rotation_inside_the_window_is_void_with_it() {
5658 let (steward, steward_pk) = generate_keypair();
5659 let (successor, _) = generate_keypair();
5660 let (recovery, recovery_pk) = generate_keypair();
5661
5662 let mut c = Chain::new();
5663 c.decision(&steward); c.decision(&steward); let routine = c.routine(&steward_pk, &successor);
5666 c.rotate(&steward, &routine); c.decision(&successor); let declaration = c.compromise(&steward_pk, &recovery, 1);
5670 c.rotate(&recovery, &declaration); let v = verify_chain(
5673 &c.links,
5674 &steward_pk,
5675 &anchored(&steward_pk),
5676 &roots(),
5677 );
5678 assert!(v.ok, "{v:#?}");
5679 assert_eq!(v.repudiated, vec![gov(2), key_id(1), gov(3)]);
5680 assert_eq!(v.public_key, (&recovery_pk).into());
5681 assert_eq!(v.keys.len(), 2, "the successor is not part of history");
5682 assert_eq!(v.keys[0].public_key, (&steward_pk).into());
5683 assert_eq!(v.keys[0].status, KeyStatus::Compromised);
5684 assert!(
5685 v.keys[0].certified,
5686 "the genesis certificate rode in on the voided rotation and \
5687 is the root's word all the same"
5688 );
5689 assert_eq!(v.keys[1].public_key, (&recovery_pk).into());
5690 }
5691
5692 #[test]
5693 fn a_compromise_must_name_a_real_head_and_the_key_that_held_it() {
5694 let (old, old_pk) = generate_keypair();
5695 let (new, new_pk) = generate_keypair();
5696 let anchor = anchored(&old_pk);
5697
5698 let mut c = Chain::new();
5701 c.decision(&old);
5702 let mut trusted = c.head(1);
5703 trusted.entry_hash = data_hash(&json!("nope"));
5704 let statement = KeyCertStatement::compromise(
5705 (&new_pk).into(),
5706 2,
5707 c.prev_hash(),
5708 trusted,
5709 );
5710 let mut rotation = c.compromise(&old_pk, &new, 1);
5711 rotation.certificate = certify(&[&root(1)], statement);
5712 c.rotate(&new, &rotation);
5713 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5714 let p = v.entries[1].problem.as_deref().unwrap();
5715 assert!(p.contains("last_trusted"), "{p}");
5716
5717 let (_, other_pk) = generate_keypair();
5719 let mut c = Chain::new();
5720 c.decision(&old);
5721 let rotation = c.compromise(&other_pk, &new, 1);
5722 c.rotate(&new, &rotation);
5723 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5724 let p = v.entries[1].problem.as_deref().unwrap();
5725 assert!(p.contains("old_key is not the key"), "{p}");
5726
5727 let mut c = Chain::new();
5729 c.decision(&old);
5730 let mut rotation = c.compromise(&old_pk, &new, 1);
5731 rotation.certificate.statement.last_trusted = None;
5732 c.rotate(&new, &rotation);
5733 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5734 let p = v.entries[1].problem.as_deref().unwrap();
5735 assert!(p.contains("must name last_trusted"), "{p}");
5736 }
5737
5738 #[test]
5739 fn a_rotation_carries_no_free_text() {
5740 let (old, old_pk) = generate_keypair();
5741 let (new, _) = generate_keypair();
5742 let mut c = Chain::new();
5743 c.decision(&old);
5744 let rotation = c.routine(&old_pk, &new);
5745 let mut value = serde_json::to_value(&rotation).unwrap();
5746 assert!(serde_json::from_value::<KeyRotation>(value.clone()).is_ok());
5747 value["note"] = json!("at the request of …");
5748 assert!(serde_json::from_value::<KeyRotation>(value).is_err());
5749
5750 let mut head = serde_json::to_value(c.head(1)).unwrap();
5751 head["comment"] = json!("the last one I remember signing");
5752 assert!(serde_json::from_value::<TrustedHead>(head).is_err());
5753
5754 let mut statement =
5755 serde_json::to_value(&rotation.certificate.statement).unwrap();
5756 statement["comment"] = json!("signed in the kitchen");
5757 assert!(serde_json::from_value::<KeyCertStatement>(statement).is_err());
5758 }
5759
5760 #[test]
5761 fn the_root_statement_bytes_are_pinned() {
5762 let key: PublicKeyHex = PUBLISHED_KEYS[0].parse().unwrap();
5763 assert_eq!(
5764 String::from_utf8(KeyCertStatement::genesis(key).signed_bytes())
5765 .unwrap(),
5766 "agora-governance-root-v1\n\
5767 {\"agora_governance_key_cert\":1,\"from_seq\":1,\
5768 \"key\":\"ebb3091dd328f1463362c171121921b2fe14628e3fc4c145deaccefb85c0e78a\",\
5769 \"last_trusted\":null,\"prev_hash\":null,\"purpose\":\"genesis\"}"
5770 );
5771
5772 let statement = KeyCertStatement::compromise(
5776 Sha256Hex::from([0xab; 32]).to_string().parse().unwrap(),
5777 15,
5778 Some([0xcd; 32].into()),
5779 TrustedHead {
5780 id: gov(10),
5781 chain_seq: 11,
5782 entry_hash: [0xef; 32].into(),
5783 },
5784 );
5785 assert_eq!(
5786 Sha256Hex::from(<[u8; 32]>::from(Sha256::digest(
5787 statement.signed_bytes()
5788 )))
5789 .to_string(),
5790 "633685771e08be126fd12ca4eb98c77120e5868236ff541ecba85ce6a21e6b68"
5791 );
5792 }
5793
5794 #[test]
5795 fn root_keys_are_curve_points_and_make_a_root_set() {
5796 let roots = RootSet::published();
5797 assert_eq!(roots.keys().count(), ROOT_KEYS.len());
5798 assert_eq!(roots.threshold(), ROOT_THRESHOLD);
5799 assert!(ROOT_THRESHOLD >= 1 && ROOT_THRESHOLD <= ROOT_KEYS.len());
5800 for root in ROOT_KEYS {
5801 let key: PublicKeyHex = root.parse().unwrap();
5802 assert!(roots.contains(&key));
5803 assert!(
5804 key.to_verifying_key().is_ok(),
5805 "{root} is not a valid Ed25519 public key"
5806 );
5807 assert!(
5808 !PUBLISHED_KEYS.contains(root),
5809 "a root key never signs entries"
5810 );
5811 }
5812 assert_eq!(RootSet::new([], 0).threshold(), 1);
5813 }
5814
5815 #[test]
5816 fn a_genesis_key_outside_the_anchor_is_reported_not_rejected() {
5817 let (key, pk) = generate_keypair();
5818 let c = chain(&key, 2);
5819 let v = verify_chain(&c, &pk, &KeyAnchor::default(), &roots());
5820 assert!(v.ok, "{v:#?}");
5821 assert_eq!(v.unanchored_keys, vec![PublicKeyHex::from(&pk)]);
5822 assert_eq!(v.keys.len(), 1);
5823 assert_eq!(v.keys[0].status, KeyStatus::Active);
5824 assert_eq!(v.keys[0].from_seq, 1);
5825 assert_eq!(v.keys[0].through_seq, None);
5826 }
5827
5828 #[test]
5829 fn published_keys_are_curve_points_and_make_an_anchor() {
5830 let anchor = KeyAnchor::published();
5831 assert!(!anchor.is_empty());
5832 assert_eq!(anchor.keys().count(), PUBLISHED_KEYS.len());
5833 for published in PUBLISHED_KEYS {
5834 let key: PublicKeyHex = published.parse().unwrap();
5835 assert!(anchor.contains(&key));
5836 assert!(
5837 key.to_verifying_key().is_ok(),
5838 "{published} is not a valid Ed25519 public key"
5839 );
5840 }
5841 let (_, other) = generate_keypair();
5842 assert!(!anchor.contains(&(&other).into()));
5843 assert!(
5844 anchor
5845 .clone()
5846 .with((&other).into())
5847 .contains(&(&other).into())
5848 );
5849 }
5850
5851 #[test]
5854 fn amendments_and_rotations_round_trip_as_entry_data() {
5855 let (key, pk) = generate_keypair();
5856 let amendment = AmendmentDraft::new(
5857 gov(1),
5858 data_hash(&json!("x")),
5859 AmendmentKind::Superseded,
5860 "Art. VI § 2",
5861 "superseded by GOV-2026-0009",
5862 )
5863 .unwrap()
5864 .with_authority(gov(9))
5865 .with_rationale("the later decision covers the same subject");
5866 let AmendmentDraft { amendment, texts } = amendment;
5867 let value = serde_json::to_value(&amendment).unwrap();
5868 assert_eq!(value["kind"], "superseded");
5869 assert_eq!(value["agora_governance_amendment"], 2);
5870 assert!(value.get("redaction").is_none(), "{value}");
5871 let text = value.to_string();
5872 assert!(!text.contains("Art. VI"), "no text in signed data: {text}");
5873 assert!(!text.contains("salt"), "and no salt: {text}");
5874 assert_eq!(
5875 value["note"]["commitment"],
5876 texts
5877 .note
5878 .as_ref()
5879 .unwrap()
5880 .commitment()
5881 .commitment
5882 .to_string()
5883 );
5884 assert_eq!(
5885 serde_json::from_value::<Amendment>(value).unwrap(),
5886 amendment
5887 );
5888 let beside = serde_json::to_value(&texts).unwrap();
5889 assert_eq!(beside["basis"]["text"], "Art. VI § 2");
5890 assert_eq!(
5891 serde_json::from_value::<AmendmentTexts>(beside).unwrap(),
5892 texts
5893 );
5894
5895 let legacy = json!({
5897 "agora_governance_amendment": 1,
5898 "target": "GOV-2026-0001",
5899 "target_entry_hash": data_hash(&json!("x")),
5900 "kind": "non_precedential",
5901 "authority": "GOV-2026-0005",
5902 "basis": "§1",
5903 "note": "diagnostic finding",
5904 });
5905 let legacy: Amendment = serde_json::from_value(legacy).unwrap();
5906 assert_eq!(legacy.validate(), Ok(()));
5907 assert_eq!(legacy.basis, AmendmentText::Plain("§1".into()));
5908
5909 let mut c = Chain::new();
5910 c.decision(&key);
5911 let rotation = c.compromise(&pk, &generate_keypair().0, 1);
5912 let value = serde_json::to_value(&rotation).unwrap();
5913 assert_eq!(value["agora_governance_key_rotation"], 2);
5914 assert_eq!(value["reason"], "compromise");
5915 let statement = &value["certificate"]["statement"];
5916 assert_eq!(statement["purpose"], "compromise");
5917 assert_eq!(statement["last_trusted"]["chain_seq"], 1);
5918 assert_eq!(
5919 value["outgoing_certificate"]["statement"]["purpose"],
5920 "genesis"
5921 );
5922 assert!(value.get("note").is_none(), "{value}");
5923 assert_eq!(
5924 serde_json::from_value::<KeyRotation>(value).unwrap(),
5925 rotation
5926 );
5927
5928 let notice =
5929 AmendmentNotice::new(amd(1), at(0), &amendment, Some(&texts));
5930 let value = serde_json::to_value(¬ice).unwrap();
5931 assert_eq!(value["id"], "AMD-2026-0001");
5932 assert_eq!(value["note"], "superseded by GOV-2026-0009");
5933 assert_eq!(
5934 serde_json::from_value::<AmendmentNotice>(value).unwrap(),
5935 notice
5936 );
5937
5938 let erased = AmendmentTexts {
5940 rationale: None,
5941 ..texts.clone()
5942 };
5943 let notice =
5944 AmendmentNotice::new(amd(1), at(0), &amendment, Some(&erased));
5945 assert_eq!(notice.note, "superseded by GOV-2026-0009");
5946 assert_eq!(notice.rationale.as_deref(), Some(WITHHELD_TEXT));
5947 let notice = AmendmentNotice::new(amd(1), at(0), &amendment, None);
5948 assert_eq!(notice.basis, WITHHELD_TEXT);
5949 let notice = AmendmentNotice::new(amd(1), at(0), &legacy, None);
5950 assert_eq!(notice.note, "diagnostic finding");
5951 }
5952
5953 #[test]
5957 fn an_amendment_verifies_with_or_without_its_optional_fields() {
5958 let (key, pk) = generate_keypair();
5959 let mut c = Chain::new();
5960 let target = c.decision(&key);
5961 let bare = AmendmentDraft::new(
5962 target.clone(),
5963 c.hash_at(1),
5964 AmendmentKind::Correction,
5965 "clerical",
5966 "typo in the citation",
5967 )
5968 .unwrap();
5969 assert!(
5970 serde_json::to_value(&bare.amendment)
5971 .unwrap()
5972 .get("rationale")
5973 .is_none()
5974 );
5975 c.amend(&key, &bare);
5976 let full = bare.clone().with_rationale("at length: …");
5977 c.amend(&key, &full);
5978 let mut future = serde_json::to_value(&full.amendment).unwrap();
5980 future["superseded_by_something_new"] = json!(["later"]);
5981 c.push(
5982 &key,
5983 amd(3),
5984 GovernanceLogEntryType::Amendment,
5985 future,
5986 true,
5987 );
5988
5989 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
5990 assert!(v.ok, "{v:#?}");
5991 assert_eq!(
5992 v.entries[0].amended_by,
5993 vec![amd(1), amd(2), amd(3)],
5994 "all three name the target"
5995 );
5996 }
5997
5998 #[test]
6001 fn pre_0_26_wire_still_deserializes() {
6002 let link: GovernanceChainLink = serde_json::from_value(json!({
6003 "id": "GOV-2026-0001",
6004 "entry_type": "council_decision",
6005 "created_at": "2023-11-14T22:13:20.123456Z",
6006 "attestation": {
6007 "envelope_version": 1,
6008 "chain_seq": 1,
6009 "prev_hash": null,
6010 "data_hash": "00".repeat(32),
6011 "entry_hash": "11".repeat(32),
6012 "signature": "22".repeat(64),
6013 "signed_at": "2023-11-14T22:13:21Z",
6014 "retroactive": false,
6015 },
6016 }))
6017 .unwrap();
6018 assert!(link.data.is_none());
6019 assert!(
6020 serde_json::to_value(&link).unwrap().get("data").is_none(),
6021 "and a link without data does not grow a null field"
6022 );
6023
6024 let verdict: EntryVerdict = serde_json::from_value(json!({
6025 "id": "GOV-2026-0001",
6026 "chain_seq": 1,
6027 "signature_valid": true,
6028 "link_valid": true,
6029 "content_matches": null,
6030 "retroactive": false,
6031 "out_of_order": false,
6032 "amended_by": [],
6033 }))
6034 .unwrap();
6035 assert!(!verdict.repudiated && !verdict.redacted);
6036 assert!(verdict.signed_by.is_none());
6037 assert!(verdict.reattested_by.is_empty());
6038
6039 let verification: GovernanceVerification =
6040 serde_json::from_value(json!({
6041 "public_key": "33".repeat(32),
6042 "ok": true,
6043 "head": "GOV-2026-0001",
6044 "entries": [],
6045 }))
6046 .unwrap();
6047 assert!(verification.keys.is_empty());
6048 assert!(verification.unanchored_keys.is_empty());
6049 assert!(verification.repudiated.is_empty());
6050 }
6051
6052 #[test]
6053 fn the_report_round_trips() {
6054 let (key, pk) = generate_keypair();
6055 let mut c = Chain::new();
6056 let target = c.decision(&key);
6057 let amendment = AmendmentDraft::new(
6058 target,
6059 c.hash_at(1),
6060 AmendmentKind::Overruled,
6061 "b",
6062 "n",
6063 )
6064 .unwrap();
6065 c.amend(&key, &amendment);
6066 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
6067 let text = serde_json::to_string(&v).unwrap();
6068 assert_eq!(
6069 serde_json::from_str::<GovernanceVerification>(&text).unwrap(),
6070 v
6071 );
6072
6073 let keys = GovernanceSigningKeys { keys: v.keys };
6074 let value = serde_json::to_value(&keys).unwrap();
6075 assert!(value["keys"].is_array(), "an object, not a bare array");
6076 assert_eq!(value["keys"][0]["status"], "active");
6077 assert_eq!(
6078 serde_json::from_value::<GovernanceSigningKeys>(value).unwrap(),
6079 keys
6080 );
6081 }
6082
6083 #[test]
6087 fn envelope_v1_preimage_and_hash_are_pinned() {
6088 let envelope = Envelope::new(
6089 gov(6),
6090 GovernanceLogEntryType::CouncilDecision,
6091 at(0),
6092 Some(Sha256Hex::from([0x11; 32])),
6093 data_hash(&json!({"outcome": "approved", "title": "Ratification"})),
6094 );
6095 assert_eq!(
6096 String::from_utf8(envelope.preimage()).unwrap(),
6097 "{\"agora_governance_log\":1,\"id\":\"GOV-2026-0006\",\
6098 \"entry_type\":\"council_decision\",\
6099 \"created_at\":1700000000123456,\
6100 \"prev_hash\":\"1111111111111111111111111111111111111111111111111111111111111111\",\
6101 \"data_hash\":\"a4adf645ae3f60c56484d01aea87d6d490321d7fc66b1607df14b023fe567c7b\"}"
6102 );
6103 assert_eq!(
6104 envelope.entry_hash().to_hex(),
6105 "ba27577432f81e415f1c01cc4cfabab6070e3ac50fd468fffe195ef19c0e9464"
6106 );
6107 }
6108
6109 #[cfg(feature = "agora-client")]
6123 #[tokio::test]
6124 #[ignore = "networked: hits the live platform"]
6125 async fn the_published_key_is_the_one_the_platform_serves() {
6126 let client = crate::client::Client::new(
6127 url::Url::parse("https://subliminal.technology").unwrap(),
6128 )
6129 .unwrap();
6130 let served = client.get_governance_signing_key().await.unwrap();
6131 assert_eq!(served.algorithm, "ed25519");
6132
6133 let genesis: PublicKeyHex = PUBLISHED_KEYS
6134 .first()
6135 .expect("PUBLISHED_KEYS is never empty")
6136 .parse()
6137 .unwrap();
6138 let links = client.get_governance_chain().await.unwrap();
6139 let report = verify_chain(
6140 &links,
6141 &genesis.to_verifying_key().unwrap(),
6142 &KeyAnchor::published(),
6143 &RootSet::published(),
6144 );
6145 let problems: Vec<_> = report
6146 .entries
6147 .iter()
6148 .filter_map(|e| {
6149 e.problem.as_ref().map(|p| (e.id.clone(), p.clone()))
6150 })
6151 .collect();
6152 assert!(
6153 report.ok,
6154 "the live chain does not verify under this build's genesis key \
6155 and roots: {problems:#?}"
6156 );
6157 assert!(report.unanchored_keys.is_empty(), "{report:#?}");
6158 assert_eq!(
6159 served.public_key, report.public_key,
6160 "the platform serves {} but the chain, followed under the \
6161 published roots, is held by {}",
6162 served.public_key, report.public_key
6163 );
6164 }
6165
6166 #[cfg(feature = "schemars")]
6167 #[test]
6168 fn wire_schemas_are_ref_free() {
6169 use crate::responses::inline_schema_for;
6170 for (name, schema) in [
6171 (
6172 "GovernanceAttestation",
6173 inline_schema_for::<GovernanceAttestation>(),
6174 ),
6175 (
6176 "GovernanceChainLink",
6177 inline_schema_for::<GovernanceChainLink>(),
6178 ),
6179 (
6180 "GovernanceSigningKey",
6181 inline_schema_for::<GovernanceSigningKey>(),
6182 ),
6183 (
6184 "GovernanceVerification",
6185 inline_schema_for::<GovernanceVerification>(),
6186 ),
6187 (
6188 "Vec<GovernanceChainLink>",
6189 inline_schema_for::<Vec<GovernanceChainLink>>(),
6190 ),
6191 ("Amendment", inline_schema_for::<Amendment>()),
6192 ("Redaction", inline_schema_for::<Redaction>()),
6193 ("Revision", inline_schema_for::<Revision>()),
6194 ("AmendmentNotice", inline_schema_for::<AmendmentNotice>()),
6195 ("KeyRotation", inline_schema_for::<KeyRotation>()),
6196 ("TrustedHead", inline_schema_for::<TrustedHead>()),
6197 (
6198 "GovernanceKeyRecord",
6199 inline_schema_for::<GovernanceKeyRecord>(),
6200 ),
6201 (
6202 "GovernanceSigningKeys",
6203 inline_schema_for::<GovernanceSigningKeys>(),
6204 ),
6205 ("AmendmentKind", inline_schema_for::<AmendmentKind>()),
6206 ("Standing", inline_schema_for::<Standing>()),
6207 ("KeyStatus", inline_schema_for::<KeyStatus>()),
6208 ("RotationReason", inline_schema_for::<RotationReason>()),
6209 ] {
6210 let text = serde_json::to_string(&schema).unwrap();
6211 assert!(!text.contains("$ref"), "{name} must be $ref-free: {text}");
6212 assert!(
6213 !text.contains("$defs"),
6214 "{name} must be $defs-free: {text}"
6215 );
6216 }
6217 let text =
6218 serde_json::to_string(&inline_schema_for::<Sha256Hex>()).unwrap();
6219 assert!(text.contains("^[0-9a-f]{64}$"), "{text}");
6220 let text = serde_json::to_string(&inline_schema_for::<SignatureHex>())
6221 .unwrap();
6222 assert!(text.contains("^[0-9a-f]{128}$"), "{text}");
6223 }
6224}