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