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