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