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