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