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