1use crate::crypto::{self, Signature, SigningKey, VerifyingKey};
55use crate::enums::GovernanceLogEntryType;
56use crate::ids::{GovernanceLogId, GovernanceLogPrefix};
57use chrono::{DateTime, Utc};
58use serde::{Deserialize, Serialize};
59use sha2::{Digest, Sha256};
60use std::collections::{HashMap, HashSet};
61
62pub use crate::enums::{AmendmentKind, CitationRelation, KeyStatus, Standing};
63
64mod texts;
65pub use texts::{
66 AmendmentText, AmendmentTextStatus, AmendmentTexts, CommittedText,
67 TextCommitment, TextStatus, WITHHELD_TEXT,
68};
69
70mod council;
71pub use council::{
72 Abstention, AgendaRanking, Ballot, CouncilAttachment,
73 CouncilDecisionRecord, CouncilMember, CouncilRound, CouncilSeat,
74 CouncilVote, DecisionCategory, FinalVotes, PlacedProposal, Rationale,
75 RecusalVote, SeatModels, SeatRanking, SeatResponse, StewardEmergency,
76 StewardRecusal,
77};
78
79mod redactable;
80pub use redactable::{REDACTION_MARKER_PATTERN, Redactable};
81
82pub mod reading;
83
84pub use json_patch;
87
88mod record;
89pub use record::{
90 RecordAttachment, RecordParticipant, STEWARD_RECORD_VERSION, StewardRecord,
91};
92
93mod root;
94pub use root::{
95 CertPurpose, CertificateError, KEY_CERT_VERSION, KeyCertStatement,
96 KeyCertificate, ROOT_DOMAIN, ROOT_KEYS, ROOT_THRESHOLD, RootSet,
97 RootSignature,
98};
99
100#[cfg(test)]
102mod vectors;
103
104pub const ENVELOPE_VERSION: u32 = 1;
106
107pub const RETROACTIVE_AFTER: chrono::Duration = chrono::Duration::seconds(60);
110
111#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
118#[error("{type_name}: expected {expected} bytes of hex, got {got:?}")]
119pub struct HexLengthError {
120 pub type_name: &'static str,
121 pub expected: usize,
122 pub got: String,
123}
124
125macro_rules! hex_bytes {
126 ($(#[$meta:meta])* $name:ident, $len:expr) => {
127 $(#[$meta])*
128 #[derive(Clone, Copy, PartialEq, Eq, Hash)]
129 #[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
130 #[cfg_attr(feature = "sqlx", sqlx(transparent))]
131 pub struct $name([u8; $len]);
132
133 impl $name {
134 pub fn as_bytes(&self) -> &[u8; $len] {
136 &self.0
137 }
138
139 pub fn to_hex(&self) -> String {
141 hex::encode(self.0)
142 }
143 }
144
145 impl From<[u8; $len]> for $name {
146 fn from(bytes: [u8; $len]) -> Self {
147 Self(bytes)
148 }
149 }
150
151 impl TryFrom<&[u8]> for $name {
152 type Error = HexLengthError;
153
154 fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
155 <[u8; $len]>::try_from(bytes).map(Self).map_err(|_| {
156 HexLengthError {
157 type_name: stringify!($name),
158 expected: $len,
159 got: hex::encode(bytes),
160 }
161 })
162 }
163 }
164
165 impl TryFrom<Vec<u8>> for $name {
166 type Error = HexLengthError;
167
168 fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
169 Self::try_from(bytes.as_slice())
170 }
171 }
172
173 impl std::str::FromStr for $name {
174 type Err = HexLengthError;
175
176 fn from_str(s: &str) -> Result<Self, Self::Err> {
177 let bytes = hex::decode(s.trim()).map_err(|_| HexLengthError {
178 type_name: stringify!($name),
179 expected: $len,
180 got: s.to_string(),
181 })?;
182 Self::try_from(bytes.as_slice())
183 }
184 }
185
186 impl std::fmt::Display for $name {
187 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188 f.write_str(&self.to_hex())
189 }
190 }
191
192 impl std::fmt::Debug for $name {
193 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
194 write!(f, "{}({})", stringify!($name), self.to_hex())
195 }
196 }
197
198 impl Serialize for $name {
199 fn serialize<S: serde::Serializer>(
200 &self,
201 s: S,
202 ) -> Result<S::Ok, S::Error> {
203 s.serialize_str(&self.to_hex())
204 }
205 }
206
207 impl<'de> Deserialize<'de> for $name {
208 fn deserialize<D: serde::Deserializer<'de>>(
209 d: D,
210 ) -> Result<Self, D::Error> {
211 let s = String::deserialize(d)?;
212 s.parse().map_err(serde::de::Error::custom)
213 }
214 }
215
216 #[cfg(feature = "schemars")]
220 impl schemars::JsonSchema for $name {
221 fn inline_schema() -> bool {
222 true
223 }
224
225 fn schema_name() -> std::borrow::Cow<'static, str> {
226 std::borrow::Cow::Borrowed(stringify!($name))
227 }
228
229 fn schema_id() -> std::borrow::Cow<'static, str> {
230 std::borrow::Cow::Borrowed(concat!(
231 module_path!(),
232 "::",
233 stringify!($name)
234 ))
235 }
236
237 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
238 schemars::json_schema!({
239 "type": "string",
240 "pattern": format!("^[0-9a-f]{{{}}}$", $len * 2),
241 "description": format!("{} bytes, lowercase hex", $len),
242 })
243 }
244 }
245 };
246}
247
248hex_bytes!(
249 Sha256Hex,
251 32
252);
253
254hex_bytes!(
255 SignatureHex,
257 64
258);
259
260hex_bytes!(
261 PublicKeyHex,
263 32
264);
265
266hex_bytes!(
267 Blind,
270 32
271);
272
273hex_bytes!(
274 TextSalt,
277 32
278);
279
280impl Blind {
281 pub fn random() -> Self {
283 use rand::RngCore;
284 let mut bytes = [0u8; 32];
285 rand::rngs::OsRng.fill_bytes(&mut bytes);
286 Self(bytes)
287 }
288}
289
290impl TextSalt {
291 pub fn random() -> Self {
293 Self(*Blind::random().as_bytes())
294 }
295}
296
297impl From<Signature> for SignatureHex {
298 fn from(sig: Signature) -> Self {
299 Self(sig.to_bytes())
300 }
301}
302
303impl From<&SignatureHex> for Signature {
304 fn from(sig: &SignatureHex) -> Self {
305 Signature::from_bytes(&sig.0)
306 }
307}
308
309impl From<&VerifyingKey> for PublicKeyHex {
310 fn from(key: &VerifyingKey) -> Self {
311 Self(key.to_bytes())
312 }
313}
314
315impl PublicKeyHex {
316 pub fn to_verifying_key(
318 &self,
319 ) -> Result<VerifyingKey, ed25519_dalek::SignatureError> {
320 VerifyingKey::from_bytes(&self.0)
321 }
322}
323
324pub fn canonical_json(value: &serde_json::Value) -> Vec<u8> {
337 let mut out = Vec::new();
338 write_canonical(value, &mut out);
339 out
340}
341
342fn write_canonical(value: &serde_json::Value, out: &mut Vec<u8>) {
343 use serde_json::Value;
344 match value {
345 Value::Null => out.extend_from_slice(b"null"),
346 Value::Bool(b) => {
347 out.extend_from_slice(if *b { b"true" } else { b"false" })
348 }
349 Value::Number(n) => serde_json::to_writer(&mut *out, n)
350 .expect("a number always serializes"),
351 Value::String(s) => serde_json::to_writer(&mut *out, s)
352 .expect("a string always serializes"),
353 Value::Array(items) => {
354 out.push(b'[');
355 for (i, item) in items.iter().enumerate() {
356 if i > 0 {
357 out.push(b',');
358 }
359 write_canonical(item, out);
360 }
361 out.push(b']');
362 }
363 Value::Object(map) => {
364 let mut keys: Vec<&String> = map.keys().collect();
365 keys.sort_unstable();
366 out.push(b'{');
367 for (i, key) in keys.into_iter().enumerate() {
368 if i > 0 {
369 out.push(b',');
370 }
371 serde_json::to_writer(&mut *out, key)
372 .expect("a string always serializes");
373 out.push(b':');
374 write_canonical(&map[key], out);
375 }
376 out.push(b'}');
377 }
378 }
379}
380
381pub fn non_integer_number(data: &serde_json::Value) -> Option<String> {
392 fn find(value: &serde_json::Value, path: &mut String) -> bool {
393 use serde_json::Value::{Array, Number, Object};
394 let mark = path.len();
395 match value {
396 Number(n) => return n.is_f64(),
397 Array(items) => {
398 for (i, item) in items.iter().enumerate() {
399 path.push_str(&format!("/{i}"));
400 if find(item, path) {
401 return true;
402 }
403 path.truncate(mark);
404 }
405 }
406 Object(map) => {
407 for (key, item) in map {
408 path.push('/');
409 path.push_str(&key.replace('~', "~0").replace('/', "~1"));
410 if find(item, path) {
411 return true;
412 }
413 path.truncate(mark);
414 }
415 }
416 _ => {}
417 }
418 false
419 }
420 let mut path = String::new();
421 find(data, &mut path).then_some(path)
422}
423
424pub fn data_hash(data: &serde_json::Value) -> Sha256Hex {
426 Sha256Hex(Sha256::digest(canonical_json(data)).into())
427}
428
429#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
434pub struct Envelope {
435 pub agora_governance_log: u32,
437 pub id: GovernanceLogId,
438 pub entry_type: GovernanceLogEntryType,
439 pub created_at: i64,
441 pub prev_hash: Option<Sha256Hex>,
442 pub data_hash: Sha256Hex,
443}
444
445impl Envelope {
446 pub fn new(
448 id: GovernanceLogId,
449 entry_type: GovernanceLogEntryType,
450 created_at: DateTime<Utc>,
451 prev_hash: Option<Sha256Hex>,
452 data_hash: Sha256Hex,
453 ) -> Self {
454 Self {
455 agora_governance_log: ENVELOPE_VERSION,
456 id,
457 entry_type,
458 created_at: created_at.timestamp_micros(),
459 prev_hash,
460 data_hash,
461 }
462 }
463
464 pub fn created_at(&self) -> DateTime<Utc> {
466 DateTime::from_timestamp_micros(self.created_at)
467 .expect("an Envelope only ever holds an in-range timestamp")
468 }
469
470 pub fn preimage(&self) -> Vec<u8> {
472 serde_json::to_vec(self).expect("an Envelope always serializes")
473 }
474
475 pub fn entry_hash(&self) -> Sha256Hex {
477 Sha256Hex(Sha256::digest(self.preimage()).into())
478 }
479}
480
481pub fn truncate_to_micros(t: DateTime<Utc>) -> DateTime<Utc> {
484 DateTime::from_timestamp_micros(t.timestamp_micros())
485 .expect("a timestamp that came from a DateTime is in range")
486}
487
488pub fn truncate_to_seconds(t: DateTime<Utc>) -> DateTime<Utc> {
491 DateTime::from_timestamp(t.timestamp(), 0)
492 .expect("a timestamp that came from a DateTime is in range")
493}
494
495pub fn is_retroactive(
499 created_at: DateTime<Utc>,
500 signed_at: DateTime<Utc>,
501) -> bool {
502 signed_at - created_at > RETROACTIVE_AFTER
503}
504
505#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
511#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
512#[cfg_attr(feature = "schemars", schemars(inline))]
513pub struct GovernanceAttestation {
514 pub envelope_version: u32,
516 pub chain_seq: u64,
519 pub prev_hash: Option<Sha256Hex>,
521 pub data_hash: Sha256Hex,
523 pub entry_hash: Sha256Hex,
525 pub signature: SignatureHex,
528 pub signed_at: DateTime<Utc>,
531 pub retroactive: bool,
535}
536
537#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
541#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
542#[cfg_attr(feature = "schemars", schemars(inline))]
543pub struct GovernanceChainLink {
544 pub id: GovernanceLogId,
545 pub entry_type: GovernanceLogEntryType,
546 pub created_at: DateTime<Utc>,
547 pub attestation: GovernanceAttestation,
548 #[serde(default, skip_serializing_if = "Option::is_none")]
555 pub data: Option<serde_json::Value>,
556 #[serde(
560 default,
561 skip_serializing_if = "Option::is_none",
562 deserialize_with = "read_as_written"
563 )]
564 pub texts: Option<AmendmentTexts>,
565}
566
567#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
570#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
571#[cfg_attr(feature = "schemars", schemars(inline))]
572pub struct GovernanceSigningKey {
573 pub algorithm: String,
575 pub public_key: PublicKeyHex,
576 pub envelope_version: u32,
578}
579
580impl GovernanceSigningKey {
581 pub fn new(key: &VerifyingKey) -> Self {
583 Self {
584 algorithm: "ed25519".to_string(),
585 public_key: key.into(),
586 envelope_version: ENVELOPE_VERSION,
587 }
588 }
589}
590
591pub const AMENDMENT_VERSION: u32 = 2;
599
600#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
602pub enum AmendmentError {
603 #[error("agora_governance_amendment is {0}, not 1 or {AMENDMENT_VERSION}")]
604 UnsupportedVersion(u32),
605 #[error(
606 "a version 1 amendment carries its texts and a version \
607 {AMENDMENT_VERSION} one commits to them; this does neither \
608 consistently"
609 )]
610 TextShape,
611 #[error("`{0}` beside the entry is not the text the entry committed to")]
612 TextMismatch(&'static str),
613 #[error("texts beside an entry that commits to none")]
614 UncommittedText,
615 #[error("kind `redaction` requires a `redaction`")]
616 MissingRedaction,
617 #[error("`redaction` is only valid on kind `redaction`")]
618 UnexpectedRedaction,
619 #[error("kind `revision` requires a `revision`")]
620 MissingRevision,
621 #[error("`revision` is only valid on kind `revision`")]
622 UnexpectedRevision,
623 #[error("the `revision` is malformed: {0}")]
624 RevisionShape(ReviseError),
625 #[error("amendment target {0} is not an entry of this chain")]
626 UnknownTarget(GovernanceLogId),
627 #[error("amendment target {0} is not an earlier entry")]
628 ForwardReference(GovernanceLogId),
629 #[error("target_entry_hash is not {0}'s entry_hash")]
630 WrongTargetHash(GovernanceLogId),
631}
632
633#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
639#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
640#[cfg_attr(feature = "schemars", schemars(inline))]
641pub struct Amendment {
642 pub agora_governance_amendment: u32,
644 pub target: GovernanceLogId,
645 pub target_entry_hash: Sha256Hex,
647 pub kind: AmendmentKind,
648 #[serde(default)]
651 pub authority: Option<GovernanceLogId>,
652 pub basis: AmendmentText,
656 pub note: AmendmentText,
658 #[serde(default, skip_serializing_if = "Option::is_none")]
660 pub rationale: Option<AmendmentText>,
661 #[serde(default, skip_serializing_if = "Option::is_none")]
663 pub redaction: Option<Redaction>,
664 #[serde(default, skip_serializing_if = "Option::is_none")]
666 pub revision: Option<Revision>,
667}
668
669#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
671#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
672#[cfg_attr(feature = "schemars", schemars(inline))]
673pub struct Redaction {
674 pub fields: Vec<String>,
677 pub resulting_data_hash: Sha256Hex,
680 #[serde(default, skip_serializing_if = "Option::is_none")]
684 pub resulting_latest_hash: Option<Sha256Hex>,
685}
686
687#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
699pub struct Revision {
700 pub patch: json_patch::Patch,
702 #[serde(default, skip_serializing_if = "Vec::is_empty")]
706 pub duplicates: Vec<(String, String)>,
707 pub resulting_data_hash: Sha256Hex,
709}
710
711#[derive(Debug, Clone, PartialEq, Eq, Default)]
714pub struct Edit {
715 pub patch: json_patch::Patch,
716 pub duplicates: Vec<(String, String)>,
718}
719
720impl From<json_patch::Patch> for Edit {
721 fn from(patch: json_patch::Patch) -> Self {
722 Self {
723 patch,
724 duplicates: Vec::new(),
725 }
726 }
727}
728
729#[cfg(feature = "schemars")]
732impl schemars::JsonSchema for Revision {
733 fn inline_schema() -> bool {
734 true
735 }
736
737 fn schema_name() -> std::borrow::Cow<'static, str> {
738 "Revision".into()
739 }
740
741 fn json_schema(
742 generator: &mut schemars::SchemaGenerator,
743 ) -> schemars::Schema {
744 let hash = generator.subschema_for::<Sha256Hex>();
745 schemars::json_schema!({
746 "type": "object",
747 "properties": {
748 "patch": {
749 "description": "RFC 6902 JSON Patch from the previous version to this one",
750 "type": "array",
751 "items": {
752 "type": "object",
753 "properties": {
754 "op": {
755 "type": "string",
756 "enum": ["add", "remove", "replace", "move", "copy", "test"]
757 },
758 "path": {"type": "string"},
759 "from": {"type": "string"},
760 "value": true
761 },
762 "required": ["op", "path"]
763 }
764 },
765 "duplicates": {
766 "description": "(removed path, the path it duplicated) for each duplicate removed",
767 "type": "array",
768 "items": {
769 "type": "array",
770 "items": {"type": "string"},
771 "minItems": 2,
772 "maxItems": 2
773 }
774 },
775 "resulting_data_hash": hash
776 },
777 "required": ["patch", "resulting_data_hash"]
778 })
779 }
780}
781
782impl Revision {
783 pub fn validate(&self) -> Result<(), ReviseError> {
786 if self.patch.is_empty() {
787 return Err(ReviseError::EmptyPatch);
788 }
789 for (path, _) in &self.duplicates {
790 let removed = self.patch.iter().any(|op| {
791 matches!(op, json_patch::PatchOperation::Remove(r) if r.path.as_str() == path)
792 });
793 if !removed {
794 return Err(ReviseError::NotRemoved(path.clone()));
795 }
796 }
797 Ok(())
798 }
799
800 pub fn remove_duplicates(
804 data: &serde_json::Value,
805 pairs: &[(&str, &str)],
806 ) -> Result<Edit, ReviseError> {
807 let mut ops = Vec::with_capacity(pairs.len());
808 for (path, _) in pairs {
809 let path = json_patch::jsonptr::PointerBuf::parse(*path)
810 .map_err(|e| ReviseError::Patch(e.to_string()))?;
811 ops.push(json_patch::PatchOperation::Remove(
812 json_patch::RemoveOperation { path },
813 ));
814 }
815 let edit = Edit {
816 patch: json_patch::Patch(ops),
817 duplicates: pairs
818 .iter()
819 .map(|(p, s)| (p.to_string(), s.to_string()))
820 .collect(),
821 };
822 check_duplicates(data, &edit)?;
823 Ok(edit)
824 }
825}
826
827fn check_duplicates(
830 data: &serde_json::Value,
831 edit: &Edit,
832) -> Result<(), ReviseError> {
833 for (path, same_as) in &edit.duplicates {
834 let resolve = |pointer: &str| {
835 data.pointer(pointer)
836 .ok_or_else(|| ReviseError::Unresolved(pointer.to_string()))
837 };
838 let (removed, kept) = (resolve(path)?, resolve(same_as)?);
839 if canonical_json(removed) != canonical_json(kept) {
840 return Err(ReviseError::NotIdentical {
841 path: path.clone(),
842 same_as: same_as.clone(),
843 });
844 }
845 }
846 let revised = apply_patch(data, &edit.patch)?;
847 for (path, same_as) in &edit.duplicates {
848 let kept = data.pointer(same_as);
849 let removes = |op: &json_patch::PatchOperation| matches!(op, json_patch::PatchOperation::Remove(r) if r.path.as_str() == path);
850 if !edit.patch.iter().any(removes) {
851 return Err(ReviseError::NotRemoved(path.clone()));
852 }
853 let survives = revised.pointer(same_as).is_some_and(|v| {
854 kept.is_some_and(|k| canonical_json(v) == canonical_json(k))
855 });
856 if !survives {
857 return Err(ReviseError::SourceRemoved {
858 path: path.clone(),
859 same_as: same_as.clone(),
860 });
861 }
862 }
863 Ok(())
864}
865
866#[derive(Debug, Clone, PartialEq, Eq)]
869pub struct AmendmentDraft {
870 pub amendment: Amendment,
871 pub texts: AmendmentTexts,
872}
873
874impl AmendmentDraft {
875 pub fn new(
882 target: GovernanceLogId,
883 target_entry_hash: Sha256Hex,
884 kind: AmendmentKind,
885 basis: impl Into<CommittedText>,
886 note: impl Into<CommittedText>,
887 ) -> Result<Self, AmendmentError> {
888 match kind {
889 AmendmentKind::Redaction => {
890 return Err(AmendmentError::MissingRedaction);
891 }
892 AmendmentKind::Revision => {
893 return Err(AmendmentError::MissingRevision);
894 }
895 _ => {}
896 }
897 let (basis, note) = (basis.into(), note.into());
898 Ok(Self {
899 amendment: Amendment {
900 agora_governance_amendment: AMENDMENT_VERSION,
901 target,
902 target_entry_hash,
903 kind,
904 authority: None,
905 basis: AmendmentText::Committed(basis.commitment()),
906 note: AmendmentText::Committed(note.commitment()),
907 rationale: None,
908 redaction: None,
909 revision: None,
910 },
911 texts: AmendmentTexts {
912 basis: Some(basis),
913 note: Some(note),
914 rationale: None,
915 },
916 })
917 }
918
919 #[allow(clippy::too_many_arguments)]
937 pub fn redaction(
938 amendment_id: &GovernanceLogId,
939 target: GovernanceLogId,
940 target_entry_hash: Sha256Hex,
941 basis: impl Into<CommittedText>,
942 note: impl Into<CommittedText>,
943 mut fields: Vec<String>,
944 data: &serde_json::Value,
945 blind: Blind,
946 revisions: &[&Revision],
947 ) -> Result<(Self, serde_json::Value), RedactError> {
948 for extra in duplicates_of(&fields, revisions) {
949 let covered = fields.iter().any(|f| {
950 extra.strip_prefix(f.as_str()).is_some_and(|rest| {
951 rest.is_empty() || rest.starts_with('/')
952 })
953 });
954 if !covered && data.pointer(&extra).is_some() {
955 fields.push(extra);
956 }
957 }
958 let redacted = redact_data(data, &fields, amendment_id, blind)?;
959 let resulting_latest_hash = match revisions {
960 [] => None,
961 _ => Some(data_hash(
962 &latest(&redacted, revisions.iter().copied())
963 .map_err(|e| RedactError::Rebase(e.to_string()))?,
964 )),
965 };
966 let (basis, note) = (basis.into(), note.into());
967 Ok((
968 Self {
969 amendment: Amendment {
970 agora_governance_amendment: AMENDMENT_VERSION,
971 target,
972 target_entry_hash,
973 kind: AmendmentKind::Redaction,
974 authority: None,
975 basis: AmendmentText::Committed(basis.commitment()),
976 note: AmendmentText::Committed(note.commitment()),
977 rationale: None,
978 redaction: Some(Redaction {
979 fields,
980 resulting_data_hash: data_hash(&redacted),
981 resulting_latest_hash,
982 }),
983 revision: None,
984 },
985 texts: AmendmentTexts {
986 basis: Some(basis),
987 note: Some(note),
988 rationale: None,
989 },
990 },
991 redacted,
992 ))
993 }
994
995 pub fn revision(
1006 target: GovernanceLogId,
1007 target_type: GovernanceLogEntryType,
1008 target_entry_hash: Sha256Hex,
1009 basis: impl Into<CommittedText>,
1010 note: impl Into<CommittedText>,
1011 latest: &serde_json::Value,
1012 edit: impl Into<Edit>,
1013 ) -> Result<(Self, serde_json::Value), ReviseError> {
1014 let Edit {
1015 mut patch,
1016 duplicates,
1017 } = edit.into();
1018 if !is_revisable(target_type) {
1019 return Err(ReviseError::NotRevisable(target_type));
1020 }
1021 if patch.is_empty() {
1022 return Err(ReviseError::EmptyPatch);
1023 }
1024 let blind_pointer = format!("/{BLIND_KEY}");
1025 for op in patch.iter() {
1026 let from = match op {
1027 json_patch::PatchOperation::Move(m) => Some(m.from.as_str()),
1028 json_patch::PatchOperation::Copy(c) => Some(c.from.as_str()),
1029 _ => None,
1030 };
1031 for pointer in
1032 [Some(op.path().as_str()), from].into_iter().flatten()
1033 {
1034 if overlaps(pointer, &blind_pointer) {
1035 return Err(ReviseError::BlindPointer(pointer.to_string()));
1036 }
1037 }
1038 }
1039 let adds = patch.iter().any(|op| {
1040 matches!(
1041 op,
1042 json_patch::PatchOperation::Add(_)
1043 | json_patch::PatchOperation::Replace(_)
1044 )
1045 });
1046 if adds && latest.is_object() && latest.get(BLIND_KEY).is_none() {
1047 patch.0.push(json_patch::PatchOperation::Add(
1048 json_patch::AddOperation {
1049 path: json_patch::jsonptr::PointerBuf::from_tokens([
1050 BLIND_KEY,
1051 ]),
1052 value: Blind::random().to_hex().into(),
1053 },
1054 ));
1055 }
1056 let edit = Edit { patch, duplicates };
1057 check_duplicates(latest, &edit)?;
1058 let Edit { patch, duplicates } = edit;
1059 let revised = apply_patch(latest, &patch)?;
1060 let (basis, note) = (basis.into(), note.into());
1061 Ok((
1062 Self {
1063 amendment: Amendment {
1064 agora_governance_amendment: AMENDMENT_VERSION,
1065 target,
1066 target_entry_hash,
1067 kind: AmendmentKind::Revision,
1068 authority: None,
1069 basis: AmendmentText::Committed(basis.commitment()),
1070 note: AmendmentText::Committed(note.commitment()),
1071 rationale: None,
1072 redaction: None,
1073 revision: Some(Revision {
1074 patch,
1075 duplicates,
1076 resulting_data_hash: data_hash(&revised),
1077 }),
1078 },
1079 texts: AmendmentTexts {
1080 basis: Some(basis),
1081 note: Some(note),
1082 rationale: None,
1083 },
1084 },
1085 revised,
1086 ))
1087 }
1088
1089 pub fn with_authority(mut self, authority: GovernanceLogId) -> Self {
1091 self.amendment.authority = Some(authority);
1092 self
1093 }
1094
1095 pub fn with_rationale(
1097 mut self,
1098 rationale: impl Into<CommittedText>,
1099 ) -> Self {
1100 let rationale = rationale.into();
1101 self.amendment.rationale =
1102 Some(AmendmentText::Committed(rationale.commitment()));
1103 self.texts.rationale = Some(rationale);
1104 self
1105 }
1106}
1107
1108impl Amendment {
1109 pub fn validate(&self) -> Result<(), AmendmentError> {
1113 let plain = match self.agora_governance_amendment {
1114 1 => true,
1115 AMENDMENT_VERSION => false,
1116 other => return Err(AmendmentError::UnsupportedVersion(other)),
1117 };
1118 let texts =
1119 [Some(&self.basis), Some(&self.note), self.rationale.as_ref()];
1120 if texts.into_iter().flatten().any(|t| t.is_plain() != plain) {
1121 return Err(AmendmentError::TextShape);
1122 }
1123 match (self.kind, &self.redaction) {
1124 (AmendmentKind::Redaction, None) => {
1125 return Err(AmendmentError::MissingRedaction);
1126 }
1127 (k, Some(_)) if k != AmendmentKind::Redaction => {
1128 return Err(AmendmentError::UnexpectedRedaction);
1129 }
1130 _ => {}
1131 }
1132 match (self.kind, &self.revision) {
1133 (AmendmentKind::Revision, None) => {
1134 Err(AmendmentError::MissingRevision)
1135 }
1136 (AmendmentKind::Revision, Some(r)) => {
1137 r.validate().map_err(AmendmentError::RevisionShape)
1138 }
1139 (_, Some(_)) => Err(AmendmentError::UnexpectedRevision),
1140 _ => Ok(()),
1141 }
1142 }
1143
1144 pub fn text_status(
1151 &self,
1152 beside: Option<&AmendmentTexts>,
1153 ) -> Result<Option<AmendmentTextStatus>, AmendmentError> {
1154 let empty = AmendmentTexts::default();
1155 let beside = beside.unwrap_or(&empty);
1156 let (Some(basis), Some(note)) = (
1157 self.basis.status(beside.basis.as_ref()),
1158 self.note.status(beside.note.as_ref()),
1159 ) else {
1160 return if beside.is_empty() {
1161 Ok(None)
1162 } else {
1163 Err(AmendmentError::UncommittedText)
1164 };
1165 };
1166 let rationale = match (&self.rationale, &beside.rationale) {
1167 (None, Some(_)) => return Err(AmendmentError::UncommittedText),
1168 (None, None) => None,
1169 (Some(text), beside) => text.status(beside.as_ref()),
1170 };
1171 for (name, status) in [
1172 ("basis", Some(basis)),
1173 ("note", Some(note)),
1174 ("rationale", rationale),
1175 ] {
1176 if status == Some(TextStatus::Mismatch) {
1177 return Err(AmendmentError::TextMismatch(name));
1178 }
1179 }
1180 Ok(Some(AmendmentTextStatus {
1181 basis,
1182 note,
1183 rationale,
1184 }))
1185 }
1186}
1187
1188pub fn kind_standing(kind: AmendmentKind) -> Option<Standing> {
1191 match kind {
1192 AmendmentKind::NonPrecedential => Some(Standing::NonPrecedential),
1193 AmendmentKind::Overruled => Some(Standing::Overruled),
1194 AmendmentKind::Superseded => Some(Standing::Superseded),
1195 AmendmentKind::Reinstated => Some(Standing::InForce),
1196 AmendmentKind::Correction
1197 | AmendmentKind::Redaction
1198 | AmendmentKind::Reattested
1199 | AmendmentKind::Revision => None,
1200 }
1201}
1202
1203pub fn standing(kinds: impl IntoIterator<Item = AmendmentKind>) -> Standing {
1206 kinds
1207 .into_iter()
1208 .filter_map(kind_standing)
1209 .last()
1210 .unwrap_or_default()
1211}
1212
1213pub const BLIND_KEY: &str = "_blind";
1216
1217pub fn is_redactable(entry_type: GovernanceLogEntryType) -> bool {
1221 !matches!(
1222 entry_type,
1223 GovernanceLogEntryType::Amendment | GovernanceLogEntryType::KeyRotation
1224 )
1225}
1226
1227#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1229pub enum BlindError {
1230 #[error("a redactable entry's data must be a JSON object")]
1231 NotAnObject,
1232 #[error(
1233 "data already has a {BLIND_KEY:?} key; the writer supplies it, not the caller"
1234 )]
1235 AlreadyBlinded,
1236 #[error(
1237 "{0:?} is a number that is not a 64-bit integer; governance data \
1238 never contains one (put a fraction in a string)"
1239 )]
1240 NonIntegerNumber(String),
1241}
1242
1243pub fn blind_data(
1261 data: &serde_json::Value,
1262 blind: Blind,
1263) -> Result<serde_json::Value, BlindError> {
1264 if let Some(pointer) = non_integer_number(data) {
1265 return Err(BlindError::NonIntegerNumber(pointer));
1266 }
1267 let mut out = data.clone();
1268 let object = out.as_object_mut().ok_or(BlindError::NotAnObject)?;
1269 if object.contains_key(BLIND_KEY) {
1270 return Err(BlindError::AlreadyBlinded);
1271 }
1272 object.insert(BLIND_KEY.to_string(), blind.to_hex().into());
1273 Ok(out)
1274}
1275
1276#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1278pub enum RedactError {
1279 #[error("pointer {0:?} does not resolve in the entry's data")]
1280 Unresolved(String),
1281 #[error("the empty pointer would redact the whole entry")]
1282 WholeEntry,
1283 #[error("a redaction names at least one pointer")]
1284 NoFields,
1285 #[error("{0:?} is the entry's blind; every redaction replaces it already")]
1286 BlindPointer(String),
1287 #[error("a revision no longer applies to the redacted data: {0}")]
1288 Rebase(String),
1289 #[error(
1290 "{0:?} is a number that is not a 64-bit integer; governance data \
1291 never contains one"
1292 )]
1293 NonIntegerNumber(String),
1294}
1295
1296pub fn redaction_marker(amendment_id: &GovernanceLogId) -> String {
1298 format!("[redacted by {amendment_id}]")
1299}
1300
1301pub fn redact_data(
1317 data: &serde_json::Value,
1318 fields: &[String],
1319 amendment_id: &GovernanceLogId,
1320 blind: Blind,
1321) -> Result<serde_json::Value, RedactError> {
1322 if fields.is_empty() {
1323 return Err(RedactError::NoFields);
1324 }
1325 if let Some(pointer) = non_integer_number(data) {
1326 return Err(RedactError::NonIntegerNumber(pointer));
1327 }
1328 let blind_pointer = format!("/{BLIND_KEY}");
1329 let marker = serde_json::Value::String(redaction_marker(amendment_id));
1330 let mut out = data.clone();
1331 for pointer in fields {
1332 if pointer.is_empty() {
1333 return Err(RedactError::WholeEntry);
1334 }
1335 if *pointer == blind_pointer {
1336 return Err(RedactError::BlindPointer(pointer.clone()));
1337 }
1338 let slot = out
1339 .pointer_mut(pointer)
1340 .ok_or_else(|| RedactError::Unresolved(pointer.clone()))?;
1341 *slot = marker.clone();
1342 }
1343 if let Some(object) = out.as_object_mut() {
1346 object.insert(BLIND_KEY.to_string(), blind.to_hex().into());
1347 }
1348 Ok(out)
1349}
1350
1351pub fn is_revisable(entry_type: GovernanceLogEntryType) -> bool {
1355 is_redactable(entry_type)
1356 && entry_type != GovernanceLogEntryType::StewardRecord
1357}
1358
1359#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1361pub enum ReviseError {
1362 #[error("a revision's patch has at least one op")]
1363 EmptyPatch,
1364 #[error("the patch does not apply: {0}")]
1365 Patch(String),
1366 #[error("{0:?} is the entry's blind, which a revision never touches")]
1367 BlindPointer(String),
1368 #[error("pointer {0:?} does not resolve")]
1369 Unresolved(String),
1370 #[error("{path:?} is not byte-identical to {same_as:?}")]
1371 NotIdentical { path: String, same_as: String },
1372 #[error("{same_as:?}, which {path:?} duplicates, has to survive the patch")]
1373 SourceRemoved { path: String, same_as: String },
1374 #[error("{0:?} is listed as a duplicate the patch does not remove")]
1375 NotRemoved(String),
1376 #[error("{0} entries are never revised")]
1377 NotRevisable(GovernanceLogEntryType),
1378 #[error(
1379 "{0:?} is a number that is not a 64-bit integer; governance data \
1380 never contains one"
1381 )]
1382 NonIntegerNumber(String),
1383}
1384
1385fn overlaps(a: &str, b: &str) -> bool {
1387 let within = |inner: &str, outer: &str| {
1388 inner
1389 .strip_prefix(outer)
1390 .is_some_and(|rest| rest.is_empty() || rest.starts_with('/'))
1391 };
1392 within(a, b) || within(b, a)
1393}
1394
1395pub fn apply_patch(
1397 data: &serde_json::Value,
1398 patch: &json_patch::Patch,
1399) -> Result<serde_json::Value, ReviseError> {
1400 let mut out = data.clone();
1401 json_patch::patch(&mut out, patch)
1402 .map_err(|e| ReviseError::Patch(e.to_string()))?;
1403 if let Some(pointer) = non_integer_number(&out) {
1404 return Err(ReviseError::NonIntegerNumber(pointer));
1405 }
1406 Ok(out)
1407}
1408
1409pub fn latest<'a>(
1414 data: &serde_json::Value,
1415 revisions: impl IntoIterator<Item = &'a Revision>,
1416) -> Result<serde_json::Value, ReviseError> {
1417 let mut current = data.clone();
1418 for revision in revisions {
1419 current = apply_patch(¤t, &revision.patch)?;
1420 }
1421 Ok(current)
1422}
1423
1424fn duplicates_of(fields: &[String], revisions: &[&Revision]) -> Vec<String> {
1428 let mut extra = Vec::new();
1429 for (removed, same_as) in revisions.iter().flat_map(|r| &r.duplicates) {
1430 for field in fields {
1431 if let Some(rest) = field.strip_prefix(same_as.as_str())
1432 && (rest.is_empty() || rest.starts_with('/'))
1433 {
1434 extra.push(format!("{removed}{rest}"));
1436 } else if overlaps(field, same_as) {
1437 extra.push(removed.clone());
1439 }
1440 }
1441 }
1442 extra
1443}
1444
1445#[derive(Debug, Clone, PartialEq, Eq, Default)]
1448pub struct RevisionHistory {
1449 pub revisions: Vec<(GovernanceLogId, Revision)>,
1451 pub rebased: usize,
1454 pub rebased_hash: Option<Sha256Hex>,
1456}
1457
1458#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1460#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1461#[cfg_attr(feature = "schemars", schemars(inline))]
1462pub struct AmendmentNotice {
1463 pub id: GovernanceLogId,
1464 pub kind: AmendmentKind,
1465 #[serde(default)]
1466 pub authority: Option<GovernanceLogId>,
1467 pub basis: String,
1468 pub note: String,
1469 #[serde(default, skip_serializing_if = "Option::is_none")]
1471 pub rationale: Option<String>,
1472 pub created_at: DateTime<Utc>,
1473}
1474
1475impl AmendmentNotice {
1476 pub fn new(
1479 id: GovernanceLogId,
1480 created_at: DateTime<Utc>,
1481 amendment: &Amendment,
1482 beside: Option<&AmendmentTexts>,
1483 ) -> Self {
1484 let beside = beside.cloned().unwrap_or_default();
1485 Self {
1486 id,
1487 kind: amendment.kind,
1488 authority: amendment.authority.clone(),
1489 basis: amendment.basis.resolve(beside.basis.as_ref()).into(),
1490 note: amendment.note.resolve(beside.note.as_ref()).into(),
1491 rationale: amendment
1492 .rationale
1493 .as_ref()
1494 .map(|r| r.resolve(beside.rationale.as_ref()).into()),
1495 created_at,
1496 }
1497 }
1498}
1499
1500#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1506#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1507#[cfg_attr(feature = "schemars", schemars(inline))]
1508pub struct GovernanceCitation {
1509 pub id: GovernanceLogId,
1510 pub entry_type: GovernanceLogEntryType,
1511 pub title: String,
1512 pub relation: CitationRelation,
1513 pub created_at: DateTime<Utc>,
1514}
1515
1516pub fn citations_in(
1522 entry_type: GovernanceLogEntryType,
1523 data: &serde_json::Value,
1524) -> Vec<(GovernanceLogId, CitationRelation)> {
1525 #[derive(Deserialize)]
1526 struct Concerns {
1527 #[serde(default)]
1528 concerns: Vec<GovernanceLogId>,
1529 }
1530 #[derive(Deserialize)]
1531 struct Authority {
1532 #[serde(default)]
1533 authority: Option<GovernanceLogId>,
1534 }
1535 #[derive(Deserialize)]
1536 struct Overrules {
1537 #[serde(default)]
1538 overrules: Vec<GovernanceLogId>,
1539 }
1540 fn each(
1541 ids: impl IntoIterator<Item = GovernanceLogId>,
1542 relation: CitationRelation,
1543 ) -> Vec<(GovernanceLogId, CitationRelation)> {
1544 ids.into_iter().map(|id| (id, relation)).collect()
1545 }
1546 match entry_type {
1547 GovernanceLogEntryType::StewardRecord => {
1548 serde_json::from_value::<Concerns>(data.clone())
1549 .map(|c| each(c.concerns, CitationRelation::Concerns))
1550 }
1551 GovernanceLogEntryType::Amendment => {
1552 serde_json::from_value::<Authority>(data.clone())
1553 .map(|a| each(a.authority, CitationRelation::Authority))
1554 }
1555 GovernanceLogEntryType::CouncilDecision => {
1556 serde_json::from_value::<Overrules>(data.clone())
1557 .map(|o| each(o.overrules, CitationRelation::Overrules))
1558 }
1559 _ => Ok(Vec::new()),
1560 }
1561 .unwrap_or_default()
1562}
1563
1564pub const KEY_ROTATION_VERSION: u32 = 2;
1570
1571pub const PUBLISHED_KEYS: &[&str] =
1578 &["ebb3091dd328f1463362c171121921b2fe14628e3fc4c145deaccefb85c0e78a"];
1579
1580#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1582#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1583#[cfg_attr(feature = "schemars", schemars(inline))]
1584#[serde(rename_all = "snake_case")]
1585pub enum RotationReason {
1586 Routine,
1588 Compromise,
1591}
1592
1593#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1595#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1596#[cfg_attr(feature = "schemars", schemars(inline))]
1597#[serde(deny_unknown_fields)]
1598pub struct TrustedHead {
1599 pub id: GovernanceLogId,
1600 pub chain_seq: u64,
1601 pub entry_hash: Sha256Hex,
1602}
1603
1604#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1606pub enum RotationError {
1607 #[error("agora_governance_key_rotation is {0}, not {KEY_ROTATION_VERSION}")]
1608 UnsupportedVersion(u32),
1609 #[error("new_key is not a valid Ed25519 public key")]
1610 BadNewKey,
1611 #[error(
1612 "the proof of possession does not verify for this rotation at this position"
1613 )]
1614 BadProof,
1615 #[error("a compromise certificate must name last_trusted")]
1616 MissingLastTrusted,
1617 #[error("certificate: {0}")]
1618 Certificate(#[from] CertificateError),
1619 #[error("outgoing_certificate: {0}")]
1620 OutgoingCertificate(CertificateError),
1621 #[error(
1622 "the chain's first rotation must carry the genesis key's \
1623 outgoing_certificate"
1624 )]
1625 MissingGenesisCertificate,
1626 #[error(
1627 "outgoing_certificate belongs on the chain's first rotation and \
1628 nowhere else"
1629 )]
1630 UnexpectedOutgoingCertificate,
1631 #[error("old_key is not the key that was in force")]
1632 WrongOldKey,
1633 #[error("last_trusted does not name an earlier entry of this chain")]
1634 UnknownLastTrusted,
1635 #[error("new_key has already held this chain; a key is never brought back")]
1636 ReusedKey,
1637 #[error("last_trusted names an entry an earlier compromise repudiated")]
1638 RepudiatedLastTrusted,
1639}
1640
1641#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1652#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1653#[cfg_attr(feature = "schemars", schemars(inline))]
1654#[serde(deny_unknown_fields)]
1655pub struct KeyRotation {
1656 pub agora_governance_key_rotation: u32,
1658 pub reason: RotationReason,
1659 pub old_key: PublicKeyHex,
1660 pub new_key: PublicKeyHex,
1661 pub proof: SignatureHex,
1665 pub proof_signed_at: i64,
1667 pub certificate: KeyCertificate,
1671 #[serde(default, skip_serializing_if = "Option::is_none")]
1675 pub outgoing_certificate: Option<KeyCertificate>,
1676}
1677
1678#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1685pub struct RotationStatement {
1686 pub agora_governance_key_rotation: u32,
1688 pub reason: RotationReason,
1689 pub old_key: PublicKeyHex,
1690 pub new_key: PublicKeyHex,
1691 pub prev_hash: Option<Sha256Hex>,
1692}
1693
1694impl RotationStatement {
1695 pub fn new(
1697 reason: RotationReason,
1698 old_key: PublicKeyHex,
1699 new_key: PublicKeyHex,
1700 prev_hash: Option<Sha256Hex>,
1701 ) -> Self {
1702 Self {
1703 agora_governance_key_rotation: KEY_ROTATION_VERSION,
1704 reason,
1705 old_key,
1706 new_key,
1707 prev_hash,
1708 }
1709 }
1710
1711 pub fn preimage(&self) -> Vec<u8> {
1713 serde_json::to_vec(self).expect("a RotationStatement always serializes")
1714 }
1715
1716 pub fn hash(&self) -> Sha256Hex {
1718 Sha256Hex(Sha256::digest(self.preimage()).into())
1719 }
1720}
1721
1722impl KeyRotation {
1723 pub fn routine(
1729 old_key: PublicKeyHex,
1730 new_signing_key: &SigningKey,
1731 prev_hash: Option<Sha256Hex>,
1732 now: DateTime<Utc>,
1733 certificate: KeyCertificate,
1734 ) -> Self {
1735 Self::build(
1736 RotationReason::Routine,
1737 old_key,
1738 new_signing_key,
1739 prev_hash,
1740 now,
1741 certificate,
1742 )
1743 }
1744
1745 pub fn compromise(
1753 old_key: PublicKeyHex,
1754 new_signing_key: &SigningKey,
1755 prev_hash: Option<Sha256Hex>,
1756 now: DateTime<Utc>,
1757 certificate: KeyCertificate,
1758 ) -> Self {
1759 Self::build(
1760 RotationReason::Compromise,
1761 old_key,
1762 new_signing_key,
1763 prev_hash,
1764 now,
1765 certificate,
1766 )
1767 }
1768
1769 fn build(
1770 reason: RotationReason,
1771 old_key: PublicKeyHex,
1772 new_signing_key: &SigningKey,
1773 prev_hash: Option<Sha256Hex>,
1774 now: DateTime<Utc>,
1775 certificate: KeyCertificate,
1776 ) -> Self {
1777 let new_key = PublicKeyHex::from(&new_signing_key.verifying_key());
1778 let proof_signed_at = truncate_to_seconds(now).timestamp();
1779 let statement =
1780 RotationStatement::new(reason, old_key, new_key, prev_hash);
1781 let proof = crypto::sign(
1782 new_signing_key,
1783 statement.hash().as_bytes(),
1784 proof_signed_at,
1785 );
1786 Self {
1787 agora_governance_key_rotation: KEY_ROTATION_VERSION,
1788 reason,
1789 old_key,
1790 new_key,
1791 proof: proof.into(),
1792 proof_signed_at,
1793 certificate,
1794 outgoing_certificate: None,
1795 }
1796 }
1797
1798 pub fn with_outgoing(mut self, certificate: KeyCertificate) -> Self {
1800 self.outgoing_certificate = Some(certificate);
1801 self
1802 }
1803
1804 pub fn statement(&self, prev_hash: Option<Sha256Hex>) -> RotationStatement {
1806 RotationStatement::new(
1807 self.reason,
1808 self.old_key,
1809 self.new_key,
1810 prev_hash,
1811 )
1812 }
1813
1814 pub fn last_trusted(&self) -> Option<&TrustedHead> {
1817 self.certificate.statement.last_trusted.as_ref()
1818 }
1819
1820 pub fn expected_statement(
1826 &self,
1827 seq: u64,
1828 prev_hash: Option<Sha256Hex>,
1829 ) -> Result<KeyCertStatement, RotationError> {
1830 match self.reason {
1831 RotationReason::Routine => {
1832 Ok(KeyCertStatement::routine(self.new_key, seq, prev_hash))
1833 }
1834 RotationReason::Compromise => Ok(KeyCertStatement::compromise(
1835 self.new_key,
1836 seq,
1837 prev_hash,
1838 self.last_trusted()
1839 .cloned()
1840 .ok_or(RotationError::MissingLastTrusted)?,
1841 )),
1842 }
1843 }
1844
1845 pub fn verify_proof(
1848 &self,
1849 prev_hash: Option<Sha256Hex>,
1850 ) -> Result<(), RotationError> {
1851 if self.agora_governance_key_rotation != KEY_ROTATION_VERSION {
1852 return Err(RotationError::UnsupportedVersion(
1853 self.agora_governance_key_rotation,
1854 ));
1855 }
1856 let new_key = self
1857 .new_key
1858 .to_verifying_key()
1859 .map_err(|_| RotationError::BadNewKey)?;
1860 crypto::verify(
1861 &new_key,
1862 self.statement(prev_hash).hash().as_bytes(),
1863 self.proof_signed_at,
1864 &Signature::from(&self.proof),
1865 )
1866 .then_some(())
1867 .ok_or(RotationError::BadProof)
1868 }
1869
1870 pub fn verify_certified(
1874 &self,
1875 seq: u64,
1876 prev_hash: Option<Sha256Hex>,
1877 roots: &RootSet,
1878 ) -> Result<(), RotationError> {
1879 self.verify_proof(prev_hash)?;
1880 let expected = self.expected_statement(seq, prev_hash)?;
1881 Ok(self.certificate.verify_for(&expected, roots)?)
1882 }
1883}
1884
1885#[derive(Debug, Clone, Default, PartialEq, Eq)]
1892pub struct KeyAnchor {
1893 keys: HashSet<PublicKeyHex>,
1894}
1895
1896impl KeyAnchor {
1897 pub fn published() -> Self {
1899 PUBLISHED_KEYS
1900 .iter()
1901 .map(|k| {
1902 k.parse()
1903 .expect("PUBLISHED_KEYS are valid 32-byte hex keys")
1904 })
1905 .collect()
1906 }
1907
1908 pub fn pinned(key: PublicKeyHex) -> Self {
1910 std::iter::once(key).collect()
1911 }
1912
1913 pub fn with(mut self, key: PublicKeyHex) -> Self {
1915 self.keys.insert(key);
1916 self
1917 }
1918
1919 pub fn contains(&self, key: &PublicKeyHex) -> bool {
1920 self.keys.contains(key)
1921 }
1922
1923 pub fn is_empty(&self) -> bool {
1924 self.keys.is_empty()
1925 }
1926
1927 pub fn keys(&self) -> impl Iterator<Item = &PublicKeyHex> {
1929 self.keys.iter()
1930 }
1931}
1932
1933impl FromIterator<PublicKeyHex> for KeyAnchor {
1934 fn from_iter<I: IntoIterator<Item = PublicKeyHex>>(iter: I) -> Self {
1935 Self {
1936 keys: iter.into_iter().collect(),
1937 }
1938 }
1939}
1940
1941#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1944#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1945#[cfg_attr(feature = "schemars", schemars(inline))]
1946pub struct GovernanceKeyRecord {
1947 pub public_key: PublicKeyHex,
1948 pub from_seq: u64,
1950 #[serde(default)]
1954 pub through_seq: Option<u64>,
1955 pub status: KeyStatus,
1956 #[serde(default)]
1959 pub introduced_by: Option<GovernanceLogId>,
1960 #[serde(default)]
1962 pub retired_by: Option<GovernanceLogId>,
1963 #[serde(default)]
1967 pub certified: bool,
1968}
1969
1970#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1975#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1976#[cfg_attr(feature = "schemars", schemars(inline))]
1977pub struct GovernanceSigningKeys {
1978 pub keys: Vec<GovernanceKeyRecord>,
1980}
1981
1982#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1984#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1985#[cfg_attr(feature = "schemars", schemars(inline))]
1986pub struct EntryVerdict {
1987 pub id: GovernanceLogId,
1988 pub chain_seq: u64,
1989 pub signature_valid: bool,
1992 pub link_valid: bool,
1995 #[serde(default)]
2002 pub content_matches: Option<bool>,
2003 pub retroactive: bool,
2005 pub out_of_order: bool,
2008 #[serde(default)]
2010 pub amended_by: Vec<GovernanceLogId>,
2011 #[serde(default, skip_serializing_if = "Option::is_none")]
2014 pub signed_by: Option<PublicKeyHex>,
2015 #[serde(default)]
2018 pub redacted: bool,
2019 #[serde(default, skip_serializing_if = "Option::is_none")]
2021 pub redacted_data_hash: Option<Sha256Hex>,
2022 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2025 pub revisions: Vec<GovernanceLogId>,
2026 #[serde(default, skip_serializing_if = "Option::is_none")]
2030 pub latest_data_hash: Option<Sha256Hex>,
2031 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2036 pub superseded_revisions: Vec<GovernanceLogId>,
2037 #[serde(skip)]
2040 pub history: RevisionHistory,
2041 #[serde(default)]
2044 pub repudiated: bool,
2045 #[serde(default)]
2048 pub reattested_by: Vec<GovernanceLogId>,
2049 #[serde(default, skip_serializing_if = "Option::is_none")]
2053 pub texts: Option<AmendmentTextStatus>,
2054 #[serde(default, skip_serializing_if = "Option::is_none")]
2056 pub problem: Option<String>,
2057}
2058
2059impl EntryVerdict {
2060 fn content_check(
2064 &mut self,
2065 attested: Sha256Hex,
2066 data: &serde_json::Value,
2067 ) -> bool {
2068 if non_integer_number(data).is_some() {
2070 return false;
2071 }
2072 let hash = data_hash(data);
2073 if hash == attested && self.redacted {
2074 return true;
2077 }
2078 if hash != attested && Some(hash) != self.redacted_data_hash {
2079 return Some(hash) == self.latest_data_hash;
2080 }
2081 let history = &self.history;
2082 let mut current = data.clone();
2083 let mut failures = Vec::new();
2084 for (i, (id, revision)) in history.revisions.iter().enumerate() {
2085 if i == history.rebased {
2086 break;
2087 }
2088 if let Some(failure) = false_duplicate(id, revision, ¤t) {
2089 failures.push(failure);
2090 break;
2091 }
2092 match apply_patch(¤t, &revision.patch) {
2093 Ok(next) => current = next,
2094 Err(_) => {
2095 failures.push(format!(
2096 "revision {id} does not apply to the redacted data"
2097 ));
2098 break;
2099 }
2100 }
2101 }
2102 if failures.is_empty()
2103 && let Some(expected) = history.rebased_hash
2104 && data_hash(¤t) != expected
2105 {
2106 failures.push(
2107 "the revisions rebased over the redacted data do not \
2108 produce the redaction's resulting_latest_hash"
2109 .to_string(),
2110 );
2111 }
2112 for (id, revision) in history.revisions.iter().skip(history.rebased) {
2113 if !failures.is_empty() {
2114 break;
2115 }
2116 if let Some(failure) = false_duplicate(id, revision, ¤t) {
2117 failures.push(failure);
2118 break;
2119 }
2120 match apply_patch(¤t, &revision.patch) {
2121 Ok(next)
2122 if data_hash(&next) == revision.resulting_data_hash =>
2123 {
2124 current = next;
2125 }
2126 Ok(_) => failures.push(format!(
2127 "revision {id} does not produce its resulting_data_hash"
2128 )),
2129 Err(_) => {
2130 failures.push(format!("revision {id} does not apply"))
2131 }
2132 }
2133 }
2134 for failure in failures {
2135 add_problem(&mut self.problem, failure);
2136 }
2137 true
2138 }
2139}
2140
2141fn add_problem(problems: &mut Option<String>, problem: String) {
2143 *problems = Some(match problems.take() {
2144 Some(p) if p.contains(&problem) => p,
2145 Some(p) => format!("{p}; {problem}"),
2146 None => problem,
2147 });
2148}
2149
2150fn false_duplicate(
2153 id: &GovernanceLogId,
2154 revision: &Revision,
2155 current: &serde_json::Value,
2156) -> Option<String> {
2157 revision.duplicates.iter().find_map(|(path, same_as)| {
2158 let same = match (current.pointer(path), current.pointer(same_as)) {
2159 (Some(a), Some(b)) => canonical_json(a) == canonical_json(b),
2160 _ => false,
2161 };
2162 (!same).then(|| {
2163 format!(
2164 "revision {id} removed {path} as a duplicate of {same_as}, \
2165 but they differ"
2166 )
2167 })
2168 })
2169}
2170
2171#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2173#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2174#[cfg_attr(feature = "schemars", schemars(inline))]
2175pub struct GovernanceVerification {
2176 pub public_key: PublicKeyHex,
2178 pub ok: bool,
2184 #[serde(default)]
2186 pub head: Option<GovernanceLogId>,
2187 pub entries: Vec<EntryVerdict>,
2189 #[serde(default)]
2191 pub keys: Vec<GovernanceKeyRecord>,
2192 #[serde(default)]
2197 pub unanchored_keys: Vec<PublicKeyHex>,
2198 #[serde(default)]
2200 pub repudiated: Vec<GovernanceLogId>,
2201}
2202
2203impl GovernanceVerification {
2204 pub fn settle(mut self) -> Self {
2206 self.ok = self.entries.iter().all(|e| {
2207 e.signature_valid
2208 && e.link_valid
2209 && e.content_matches != Some(false)
2210 && e.problem.is_none()
2211 });
2212 self
2213 }
2214
2215 pub fn check_content(
2226 &mut self,
2227 link: &GovernanceChainLink,
2228 data: &serde_json::Value,
2229 ) -> bool {
2230 let Some(entry) = self.entries.iter_mut().find(|e| e.id == link.id)
2231 else {
2232 return false;
2233 };
2234 let ok = entry.content_check(link.attestation.data_hash, data);
2235 entry.content_matches = Some(ok);
2236 ok
2237 }
2238}
2239
2240pub fn attest(
2251 key: &SigningKey,
2252 envelope: &Envelope,
2253 chain_seq: u64,
2254 signed_at: DateTime<Utc>,
2255) -> GovernanceAttestation {
2256 let signed_at = truncate_to_seconds(signed_at);
2257 let entry_hash = envelope.entry_hash();
2258 let signature =
2259 crypto::sign(key, entry_hash.as_bytes(), signed_at.timestamp());
2260 GovernanceAttestation {
2261 envelope_version: envelope.agora_governance_log,
2262 chain_seq,
2263 prev_hash: envelope.prev_hash,
2264 data_hash: envelope.data_hash,
2265 entry_hash,
2266 signature: signature.into(),
2267 signed_at,
2268 retroactive: is_retroactive(envelope.created_at(), signed_at),
2269 }
2270}
2271
2272#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
2278pub enum LinkError {
2279 #[error(
2280 "envelope version {0} is not supported (this verifier knows {ENVELOPE_VERSION})"
2281 )]
2282 UnsupportedVersion(u32),
2283 #[error("entry_hash does not recompute from the envelope fields")]
2284 HashMismatch,
2285 #[error("signature does not verify under the published key")]
2286 BadSignature,
2287}
2288
2289pub fn recompute_entry_hash(link: &GovernanceChainLink) -> Sha256Hex {
2291 Envelope::new(
2292 link.id.clone(),
2293 link.entry_type,
2294 link.created_at,
2295 link.attestation.prev_hash,
2296 link.attestation.data_hash,
2297 )
2298 .entry_hash()
2299}
2300
2301pub fn verify_link(
2303 link: &GovernanceChainLink,
2304 key: &VerifyingKey,
2305) -> Result<(), LinkError> {
2306 let a = &link.attestation;
2307 if a.envelope_version != ENVELOPE_VERSION {
2308 return Err(LinkError::UnsupportedVersion(a.envelope_version));
2309 }
2310 if recompute_entry_hash(link) != a.entry_hash {
2311 return Err(LinkError::HashMismatch);
2312 }
2313 if !crypto::verify(
2314 key,
2315 a.entry_hash.as_bytes(),
2316 a.signed_at.timestamp(),
2317 &Signature::from(&a.signature),
2318 ) {
2319 return Err(LinkError::BadSignature);
2320 }
2321 Ok(())
2322}
2323
2324pub fn verify_data(
2326 link: &GovernanceChainLink,
2327 data: &serde_json::Value,
2328) -> bool {
2329 data_hash(data) == link.attestation.data_hash
2330}
2331
2332fn same_shape(written: &serde_json::Value, read: &serde_json::Value) -> bool {
2342 use serde_json::Value::{Array, Null, Object};
2343 match (written, read) {
2344 (Object(w), Object(r)) => r.iter().all(|(k, r)| match w.get(k) {
2345 Some(w) => same_shape(w, r),
2346 None => r.is_null(),
2347 }),
2348 (Array(w), Array(r)) => {
2349 w.len() == r.len() && w.iter().zip(r).all(|(w, r)| same_shape(w, r))
2350 }
2351 (_, Object(_) | Array(_)) => false,
2352 (Object(_) | Array(_), Null) => false,
2353 _ => true,
2354 }
2355}
2356
2357fn read_strictly<T>(written: &serde_json::Value) -> Result<T, String>
2359where
2360 T: Serialize + serde::de::DeserializeOwned,
2361{
2362 let read: T =
2363 serde_json::from_value(written.clone()).map_err(|e| e.to_string())?;
2364 let again = serde_json::to_value(&read).map_err(|e| e.to_string())?;
2365 if same_shape(written, &again) {
2366 Ok(read)
2367 } else {
2368 Err("an array where an object belongs, or the reverse".into())
2369 }
2370}
2371
2372pub fn links_from_json(
2379 chain: &serde_json::Value,
2380) -> Result<Vec<GovernanceChainLink>, String> {
2381 chain
2382 .as_array()
2383 .ok_or("a chain is an array of links")?
2384 .iter()
2385 .map(read_strictly)
2386 .collect()
2387}
2388
2389fn read_as_written<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
2391where
2392 D: serde::Deserializer<'de>,
2393 T: Serialize + serde::de::DeserializeOwned,
2394{
2395 let written = serde_json::Value::deserialize(deserializer)?;
2396 if written.is_null() {
2397 return Ok(None);
2398 }
2399 read_strictly(&written)
2400 .map(Some)
2401 .map_err(serde::de::Error::custom)
2402}
2403
2404fn reserved_prefix(
2407 entry_type: GovernanceLogEntryType,
2408) -> Option<GovernanceLogPrefix> {
2409 match entry_type {
2410 GovernanceLogEntryType::Amendment => Some(GovernanceLogPrefix::Amd),
2411 GovernanceLogEntryType::KeyRotation => Some(GovernanceLogPrefix::Key),
2412 GovernanceLogEntryType::StewardRecord => Some(GovernanceLogPrefix::Rec),
2413 GovernanceLogEntryType::CouncilDecision
2414 | GovernanceLogEntryType::AppealsCourtDecision
2415 | GovernanceLogEntryType::EmergencyAction
2416 | GovernanceLogEntryType::PolicyChange
2417 | GovernanceLogEntryType::StewardVeto => None,
2418 }
2419}
2420
2421fn reserved_for(prefix: GovernanceLogPrefix) -> Option<GovernanceLogEntryType> {
2423 match prefix {
2424 GovernanceLogPrefix::Amd => Some(GovernanceLogEntryType::Amendment),
2425 GovernanceLogPrefix::Key => Some(GovernanceLogEntryType::KeyRotation),
2426 GovernanceLogPrefix::Rec => Some(GovernanceLogEntryType::StewardRecord),
2427 GovernanceLogPrefix::Gov | GovernanceLogPrefix::App => None,
2428 }
2429}
2430
2431fn prefix_problem(link: &GovernanceChainLink) -> Option<String> {
2433 let prefix = link.id.prefix();
2434 match (reserved_prefix(link.entry_type), reserved_for(prefix)) {
2435 (Some(want), _) if prefix != want => Some(format!(
2436 "the id of a {} entry must be in the {want}- series, not {}",
2437 link.entry_type, link.id
2438 )),
2439 (None, Some(owner)) => Some(format!(
2440 "{prefix}- ids are reserved for {owner} entries, but {} is a {}",
2441 link.id, link.entry_type
2442 )),
2443 _ => None,
2444 }
2445}
2446
2447fn amendment_target(
2449 amendment: &Amendment,
2450 seq: u64,
2451 seq_of: &HashMap<&str, u64>,
2452 links: &[&GovernanceChainLink],
2453) -> Result<u64, AmendmentError> {
2454 amendment.validate()?;
2455 let target = *seq_of.get(amendment.target.as_str()).ok_or_else(|| {
2456 AmendmentError::UnknownTarget(amendment.target.clone())
2457 })?;
2458 if target >= seq {
2459 return Err(AmendmentError::ForwardReference(amendment.target.clone()));
2460 }
2461 if links[target as usize - 1].attestation.entry_hash
2462 != amendment.target_entry_hash
2463 {
2464 return Err(AmendmentError::WrongTargetHash(amendment.target.clone()));
2465 }
2466 Ok(target)
2467}
2468
2469struct KeyWalk {
2471 history: Vec<(GovernanceKeyRecord, VerifyingKey)>,
2473 unanchored: Vec<PublicKeyHex>,
2474 seen: HashSet<PublicKeyHex>,
2478 repudiated: HashSet<u64>,
2480 genesis: PublicKeyHex,
2481 genesis_certified: bool,
2483}
2484
2485impl KeyWalk {
2486 fn new(genesis: &VerifyingKey, anchor: &KeyAnchor) -> Self {
2487 let public_key = PublicKeyHex::from(genesis);
2488 Self {
2489 genesis: public_key,
2490 genesis_certified: false,
2491 history: vec![(
2492 GovernanceKeyRecord {
2493 public_key,
2494 from_seq: 1,
2495 through_seq: None,
2496 status: KeyStatus::Active,
2497 introduced_by: None,
2498 retired_by: None,
2499 certified: false,
2500 },
2501 *genesis,
2502 )],
2503 unanchored: if anchor.contains(&public_key) {
2504 Vec::new()
2505 } else {
2506 vec![public_key]
2507 },
2508 seen: HashSet::from([public_key]),
2509 repudiated: HashSet::new(),
2510 }
2511 }
2512
2513 fn in_force(&self, seq: u64) -> (PublicKeyHex, VerifyingKey) {
2515 let (record, key) = self
2516 .history
2517 .iter()
2518 .rev()
2519 .find(|(r, _)| r.from_seq <= seq)
2520 .unwrap_or(&self.history[0]);
2521 (record.public_key, *key)
2522 }
2523
2524 fn active(&self) -> PublicKeyHex {
2526 self.history
2527 .last()
2528 .map(|(r, _)| r.public_key)
2529 .expect("the genesis key is always in the history")
2530 }
2531
2532 fn close(
2533 &mut self,
2534 through_seq: u64,
2535 status: KeyStatus,
2536 by: &GovernanceLogId,
2537 ) {
2538 if let Some((record, _)) = self.history.last_mut() {
2539 record.through_seq = Some(through_seq);
2540 record.status = status;
2541 record.retired_by = Some(by.clone());
2542 }
2543 }
2544
2545 fn open(
2546 &mut self,
2547 public_key: PublicKeyHex,
2548 key: VerifyingKey,
2549 from_seq: u64,
2550 by: &GovernanceLogId,
2551 ) {
2552 self.history.push((
2553 GovernanceKeyRecord {
2554 public_key,
2555 from_seq,
2556 through_seq: None,
2557 status: KeyStatus::Active,
2558 introduced_by: Some(by.clone()),
2559 retired_by: None,
2560 certified: true,
2561 },
2562 key,
2563 ));
2564 }
2565
2566 fn apply(
2568 &mut self,
2569 rotation: &KeyRotation,
2570 link: &GovernanceChainLink,
2571 seq: u64,
2572 links: &[&GovernanceChainLink],
2573 roots: &RootSet,
2574 ) -> Result<(), RotationError> {
2575 rotation.verify_certified(seq, link.attestation.prev_hash, roots)?;
2576 let new_key = rotation
2577 .new_key
2578 .to_verifying_key()
2579 .map_err(|_| RotationError::BadNewKey)?;
2580 if self.seen.contains(&rotation.new_key) {
2581 return Err(RotationError::ReusedKey);
2582 }
2583 match (&rotation.outgoing_certificate, self.genesis_certified) {
2588 (None, false) => {
2589 return Err(RotationError::MissingGenesisCertificate);
2590 }
2591 (Some(_), true) => {
2592 return Err(RotationError::UnexpectedOutgoingCertificate);
2593 }
2594 (Some(certificate), false) => certificate
2595 .verify_for(&KeyCertStatement::genesis(self.genesis), roots)
2596 .map_err(RotationError::OutgoingCertificate)?,
2597 (None, true) => {}
2598 }
2599 match rotation.reason {
2600 RotationReason::Routine => {
2601 if rotation.old_key != self.in_force(seq).0 {
2602 return Err(RotationError::WrongOldKey);
2603 }
2604 self.close(seq, KeyStatus::Retired, &link.id);
2605 self.open(rotation.new_key, new_key, seq + 1, &link.id);
2606 }
2607 RotationReason::Compromise => {
2608 let head = rotation
2609 .last_trusted()
2610 .ok_or(RotationError::MissingLastTrusted)?;
2611 let trusted_seq = head.chain_seq;
2612 let names_an_earlier_entry = trusted_seq >= 1
2613 && trusted_seq < seq
2614 && links[trusted_seq as usize - 1].id == head.id
2615 && links[trusted_seq as usize - 1].attestation.entry_hash
2616 == head.entry_hash;
2617 if !names_an_earlier_entry {
2618 return Err(RotationError::UnknownLastTrusted);
2619 }
2620 if self.repudiated.contains(&trusted_seq) {
2623 return Err(RotationError::RepudiatedLastTrusted);
2624 }
2625 if rotation.old_key != self.in_force(trusted_seq).0 {
2628 return Err(RotationError::WrongOldKey);
2629 }
2630 self.history.retain(|(r, _)| r.from_seq <= trusted_seq);
2631 self.close(trusted_seq, KeyStatus::Compromised, &link.id);
2632 self.open(rotation.new_key, new_key, seq, &link.id);
2633 self.repudiated.extend(trusted_seq + 1..seq);
2634 }
2635 }
2636 self.seen.insert(rotation.new_key);
2637 if !self.genesis_certified {
2638 self.genesis_certified = true;
2639 self.history[0].0.certified = true;
2640 self.unanchored.clear();
2641 }
2642 Ok(())
2643 }
2644}
2645
2646pub fn verify_chain(
2677 links: &[GovernanceChainLink],
2678 genesis_key: &VerifyingKey,
2679 anchor: &KeyAnchor,
2680 roots: &RootSet,
2681) -> GovernanceVerification {
2682 let mut links: Vec<&GovernanceChainLink> = links.iter().collect();
2683 links.sort_by_key(|l| l.attestation.chain_seq);
2684
2685 let mut seq_of: HashMap<&str, u64> = HashMap::new();
2686 let mut duplicates: HashSet<&str> = HashSet::new();
2687 for (i, link) in links.iter().enumerate() {
2688 if seq_of.insert(link.id.as_str(), i as u64 + 1).is_some() {
2689 duplicates.insert(link.id.as_str());
2690 }
2691 }
2692
2693 let mut walk = KeyWalk::new(genesis_key, anchor);
2694 let mut amendments: Vec<(u64, GovernanceLogId, Amendment, u64)> =
2696 Vec::new();
2697 let mut entries: Vec<EntryVerdict> = Vec::with_capacity(links.len());
2698 let mut prev: Option<&GovernanceChainLink> = None;
2699
2700 for (i, link) in links.iter().enumerate() {
2701 let a = &link.attestation;
2702 let expected_seq = i as u64 + 1;
2703 let mut problems: Vec<String> = Vec::new();
2704
2705 if let Some(problem) = prefix_problem(link) {
2706 problems.push(problem);
2707 }
2708 if duplicates.contains(link.id.as_str()) {
2709 problems.push(format!("{} appears more than once", link.id));
2710 }
2711
2712 let carries_meaning = matches!(
2716 link.entry_type,
2717 GovernanceLogEntryType::Amendment
2718 | GovernanceLogEntryType::KeyRotation
2719 );
2720 let mut amendment: Option<Amendment> = None;
2721 let mut rotation: Option<KeyRotation> = None;
2722 let mut content_matches: Option<bool> = None;
2723 match &link.data {
2724 Some(data) if non_integer_number(data).is_some() => {
2725 content_matches = Some(false);
2726 problems.push(format!(
2727 "`data` has a number that is not a 64-bit integer at {:?}; \
2728 governance data never contains one",
2729 non_integer_number(data).unwrap_or_default()
2730 ));
2731 }
2732 Some(data) => {
2733 let matched = data_hash(data) == a.data_hash;
2734 content_matches = Some(matched);
2735 if !matched {
2736 if carries_meaning {
2737 problems.push(
2738 "`data` does not hash to the attested data_hash"
2739 .into(),
2740 );
2741 }
2742 } else {
2743 match link.entry_type {
2744 GovernanceLogEntryType::Amendment => {
2745 match read_strictly(data) {
2746 Ok(v) => amendment = Some(v),
2747 Err(e) => problems.push(format!(
2748 "amendment `data` is malformed: {e}"
2749 )),
2750 }
2751 }
2752 GovernanceLogEntryType::KeyRotation => {
2753 match read_strictly(data) {
2754 Ok(v) => rotation = Some(v),
2755 Err(e) => problems.push(format!(
2756 "key_rotation `data` is malformed: {e}"
2757 )),
2758 }
2759 }
2760 _ => {}
2761 }
2762 }
2763 }
2764 None if carries_meaning => problems.push(format!(
2765 "a {} entry must carry its `data`",
2766 link.entry_type
2767 )),
2768 None => {}
2769 }
2770
2771 let declared = rotation
2775 .as_ref()
2776 .filter(|r| r.reason == RotationReason::Compromise)
2777 .map(|r| {
2778 if walk.seen.contains(&r.new_key) {
2779 return Err(RotationError::ReusedKey);
2780 }
2781 r.verify_certified(expected_seq, a.prev_hash, roots)?;
2782 r.new_key
2783 .to_verifying_key()
2784 .map_err(|_| RotationError::BadNewKey)
2785 });
2786 let (key_hex, key) = match &declared {
2787 Some(Ok(k)) => (PublicKeyHex::from(k), *k),
2788 _ => walk.in_force(expected_seq),
2789 };
2790 if let Some(Err(e)) = &declared {
2793 problems.push(e.to_string());
2794 }
2795
2796 let (hash_ok, signature_valid) = match verify_link(link, &key) {
2797 Ok(()) => (true, true),
2798 Err(LinkError::BadSignature) => {
2799 problems.push(LinkError::BadSignature.to_string());
2800 (true, false)
2801 }
2802 Err(e) => {
2803 problems.push(e.to_string());
2804 (false, false)
2805 }
2806 };
2807
2808 let mut link_valid = hash_ok;
2809 if a.chain_seq != expected_seq {
2810 link_valid = false;
2811 problems.push(format!(
2812 "chain_seq {} where {expected_seq} was expected",
2813 a.chain_seq
2814 ));
2815 }
2816 let expected_prev = prev.map(|p| p.attestation.entry_hash);
2817 if a.prev_hash != expected_prev {
2818 link_valid = false;
2819 problems.push(match (a.prev_hash, expected_prev) {
2820 (Some(_), None) => "first entry names a predecessor".into(),
2821 (None, Some(_)) => "prev_hash is null mid-chain".into(),
2822 _ => "prev_hash is not the previous entry's entry_hash".into(),
2823 });
2824 }
2825 let out_of_order = prev.is_some_and(|p| link.created_at < p.created_at);
2826
2827 let authentic = signature_valid && link_valid;
2831 if let Some(rotation) = rotation.as_ref().filter(|_| authentic)
2832 && let Err(e) =
2833 walk.apply(rotation, link, expected_seq, &links, roots)
2834 {
2835 let problem = e.to_string();
2836 if !problems.contains(&problem) {
2837 problems.push(problem);
2838 }
2839 }
2840 let mut texts = None;
2844 if let Some(amendment) = amendment
2845 .as_ref()
2846 .filter(|a| authentic && a.validate().is_ok())
2847 {
2848 match amendment.text_status(link.texts.as_ref()) {
2849 Ok(status) => texts = status,
2850 Err(e) => problems.push(e.to_string()),
2851 }
2852 } else if link.texts.as_ref().is_some_and(|t| !t.is_empty()) {
2853 problems.push(AmendmentError::UncommittedText.to_string());
2854 }
2855 if let Some(amendment) = amendment.filter(|_| authentic) {
2856 match amendment_target(&amendment, expected_seq, &seq_of, &links) {
2857 Ok(target) => amendments.push((
2858 expected_seq,
2859 link.id.clone(),
2860 amendment,
2861 target,
2862 )),
2863 Err(e) => problems.push(e.to_string()),
2864 }
2865 }
2866
2867 entries.push(EntryVerdict {
2868 id: link.id.clone(),
2869 chain_seq: a.chain_seq,
2870 signature_valid,
2871 link_valid,
2872 content_matches,
2873 retroactive: is_retroactive(link.created_at, a.signed_at),
2874 out_of_order,
2875 amended_by: Vec::new(),
2876 signed_by: Some(key_hex),
2877 redacted: false,
2878 redacted_data_hash: None,
2879 revisions: Vec::new(),
2880 latest_data_hash: None,
2881 superseded_revisions: Vec::new(),
2882 history: RevisionHistory::default(),
2883 repudiated: false,
2884 reattested_by: Vec::new(),
2885 texts,
2886 problem: (!problems.is_empty()).then(|| problems.join("; ")),
2887 });
2888 prev = Some(link);
2889 }
2890
2891 let mut applied = vec![false; amendments.len()];
2895 loop {
2896 let mut changed = false;
2897 for (i, (seq, id, amendment, target)) in amendments.iter().enumerate() {
2898 if applied[i]
2899 || amendment.kind != AmendmentKind::Reattested
2900 || walk.repudiated.contains(seq)
2901 {
2902 continue;
2903 }
2904 applied[i] = true;
2905 entries[*target as usize - 1].reattested_by.push(id.clone());
2906 walk.repudiated.remove(target);
2907 changed = true;
2908 }
2909 if !changed {
2910 break;
2911 }
2912 }
2913
2914 for (seq, id, amendment, target) in &amendments {
2915 if walk.repudiated.contains(seq) {
2916 continue;
2917 }
2918 let entry = &mut entries[*target as usize - 1];
2919 entry.amended_by.push(id.clone());
2920 let mut unrebased = false;
2921 if let Some(redaction) = &amendment.redaction {
2922 unrebased = !entry.revisions.is_empty()
2925 && redaction.resulting_latest_hash.is_none();
2926 entry.redacted = true;
2927 entry.redacted_data_hash = Some(redaction.resulting_data_hash);
2928 entry.history.rebased = entry.history.revisions.len();
2930 entry.history.rebased_hash = redaction.resulting_latest_hash;
2931 entry.latest_data_hash = redaction.resulting_latest_hash;
2932 entry.superseded_revisions = entry.revisions.clone();
2933 }
2934 if let Some(revision) = &amendment.revision {
2935 entry.revisions.push(id.clone());
2936 entry.latest_data_hash = Some(revision.resulting_data_hash);
2937 entry.history.revisions.push((id.clone(), revision.clone()));
2938 }
2939 if unrebased {
2940 let target = entry.id.clone();
2941 add_problem(
2942 &mut entries[*seq as usize - 1].problem,
2943 format!(
2944 "the redaction of {target}, which has revisions, names no \
2945 resulting_latest_hash"
2946 ),
2947 );
2948 }
2949 }
2950
2951 for (i, link) in links.iter().enumerate() {
2954 let entry = &mut entries[i];
2955 if let Some(data) = &link.data
2956 && entry.content_matches.is_some()
2957 && (entry.redacted || !entry.history.revisions.is_empty())
2958 {
2959 entry.content_matches =
2960 Some(entry.content_check(link.attestation.data_hash, data));
2961 }
2962 }
2963
2964 let mut seen = HashSet::new();
2965 walk.unanchored.retain(|key| seen.insert(*key));
2966
2967 let mut repudiated = Vec::new();
2968 for (i, entry) in entries.iter_mut().enumerate() {
2969 if walk.repudiated.contains(&(i as u64 + 1)) {
2970 entry.repudiated = true;
2971 repudiated.push(entry.id.clone());
2972 }
2973 }
2974
2975 GovernanceVerification {
2976 public_key: walk.active(),
2977 ok: false,
2978 head: prev.map(|p| p.id.clone()),
2979 entries,
2980 keys: walk.history.into_iter().map(|(record, _)| record).collect(),
2981 unanchored_keys: walk.unanchored,
2982 repudiated,
2983 }
2984 .settle()
2985}
2986
2987#[cfg(test)]
2988mod tests {
2989 use super::*;
2990
2991 #[test]
2994 fn citations_are_read_from_each_records_own_field() {
2995 use serde_json::json;
2996 let id = |s: &str| s.parse::<GovernanceLogId>().unwrap();
2997 assert_eq!(
2998 citations_in(
2999 GovernanceLogEntryType::StewardRecord,
3000 &json!({"title": "t", "concerns": ["GOV-2026-0011", "AMD-2026-0004"]}),
3001 ),
3002 vec![
3003 (id("GOV-2026-0011"), CitationRelation::Concerns),
3004 (id("AMD-2026-0004"), CitationRelation::Concerns),
3005 ]
3006 );
3007 assert_eq!(
3008 citations_in(
3009 GovernanceLogEntryType::Amendment,
3010 &json!({"target": "APP-2026-0001", "authority": "GOV-2026-0005"}),
3011 ),
3012 vec![(id("GOV-2026-0005"), CitationRelation::Authority)],
3013 "the target is not a citation: it is listed under `amendments`"
3014 );
3015 assert_eq!(
3016 citations_in(
3017 GovernanceLogEntryType::CouncilDecision,
3018 &json!({"overrules": ["APP-2026-0002"]}),
3019 ),
3020 vec![(id("APP-2026-0002"), CitationRelation::Overrules)]
3021 );
3022 assert!(
3023 citations_in(
3024 GovernanceLogEntryType::Amendment,
3025 &json!({"authority": null})
3026 )
3027 .is_empty()
3028 );
3029 assert!(
3030 citations_in(
3031 GovernanceLogEntryType::StewardRecord,
3032 &json!({"concerns": "GOV-2026-0011"})
3033 )
3034 .is_empty()
3035 );
3036 assert!(
3037 citations_in(
3038 GovernanceLogEntryType::AppealsCourtDecision,
3039 &json!({"concerns": ["GOV-2026-0011"]}),
3040 )
3041 .is_empty()
3042 );
3043 }
3044 use crate::crypto::generate_keypair;
3045 use serde_json::json;
3046
3047 pub(super) fn gov(n: u32) -> GovernanceLogId {
3048 format!("GOV-2026-{n:04}").parse().unwrap()
3049 }
3050
3051 pub(super) fn amd(n: u32) -> GovernanceLogId {
3052 format!("AMD-2026-{n:04}").parse().unwrap()
3053 }
3054
3055 pub(super) fn key_id(n: u32) -> GovernanceLogId {
3056 format!("KEY-2026-{n:04}").parse().unwrap()
3057 }
3058
3059 pub(super) fn rec(n: u32) -> GovernanceLogId {
3060 format!("REC-2026-{n:04}").parse().unwrap()
3061 }
3062
3063 pub(super) fn at(secs: i64) -> DateTime<Utc> {
3064 DateTime::from_timestamp(1_700_000_000 + secs, 123_456_789).unwrap()
3065 }
3066
3067 fn anchored(key: &VerifyingKey) -> KeyAnchor {
3069 KeyAnchor::pinned(key.into())
3070 }
3071
3072 pub(super) fn resalted(draft: &AmendmentDraft, seq: u64) -> AmendmentDraft {
3075 let fix = |field: &str, t: &Option<CommittedText>| {
3076 t.as_ref().map(|t| {
3077 let salt = Sha256::digest(format!("{seq}/{field}/{}", t.text));
3078 CommittedText::with_salt(
3079 TextSalt::from(<[u8; 32]>::from(salt)),
3080 t.text.clone(),
3081 )
3082 })
3083 };
3084 let texts = AmendmentTexts {
3085 basis: fix("basis", &draft.texts.basis),
3086 note: fix("note", &draft.texts.note),
3087 rationale: fix("rationale", &draft.texts.rationale),
3088 };
3089 let commit = |t: &Option<CommittedText>| {
3090 t.as_ref().map(|t| AmendmentText::Committed(t.commitment()))
3091 };
3092 AmendmentDraft {
3093 amendment: Amendment {
3094 basis: commit(&texts.basis).unwrap(),
3095 note: commit(&texts.note).unwrap(),
3096 rationale: commit(&texts.rationale),
3097 ..draft.amendment.clone()
3098 },
3099 texts,
3100 }
3101 }
3102
3103 pub(super) fn v1(draft: AmendmentDraft) -> Amendment {
3105 let plain =
3106 |t: Option<CommittedText>| t.map(|t| AmendmentText::Plain(t.text));
3107 Amendment {
3108 agora_governance_amendment: 1,
3109 basis: plain(draft.texts.basis).unwrap(),
3110 note: plain(draft.texts.note).unwrap(),
3111 rationale: plain(draft.texts.rationale),
3112 ..draft.amendment
3113 }
3114 }
3115
3116 pub(super) fn root(n: u8) -> SigningKey {
3119 SigningKey::from_bytes(&[0xA0 + n; 32])
3120 }
3121
3122 pub(super) fn roots() -> RootSet {
3124 RootSet::new(
3125 [1, 2].map(|n| PublicKeyHex::from(&root(n).verifying_key())),
3126 1,
3127 )
3128 }
3129
3130 pub(super) fn certify(
3132 signers: &[&SigningKey],
3133 statement: KeyCertStatement,
3134 ) -> KeyCertificate {
3135 use ed25519_dalek::Signer;
3136 let message = statement.signed_bytes();
3137 signers.iter().fold(
3138 KeyCertificate::unsigned(statement),
3139 |certificate, signer| {
3140 certificate.with(RootSignature {
3141 root_key: (&signer.verifying_key()).into(),
3142 signature: signer.sign(&message).into(),
3143 })
3144 },
3145 )
3146 }
3147
3148 fn for_new_at(c: &Chain, key: &VerifyingKey) -> KeyCertificate {
3150 certify(
3151 &[&root(1)],
3152 KeyCertStatement::routine(key.into(), c.next_seq(), c.prev_hash()),
3153 )
3154 }
3155
3156 pub(super) fn link(
3157 key: &SigningKey,
3158 n: u32,
3159 prev: Option<&GovernanceChainLink>,
3160 data: &serde_json::Value,
3161 signed_at: DateTime<Utc>,
3162 ) -> GovernanceChainLink {
3163 let created_at = truncate_to_micros(at(n as i64 * 10));
3164 let envelope = Envelope::new(
3165 gov(n),
3166 GovernanceLogEntryType::CouncilDecision,
3167 created_at,
3168 prev.map(|p| p.attestation.entry_hash),
3169 data_hash(data),
3170 );
3171 let attestation = attest(
3172 key,
3173 &envelope,
3174 prev.map_or(1, |p| p.attestation.chain_seq + 1),
3175 signed_at,
3176 );
3177 GovernanceChainLink {
3178 id: gov(n),
3179 entry_type: GovernanceLogEntryType::CouncilDecision,
3180 created_at,
3181 attestation,
3182 data: None,
3183 texts: None,
3184 }
3185 }
3186
3187 pub(super) fn chain(key: &SigningKey, n: u32) -> Vec<GovernanceChainLink> {
3188 let mut out: Vec<GovernanceChainLink> = Vec::new();
3189 for i in 1..=n {
3190 let data = json!({"title": format!("Decision {i}"), "outcome": "approved"});
3191 let l = link(key, i, out.last(), &data, at(i as i64 * 10 + 1));
3192 out.push(l);
3193 }
3194 out
3195 }
3196
3197 pub(super) struct Chain {
3201 pub(super) links: Vec<GovernanceChainLink>,
3202 gov: u32,
3203 amd: u32,
3204 key: u32,
3205 genesis: Option<PublicKeyHex>,
3207 }
3208
3209 impl Chain {
3210 pub(super) fn new() -> Self {
3211 Self {
3212 links: Vec::new(),
3213 gov: 0,
3214 amd: 0,
3215 key: 0,
3216 genesis: None,
3217 }
3218 }
3219
3220 pub(super) fn prev_hash(&self) -> Option<Sha256Hex> {
3221 self.links.last().map(|l| l.attestation.entry_hash)
3222 }
3223
3224 pub(super) fn hash_at(&self, seq: usize) -> Sha256Hex {
3226 self.links[seq - 1].attestation.entry_hash
3227 }
3228
3229 pub(super) fn next_seq(&self) -> u64 {
3231 self.links.len() as u64 + 1
3232 }
3233
3234 pub(super) fn head(&self, seq: usize) -> TrustedHead {
3236 TrustedHead {
3237 id: self.links[seq - 1].id.clone(),
3238 chain_seq: seq as u64,
3239 entry_hash: self.hash_at(seq),
3240 }
3241 }
3242
3243 fn outgoing(&self, rotation: KeyRotation) -> KeyRotation {
3245 match (self.key, self.genesis) {
3246 (0, Some(genesis)) => rotation.with_outgoing(certify(
3247 &[&root(1)],
3248 KeyCertStatement::genesis(genesis),
3249 )),
3250 _ => rotation,
3251 }
3252 }
3253
3254 pub(super) fn routine(
3257 &self,
3258 old: &VerifyingKey,
3259 new: &SigningKey,
3260 ) -> KeyRotation {
3261 let statement = KeyCertStatement::routine(
3262 (&new.verifying_key()).into(),
3263 self.next_seq(),
3264 self.prev_hash(),
3265 );
3266 self.outgoing(KeyRotation::routine(
3267 old.into(),
3268 new,
3269 self.prev_hash(),
3270 at(self.next_seq() as i64 * 10 + 5),
3271 certify(&[&root(1)], statement),
3272 ))
3273 }
3274
3275 pub(super) fn compromise(
3278 &self,
3279 old: &VerifyingKey,
3280 new: &SigningKey,
3281 trusted: usize,
3282 ) -> KeyRotation {
3283 let statement = KeyCertStatement::compromise(
3284 (&new.verifying_key()).into(),
3285 self.next_seq(),
3286 self.prev_hash(),
3287 self.head(trusted),
3288 );
3289 self.outgoing(KeyRotation::compromise(
3290 old.into(),
3291 new,
3292 self.prev_hash(),
3293 at(self.next_seq() as i64 * 10 + 5),
3294 certify(&[&root(1)], statement),
3295 ))
3296 }
3297
3298 pub(super) fn next_amd(&self) -> GovernanceLogId {
3300 amd(self.amd + 1)
3301 }
3302
3303 pub(super) fn push(
3304 &mut self,
3305 signer: &SigningKey,
3306 id: GovernanceLogId,
3307 entry_type: GovernanceLogEntryType,
3308 data: serde_json::Value,
3309 carry: bool,
3310 ) -> GovernanceLogId {
3311 self.genesis
3312 .get_or_insert_with(|| (&signer.verifying_key()).into());
3313 let n = self.links.len() as i64 + 1;
3314 let created_at = truncate_to_micros(at(n * 10));
3315 let envelope = Envelope::new(
3316 id.clone(),
3317 entry_type,
3318 created_at,
3319 self.prev_hash(),
3320 data_hash(&data),
3321 );
3322 let attestation =
3323 attest(signer, &envelope, n as u64, at(n * 10 + 1));
3324 self.links.push(GovernanceChainLink {
3325 id: id.clone(),
3326 entry_type,
3327 created_at,
3328 attestation,
3329 data: carry.then_some(data),
3330 texts: None,
3331 });
3332 id
3333 }
3334
3335 pub(super) fn entry(
3338 &mut self,
3339 signer: &SigningKey,
3340 data: serde_json::Value,
3341 ) -> GovernanceLogId {
3342 self.gov += 1;
3343 let id = gov(self.gov);
3344 self.push(
3345 signer,
3346 id,
3347 GovernanceLogEntryType::CouncilDecision,
3348 data,
3349 false,
3350 )
3351 }
3352
3353 pub(super) fn decision(
3354 &mut self,
3355 signer: &SigningKey,
3356 ) -> GovernanceLogId {
3357 let data = json!({"title": format!("Decision {}", self.gov + 1)});
3358 self.entry(signer, data)
3359 }
3360
3361 pub(super) fn amend(
3363 &mut self,
3364 signer: &SigningKey,
3365 draft: &AmendmentDraft,
3366 ) -> GovernanceLogId {
3367 let draft = resalted(draft, self.next_seq());
3368 let id = self.amend_v1(signer, &draft.amendment);
3369 let link = self.links.last_mut().unwrap();
3370 link.texts = Some(draft.texts);
3371 id
3372 }
3373
3374 pub(super) fn amend_v1(
3377 &mut self,
3378 signer: &SigningKey,
3379 amendment: &Amendment,
3380 ) -> GovernanceLogId {
3381 self.amd += 1;
3382 let id = amd(self.amd);
3383 self.push(
3384 signer,
3385 id,
3386 GovernanceLogEntryType::Amendment,
3387 serde_json::to_value(amendment).unwrap(),
3388 true,
3389 )
3390 }
3391
3392 pub(super) fn rotate(
3393 &mut self,
3394 signer: &SigningKey,
3395 rotation: &KeyRotation,
3396 ) -> GovernanceLogId {
3397 self.key += 1;
3398 let id = key_id(self.key);
3399 self.push(
3400 signer,
3401 id,
3402 GovernanceLogEntryType::KeyRotation,
3403 serde_json::to_value(rotation).unwrap(),
3404 true,
3405 )
3406 }
3407 }
3408
3409 #[test]
3412 fn canonical_json_sorts_keys_at_every_level() {
3413 let v = json!({"b": {"z": 1, "a": [{"y": 2, "x": 3}]}, "a": null});
3414 assert_eq!(
3415 canonical_json(&v),
3416 br#"{"a":null,"b":{"a":[{"x":3,"y":2}],"z":1}}"#
3417 );
3418 }
3419
3420 #[test]
3421 fn canonical_json_is_compact_and_escapes_like_serde() {
3422 let v = json!({"s": "tab\there \"q\" ünïcode \u{1F600}", "n": [1, -2, 3.5, true, false]});
3423 let bytes = canonical_json(&v);
3424 let text = std::str::from_utf8(&bytes).unwrap();
3425 assert_eq!(
3426 text,
3427 r#"{"n":[1,-2,3.5,true,false],"s":"tab\there \"q\" ünïcode 😀"}"#
3428 );
3429 }
3430
3431 #[test]
3432 fn canonical_json_ignores_insertion_order() {
3433 let mut a = serde_json::Map::new();
3434 a.insert("z".into(), json!(1));
3435 a.insert("a".into(), json!(2));
3436 let mut b = serde_json::Map::new();
3437 b.insert("a".into(), json!(2));
3438 b.insert("z".into(), json!(1));
3439 assert_eq!(
3440 canonical_json(&serde_json::Value::Object(a)),
3441 canonical_json(&serde_json::Value::Object(b))
3442 );
3443 }
3444
3445 #[test]
3446 fn canonical_json_empty_containers() {
3447 assert_eq!(canonical_json(&json!({})), b"{}");
3448 assert_eq!(canonical_json(&json!([])), b"[]");
3449 assert_eq!(
3450 canonical_json(&json!({"a": {}, "b": []})),
3451 br#"{"a":{},"b":[]}"#
3452 );
3453 }
3454
3455 #[test]
3458 fn preimage_is_declaration_ordered_json() {
3459 let e = Envelope::new(
3460 gov(1),
3461 GovernanceLogEntryType::AppealsCourtDecision,
3462 at(0),
3463 None,
3464 data_hash(&json!({})),
3465 );
3466 let text = String::from_utf8(e.preimage()).unwrap();
3467 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}");
3468 }
3469
3470 #[test]
3471 fn every_envelope_field_changes_the_hash() {
3472 let base = Envelope::new(
3473 gov(1),
3474 GovernanceLogEntryType::CouncilDecision,
3475 at(0),
3476 None,
3477 data_hash(&json!({"a":1})),
3478 );
3479 let h = base.entry_hash();
3480 let mut e = base.clone();
3481 e.id = gov(2);
3482 assert_ne!(e.entry_hash(), h);
3483 let mut e = base.clone();
3484 e.entry_type = GovernanceLogEntryType::PolicyChange;
3485 assert_ne!(e.entry_hash(), h);
3486 let mut e = base.clone();
3487 e.created_at += 1;
3488 assert_ne!(e.entry_hash(), h);
3489 let mut e = base.clone();
3490 e.prev_hash = Some(h);
3491 assert_ne!(e.entry_hash(), h);
3492 let mut e = base.clone();
3493 e.data_hash = data_hash(&json!({"a":2}));
3494 assert_ne!(e.entry_hash(), h);
3495 assert_eq!(base.entry_hash(), h, "and it is deterministic");
3496 }
3497
3498 #[test]
3499 fn truncation_matches_what_the_envelope_carries() {
3500 let t = at(0);
3501 assert_eq!(truncate_to_micros(t).timestamp_subsec_nanos(), 123_456_000);
3502 assert_eq!(truncate_to_seconds(t).timestamp_subsec_nanos(), 0);
3503 assert_eq!(
3504 Envelope::new(
3505 gov(1),
3506 GovernanceLogEntryType::CouncilDecision,
3507 t,
3508 None,
3509 data_hash(&json!(null))
3510 )
3511 .created_at,
3512 truncate_to_micros(t).timestamp_micros()
3513 );
3514 }
3515
3516 #[test]
3519 fn hex_newtypes_round_trip_and_reject_wrong_lengths() {
3520 let h = data_hash(&json!(1));
3521 let s = serde_json::to_string(&h).unwrap();
3522 assert_eq!(s.len(), 66);
3523 let back: Sha256Hex = serde_json::from_str(&s).unwrap();
3524 assert_eq!(back, h);
3525 assert!(serde_json::from_str::<Sha256Hex>("\"abcd\"").is_err());
3526 assert!("zz".repeat(32).parse::<Sha256Hex>().is_err());
3527 assert!(Sha256Hex::try_from(vec![0u8; 31]).is_err());
3528 assert_eq!(format!("{h:?}"), format!("Sha256Hex({h})"));
3529 }
3530
3531 #[test]
3534 fn attest_then_verify_link() {
3535 let (key, pk) = generate_keypair();
3536 let l = link(&key, 1, None, &json!({"a": 1}), at(5));
3537 assert_eq!(verify_link(&l, &pk), Ok(()));
3538 assert!(verify_data(&l, &json!({"a": 1})));
3539 assert!(!verify_data(&l, &json!({"a": 2})));
3540 assert!(!l.attestation.retroactive);
3541 }
3542
3543 #[test]
3544 fn wrong_key_fails_signature_only() {
3545 let (key, _) = generate_keypair();
3546 let (_, other) = generate_keypair();
3547 let l = link(&key, 1, None, &json!({}), at(5));
3548 assert_eq!(verify_link(&l, &other), Err(LinkError::BadSignature));
3549 }
3550
3551 #[test]
3552 fn tampering_with_any_attested_field_is_detected() {
3553 let (key, pk) = generate_keypair();
3554 let l = link(&key, 1, None, &json!({"a": 1}), at(5));
3555
3556 let mut t = l.clone();
3557 t.created_at += chrono::Duration::microseconds(1);
3558 assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
3559
3560 let mut t = l.clone();
3561 t.entry_type = GovernanceLogEntryType::StewardVeto;
3562 assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
3563
3564 let mut t = l.clone();
3565 t.attestation.data_hash = data_hash(&json!({"a": 2}));
3566 assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
3567
3568 let mut t = l.clone();
3571 t.attestation.signed_at -= chrono::Duration::seconds(1);
3572 assert_eq!(verify_link(&t, &pk), Err(LinkError::BadSignature));
3573
3574 let mut t = l.clone();
3575 t.attestation.envelope_version = 2;
3576 assert_eq!(verify_link(&t, &pk), Err(LinkError::UnsupportedVersion(2)));
3577 }
3578
3579 #[test]
3580 fn a_good_chain_verifies_in_any_input_order() {
3581 let (key, pk) = generate_keypair();
3582 let mut c = chain(&key, 4);
3583 c.reverse();
3584 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
3585 assert!(v.ok, "{v:#?}");
3586 assert_eq!(v.head, Some(gov(4)));
3587 assert_eq!(
3588 v.entries.iter().map(|e| e.chain_seq).collect::<Vec<_>>(),
3589 [1, 2, 3, 4]
3590 );
3591 assert!(v.entries.iter().all(|e| e.content_matches.is_none()
3592 && e.problem.is_none()
3593 && !e.out_of_order));
3594 assert_eq!(v.public_key, PublicKeyHex::from(&pk));
3595 }
3596
3597 #[test]
3598 fn empty_chain_is_ok_with_no_head() {
3599 let (_, pk) = generate_keypair();
3600 let v = verify_chain(&[], &pk, &anchored(&pk), &roots());
3601 assert!(v.ok);
3602 assert!(v.head.is_none());
3603 assert!(v.entries.is_empty());
3604 }
3605
3606 #[test]
3607 fn a_changed_entry_breaks_its_signature_and_the_next_link() {
3608 let (key, pk) = generate_keypair();
3609 let mut c = chain(&key, 3);
3610 let rewritten = link(
3613 &key,
3614 2,
3615 Some(&c[0]),
3616 &json!({"title": "Decision 2", "outcome": "REJECTED"}),
3617 at(21),
3618 );
3619 c[1] = rewritten;
3620 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
3621 assert!(!v.ok);
3622 assert!(
3623 v.entries[1].signature_valid && v.entries[1].link_valid,
3624 "the rewrite itself is well-formed: {:#?}",
3625 v.entries[1]
3626 );
3627 assert!(
3628 !v.entries[2].link_valid,
3629 "but entry 3 no longer points at it: {:#?}",
3630 v.entries[2]
3631 );
3632 assert!(
3633 v.entries[2]
3634 .problem
3635 .as_deref()
3636 .unwrap()
3637 .contains("prev_hash")
3638 );
3639 }
3640
3641 #[test]
3642 fn a_removed_entry_is_a_gap_and_a_broken_link() {
3643 let (key, pk) = generate_keypair();
3644 let mut c = chain(&key, 3);
3645 c.remove(1);
3646 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
3647 assert!(!v.ok);
3648 assert!(v.entries[0].link_valid);
3649 let p = v.entries[1].problem.as_deref().unwrap();
3650 assert!(p.contains("chain_seq 3 where 2 was expected"), "{p}");
3651 assert!(p.contains("prev_hash"), "{p}");
3652 }
3653
3654 #[test]
3655 fn a_second_genesis_is_rejected() {
3656 let (key, pk) = generate_keypair();
3657 let mut c = chain(&key, 2);
3658 let rogue = link(&key, 2, None, &json!({}), at(21));
3659 c[1] = rogue;
3660 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
3661 assert!(!v.ok);
3662 assert!(
3663 v.entries[1]
3664 .problem
3665 .as_deref()
3666 .unwrap()
3667 .contains("null mid-chain")
3668 );
3669 }
3670
3671 #[test]
3672 fn retroactive_and_out_of_order_are_recomputed_not_copied() {
3673 let (key, pk) = generate_keypair();
3674 let first = link(&key, 1, None, &json!({}), at(10 + 3600));
3675 let created = truncate_to_micros(at(5));
3678 let envelope = Envelope::new(
3679 gov(2),
3680 GovernanceLogEntryType::CouncilDecision,
3681 created,
3682 Some(first.attestation.entry_hash),
3683 data_hash(&json!({})),
3684 );
3685 let attestation = attest(&key, &envelope, 2, at(6));
3686 let mut second = GovernanceChainLink {
3687 id: gov(2),
3688 entry_type: GovernanceLogEntryType::CouncilDecision,
3689 created_at: created,
3690 attestation,
3691 data: None,
3692 texts: None,
3693 };
3694 second.attestation.retroactive = true; let v = verify_chain(&[first, second], &pk, &anchored(&pk), &roots());
3696 assert!(v.ok, "{v:#?}");
3697 assert!(v.entries[0].retroactive);
3698 assert!(!v.entries[1].retroactive, "recomputed from timestamps");
3699 assert!(v.entries[1].out_of_order);
3700 }
3701
3702 #[test]
3703 fn content_mismatch_settles_to_not_ok() {
3704 let (key, pk) = generate_keypair();
3705 let c = chain(&key, 1);
3706 let mut v = verify_chain(&c, &pk, &anchored(&pk), &roots());
3707 v.entries[0].content_matches = Some(true);
3708 assert!(v.clone().settle().ok);
3709 v.entries[0].content_matches = Some(false);
3710 assert!(!v.settle().ok);
3711 }
3712
3713 #[test]
3716 fn an_amendment_fills_amended_by_on_its_target() {
3717 let (key, pk) = generate_keypair();
3718 let mut c = Chain::new();
3719 let target = c.decision(&key);
3720 c.decision(&key);
3721 let amendment = AmendmentDraft::new(
3722 target,
3723 c.hash_at(1),
3724 AmendmentKind::NonPrecedential,
3725 "§1 (Red Team Cases Recharacterized)",
3726 "diagnostic finding — not citable as moderation precedent",
3727 )
3728 .unwrap()
3729 .with_authority(gov(5))
3730 .with_rationale("§5 leaves the ruling itself standing");
3731 let id = c.amend(&key, &amendment);
3732
3733 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3734 assert!(v.ok, "{v:#?}");
3735 assert_eq!(v.entries[0].amended_by, vec![id]);
3736 assert!(v.entries[1].amended_by.is_empty());
3737 assert!(!v.entries[0].redacted);
3738 assert_eq!(v.head, Some(amd(1)));
3739 assert_eq!(v.entries[2].content_matches, Some(true));
3741 assert_eq!(v.entries[0].content_matches, None);
3742 assert_eq!(
3743 standing([amendment.amendment.kind]),
3744 Standing::NonPrecedential
3745 );
3746 }
3747
3748 #[test]
3749 fn an_amendment_must_name_an_earlier_entry_by_its_exact_hash() {
3750 let (key, pk) = generate_keypair();
3751 let problem = |c: &Chain, at: usize| -> String {
3752 verify_chain(&c.links, &pk, &anchored(&pk), &roots()).entries[at]
3753 .problem
3754 .clone()
3755 .unwrap_or_default()
3756 };
3757
3758 let mut c = Chain::new();
3759 c.decision(&key);
3760 let unknown = AmendmentDraft::new(
3761 gov(99),
3762 c.hash_at(1),
3763 AmendmentKind::Overruled,
3764 "b",
3765 "n",
3766 )
3767 .unwrap();
3768 c.amend(&key, &unknown);
3769 assert!(
3770 problem(&c, 1).contains("is not an entry of this chain"),
3771 "{}",
3772 problem(&c, 1)
3773 );
3774 assert!(!verify_chain(&c.links, &pk, &anchored(&pk), &roots()).ok);
3775
3776 let mut c = Chain::new();
3778 c.decision(&key);
3779 let forward = AmendmentDraft::new(
3780 gov(2),
3781 c.hash_at(1),
3782 AmendmentKind::Overruled,
3783 "b",
3784 "n",
3785 )
3786 .unwrap();
3787 c.amend(&key, &forward);
3788 c.decision(&key);
3789 assert!(
3790 problem(&c, 1).contains("not an earlier entry"),
3791 "{}",
3792 problem(&c, 1)
3793 );
3794
3795 let mut c = Chain::new();
3797 let target = c.decision(&key);
3798 let wrong_hash = AmendmentDraft::new(
3799 target,
3800 data_hash(&json!("some other entry")),
3801 AmendmentKind::Overruled,
3802 "b",
3803 "n",
3804 )
3805 .unwrap();
3806 c.amend(&key, &wrong_hash);
3807 assert!(problem(&c, 1).contains("entry_hash"), "{}", problem(&c, 1));
3808 assert!(
3809 verify_chain(&c.links, &pk, &anchored(&pk), &roots()).entries[0]
3810 .amended_by
3811 .is_empty()
3812 );
3813 }
3814
3815 #[test]
3816 fn redaction_shape_violations_fail_verification() {
3817 let (key, pk) = generate_keypair();
3818 let amend_with = |mutate: &dyn Fn(&mut Amendment)| -> String {
3819 let mut c = Chain::new();
3820 let target = c.decision(&key);
3821 let mut amendment = AmendmentDraft::new(
3822 target,
3823 c.hash_at(1),
3824 AmendmentKind::Correction,
3825 "b",
3826 "n",
3827 )
3828 .unwrap();
3829 mutate(&mut amendment.amendment);
3830 c.amend_v1(&key, &amendment.amendment);
3831 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3832 assert!(!v.ok, "{v:#?}");
3833 v.entries[1].problem.clone().unwrap_or_default()
3834 };
3835
3836 assert_eq!(
3839 AmendmentDraft::new(
3840 gov(1),
3841 data_hash(&json!(null)),
3842 AmendmentKind::Redaction,
3843 "b",
3844 "n"
3845 ),
3846 Err(AmendmentError::MissingRedaction)
3847 );
3848 let p = amend_with(&|a| a.kind = AmendmentKind::Redaction);
3849 assert!(p.contains("requires a `redaction`"), "{p}");
3850
3851 let p = amend_with(&|a| {
3852 a.redaction = Some(Redaction {
3853 fields: vec!["/x".into()],
3854 resulting_data_hash: data_hash(&json!({})),
3855 resulting_latest_hash: None,
3856 })
3857 });
3858 assert!(p.contains("only valid on kind"), "{p}");
3859
3860 let p = amend_with(&|a| a.agora_governance_amendment = 3);
3861 assert!(p.contains("agora_governance_amendment is 3"), "{p}");
3862
3863 let p = amend_with(&|a| a.agora_governance_amendment = 1);
3866 assert!(p.contains("does neither consistently"), "{p}");
3867 let p = amend_with(&|a| a.note = AmendmentText::Plain("n".into()));
3868 assert!(p.contains("does neither consistently"), "{p}");
3869 }
3870
3871 #[test]
3872 fn governance_data_holds_no_number_that_is_not_a_64_bit_integer() {
3873 let fine = json!({"a": [1, -2, u64::MAX, i64::MIN], "b": {"c": "0.5"}});
3874 assert_eq!(non_integer_number(&fine), None);
3875 assert!(blind_data(&fine, Blind::random()).is_ok());
3876
3877 for (text, pointer) in [
3878 (r#"{"a": {"b/c": [1, 0.5]}}"#, "/a/b~1c/1"),
3879 (r#"{"n": 1.0}"#, "/n"),
3880 (r#"{"n": 1e3}"#, "/n"),
3881 (r#"{"n": 18446744073709551616}"#, "/n"),
3882 (r#"{"n": -9223372036854775809}"#, "/n"),
3883 ] {
3884 let data: serde_json::Value = serde_json::from_str(text).unwrap();
3885 assert_eq!(non_integer_number(&data).as_deref(), Some(pointer));
3886 assert_eq!(
3887 blind_data(&data, Blind::random()),
3888 Err(BlindError::NonIntegerNumber(pointer.into())),
3889 "the writer refuses it"
3890 );
3891 assert_eq!(
3892 redact_data(&data, &["/n".into()], &amd(1), Blind::random()),
3893 Err(RedactError::NonIntegerNumber(pointer.into()))
3894 );
3895 }
3896 }
3897
3898 #[test]
3899 fn standing_is_the_last_amendment_that_changes_it() {
3900 use AmendmentKind::*;
3901 assert_eq!(standing([]), Standing::InForce);
3902 assert_eq!(standing([Correction, Redaction]), Standing::InForce);
3903 assert_eq!(
3904 standing([Correction, NonPrecedential, Redaction]),
3905 Standing::NonPrecedential
3906 );
3907 assert_eq!(standing([Overruled, Reinstated]), Standing::InForce);
3908 assert_eq!(standing([Reinstated, Superseded]), Standing::Superseded);
3909 assert_eq!(kind_standing(Reattested), None);
3910 assert_eq!(kind_standing(Reinstated), Some(Standing::InForce));
3911 }
3912
3913 #[test]
3914 fn a_redaction_verifies_against_the_amendment_and_nothing_else() {
3915 let (key, pk) = generate_keypair();
3916 let data = json!({
3917 "finding": "upheld",
3918 "subject": {"handle": "someone", "detail": "personal"},
3919 });
3920 let mut c = Chain::new();
3921 let target = c.entry(&key, data.clone());
3922 let amendment_id = c.next_amd();
3923 let (amendment, redacted) = AmendmentDraft::redaction(
3924 &amendment_id,
3925 target,
3926 c.hash_at(1),
3927 "GDPR Art. 17(1)(a)",
3928 "personal data removed on request",
3929 vec!["/subject/handle".into(), "/subject/detail".into()],
3930 &data,
3931 Blind::from([7; 32]),
3932 &[],
3933 )
3934 .unwrap();
3935 assert_eq!(c.amend(&key, &amendment), amendment_id);
3936 assert_eq!(redacted[BLIND_KEY], json!(Blind::from([7; 32])));
3937
3938 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3939 assert!(v.ok, "{v:#?}");
3940 assert!(v.entries[0].redacted);
3941 assert_eq!(v.entries[0].redacted_data_hash, Some(data_hash(&redacted)));
3942 assert_eq!(
3943 redacted["subject"]["handle"],
3944 json!(format!("[redacted by {amendment_id}]"))
3945 );
3946 assert_eq!(redacted["finding"], json!("upheld"), "and nothing else");
3947
3948 assert!(v.check_content(&c.links[0], &redacted));
3950 assert_eq!(v.entries[0].content_matches, Some(true));
3951 assert!(v.clone().settle().ok);
3952 assert!(v.check_content(&c.links[0], &data));
3954
3955 let mut tampered = redacted.clone();
3957 tampered["finding"] = json!("overturned");
3958 assert!(!v.check_content(&c.links[0], &tampered));
3959 assert_eq!(v.entries[0].content_matches, Some(false));
3960 assert!(!v.settle().ok);
3961 }
3962
3963 fn seats() -> serde_json::Value {
3966 json!({
3967 "title": "A motion",
3968 "rounds": [{
3969 "number": 1,
3970 "responses": [
3971 {"role": "lawyer", "rationale": "Because.", "raw_text": "Because.", "vote": "yes"},
3972 {"role": "artist", "rationale": "Why not.", "raw_text": "Why not?", "vote": "no"},
3973 ],
3974 }],
3975 "subject": {"handle": "someone"},
3976 BLIND_KEY: Blind::from([3; 32]),
3977 })
3978 }
3979
3980 const LAWYER: &str = "/rounds/0/responses/0";
3981
3982 fn patch(ops: serde_json::Value) -> json_patch::Patch {
3984 serde_json::from_value(ops).unwrap()
3985 }
3986
3987 fn dedup(data: &serde_json::Value) -> Edit {
3989 let (raw, rationale) =
3990 (format!("{LAWYER}/raw_text"), format!("{LAWYER}/rationale"));
3991 Revision::remove_duplicates(data, &[(&raw, &rationale)]).unwrap()
3992 }
3993
3994 fn revise(
3995 c: &Chain,
3996 target: &GovernanceLogId,
3997 latest: &serde_json::Value,
3998 patch: impl Into<Edit>,
3999 ) -> (AmendmentDraft, serde_json::Value) {
4000 AmendmentDraft::revision(
4001 target.clone(),
4002 GovernanceLogEntryType::CouncilDecision,
4003 c.hash_at(1),
4004 "Steward's record REC-2026-0001",
4005 "raw_text identical to the rationale removed",
4006 latest,
4007 patch,
4008 )
4009 .unwrap()
4010 }
4011
4012 fn revision_of(draft: &AmendmentDraft) -> &Revision {
4013 draft.amendment.revision.as_ref().unwrap()
4014 }
4015
4016 #[test]
4017 fn a_revision_overwrites_nothing_and_folds_to_the_latest() {
4018 let (key, pk) = generate_keypair();
4019 let data = seats();
4020 let mut c = Chain::new();
4021 let target = c.entry(&key, data.clone());
4022 let (first, v1) = revise(&c, &target, &data, dedup(&data));
4023 c.amend(&key, &first);
4024 let (second, v2) = revise(
4025 &c,
4026 &target,
4027 &v1,
4028 patch(json!([{"op": "move", "from": "/title", "path": "/motion"}])),
4029 );
4030 c.amend(&key, &second);
4031
4032 let seat = &v1["rounds"][0]["responses"];
4033 assert!(seat[0].get("raw_text").is_none(), "removed, no marker");
4034 assert_eq!(seat[1]["raw_text"], json!("Why not?"), "the other stays");
4035 assert_eq!(v2["motion"], json!("A motion"));
4036 assert_eq!(v2[BLIND_KEY], data[BLIND_KEY], "blind kept");
4037 assert_eq!(
4038 latest(&data, [revision_of(&first), revision_of(&second)]).unwrap(),
4039 v2
4040 );
4041
4042 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4043 assert!(v.ok, "{v:#?}");
4044 let entry = &v.entries[0];
4045 assert_eq!(entry.revisions, [amd(1), amd(2)]);
4046 assert_eq!(entry.latest_data_hash, Some(data_hash(&v2)));
4047 assert!(!entry.redacted && entry.redacted_data_hash.is_none());
4048
4049 assert!(v.check_content(&c.links[0], &data));
4051 assert!(v.check_content(&c.links[0], &v2));
4052 assert!(!v.check_content(&c.links[0], &v1), "not the latest");
4053 v.check_content(&c.links[0], &data);
4054 assert!(v.clone().settle().ok);
4055
4056 let wire = serde_json::to_value(&v).unwrap();
4059 assert!(wire["entries"][0].get("history").is_none());
4060 let mut old = wire.clone();
4061 for field in ["revisions", "latest_data_hash", "superseded_revisions"] {
4062 old["entries"][0].as_object_mut().unwrap().remove(field);
4063 }
4064 let old: GovernanceVerification = serde_json::from_value(old).unwrap();
4065 assert!(old.entries[0].revisions.is_empty());
4066 }
4067
4068 #[test]
4070 fn a_revision_that_does_not_produce_its_hash_fails() {
4071 let (key, pk) = generate_keypair();
4072 let data = seats();
4073 let mut c = Chain::new();
4074 let target = c.entry(&key, data.clone());
4075 let (mut draft, _) = revise(&c, &target, &data, dedup(&data));
4076 draft
4077 .amendment
4078 .revision
4079 .as_mut()
4080 .unwrap()
4081 .resulting_data_hash = data_hash(&json!({"something": "else"}));
4082 c.amend(&key, &draft);
4083
4084 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4085 assert!(v.ok, "the chain itself is sound: {v:#?}");
4086 assert!(v.check_content(&c.links[0], &data), "the stored data is");
4087 assert!(
4088 v.entries[0]
4089 .problem
4090 .as_deref()
4091 .is_some_and(|p| p.contains("does not produce")),
4092 "{:?}",
4093 v.entries[0].problem
4094 );
4095 assert!(!v.settle().ok);
4096
4097 c.links[0].data = Some(data);
4099 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4100 assert!(!v.ok);
4101 }
4102
4103 fn redact(
4104 c: &Chain,
4105 target: &GovernanceLogId,
4106 fields: &[&str],
4107 data: &serde_json::Value,
4108 revisions: &[&Revision],
4109 ) -> Result<(AmendmentDraft, serde_json::Value), RedactError> {
4110 AmendmentDraft::redaction(
4111 &c.next_amd(),
4112 target.clone(),
4113 c.hash_at(1),
4114 "GDPR Art. 17(1)(a)",
4115 "removed on request",
4116 fields.iter().map(|f| f.to_string()).collect(),
4117 data,
4118 Blind::from([9; 32]),
4119 revisions,
4120 )
4121 }
4122
4123 #[test]
4126 fn a_revision_then_a_redaction() {
4127 let (key, pk) = generate_keypair();
4128 let data = seats();
4129 let mut c = Chain::new();
4130 let target = c.entry(&key, data.clone());
4131 let (revision, _) = revise(&c, &target, &data, dedup(&data));
4132 c.amend(&key, &revision);
4133 let rationale = format!("{LAWYER}/rationale");
4134 let (redaction, redacted) = redact(
4135 &c,
4136 &target,
4137 &[&rationale],
4138 &data,
4139 &[revision_of(&revision)],
4140 )
4141 .unwrap();
4142 c.amend(&key, &redaction);
4143
4144 let fields = &redaction.amendment.redaction.as_ref().unwrap().fields;
4146 assert_eq!(fields, &[rationale, format!("{LAWYER}/raw_text")]);
4147 let seat = &redacted["rounds"][0]["responses"][0];
4148 assert_eq!(seat["raw_text"], seat["rationale"]);
4149 assert!(seat["raw_text"].as_str().unwrap().starts_with("[redacted"));
4150
4151 let rebased = latest(&redacted, [revision_of(&revision)]).unwrap();
4152 assert!(
4153 rebased["rounds"][0]["responses"][0]
4154 .get("raw_text")
4155 .is_none()
4156 );
4157 assert_eq!(
4158 redaction
4159 .amendment
4160 .redaction
4161 .as_ref()
4162 .unwrap()
4163 .resulting_latest_hash,
4164 Some(data_hash(&rebased))
4165 );
4166
4167 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4168 assert!(v.ok, "{v:#?}");
4169 let entry = &v.entries[0];
4170 assert_eq!(entry.latest_data_hash, Some(data_hash(&rebased)));
4171 assert_eq!(entry.superseded_revisions, [amd(1)]);
4172 assert!(v.check_content(&c.links[0], &redacted));
4173 assert!(v.check_content(&c.links[0], &rebased));
4174 assert!(
4175 v.check_content(&c.links[0], &data),
4176 "for whoever kept a copy"
4177 );
4178 v.check_content(&c.links[0], &redacted);
4179 assert!(v.clone().settle().ok, "{v:#?}");
4180
4181 let mut c2 = Chain::new();
4183 let target = c2.entry(&key, data.clone());
4184 c2.amend(&key, &revision);
4185 let (mut lying, _) = redact(
4186 &c2,
4187 &target,
4188 &["/subject/handle"],
4189 &data,
4190 &[revision_of(&revision)],
4191 )
4192 .unwrap();
4193 let (_, honest) = redact(
4194 &c2,
4195 &target,
4196 &["/subject/handle"],
4197 &data,
4198 &[revision_of(&revision)],
4199 )
4200 .unwrap();
4201 lying
4202 .amendment
4203 .redaction
4204 .as_mut()
4205 .unwrap()
4206 .resulting_latest_hash = Some(data_hash(&json!({})));
4207 c2.amend(&key, &lying);
4208 let mut v = verify_chain(&c2.links, &pk, &anchored(&pk), &roots());
4209 assert!(v.check_content(&c2.links[0], &honest));
4210 assert!(!v.settle().ok);
4211 }
4212
4213 #[test]
4216 fn a_redaction_of_a_revised_entry_must_name_the_latest_hash() {
4217 let (key, pk) = generate_keypair();
4218 let data = seats();
4219 let mut c = Chain::new();
4220 let target = c.entry(&key, data.clone());
4221 let (revision, _) = revise(&c, &target, &data, dedup(&data));
4222 c.amend(&key, &revision);
4223 let (redaction, _) =
4225 redact(&c, &target, &["/subject/handle"], &data, &[]).unwrap();
4226 c.amend(&key, &redaction);
4227 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4228 assert!(!v.ok);
4229 assert!(
4230 v.entries[2]
4231 .problem
4232 .as_deref()
4233 .is_some_and(|p| p.contains("names no resulting_latest_hash")),
4234 "{v:#?}"
4235 );
4236 }
4237
4238 #[test]
4241 fn a_false_duplicate_fails() {
4242 let (key, pk) = generate_keypair();
4243 let data = seats();
4244 let mut c = Chain::new();
4245 let target = c.entry(&key, data.clone());
4246 let artist = "/rounds/0/responses/1";
4247 let (raw, rationale) =
4248 (format!("{artist}/raw_text"), format!("{artist}/rationale"));
4249 let (mut lying, _) = revise(
4251 &c,
4252 &target,
4253 &data,
4254 patch(json!([{"op": "remove", "path": raw}])),
4255 );
4256 let revised = apply_patch(&data, &revision_of(&lying).patch).unwrap();
4257 let r = lying.amendment.revision.as_mut().unwrap();
4258 r.duplicates = vec![(raw, rationale)];
4259 r.resulting_data_hash = data_hash(&revised);
4260 c.amend(&key, &lying);
4261
4262 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4263 assert!(v.ok, "unread, the claim is unchecked: {v:#?}");
4264 assert!(v.check_content(&c.links[0], &data));
4265 assert!(
4266 v.entries[0]
4267 .problem
4268 .as_deref()
4269 .is_some_and(|p| p.contains("but they differ")),
4270 "{v:#?}"
4271 );
4272 assert!(!v.settle().ok);
4273 }
4274
4275 #[test]
4277 fn a_redaction_that_breaks_a_revision_is_refused() {
4278 let (key, _) = generate_keypair();
4279 let data = seats();
4280 let mut c = Chain::new();
4281 let target = c.entry(&key, data.clone());
4282 let (moved, _) = revise(
4283 &c,
4284 &target,
4285 &data,
4286 patch(
4287 json!([{"op": "move", "from": "/subject/handle", "path": "/handle"}]),
4288 ),
4289 );
4290 c.amend(&key, &moved);
4291 let err =
4292 redact(&c, &target, &["/subject"], &data, &[revision_of(&moved)])
4293 .unwrap_err();
4294 assert!(matches!(err, RedactError::Rebase(_)), "{err}");
4295 assert!(
4297 redact(
4298 &c,
4299 &target,
4300 &["/subject/handle"],
4301 &data,
4302 &[revision_of(&moved)]
4303 )
4304 .is_ok()
4305 );
4306 }
4307
4308 #[test]
4311 fn a_redaction_follows_every_duplicate_of_what_it_erases() {
4312 let revision = Revision {
4313 patch: patch(json!([{"op": "remove", "path": "/copy"}])),
4314 duplicates: vec![("/copy".into(), "/source/inner".into())],
4315 resulting_data_hash: data_hash(&json!(null)),
4316 };
4317 let extra = |fields: &[&str]| {
4318 let fields: Vec<String> =
4319 fields.iter().map(|f| f.to_string()).collect();
4320 duplicates_of(&fields, &[&revision])
4321 };
4322 assert_eq!(extra(&["/source/inner"]), ["/copy"]);
4323 assert_eq!(extra(&["/source/inner/name"]), ["/copy/name"]);
4324 assert_eq!(extra(&["/source"]), ["/copy"]);
4325 assert!(extra(&["/source/other"]).is_empty());
4326 assert!(extra(&["/source/inn"]).is_empty());
4327 }
4328
4329 #[test]
4332 fn a_redaction_then_a_revision() {
4333 let (key, pk) = generate_keypair();
4334 let data = seats();
4335 let mut c = Chain::new();
4336 let target = c.entry(&key, data.clone());
4337 let (redaction, redacted) =
4338 redact(&c, &target, &["/subject/handle"], &data, &[]).unwrap();
4339 assert_eq!(
4340 redaction
4341 .amendment
4342 .redaction
4343 .as_ref()
4344 .unwrap()
4345 .resulting_latest_hash,
4346 None,
4347 "nothing to rebase"
4348 );
4349 c.amend(&key, &redaction);
4350 let (revision, revised) =
4351 revise(&c, &target, &redacted, dedup(&redacted));
4352 c.amend(&key, &revision);
4353
4354 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4355 assert!(v.ok, "{v:#?}");
4356 assert_eq!(v.entries[0].latest_data_hash, Some(data_hash(&revised)));
4357 assert!(v.entries[0].superseded_revisions.is_empty());
4358 assert!(v.check_content(&c.links[0], &redacted));
4359 assert!(v.check_content(&c.links[0], &revised));
4360 assert!(v.settle().ok);
4361 }
4362
4363 #[test]
4364 fn a_revision_refuses_what_it_should() {
4365 let data = seats();
4366 let raw = &format!("{LAWYER}/raw_text");
4367 let rationale = &format!("{LAWYER}/rationale");
4368 let artist = "/rounds/0/responses/1";
4369 let dedup =
4370 |pairs: &[(&str, &str)]| Revision::remove_duplicates(&data, pairs);
4371
4372 assert_eq!(
4373 dedup(&[(
4374 &format!("{artist}/raw_text"),
4375 &format!("{artist}/rationale")
4376 )]),
4377 Err(ReviseError::NotIdentical {
4378 path: format!("{artist}/raw_text"),
4379 same_as: format!("{artist}/rationale"),
4380 })
4381 );
4382 assert_eq!(
4383 dedup(&[(raw, rationale), (rationale, raw)]),
4384 Err(ReviseError::SourceRemoved {
4385 path: raw.clone(),
4386 same_as: rationale.clone(),
4387 }),
4388 "a same_as the patch removes"
4389 );
4390 assert_eq!(
4391 dedup(&[("/nope", rationale)]),
4392 Err(ReviseError::Unresolved("/nope".into()))
4393 );
4394
4395 let make = |target_type, p: Edit| {
4396 AmendmentDraft::revision(
4397 gov(1),
4398 target_type,
4399 data_hash(&json!(null)),
4400 "b",
4401 "n",
4402 &data,
4403 p,
4404 )
4405 .map(|(_, v)| v)
4406 };
4407 let council = GovernanceLogEntryType::CouncilDecision;
4408 assert_eq!(
4409 make(council, patch(json!([])).into()),
4410 Err(ReviseError::EmptyPatch)
4411 );
4412 assert!(matches!(
4413 make(
4414 council,
4415 patch(json!([{"op": "remove", "path": "/nope"}])).into()
4416 ),
4417 Err(ReviseError::Patch(_))
4418 ));
4419 assert_eq!(
4420 make(
4421 council,
4422 patch(json!([{"op": "remove", "path": "/_blind"}])).into()
4423 ),
4424 Err(ReviseError::BlindPointer("/_blind".into()))
4425 );
4426 assert_eq!(
4427 make(
4428 council,
4429 patch(json!([{"op": "copy", "from": "/_blind", "path": "/b"}]))
4430 .into()
4431 ),
4432 Err(ReviseError::BlindPointer("/_blind".into()))
4433 );
4434 for entry_type in [
4435 GovernanceLogEntryType::Amendment,
4436 GovernanceLogEntryType::KeyRotation,
4437 GovernanceLogEntryType::StewardRecord,
4438 ] {
4439 assert!(!is_revisable(entry_type));
4440 assert_eq!(
4441 make(entry_type, dedup(&[(raw, rationale)]).unwrap()),
4442 Err(ReviseError::NotRevisable(entry_type))
4443 );
4444 }
4445 assert!(is_revisable(council));
4446 }
4447
4448 #[test]
4450 fn a_revision_that_adds_content_blinds_the_target() {
4451 let unblinded =
4452 json!({"title": "Old", "rationale": "r", "raw_text": "r"});
4453 let revise = |data: &serde_json::Value, p: serde_json::Value| {
4454 AmendmentDraft::revision(
4455 gov(1),
4456 GovernanceLogEntryType::CouncilDecision,
4457 data_hash(&json!(null)),
4458 "b",
4459 "n",
4460 data,
4461 patch(p),
4462 )
4463 .unwrap()
4464 };
4465 let add = json!([{"op": "add", "path": "/attachments", "value": []}]);
4466 let (draft, revised) = revise(&unblinded, add.clone());
4467 assert!(revised.get(BLIND_KEY).is_some());
4468 assert_eq!(revision_of(&draft).patch.len(), 2, "in the same patch");
4469 assert_eq!(
4470 latest(&unblinded, [revision_of(&draft)]).unwrap(),
4471 revised,
4472 "so the fold reproduces it"
4473 );
4474
4475 let (_, revised) =
4476 revise(&unblinded, json!([{"op": "remove", "path": "/raw_text"}]));
4477 assert!(revised.get(BLIND_KEY).is_none(), "a removal adds nothing");
4478
4479 let (draft, revised) = revise(&seats(), add);
4480 assert_eq!(revised[BLIND_KEY], seats()[BLIND_KEY], "never rotated");
4481 assert_eq!(revision_of(&draft).patch.len(), 1);
4482 }
4483
4484 #[test]
4485 fn revision_shape_violations_fail_verification() {
4486 let (key, pk) = generate_keypair();
4487 let amend_with = |mutate: &dyn Fn(&mut Amendment)| -> String {
4488 let mut c = Chain::new();
4489 let target = c.entry(&key, seats());
4490 let (mut draft, _) = revise(&c, &target, &seats(), dedup(&seats()));
4491 mutate(&mut draft.amendment);
4492 c.amend_v1(&key, &draft.amendment);
4493 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4494 assert!(!v.ok, "{v:#?}");
4495 v.entries[1].problem.clone().unwrap_or_default()
4496 };
4497 assert_eq!(
4498 AmendmentDraft::new(
4499 gov(1),
4500 data_hash(&json!(null)),
4501 AmendmentKind::Revision,
4502 "b",
4503 "n"
4504 ),
4505 Err(AmendmentError::MissingRevision)
4506 );
4507 let p = amend_with(&|a| a.revision = None);
4508 assert!(p.contains("requires a `revision`"), "{p}");
4509 let p = amend_with(&|a| a.kind = AmendmentKind::Correction);
4510 assert!(p.contains("only valid on kind `revision`"), "{p}");
4511 let p = amend_with(&|a| a.revision.as_mut().unwrap().patch.0.clear());
4512 assert!(p.contains("at least one op"), "{p}");
4513 }
4514
4515 #[cfg(feature = "schemars")]
4517 #[test]
4518 fn the_revision_schema_describes_a_patch() {
4519 let schema = crate::responses::inline_schema_for::<Amendment>();
4520 let revision = &schema["properties"]["revision"]["properties"];
4521 assert_eq!(revision["patch"]["type"], json!("array"), "{schema}");
4522 assert_eq!(
4523 revision["patch"]["items"]["properties"]["op"]["enum"],
4524 json!(["add", "remove", "replace", "move", "copy", "test"])
4525 );
4526 assert_eq!(revision["resulting_data_hash"]["type"], json!("string"));
4527 }
4528
4529 #[test]
4530 fn content_that_matches_nothing_fails_without_an_amendment() {
4531 let (key, pk) = generate_keypair();
4532 let mut c = Chain::new();
4533 c.entry(&key, json!({"a": 1}));
4534 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4535 assert!(!v.check_content(&c.links[0], &json!({"a": 2})));
4536 assert!(!v.clone().settle().ok);
4537 let other = link(&key, 9, None, &json!({}), at(9));
4539 assert!(!v.check_content(&other, &json!({})));
4540 }
4541
4542 #[test]
4543 fn tampering_with_an_amendments_data_is_caught_by_the_data_hash() {
4544 let (key, pk) = generate_keypair();
4545 let mut c = Chain::new();
4546 let target = c.decision(&key);
4547 let amendment = AmendmentDraft::new(
4548 target,
4549 c.hash_at(1),
4550 AmendmentKind::Overruled,
4551 "b",
4552 "overruled by a later decision",
4553 )
4554 .unwrap();
4555 c.amend(&key, &amendment);
4556 c.links[1].data.as_mut().unwrap()["note"] =
4557 json!("reinstated, actually");
4558
4559 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4560 assert!(!v.ok, "{v:#?}");
4561 let p = v.entries[1].problem.as_deref().unwrap();
4562 assert!(p.contains("does not hash to the attested data_hash"), "{p}");
4563 assert!(
4564 v.entries[0].amended_by.is_empty(),
4565 "an unreadable amendment has no effect"
4566 );
4567 assert!(
4568 v.entries[1].signature_valid && v.entries[1].link_valid,
4569 "the envelope is untouched — only the content is not what it \
4570 committed to"
4571 );
4572 }
4573
4574 #[test]
4575 fn an_amendment_or_rotation_must_carry_its_data() {
4576 let (key, pk) = generate_keypair();
4577 let mut c = Chain::new();
4578 let target = c.decision(&key);
4579 let amendment = AmendmentDraft::new(
4580 target,
4581 c.hash_at(1),
4582 AmendmentKind::Correction,
4583 "b",
4584 "n",
4585 )
4586 .unwrap();
4587 c.amend(&key, &amendment);
4588 let rotation = c.routine(&pk, &generate_keypair().0);
4589 c.rotate(&key, &rotation);
4590 c.links[1].data = None;
4591 c.links[2].data = None;
4592
4593 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4594 assert!(!v.ok, "{v:#?}");
4595 for (i, entry_type) in [(1, "amendment"), (2, "key_rotation")] {
4596 let p = v.entries[i].problem.as_deref().unwrap();
4597 assert!(p.contains(&format!("a {entry_type} entry")), "{p}");
4598 assert!(p.contains("must carry its `data`"), "{p}");
4599 }
4600 }
4601
4602 #[test]
4603 fn the_id_series_must_match_the_entry_type() {
4604 let (key, pk) = generate_keypair();
4605 let mut c = Chain::new();
4606 let target = c.decision(&key);
4607 let amendment = AmendmentDraft::new(
4608 target,
4609 c.hash_at(1),
4610 AmendmentKind::Correction,
4611 "b",
4612 "n",
4613 )
4614 .unwrap();
4615 c.push(
4616 &key,
4617 gov(7),
4618 GovernanceLogEntryType::Amendment,
4619 serde_json::to_value(&amendment.amendment).unwrap(),
4620 true,
4621 );
4622 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4623 assert!(!v.ok, "{v:#?}");
4624 let p = v.entries[1].problem.as_deref().unwrap();
4625 assert!(p.contains("must be in the AMD- series"), "{p}");
4626
4627 let mut c = Chain::new();
4628 c.push(
4629 &key,
4630 key_id(1),
4631 GovernanceLogEntryType::CouncilDecision,
4632 json!({}),
4633 false,
4634 );
4635 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4636 let p = v.entries[0].problem.as_deref().unwrap();
4637 assert!(p.contains("reserved"), "{p}");
4638 }
4639
4640 #[test]
4641 fn redact_data_replaces_whole_values_and_refuses_the_rest() {
4642 let id = amd(3);
4643 let blind = Blind::from([9; 32]);
4644 let data = json!({"a": {"b": [1, {"c": "secret"}]}, "d/e": "slash"});
4645 let out = redact_data(
4646 &data,
4647 &["/a/b/1/c".into(), "/d~1e".into()],
4648 &id,
4649 blind,
4650 )
4651 .unwrap();
4652 assert_eq!(out["a"]["b"][1]["c"], json!(redaction_marker(&id)));
4653 assert_eq!(out["d/e"], json!(redaction_marker(&id)));
4654 assert_eq!(out["a"]["b"][0], json!(1), "untouched");
4655 assert_eq!(out[BLIND_KEY], json!(blind), "a legacy entry gains one");
4656
4657 assert_eq!(
4658 redact_data(&data, &["/a/nope".into()], &id, blind),
4659 Err(RedactError::Unresolved("/a/nope".into()))
4660 );
4661 assert_eq!(
4662 redact_data(&data, &["".into()], &id, blind),
4663 Err(RedactError::WholeEntry)
4664 );
4665 assert_eq!(
4666 redact_data(&data, &[], &id, blind),
4667 Err(RedactError::NoFields)
4668 );
4669 assert_eq!(
4670 redact_data(&data, &["/_blind".into()], &id, blind),
4671 Err(RedactError::BlindPointer("/_blind".into()))
4672 );
4673 }
4674
4675 #[test]
4676 fn blind_data_is_for_objects_and_is_the_writers_to_supply() {
4677 let blind = Blind::from([1; 32]);
4678 let out = blind_data(&json!({"finding": "upheld"}), blind).unwrap();
4679 assert_eq!(out, json!({"finding": "upheld", "_blind": blind}));
4680 assert_eq!(
4681 blind_data(&json!([1]), blind),
4682 Err(BlindError::NotAnObject)
4683 );
4684 assert_eq!(blind_data(&out, blind), Err(BlindError::AlreadyBlinded));
4685 assert_ne!(Blind::random(), Blind::random());
4686
4687 use GovernanceLogEntryType::*;
4688 assert!(!is_redactable(Amendment) && !is_redactable(KeyRotation));
4689 assert!(
4690 is_redactable(CouncilDecision) && is_redactable(EmergencyAction)
4691 );
4692 }
4693
4694 #[test]
4698 fn a_removed_value_cannot_be_confirmed_by_guessing_it() {
4699 fn confirms(
4701 public: &serde_json::Value,
4702 pointer: &str,
4703 guess: &str,
4704 hash: Sha256Hex,
4705 ) -> bool {
4706 let mut attempt = public.clone();
4707 *attempt.pointer_mut(pointer).unwrap() = json!(guess);
4708 data_hash(&attempt) == hash
4709 }
4710 let legacy = json!({"finding": "upheld", "handle": "someone", "city": "Utrecht"});
4711
4712 let written = blind_data(&legacy, Blind::random()).unwrap();
4714 let first = redact_data(
4715 &written,
4716 &["/handle".into()],
4717 &amd(1),
4718 Blind::random(),
4719 )
4720 .unwrap();
4721 assert!(!confirms(&first, "/handle", "someone", data_hash(&written)));
4722
4723 let second =
4726 redact_data(&first, &["/city".into()], &amd(2), Blind::random())
4727 .unwrap();
4728 assert!(!confirms(&second, "/city", "Utrecht", data_hash(&first)));
4729
4730 let mut stripped =
4734 redact_data(&legacy, &["/handle".into()], &amd(3), Blind::random())
4735 .unwrap();
4736 let legacy_first = stripped.clone();
4737 stripped.as_object_mut().unwrap().remove(BLIND_KEY);
4738 assert!(confirms(
4739 &stripped,
4740 "/handle",
4741 "someone",
4742 data_hash(&legacy)
4743 ));
4744 let legacy_second = redact_data(
4746 &legacy_first,
4747 &["/city".into()],
4748 &amd(4),
4749 Blind::random(),
4750 )
4751 .unwrap();
4752 assert!(!confirms(
4753 &legacy_second,
4754 "/city",
4755 "Utrecht",
4756 data_hash(&legacy_first)
4757 ));
4758 }
4759
4760 #[test]
4763 fn a_routine_rotation_moves_the_chain_to_the_new_key() {
4764 let (old, old_pk) = generate_keypair();
4765 let (new, new_pk) = generate_keypair();
4766 let mut c = Chain::new();
4767 c.decision(&old);
4768 let rotation = c.routine(&old_pk, &new);
4769 let rotation_id = c.rotate(&old, &rotation);
4770 c.decision(&new);
4771
4772 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
4775 assert!(v.ok, "{v:#?}");
4776 assert_eq!(v.public_key, (&new_pk).into());
4777 assert_eq!(
4778 v.entries[1].signed_by,
4779 Some((&old_pk).into()),
4780 "the rotation itself is signed by the old key"
4781 );
4782 assert_eq!(v.entries[2].signed_by, Some((&new_pk).into()));
4783 assert!(v.unanchored_keys.is_empty());
4784 assert!(v.repudiated.is_empty());
4785 assert_eq!(v.keys.len(), 2);
4786 assert_eq!(v.keys[0].public_key, (&old_pk).into());
4787 assert_eq!(v.keys[0].from_seq, 1);
4788 assert_eq!(v.keys[0].through_seq, Some(2));
4789 assert_eq!(v.keys[0].status, KeyStatus::Retired);
4790 assert_eq!(v.keys[0].introduced_by, None);
4791 assert_eq!(v.keys[0].retired_by.as_ref(), Some(&rotation_id));
4792 assert!(v.keys[0].certified, "retroactively, by the rotation");
4793 assert_eq!(v.keys[1].from_seq, 3);
4794 assert_eq!(v.keys[1].through_seq, None);
4795 assert_eq!(v.keys[1].status, KeyStatus::Active);
4796 assert_eq!(v.keys[1].introduced_by.as_ref(), Some(&rotation_id));
4797 assert!(v.keys[1].certified);
4798 }
4799
4800 #[test]
4801 fn the_old_key_cannot_sign_after_a_routine_rotation() {
4802 let (old, old_pk) = generate_keypair();
4803 let (new, _) = generate_keypair();
4804 let mut c = Chain::new();
4805 c.decision(&old);
4806 let rotation = c.routine(&old_pk, &new);
4807 c.rotate(&old, &rotation);
4808 c.decision(&old);
4809
4810 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
4811 assert!(!v.ok, "{v:#?}");
4812 assert!(!v.entries[2].signature_valid);
4813 assert!(
4814 v.entries[2].link_valid,
4815 "the linkage is fine; the key is not"
4816 );
4817 }
4818
4819 #[test]
4822 fn the_online_key_cannot_certify_its_own_successor() {
4823 let (old, old_pk) = generate_keypair();
4824 let (thief, _) = generate_keypair();
4825 let mut c = Chain::new();
4826 c.decision(&old);
4827 let mut rotation = c.routine(&old_pk, &thief);
4828 let statement = rotation.certificate.statement.clone();
4830 rotation.certificate = certify(&[&old], statement);
4831 c.rotate(&old, &rotation);
4832 c.decision(&thief);
4833
4834 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
4835 assert!(!v.ok, "{v:#?}");
4836 let p = v.entries[1].problem.as_deref().unwrap();
4837 assert!(p.contains("0 valid root signature"), "{p}");
4838 assert_eq!(v.public_key, (&old_pk).into(), "the chain does not move");
4839 assert!(!v.entries[2].signature_valid);
4840 assert_eq!(v.keys.len(), 1);
4841 }
4842
4843 #[test]
4844 fn a_certificate_counts_distinct_known_roots_only() {
4845 let (old, old_pk) = generate_keypair();
4846 let (new, _) = generate_keypair();
4847 let (stranger, _) = generate_keypair();
4848 let two_of_two = RootSet::new(
4849 [
4850 (&root(1).verifying_key()).into(),
4851 (&root(2).verifying_key()).into(),
4852 ],
4853 2,
4854 );
4855 let verdict = |signers: &[&SigningKey], roots: &RootSet| {
4856 let mut c = Chain::new();
4857 c.decision(&old);
4858 let mut rotation = c.routine(&old_pk, &new);
4859 let statement = rotation.certificate.statement.clone();
4860 rotation.certificate = certify(signers, statement);
4861 let genesis = KeyCertStatement::genesis((&old_pk).into());
4863 rotation.outgoing_certificate =
4864 Some(certify(&[&root(1), &root(2)], genesis));
4865 c.rotate(&old, &rotation);
4866 verify_chain(&c.links, &old_pk, &anchored(&old_pk), roots)
4867 };
4868
4869 assert!(verdict(&[&root(1), &root(2)], &two_of_two).ok);
4870 assert!(verdict(&[&root(2)], &roots()).ok, "either root, 1-of-2");
4871
4872 for (signers, why) in [
4873 (vec![&root(1)], "below the threshold"),
4874 (vec![&root(1), &root(1)], "one root twice is one root"),
4875 (vec![&root(1), &stranger], "an unknown root is nobody"),
4876 (vec![], "unsigned"),
4877 ] {
4878 let v = verdict(&signers, &two_of_two);
4879 assert!(!v.ok, "{why}: {v:#?}");
4880 let p = v.entries[1].problem.as_deref().unwrap();
4881 assert!(p.contains("where 2 are needed"), "{why}: {p}");
4882 }
4883
4884 assert!(verdict(&[&stranger, &root(1)], &roots()).ok);
4886 }
4887
4888 #[test]
4889 fn a_certificate_is_good_for_one_statement_at_one_position() {
4890 let (old, old_pk) = generate_keypair();
4891 let (new, new_pk) = generate_keypair();
4892 let (other, other_pk) = generate_keypair();
4893 let anchor = anchored(&old_pk);
4894
4895 let mut c = Chain::new();
4899 c.decision(&old);
4900 let early = c.routine(&old_pk, &new);
4901 c.decision(&old);
4902 let mut rotation = c.routine(&old_pk, &new);
4903 rotation.certificate = early.certificate;
4904 c.rotate(&old, &rotation);
4905 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
4906 assert!(!v.ok, "{v:#?}");
4907 let p = v.entries[2].problem.as_deref().unwrap();
4908 assert!(
4909 p.contains("different key, purpose or chain position"),
4910 "{p}"
4911 );
4912 assert_eq!(v.public_key, (&old_pk).into());
4913
4914 let mut c = Chain::new();
4916 c.decision(&old);
4917 let for_new = c.routine(&old_pk, &new);
4918 let mut rotation = c.routine(&old_pk, &other);
4919 rotation.certificate = for_new.certificate;
4920 c.rotate(&old, &rotation);
4921 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
4922 let p = v.entries[1].problem.as_deref().unwrap();
4923 assert!(
4924 p.contains("different key, purpose or chain position"),
4925 "{p}"
4926 );
4927
4928 let mut c = Chain::new();
4930 c.decision(&old);
4931 let mut rotation = c.routine(&old_pk, &other);
4932 rotation.certificate = for_new_at(&c, &new_pk);
4933 rotation.certificate.statement.key = (&other_pk).into();
4934 c.rotate(&old, &rotation);
4935 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
4936 let p = v.entries[1].problem.as_deref().unwrap();
4937 assert!(p.contains("0 valid root signature"), "{p}");
4938
4939 let mut c = Chain::new();
4941 c.decision(&old);
4942 c.decision(&old);
4943 let mut rotation = c.compromise(&old_pk, &new, 1);
4944 rotation.certificate = for_new_at(&c, &new_pk);
4945 c.rotate(&new, &rotation);
4946 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
4947 assert!(!v.ok);
4948 assert!(v.repudiated.is_empty(), "{v:#?}");
4949 assert_eq!(v.public_key, (&old_pk).into());
4950 }
4951
4952 #[test]
4955 fn a_root_signature_without_the_domain_prefix_certifies_nothing() {
4956 use ed25519_dalek::Signer;
4957 let (old, old_pk) = generate_keypair();
4958 let (new, _) = generate_keypair();
4959 let mut c = Chain::new();
4960 c.decision(&old);
4961 let mut rotation = c.routine(&old_pk, &new);
4962 let statement = rotation.certificate.statement.clone();
4963 let bare = canonical_json(&serde_json::to_value(&statement).unwrap());
4964 assert_eq!(
4965 statement.signed_bytes(),
4966 [ROOT_DOMAIN, bare.as_slice()].concat()
4967 );
4968 rotation.certificate =
4969 KeyCertificate::unsigned(statement).with(RootSignature {
4970 root_key: (&root(1).verifying_key()).into(),
4971 signature: root(1).sign(&bare).into(),
4972 });
4973 c.rotate(&old, &rotation);
4974 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
4975 assert!(!v.ok, "{v:#?}");
4976 assert_eq!(v.public_key, (&old_pk).into());
4977 }
4978
4979 #[test]
4980 fn the_first_rotation_carries_the_genesis_certificate_and_only_it_does() {
4981 let (k1, k1_pk) = generate_keypair();
4982 let (k2, k2_pk) = generate_keypair();
4983 let (k3, _) = generate_keypair();
4984 let genesis =
4985 || certify(&[&root(1)], KeyCertStatement::genesis((&k1_pk).into()));
4986
4987 let mut c = Chain::new();
4989 c.decision(&k1);
4990 let mut rotation = c.routine(&k1_pk, &k2);
4991 rotation.outgoing_certificate = None;
4992 c.rotate(&k1, &rotation);
4993 let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
4994 assert!(!v.ok);
4995 let p = v.entries[1].problem.as_deref().unwrap();
4996 assert!(p.contains("genesis key's outgoing_certificate"), "{p}");
4997 assert_eq!(v.public_key, (&k1_pk).into());
4998
4999 let mut c = Chain::new();
5001 c.decision(&k1);
5002 let rotation = c.routine(&k1_pk, &k2).with_outgoing(certify(
5003 &[&root(1)],
5004 KeyCertStatement::genesis((&k2_pk).into()),
5005 ));
5006 c.rotate(&k1, &rotation);
5007 let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5008 let p = v.entries[1].problem.as_deref().unwrap();
5009 assert!(p.starts_with("outgoing_certificate:"), "{p}");
5010
5011 let mut c = Chain::new();
5014 c.decision(&k1);
5015 let rotation = c.routine(&k1_pk, &k2);
5016 assert_eq!(rotation.outgoing_certificate, Some(genesis()));
5017 c.rotate(&k1, &rotation);
5018 let v = verify_chain(&c.links, &k1_pk, &KeyAnchor::default(), &roots());
5019 assert!(v.ok, "{v:#?}");
5020 assert!(v.unanchored_keys.is_empty(), "{v:#?}");
5021
5022 let again = c.routine(&k2_pk, &k3).with_outgoing(genesis());
5024 c.rotate(&k2, &again);
5025 let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5026 assert!(!v.ok);
5027 let p = v.entries[2].problem.as_deref().unwrap();
5028 assert!(p.contains("nowhere else"), "{p}");
5029 }
5030
5031 #[test]
5032 fn a_forged_proof_of_possession_is_rejected() {
5033 let (old, old_pk) = generate_keypair();
5034 let (_, new_pk) = generate_keypair();
5035 let (thief, _) = generate_keypair();
5036 let mut c = Chain::new();
5037 c.decision(&old);
5038 let mut rotation = c.routine(&old_pk, &thief);
5041 rotation.new_key = (&new_pk).into();
5042 rotation.certificate = for_new_at(&c, &new_pk);
5043 let statement = rotation.statement(c.prev_hash());
5044 rotation.proof = crypto::sign(
5045 &old,
5046 statement.hash().as_bytes(),
5047 rotation.proof_signed_at,
5048 )
5049 .into();
5050 c.rotate(&old, &rotation);
5051
5052 assert_eq!(
5053 rotation.verify_proof(c.links[1].attestation.prev_hash),
5054 Err(RotationError::BadProof)
5055 );
5056 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5057 assert!(!v.ok, "{v:#?}");
5058 assert!(
5059 v.entries[1]
5060 .problem
5061 .as_deref()
5062 .unwrap()
5063 .contains("proof of possession")
5064 );
5065 assert_eq!(v.public_key, (&old_pk).into(), "the chain does not move");
5066 }
5067
5068 #[test]
5069 fn a_compromise_repudiates_the_window_and_a_reattestation_restores_one() {
5070 let (old, old_pk) = generate_keypair();
5071 let (new, new_pk) = generate_keypair();
5072 let mut c = Chain::new();
5073 c.decision(&old); c.decision(&old); let reattested = c.decision(&old); let rotation = c.compromise(&old_pk, &new, 1);
5077 let rotation_id = c.rotate(&new, &rotation); let vouch = AmendmentDraft::new(
5079 reattested,
5080 c.hash_at(3),
5081 AmendmentKind::Reattested,
5082 "Art. VII",
5083 "independently verified; the Steward vouches for it",
5084 )
5085 .unwrap();
5086 let vouch_id = c.amend(&new, &vouch); let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5089 assert!(
5090 v.ok,
5091 "repudiation is a declared state, not a defect: {v:#?}"
5092 );
5093 assert_eq!(v.repudiated, vec![gov(2)]);
5094 assert!(v.entries[1].repudiated);
5095 assert!(!v.entries[2].repudiated);
5096 assert_eq!(v.entries[2].reattested_by, vec![vouch_id.clone()]);
5097 assert_eq!(v.entries[2].amended_by, vec![vouch_id]);
5098 assert_eq!(v.entries[3].signed_by, Some((&new_pk).into()));
5099 assert_eq!(v.public_key, (&new_pk).into());
5100 assert_eq!(v.keys.len(), 2);
5101 assert_eq!(v.keys[0].status, KeyStatus::Compromised);
5102 assert_eq!(
5103 v.keys[0].through_seq,
5104 Some(1),
5105 "trusted through the last trusted entry, not through the \
5106 declaration"
5107 );
5108 assert_eq!(v.keys[0].retired_by.as_ref(), Some(&rotation_id));
5109 assert_eq!(v.keys[1].from_seq, 4, "the declaration is its own first");
5110 }
5111
5112 #[test]
5115 fn last_trusted_is_the_roots_to_say() {
5116 let (old, old_pk) = generate_keypair();
5117 let (new, _) = generate_keypair();
5118 let mut c = Chain::new();
5119 c.decision(&old); c.decision(&old); let mut rotation = c.compromise(&old_pk, &new, 1);
5122 rotation.certificate.statement.last_trusted = Some(c.head(2));
5123 c.rotate(&new, &rotation);
5124 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5125 assert!(!v.ok, "{v:#?}");
5126 assert!(v.repudiated.is_empty());
5127 assert_eq!(v.public_key, (&old_pk).into());
5128 }
5129
5130 #[test]
5131 fn a_stolen_key_cannot_be_rotated_back_in() {
5132 let (k1, k1_pk) = generate_keypair();
5136 let (k2, k2_pk) = generate_keypair();
5137 let mut c = Chain::new();
5138 c.decision(&k1); let real = c.compromise(&k1_pk, &k2, 1);
5140 c.rotate(&k2, &real); c.decision(&k2); let honest =
5143 verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5144 assert!(honest.ok, "{honest:#?}");
5145
5146 let hijack = c.compromise(&k2_pk, &k1, 3);
5147 c.rotate(&k1, &hijack); c.decision(&k1); let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5151 assert!(!v.ok);
5152 assert_eq!(v.public_key, (&k2_pk).into(), "the chain stays with K2");
5153 let p = v.entries[3].problem.as_deref().unwrap();
5154 assert!(p.contains("never brought back"), "{p}");
5155 assert!(!v.entries[3].signature_valid, "{:#?}", v.entries[3]);
5156 assert!(!v.entries[4].signature_valid, "K1 signs nothing again");
5157 assert_eq!(v.keys.len(), 2);
5158 assert_eq!(v.keys[1].status, KeyStatus::Active);
5159 }
5160
5161 #[test]
5162 fn a_routine_rotation_cannot_reuse_a_key_either() {
5163 let (k1, k1_pk) = generate_keypair();
5164 let (k2, k2_pk) = generate_keypair();
5165 let mut c = Chain::new();
5166 c.decision(&k1);
5167 let out = c.routine(&k1_pk, &k2);
5168 c.rotate(&k1, &out);
5169 let back = c.routine(&k2_pk, &k1);
5170 c.rotate(&k2, &back);
5171 let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5172 assert!(!v.ok);
5173 assert!(
5174 v.entries[2]
5175 .problem
5176 .as_deref()
5177 .unwrap()
5178 .contains("never brought back"),
5179 "{:#?}",
5180 v.entries[2]
5181 );
5182 assert_eq!(v.public_key, (&k2_pk).into());
5183 }
5184
5185 #[test]
5186 fn a_forged_entry_has_no_effects() {
5187 let (steward, steward_pk) = generate_keypair();
5188 let (forger, _) = generate_keypair();
5189 let anchor = anchored(&steward_pk);
5190 let mut c = Chain::new();
5191 let target = c.decision(&steward); let fake = AmendmentDraft::new(
5193 target,
5194 c.hash_at(1),
5195 AmendmentKind::Overruled,
5196 "none",
5197 "overruled, says nobody with the key",
5198 )
5199 .unwrap();
5200 c.amend(&forger, &fake); let grab = c.routine(&steward_pk, &forger);
5204 c.rotate(&forger, &grab); let v = verify_chain(&c.links, &steward_pk, &anchor, &roots());
5207 assert!(!v.ok);
5208 assert!(v.entries[0].amended_by.is_empty(), "{:#?}", v.entries[0]);
5209 assert_eq!(v.public_key, (&steward_pk).into());
5210 assert_eq!(v.keys.len(), 1);
5211 assert!(v.unanchored_keys.is_empty());
5212 }
5213
5214 #[test]
5215 fn a_repeated_id_is_a_problem() {
5216 let (key, pk) = generate_keypair();
5217 let mut c = Chain::new();
5218 c.decision(&key);
5219 c.gov = 0;
5220 c.decision(&key); let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
5222 assert!(!v.ok);
5223 assert!(v.entries.iter().all(|e| {
5224 e.problem
5225 .as_deref()
5226 .is_some_and(|p| p.contains("more than once"))
5227 }));
5228 }
5229
5230 #[test]
5231 fn a_second_compromise_cannot_anchor_inside_the_first_window() {
5232 let (k1, k1_pk) = generate_keypair();
5233 let (k2, _) = generate_keypair();
5234 let (k3, _) = generate_keypair();
5235 let mut c = Chain::new();
5236 c.decision(&k1); c.decision(&k1); let first = c.compromise(&k1_pk, &k2, 1);
5239 c.rotate(&k2, &first); let second = c.compromise(&k1_pk, &k3, 2);
5241 c.rotate(&k3, &second); let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5244 assert!(!v.ok);
5245 let p = v.entries[3].problem.as_deref().unwrap();
5246 assert!(p.contains("repudiated"), "{p}");
5247 assert_eq!(v.keys.len(), 2, "K1 and K2; K3 never took the chain");
5248 assert_eq!(v.keys[0].through_seq, Some(1));
5249 }
5250
5251 #[test]
5252 fn an_uncertified_compromise_fails_closed() {
5253 let (old, old_pk) = generate_keypair();
5254 let (new, new_pk) = generate_keypair();
5255 let mut c = Chain::new();
5256 c.decision(&old);
5257 c.decision(&old);
5258 let mut rotation = c.compromise(&old_pk, &new, 1);
5259 let statement = rotation.certificate.statement.clone();
5260 rotation.certificate = certify(&[&new], statement);
5261 c.rotate(&new, &rotation);
5262
5263 let anchor = anchored(&old_pk).with((&new_pk).into());
5265 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5266 assert!(!v.ok, "{v:#?}");
5267 let p = v.entries[2].problem.as_deref().unwrap();
5268 assert!(p.contains("0 valid root signature"), "{p}");
5269 assert!(!v.entries[2].signature_valid, "checked under the old key");
5270 assert!(v.repudiated.is_empty(), "and nothing is repudiated");
5271 assert_eq!(v.public_key, (&old_pk).into());
5272 }
5273
5274 #[test]
5279 fn a_rotation_inside_the_window_is_void_with_it() {
5280 let (steward, steward_pk) = generate_keypair();
5281 let (successor, _) = generate_keypair();
5282 let (recovery, recovery_pk) = generate_keypair();
5283
5284 let mut c = Chain::new();
5285 c.decision(&steward); c.decision(&steward); let routine = c.routine(&steward_pk, &successor);
5288 c.rotate(&steward, &routine); c.decision(&successor); let declaration = c.compromise(&steward_pk, &recovery, 1);
5292 c.rotate(&recovery, &declaration); let v = verify_chain(
5295 &c.links,
5296 &steward_pk,
5297 &anchored(&steward_pk),
5298 &roots(),
5299 );
5300 assert!(v.ok, "{v:#?}");
5301 assert_eq!(v.repudiated, vec![gov(2), key_id(1), gov(3)]);
5302 assert_eq!(v.public_key, (&recovery_pk).into());
5303 assert_eq!(v.keys.len(), 2, "the successor is not part of history");
5304 assert_eq!(v.keys[0].public_key, (&steward_pk).into());
5305 assert_eq!(v.keys[0].status, KeyStatus::Compromised);
5306 assert!(
5307 v.keys[0].certified,
5308 "the genesis certificate rode in on the voided rotation and \
5309 is the root's word all the same"
5310 );
5311 assert_eq!(v.keys[1].public_key, (&recovery_pk).into());
5312 }
5313
5314 #[test]
5315 fn a_compromise_must_name_a_real_head_and_the_key_that_held_it() {
5316 let (old, old_pk) = generate_keypair();
5317 let (new, new_pk) = generate_keypair();
5318 let anchor = anchored(&old_pk);
5319
5320 let mut c = Chain::new();
5323 c.decision(&old);
5324 let mut trusted = c.head(1);
5325 trusted.entry_hash = data_hash(&json!("nope"));
5326 let statement = KeyCertStatement::compromise(
5327 (&new_pk).into(),
5328 2,
5329 c.prev_hash(),
5330 trusted,
5331 );
5332 let mut rotation = c.compromise(&old_pk, &new, 1);
5333 rotation.certificate = certify(&[&root(1)], statement);
5334 c.rotate(&new, &rotation);
5335 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5336 let p = v.entries[1].problem.as_deref().unwrap();
5337 assert!(p.contains("last_trusted"), "{p}");
5338
5339 let (_, other_pk) = generate_keypair();
5341 let mut c = Chain::new();
5342 c.decision(&old);
5343 let rotation = c.compromise(&other_pk, &new, 1);
5344 c.rotate(&new, &rotation);
5345 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5346 let p = v.entries[1].problem.as_deref().unwrap();
5347 assert!(p.contains("old_key is not the key"), "{p}");
5348
5349 let mut c = Chain::new();
5351 c.decision(&old);
5352 let mut rotation = c.compromise(&old_pk, &new, 1);
5353 rotation.certificate.statement.last_trusted = None;
5354 c.rotate(&new, &rotation);
5355 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5356 let p = v.entries[1].problem.as_deref().unwrap();
5357 assert!(p.contains("must name last_trusted"), "{p}");
5358 }
5359
5360 #[test]
5361 fn a_rotation_carries_no_free_text() {
5362 let (old, old_pk) = generate_keypair();
5363 let (new, _) = generate_keypair();
5364 let mut c = Chain::new();
5365 c.decision(&old);
5366 let rotation = c.routine(&old_pk, &new);
5367 let mut value = serde_json::to_value(&rotation).unwrap();
5368 assert!(serde_json::from_value::<KeyRotation>(value.clone()).is_ok());
5369 value["note"] = json!("at the request of …");
5370 assert!(serde_json::from_value::<KeyRotation>(value).is_err());
5371
5372 let mut head = serde_json::to_value(c.head(1)).unwrap();
5373 head["comment"] = json!("the last one I remember signing");
5374 assert!(serde_json::from_value::<TrustedHead>(head).is_err());
5375
5376 let mut statement =
5377 serde_json::to_value(&rotation.certificate.statement).unwrap();
5378 statement["comment"] = json!("signed in the kitchen");
5379 assert!(serde_json::from_value::<KeyCertStatement>(statement).is_err());
5380 }
5381
5382 #[test]
5383 fn the_root_statement_bytes_are_pinned() {
5384 let key: PublicKeyHex = PUBLISHED_KEYS[0].parse().unwrap();
5385 assert_eq!(
5386 String::from_utf8(KeyCertStatement::genesis(key).signed_bytes())
5387 .unwrap(),
5388 "agora-governance-root-v1\n\
5389 {\"agora_governance_key_cert\":1,\"from_seq\":1,\
5390 \"key\":\"ebb3091dd328f1463362c171121921b2fe14628e3fc4c145deaccefb85c0e78a\",\
5391 \"last_trusted\":null,\"prev_hash\":null,\"purpose\":\"genesis\"}"
5392 );
5393
5394 let statement = KeyCertStatement::compromise(
5398 Sha256Hex::from([0xab; 32]).to_string().parse().unwrap(),
5399 15,
5400 Some([0xcd; 32].into()),
5401 TrustedHead {
5402 id: gov(10),
5403 chain_seq: 11,
5404 entry_hash: [0xef; 32].into(),
5405 },
5406 );
5407 assert_eq!(
5408 Sha256Hex::from(<[u8; 32]>::from(Sha256::digest(
5409 statement.signed_bytes()
5410 )))
5411 .to_string(),
5412 "633685771e08be126fd12ca4eb98c77120e5868236ff541ecba85ce6a21e6b68"
5413 );
5414 }
5415
5416 #[test]
5417 fn root_keys_are_curve_points_and_make_a_root_set() {
5418 let roots = RootSet::published();
5419 assert_eq!(roots.keys().count(), ROOT_KEYS.len());
5420 assert_eq!(roots.threshold(), ROOT_THRESHOLD);
5421 assert!(ROOT_THRESHOLD >= 1 && ROOT_THRESHOLD <= ROOT_KEYS.len());
5422 for root in ROOT_KEYS {
5423 let key: PublicKeyHex = root.parse().unwrap();
5424 assert!(roots.contains(&key));
5425 assert!(
5426 key.to_verifying_key().is_ok(),
5427 "{root} is not a valid Ed25519 public key"
5428 );
5429 assert!(
5430 !PUBLISHED_KEYS.contains(root),
5431 "a root key never signs entries"
5432 );
5433 }
5434 assert_eq!(RootSet::new([], 0).threshold(), 1);
5435 }
5436
5437 #[test]
5438 fn a_genesis_key_outside_the_anchor_is_reported_not_rejected() {
5439 let (key, pk) = generate_keypair();
5440 let c = chain(&key, 2);
5441 let v = verify_chain(&c, &pk, &KeyAnchor::default(), &roots());
5442 assert!(v.ok, "{v:#?}");
5443 assert_eq!(v.unanchored_keys, vec![PublicKeyHex::from(&pk)]);
5444 assert_eq!(v.keys.len(), 1);
5445 assert_eq!(v.keys[0].status, KeyStatus::Active);
5446 assert_eq!(v.keys[0].from_seq, 1);
5447 assert_eq!(v.keys[0].through_seq, None);
5448 }
5449
5450 #[test]
5451 fn published_keys_are_curve_points_and_make_an_anchor() {
5452 let anchor = KeyAnchor::published();
5453 assert!(!anchor.is_empty());
5454 assert_eq!(anchor.keys().count(), PUBLISHED_KEYS.len());
5455 for published in PUBLISHED_KEYS {
5456 let key: PublicKeyHex = published.parse().unwrap();
5457 assert!(anchor.contains(&key));
5458 assert!(
5459 key.to_verifying_key().is_ok(),
5460 "{published} is not a valid Ed25519 public key"
5461 );
5462 }
5463 let (_, other) = generate_keypair();
5464 assert!(!anchor.contains(&(&other).into()));
5465 assert!(
5466 anchor
5467 .clone()
5468 .with((&other).into())
5469 .contains(&(&other).into())
5470 );
5471 }
5472
5473 #[test]
5476 fn amendments_and_rotations_round_trip_as_entry_data() {
5477 let (key, pk) = generate_keypair();
5478 let amendment = AmendmentDraft::new(
5479 gov(1),
5480 data_hash(&json!("x")),
5481 AmendmentKind::Superseded,
5482 "Art. VI § 2",
5483 "superseded by GOV-2026-0009",
5484 )
5485 .unwrap()
5486 .with_authority(gov(9))
5487 .with_rationale("the later decision covers the same subject");
5488 let AmendmentDraft { amendment, texts } = amendment;
5489 let value = serde_json::to_value(&amendment).unwrap();
5490 assert_eq!(value["kind"], "superseded");
5491 assert_eq!(value["agora_governance_amendment"], 2);
5492 assert!(value.get("redaction").is_none(), "{value}");
5493 let text = value.to_string();
5494 assert!(!text.contains("Art. VI"), "no text in signed data: {text}");
5495 assert!(!text.contains("salt"), "and no salt: {text}");
5496 assert_eq!(
5497 value["note"]["commitment"],
5498 texts
5499 .note
5500 .as_ref()
5501 .unwrap()
5502 .commitment()
5503 .commitment
5504 .to_string()
5505 );
5506 assert_eq!(
5507 serde_json::from_value::<Amendment>(value).unwrap(),
5508 amendment
5509 );
5510 let beside = serde_json::to_value(&texts).unwrap();
5511 assert_eq!(beside["basis"]["text"], "Art. VI § 2");
5512 assert_eq!(
5513 serde_json::from_value::<AmendmentTexts>(beside).unwrap(),
5514 texts
5515 );
5516
5517 let legacy = json!({
5519 "agora_governance_amendment": 1,
5520 "target": "GOV-2026-0001",
5521 "target_entry_hash": data_hash(&json!("x")),
5522 "kind": "non_precedential",
5523 "authority": "GOV-2026-0005",
5524 "basis": "§1",
5525 "note": "diagnostic finding",
5526 });
5527 let legacy: Amendment = serde_json::from_value(legacy).unwrap();
5528 assert_eq!(legacy.validate(), Ok(()));
5529 assert_eq!(legacy.basis, AmendmentText::Plain("§1".into()));
5530
5531 let mut c = Chain::new();
5532 c.decision(&key);
5533 let rotation = c.compromise(&pk, &generate_keypair().0, 1);
5534 let value = serde_json::to_value(&rotation).unwrap();
5535 assert_eq!(value["agora_governance_key_rotation"], 2);
5536 assert_eq!(value["reason"], "compromise");
5537 let statement = &value["certificate"]["statement"];
5538 assert_eq!(statement["purpose"], "compromise");
5539 assert_eq!(statement["last_trusted"]["chain_seq"], 1);
5540 assert_eq!(
5541 value["outgoing_certificate"]["statement"]["purpose"],
5542 "genesis"
5543 );
5544 assert!(value.get("note").is_none(), "{value}");
5545 assert_eq!(
5546 serde_json::from_value::<KeyRotation>(value).unwrap(),
5547 rotation
5548 );
5549
5550 let notice =
5551 AmendmentNotice::new(amd(1), at(0), &amendment, Some(&texts));
5552 let value = serde_json::to_value(¬ice).unwrap();
5553 assert_eq!(value["id"], "AMD-2026-0001");
5554 assert_eq!(value["note"], "superseded by GOV-2026-0009");
5555 assert_eq!(
5556 serde_json::from_value::<AmendmentNotice>(value).unwrap(),
5557 notice
5558 );
5559
5560 let erased = AmendmentTexts {
5562 rationale: None,
5563 ..texts.clone()
5564 };
5565 let notice =
5566 AmendmentNotice::new(amd(1), at(0), &amendment, Some(&erased));
5567 assert_eq!(notice.note, "superseded by GOV-2026-0009");
5568 assert_eq!(notice.rationale.as_deref(), Some(WITHHELD_TEXT));
5569 let notice = AmendmentNotice::new(amd(1), at(0), &amendment, None);
5570 assert_eq!(notice.basis, WITHHELD_TEXT);
5571 let notice = AmendmentNotice::new(amd(1), at(0), &legacy, None);
5572 assert_eq!(notice.note, "diagnostic finding");
5573 }
5574
5575 #[test]
5579 fn an_amendment_verifies_with_or_without_its_optional_fields() {
5580 let (key, pk) = generate_keypair();
5581 let mut c = Chain::new();
5582 let target = c.decision(&key);
5583 let bare = AmendmentDraft::new(
5584 target.clone(),
5585 c.hash_at(1),
5586 AmendmentKind::Correction,
5587 "clerical",
5588 "typo in the citation",
5589 )
5590 .unwrap();
5591 assert!(
5592 serde_json::to_value(&bare.amendment)
5593 .unwrap()
5594 .get("rationale")
5595 .is_none()
5596 );
5597 c.amend(&key, &bare);
5598 let full = bare.clone().with_rationale("at length: …");
5599 c.amend(&key, &full);
5600 let mut future = serde_json::to_value(&full.amendment).unwrap();
5602 future["superseded_by_something_new"] = json!(["later"]);
5603 c.push(
5604 &key,
5605 amd(3),
5606 GovernanceLogEntryType::Amendment,
5607 future,
5608 true,
5609 );
5610
5611 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
5612 assert!(v.ok, "{v:#?}");
5613 assert_eq!(
5614 v.entries[0].amended_by,
5615 vec![amd(1), amd(2), amd(3)],
5616 "all three name the target"
5617 );
5618 }
5619
5620 #[test]
5623 fn pre_0_26_wire_still_deserializes() {
5624 let link: GovernanceChainLink = serde_json::from_value(json!({
5625 "id": "GOV-2026-0001",
5626 "entry_type": "council_decision",
5627 "created_at": "2023-11-14T22:13:20.123456Z",
5628 "attestation": {
5629 "envelope_version": 1,
5630 "chain_seq": 1,
5631 "prev_hash": null,
5632 "data_hash": "00".repeat(32),
5633 "entry_hash": "11".repeat(32),
5634 "signature": "22".repeat(64),
5635 "signed_at": "2023-11-14T22:13:21Z",
5636 "retroactive": false,
5637 },
5638 }))
5639 .unwrap();
5640 assert!(link.data.is_none());
5641 assert!(
5642 serde_json::to_value(&link).unwrap().get("data").is_none(),
5643 "and a link without data does not grow a null field"
5644 );
5645
5646 let verdict: EntryVerdict = serde_json::from_value(json!({
5647 "id": "GOV-2026-0001",
5648 "chain_seq": 1,
5649 "signature_valid": true,
5650 "link_valid": true,
5651 "content_matches": null,
5652 "retroactive": false,
5653 "out_of_order": false,
5654 "amended_by": [],
5655 }))
5656 .unwrap();
5657 assert!(!verdict.repudiated && !verdict.redacted);
5658 assert!(verdict.signed_by.is_none());
5659 assert!(verdict.reattested_by.is_empty());
5660
5661 let verification: GovernanceVerification =
5662 serde_json::from_value(json!({
5663 "public_key": "33".repeat(32),
5664 "ok": true,
5665 "head": "GOV-2026-0001",
5666 "entries": [],
5667 }))
5668 .unwrap();
5669 assert!(verification.keys.is_empty());
5670 assert!(verification.unanchored_keys.is_empty());
5671 assert!(verification.repudiated.is_empty());
5672 }
5673
5674 #[test]
5675 fn the_report_round_trips() {
5676 let (key, pk) = generate_keypair();
5677 let mut c = Chain::new();
5678 let target = c.decision(&key);
5679 let amendment = AmendmentDraft::new(
5680 target,
5681 c.hash_at(1),
5682 AmendmentKind::Overruled,
5683 "b",
5684 "n",
5685 )
5686 .unwrap();
5687 c.amend(&key, &amendment);
5688 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
5689 let text = serde_json::to_string(&v).unwrap();
5690 assert_eq!(
5691 serde_json::from_str::<GovernanceVerification>(&text).unwrap(),
5692 v
5693 );
5694
5695 let keys = GovernanceSigningKeys { keys: v.keys };
5696 let value = serde_json::to_value(&keys).unwrap();
5697 assert!(value["keys"].is_array(), "an object, not a bare array");
5698 assert_eq!(value["keys"][0]["status"], "active");
5699 assert_eq!(
5700 serde_json::from_value::<GovernanceSigningKeys>(value).unwrap(),
5701 keys
5702 );
5703 }
5704
5705 #[test]
5709 fn envelope_v1_preimage_and_hash_are_pinned() {
5710 let envelope = Envelope::new(
5711 gov(6),
5712 GovernanceLogEntryType::CouncilDecision,
5713 at(0),
5714 Some(Sha256Hex::from([0x11; 32])),
5715 data_hash(&json!({"outcome": "approved", "title": "Ratification"})),
5716 );
5717 assert_eq!(
5718 String::from_utf8(envelope.preimage()).unwrap(),
5719 "{\"agora_governance_log\":1,\"id\":\"GOV-2026-0006\",\
5720 \"entry_type\":\"council_decision\",\
5721 \"created_at\":1700000000123456,\
5722 \"prev_hash\":\"1111111111111111111111111111111111111111111111111111111111111111\",\
5723 \"data_hash\":\"a4adf645ae3f60c56484d01aea87d6d490321d7fc66b1607df14b023fe567c7b\"}"
5724 );
5725 assert_eq!(
5726 envelope.entry_hash().to_hex(),
5727 "ba27577432f81e415f1c01cc4cfabab6070e3ac50fd468fffe195ef19c0e9464"
5728 );
5729 }
5730
5731 #[cfg(feature = "agora-client")]
5745 #[tokio::test]
5746 #[ignore = "networked: hits the live platform"]
5747 async fn the_published_key_is_the_one_the_platform_serves() {
5748 let client = crate::client::Client::new(
5749 url::Url::parse("https://subliminal.technology").unwrap(),
5750 )
5751 .unwrap();
5752 let served = client.get_governance_signing_key().await.unwrap();
5753 assert_eq!(served.algorithm, "ed25519");
5754
5755 let genesis: PublicKeyHex = PUBLISHED_KEYS
5756 .first()
5757 .expect("PUBLISHED_KEYS is never empty")
5758 .parse()
5759 .unwrap();
5760 let links = client.get_governance_chain().await.unwrap();
5761 let report = verify_chain(
5762 &links,
5763 &genesis.to_verifying_key().unwrap(),
5764 &KeyAnchor::published(),
5765 &RootSet::published(),
5766 );
5767 let problems: Vec<_> = report
5768 .entries
5769 .iter()
5770 .filter_map(|e| {
5771 e.problem.as_ref().map(|p| (e.id.clone(), p.clone()))
5772 })
5773 .collect();
5774 assert!(
5775 report.ok,
5776 "the live chain does not verify under this build's genesis key \
5777 and roots: {problems:#?}"
5778 );
5779 assert!(report.unanchored_keys.is_empty(), "{report:#?}");
5780 assert_eq!(
5781 served.public_key, report.public_key,
5782 "the platform serves {} but the chain, followed under the \
5783 published roots, is held by {}",
5784 served.public_key, report.public_key
5785 );
5786 }
5787
5788 #[cfg(feature = "schemars")]
5789 #[test]
5790 fn wire_schemas_are_ref_free() {
5791 use crate::responses::inline_schema_for;
5792 for (name, schema) in [
5793 (
5794 "GovernanceAttestation",
5795 inline_schema_for::<GovernanceAttestation>(),
5796 ),
5797 (
5798 "GovernanceChainLink",
5799 inline_schema_for::<GovernanceChainLink>(),
5800 ),
5801 (
5802 "GovernanceSigningKey",
5803 inline_schema_for::<GovernanceSigningKey>(),
5804 ),
5805 (
5806 "GovernanceVerification",
5807 inline_schema_for::<GovernanceVerification>(),
5808 ),
5809 (
5810 "Vec<GovernanceChainLink>",
5811 inline_schema_for::<Vec<GovernanceChainLink>>(),
5812 ),
5813 ("Amendment", inline_schema_for::<Amendment>()),
5814 ("Redaction", inline_schema_for::<Redaction>()),
5815 ("Revision", inline_schema_for::<Revision>()),
5816 ("AmendmentNotice", inline_schema_for::<AmendmentNotice>()),
5817 ("KeyRotation", inline_schema_for::<KeyRotation>()),
5818 ("TrustedHead", inline_schema_for::<TrustedHead>()),
5819 (
5820 "GovernanceKeyRecord",
5821 inline_schema_for::<GovernanceKeyRecord>(),
5822 ),
5823 (
5824 "GovernanceSigningKeys",
5825 inline_schema_for::<GovernanceSigningKeys>(),
5826 ),
5827 ("AmendmentKind", inline_schema_for::<AmendmentKind>()),
5828 ("Standing", inline_schema_for::<Standing>()),
5829 ("KeyStatus", inline_schema_for::<KeyStatus>()),
5830 ("RotationReason", inline_schema_for::<RotationReason>()),
5831 ] {
5832 let text = serde_json::to_string(&schema).unwrap();
5833 assert!(!text.contains("$ref"), "{name} must be $ref-free: {text}");
5834 assert!(
5835 !text.contains("$defs"),
5836 "{name} must be $defs-free: {text}"
5837 );
5838 }
5839 let text =
5840 serde_json::to_string(&inline_schema_for::<Sha256Hex>()).unwrap();
5841 assert!(text.contains("^[0-9a-f]{64}$"), "{text}");
5842 let text = serde_json::to_string(&inline_schema_for::<SignatureHex>())
5843 .unwrap();
5844 assert!(text.contains("^[0-9a-f]{128}$"), "{text}");
5845 }
5846}