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