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