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, 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
1500pub const KEY_ROTATION_VERSION: u32 = 2;
1506
1507pub const PUBLISHED_KEYS: &[&str] =
1514 &["ebb3091dd328f1463362c171121921b2fe14628e3fc4c145deaccefb85c0e78a"];
1515
1516#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1518#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1519#[cfg_attr(feature = "schemars", schemars(inline))]
1520#[serde(rename_all = "snake_case")]
1521pub enum RotationReason {
1522 Routine,
1524 Compromise,
1527}
1528
1529#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1531#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1532#[cfg_attr(feature = "schemars", schemars(inline))]
1533#[serde(deny_unknown_fields)]
1534pub struct TrustedHead {
1535 pub id: GovernanceLogId,
1536 pub chain_seq: u64,
1537 pub entry_hash: Sha256Hex,
1538}
1539
1540#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1542pub enum RotationError {
1543 #[error("agora_governance_key_rotation is {0}, not {KEY_ROTATION_VERSION}")]
1544 UnsupportedVersion(u32),
1545 #[error("new_key is not a valid Ed25519 public key")]
1546 BadNewKey,
1547 #[error(
1548 "the proof of possession does not verify for this rotation at this position"
1549 )]
1550 BadProof,
1551 #[error("a compromise certificate must name last_trusted")]
1552 MissingLastTrusted,
1553 #[error("certificate: {0}")]
1554 Certificate(#[from] CertificateError),
1555 #[error("outgoing_certificate: {0}")]
1556 OutgoingCertificate(CertificateError),
1557 #[error(
1558 "the chain's first rotation must carry the genesis key's \
1559 outgoing_certificate"
1560 )]
1561 MissingGenesisCertificate,
1562 #[error(
1563 "outgoing_certificate belongs on the chain's first rotation and \
1564 nowhere else"
1565 )]
1566 UnexpectedOutgoingCertificate,
1567 #[error("old_key is not the key that was in force")]
1568 WrongOldKey,
1569 #[error("last_trusted does not name an earlier entry of this chain")]
1570 UnknownLastTrusted,
1571 #[error("new_key has already held this chain; a key is never brought back")]
1572 ReusedKey,
1573 #[error("last_trusted names an entry an earlier compromise repudiated")]
1574 RepudiatedLastTrusted,
1575}
1576
1577#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1588#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1589#[cfg_attr(feature = "schemars", schemars(inline))]
1590#[serde(deny_unknown_fields)]
1591pub struct KeyRotation {
1592 pub agora_governance_key_rotation: u32,
1594 pub reason: RotationReason,
1595 pub old_key: PublicKeyHex,
1596 pub new_key: PublicKeyHex,
1597 pub proof: SignatureHex,
1601 pub proof_signed_at: i64,
1603 pub certificate: KeyCertificate,
1607 #[serde(default, skip_serializing_if = "Option::is_none")]
1611 pub outgoing_certificate: Option<KeyCertificate>,
1612}
1613
1614#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1621pub struct RotationStatement {
1622 pub agora_governance_key_rotation: u32,
1624 pub reason: RotationReason,
1625 pub old_key: PublicKeyHex,
1626 pub new_key: PublicKeyHex,
1627 pub prev_hash: Option<Sha256Hex>,
1628}
1629
1630impl RotationStatement {
1631 pub fn new(
1633 reason: RotationReason,
1634 old_key: PublicKeyHex,
1635 new_key: PublicKeyHex,
1636 prev_hash: Option<Sha256Hex>,
1637 ) -> Self {
1638 Self {
1639 agora_governance_key_rotation: KEY_ROTATION_VERSION,
1640 reason,
1641 old_key,
1642 new_key,
1643 prev_hash,
1644 }
1645 }
1646
1647 pub fn preimage(&self) -> Vec<u8> {
1649 serde_json::to_vec(self).expect("a RotationStatement always serializes")
1650 }
1651
1652 pub fn hash(&self) -> Sha256Hex {
1654 Sha256Hex(Sha256::digest(self.preimage()).into())
1655 }
1656}
1657
1658impl KeyRotation {
1659 pub fn routine(
1665 old_key: PublicKeyHex,
1666 new_signing_key: &SigningKey,
1667 prev_hash: Option<Sha256Hex>,
1668 now: DateTime<Utc>,
1669 certificate: KeyCertificate,
1670 ) -> Self {
1671 Self::build(
1672 RotationReason::Routine,
1673 old_key,
1674 new_signing_key,
1675 prev_hash,
1676 now,
1677 certificate,
1678 )
1679 }
1680
1681 pub fn compromise(
1689 old_key: PublicKeyHex,
1690 new_signing_key: &SigningKey,
1691 prev_hash: Option<Sha256Hex>,
1692 now: DateTime<Utc>,
1693 certificate: KeyCertificate,
1694 ) -> Self {
1695 Self::build(
1696 RotationReason::Compromise,
1697 old_key,
1698 new_signing_key,
1699 prev_hash,
1700 now,
1701 certificate,
1702 )
1703 }
1704
1705 fn build(
1706 reason: RotationReason,
1707 old_key: PublicKeyHex,
1708 new_signing_key: &SigningKey,
1709 prev_hash: Option<Sha256Hex>,
1710 now: DateTime<Utc>,
1711 certificate: KeyCertificate,
1712 ) -> Self {
1713 let new_key = PublicKeyHex::from(&new_signing_key.verifying_key());
1714 let proof_signed_at = truncate_to_seconds(now).timestamp();
1715 let statement =
1716 RotationStatement::new(reason, old_key, new_key, prev_hash);
1717 let proof = crypto::sign(
1718 new_signing_key,
1719 statement.hash().as_bytes(),
1720 proof_signed_at,
1721 );
1722 Self {
1723 agora_governance_key_rotation: KEY_ROTATION_VERSION,
1724 reason,
1725 old_key,
1726 new_key,
1727 proof: proof.into(),
1728 proof_signed_at,
1729 certificate,
1730 outgoing_certificate: None,
1731 }
1732 }
1733
1734 pub fn with_outgoing(mut self, certificate: KeyCertificate) -> Self {
1736 self.outgoing_certificate = Some(certificate);
1737 self
1738 }
1739
1740 pub fn statement(&self, prev_hash: Option<Sha256Hex>) -> RotationStatement {
1742 RotationStatement::new(
1743 self.reason,
1744 self.old_key,
1745 self.new_key,
1746 prev_hash,
1747 )
1748 }
1749
1750 pub fn last_trusted(&self) -> Option<&TrustedHead> {
1753 self.certificate.statement.last_trusted.as_ref()
1754 }
1755
1756 pub fn expected_statement(
1762 &self,
1763 seq: u64,
1764 prev_hash: Option<Sha256Hex>,
1765 ) -> Result<KeyCertStatement, RotationError> {
1766 match self.reason {
1767 RotationReason::Routine => {
1768 Ok(KeyCertStatement::routine(self.new_key, seq, prev_hash))
1769 }
1770 RotationReason::Compromise => Ok(KeyCertStatement::compromise(
1771 self.new_key,
1772 seq,
1773 prev_hash,
1774 self.last_trusted()
1775 .cloned()
1776 .ok_or(RotationError::MissingLastTrusted)?,
1777 )),
1778 }
1779 }
1780
1781 pub fn verify_proof(
1784 &self,
1785 prev_hash: Option<Sha256Hex>,
1786 ) -> Result<(), RotationError> {
1787 if self.agora_governance_key_rotation != KEY_ROTATION_VERSION {
1788 return Err(RotationError::UnsupportedVersion(
1789 self.agora_governance_key_rotation,
1790 ));
1791 }
1792 let new_key = self
1793 .new_key
1794 .to_verifying_key()
1795 .map_err(|_| RotationError::BadNewKey)?;
1796 crypto::verify(
1797 &new_key,
1798 self.statement(prev_hash).hash().as_bytes(),
1799 self.proof_signed_at,
1800 &Signature::from(&self.proof),
1801 )
1802 .then_some(())
1803 .ok_or(RotationError::BadProof)
1804 }
1805
1806 pub fn verify_certified(
1810 &self,
1811 seq: u64,
1812 prev_hash: Option<Sha256Hex>,
1813 roots: &RootSet,
1814 ) -> Result<(), RotationError> {
1815 self.verify_proof(prev_hash)?;
1816 let expected = self.expected_statement(seq, prev_hash)?;
1817 Ok(self.certificate.verify_for(&expected, roots)?)
1818 }
1819}
1820
1821#[derive(Debug, Clone, Default, PartialEq, Eq)]
1828pub struct KeyAnchor {
1829 keys: HashSet<PublicKeyHex>,
1830}
1831
1832impl KeyAnchor {
1833 pub fn published() -> Self {
1835 PUBLISHED_KEYS
1836 .iter()
1837 .map(|k| {
1838 k.parse()
1839 .expect("PUBLISHED_KEYS are valid 32-byte hex keys")
1840 })
1841 .collect()
1842 }
1843
1844 pub fn pinned(key: PublicKeyHex) -> Self {
1846 std::iter::once(key).collect()
1847 }
1848
1849 pub fn with(mut self, key: PublicKeyHex) -> Self {
1851 self.keys.insert(key);
1852 self
1853 }
1854
1855 pub fn contains(&self, key: &PublicKeyHex) -> bool {
1856 self.keys.contains(key)
1857 }
1858
1859 pub fn is_empty(&self) -> bool {
1860 self.keys.is_empty()
1861 }
1862
1863 pub fn keys(&self) -> impl Iterator<Item = &PublicKeyHex> {
1865 self.keys.iter()
1866 }
1867}
1868
1869impl FromIterator<PublicKeyHex> for KeyAnchor {
1870 fn from_iter<I: IntoIterator<Item = PublicKeyHex>>(iter: I) -> Self {
1871 Self {
1872 keys: iter.into_iter().collect(),
1873 }
1874 }
1875}
1876
1877#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1880#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1881#[cfg_attr(feature = "schemars", schemars(inline))]
1882pub struct GovernanceKeyRecord {
1883 pub public_key: PublicKeyHex,
1884 pub from_seq: u64,
1886 #[serde(default)]
1890 pub through_seq: Option<u64>,
1891 pub status: KeyStatus,
1892 #[serde(default)]
1895 pub introduced_by: Option<GovernanceLogId>,
1896 #[serde(default)]
1898 pub retired_by: Option<GovernanceLogId>,
1899 #[serde(default)]
1903 pub certified: bool,
1904}
1905
1906#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1911#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1912#[cfg_attr(feature = "schemars", schemars(inline))]
1913pub struct GovernanceSigningKeys {
1914 pub keys: Vec<GovernanceKeyRecord>,
1916}
1917
1918#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1920#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1921#[cfg_attr(feature = "schemars", schemars(inline))]
1922pub struct EntryVerdict {
1923 pub id: GovernanceLogId,
1924 pub chain_seq: u64,
1925 pub signature_valid: bool,
1928 pub link_valid: bool,
1931 #[serde(default)]
1938 pub content_matches: Option<bool>,
1939 pub retroactive: bool,
1941 pub out_of_order: bool,
1944 #[serde(default)]
1946 pub amended_by: Vec<GovernanceLogId>,
1947 #[serde(default, skip_serializing_if = "Option::is_none")]
1950 pub signed_by: Option<PublicKeyHex>,
1951 #[serde(default)]
1954 pub redacted: bool,
1955 #[serde(default, skip_serializing_if = "Option::is_none")]
1957 pub redacted_data_hash: Option<Sha256Hex>,
1958 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1961 pub revisions: Vec<GovernanceLogId>,
1962 #[serde(default, skip_serializing_if = "Option::is_none")]
1966 pub latest_data_hash: Option<Sha256Hex>,
1967 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1972 pub superseded_revisions: Vec<GovernanceLogId>,
1973 #[serde(skip)]
1976 pub history: RevisionHistory,
1977 #[serde(default)]
1980 pub repudiated: bool,
1981 #[serde(default)]
1984 pub reattested_by: Vec<GovernanceLogId>,
1985 #[serde(default, skip_serializing_if = "Option::is_none")]
1989 pub texts: Option<AmendmentTextStatus>,
1990 #[serde(default, skip_serializing_if = "Option::is_none")]
1992 pub problem: Option<String>,
1993}
1994
1995impl EntryVerdict {
1996 fn content_check(
2000 &mut self,
2001 attested: Sha256Hex,
2002 data: &serde_json::Value,
2003 ) -> bool {
2004 if non_integer_number(data).is_some() {
2006 return false;
2007 }
2008 let hash = data_hash(data);
2009 if hash == attested && self.redacted {
2010 return true;
2013 }
2014 if hash != attested && Some(hash) != self.redacted_data_hash {
2015 return Some(hash) == self.latest_data_hash;
2016 }
2017 let history = &self.history;
2018 let mut current = data.clone();
2019 let mut failures = Vec::new();
2020 for (i, (id, revision)) in history.revisions.iter().enumerate() {
2021 if i == history.rebased {
2022 break;
2023 }
2024 if let Some(failure) = false_duplicate(id, revision, ¤t) {
2025 failures.push(failure);
2026 break;
2027 }
2028 match apply_patch(¤t, &revision.patch) {
2029 Ok(next) => current = next,
2030 Err(_) => {
2031 failures.push(format!(
2032 "revision {id} does not apply to the redacted data"
2033 ));
2034 break;
2035 }
2036 }
2037 }
2038 if failures.is_empty()
2039 && let Some(expected) = history.rebased_hash
2040 && data_hash(¤t) != expected
2041 {
2042 failures.push(
2043 "the revisions rebased over the redacted data do not \
2044 produce the redaction's resulting_latest_hash"
2045 .to_string(),
2046 );
2047 }
2048 for (id, revision) in history.revisions.iter().skip(history.rebased) {
2049 if !failures.is_empty() {
2050 break;
2051 }
2052 if let Some(failure) = false_duplicate(id, revision, ¤t) {
2053 failures.push(failure);
2054 break;
2055 }
2056 match apply_patch(¤t, &revision.patch) {
2057 Ok(next)
2058 if data_hash(&next) == revision.resulting_data_hash =>
2059 {
2060 current = next;
2061 }
2062 Ok(_) => failures.push(format!(
2063 "revision {id} does not produce its resulting_data_hash"
2064 )),
2065 Err(_) => {
2066 failures.push(format!("revision {id} does not apply"))
2067 }
2068 }
2069 }
2070 for failure in failures {
2071 add_problem(&mut self.problem, failure);
2072 }
2073 true
2074 }
2075}
2076
2077fn add_problem(problems: &mut Option<String>, problem: String) {
2079 *problems = Some(match problems.take() {
2080 Some(p) if p.contains(&problem) => p,
2081 Some(p) => format!("{p}; {problem}"),
2082 None => problem,
2083 });
2084}
2085
2086fn false_duplicate(
2089 id: &GovernanceLogId,
2090 revision: &Revision,
2091 current: &serde_json::Value,
2092) -> Option<String> {
2093 revision.duplicates.iter().find_map(|(path, same_as)| {
2094 let same = match (current.pointer(path), current.pointer(same_as)) {
2095 (Some(a), Some(b)) => canonical_json(a) == canonical_json(b),
2096 _ => false,
2097 };
2098 (!same).then(|| {
2099 format!(
2100 "revision {id} removed {path} as a duplicate of {same_as}, \
2101 but they differ"
2102 )
2103 })
2104 })
2105}
2106
2107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2109#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2110#[cfg_attr(feature = "schemars", schemars(inline))]
2111pub struct GovernanceVerification {
2112 pub public_key: PublicKeyHex,
2114 pub ok: bool,
2120 #[serde(default)]
2122 pub head: Option<GovernanceLogId>,
2123 pub entries: Vec<EntryVerdict>,
2125 #[serde(default)]
2127 pub keys: Vec<GovernanceKeyRecord>,
2128 #[serde(default)]
2133 pub unanchored_keys: Vec<PublicKeyHex>,
2134 #[serde(default)]
2136 pub repudiated: Vec<GovernanceLogId>,
2137}
2138
2139impl GovernanceVerification {
2140 pub fn settle(mut self) -> Self {
2142 self.ok = self.entries.iter().all(|e| {
2143 e.signature_valid
2144 && e.link_valid
2145 && e.content_matches != Some(false)
2146 && e.problem.is_none()
2147 });
2148 self
2149 }
2150
2151 pub fn check_content(
2162 &mut self,
2163 link: &GovernanceChainLink,
2164 data: &serde_json::Value,
2165 ) -> bool {
2166 let Some(entry) = self.entries.iter_mut().find(|e| e.id == link.id)
2167 else {
2168 return false;
2169 };
2170 let ok = entry.content_check(link.attestation.data_hash, data);
2171 entry.content_matches = Some(ok);
2172 ok
2173 }
2174}
2175
2176pub fn attest(
2187 key: &SigningKey,
2188 envelope: &Envelope,
2189 chain_seq: u64,
2190 signed_at: DateTime<Utc>,
2191) -> GovernanceAttestation {
2192 let signed_at = truncate_to_seconds(signed_at);
2193 let entry_hash = envelope.entry_hash();
2194 let signature =
2195 crypto::sign(key, entry_hash.as_bytes(), signed_at.timestamp());
2196 GovernanceAttestation {
2197 envelope_version: envelope.agora_governance_log,
2198 chain_seq,
2199 prev_hash: envelope.prev_hash,
2200 data_hash: envelope.data_hash,
2201 entry_hash,
2202 signature: signature.into(),
2203 signed_at,
2204 retroactive: is_retroactive(envelope.created_at(), signed_at),
2205 }
2206}
2207
2208#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
2214pub enum LinkError {
2215 #[error(
2216 "envelope version {0} is not supported (this verifier knows {ENVELOPE_VERSION})"
2217 )]
2218 UnsupportedVersion(u32),
2219 #[error("entry_hash does not recompute from the envelope fields")]
2220 HashMismatch,
2221 #[error("signature does not verify under the published key")]
2222 BadSignature,
2223}
2224
2225pub fn recompute_entry_hash(link: &GovernanceChainLink) -> Sha256Hex {
2227 Envelope::new(
2228 link.id.clone(),
2229 link.entry_type,
2230 link.created_at,
2231 link.attestation.prev_hash,
2232 link.attestation.data_hash,
2233 )
2234 .entry_hash()
2235}
2236
2237pub fn verify_link(
2239 link: &GovernanceChainLink,
2240 key: &VerifyingKey,
2241) -> Result<(), LinkError> {
2242 let a = &link.attestation;
2243 if a.envelope_version != ENVELOPE_VERSION {
2244 return Err(LinkError::UnsupportedVersion(a.envelope_version));
2245 }
2246 if recompute_entry_hash(link) != a.entry_hash {
2247 return Err(LinkError::HashMismatch);
2248 }
2249 if !crypto::verify(
2250 key,
2251 a.entry_hash.as_bytes(),
2252 a.signed_at.timestamp(),
2253 &Signature::from(&a.signature),
2254 ) {
2255 return Err(LinkError::BadSignature);
2256 }
2257 Ok(())
2258}
2259
2260pub fn verify_data(
2262 link: &GovernanceChainLink,
2263 data: &serde_json::Value,
2264) -> bool {
2265 data_hash(data) == link.attestation.data_hash
2266}
2267
2268fn same_shape(written: &serde_json::Value, read: &serde_json::Value) -> bool {
2278 use serde_json::Value::{Array, Null, Object};
2279 match (written, read) {
2280 (Object(w), Object(r)) => r.iter().all(|(k, r)| match w.get(k) {
2281 Some(w) => same_shape(w, r),
2282 None => r.is_null(),
2283 }),
2284 (Array(w), Array(r)) => {
2285 w.len() == r.len() && w.iter().zip(r).all(|(w, r)| same_shape(w, r))
2286 }
2287 (_, Object(_) | Array(_)) => false,
2288 (Object(_) | Array(_), Null) => false,
2289 _ => true,
2290 }
2291}
2292
2293fn read_strictly<T>(written: &serde_json::Value) -> Result<T, String>
2295where
2296 T: Serialize + serde::de::DeserializeOwned,
2297{
2298 let read: T =
2299 serde_json::from_value(written.clone()).map_err(|e| e.to_string())?;
2300 let again = serde_json::to_value(&read).map_err(|e| e.to_string())?;
2301 if same_shape(written, &again) {
2302 Ok(read)
2303 } else {
2304 Err("an array where an object belongs, or the reverse".into())
2305 }
2306}
2307
2308pub fn links_from_json(
2315 chain: &serde_json::Value,
2316) -> Result<Vec<GovernanceChainLink>, String> {
2317 chain
2318 .as_array()
2319 .ok_or("a chain is an array of links")?
2320 .iter()
2321 .map(read_strictly)
2322 .collect()
2323}
2324
2325fn read_as_written<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
2327where
2328 D: serde::Deserializer<'de>,
2329 T: Serialize + serde::de::DeserializeOwned,
2330{
2331 let written = serde_json::Value::deserialize(deserializer)?;
2332 if written.is_null() {
2333 return Ok(None);
2334 }
2335 read_strictly(&written)
2336 .map(Some)
2337 .map_err(serde::de::Error::custom)
2338}
2339
2340fn reserved_prefix(
2343 entry_type: GovernanceLogEntryType,
2344) -> Option<GovernanceLogPrefix> {
2345 match entry_type {
2346 GovernanceLogEntryType::Amendment => Some(GovernanceLogPrefix::Amd),
2347 GovernanceLogEntryType::KeyRotation => Some(GovernanceLogPrefix::Key),
2348 GovernanceLogEntryType::StewardRecord => Some(GovernanceLogPrefix::Rec),
2349 GovernanceLogEntryType::CouncilDecision
2350 | GovernanceLogEntryType::AppealsCourtDecision
2351 | GovernanceLogEntryType::EmergencyAction
2352 | GovernanceLogEntryType::PolicyChange
2353 | GovernanceLogEntryType::StewardVeto => None,
2354 }
2355}
2356
2357fn reserved_for(prefix: GovernanceLogPrefix) -> Option<GovernanceLogEntryType> {
2359 match prefix {
2360 GovernanceLogPrefix::Amd => Some(GovernanceLogEntryType::Amendment),
2361 GovernanceLogPrefix::Key => Some(GovernanceLogEntryType::KeyRotation),
2362 GovernanceLogPrefix::Rec => Some(GovernanceLogEntryType::StewardRecord),
2363 GovernanceLogPrefix::Gov | GovernanceLogPrefix::App => None,
2364 }
2365}
2366
2367fn prefix_problem(link: &GovernanceChainLink) -> Option<String> {
2369 let prefix = link.id.prefix();
2370 match (reserved_prefix(link.entry_type), reserved_for(prefix)) {
2371 (Some(want), _) if prefix != want => Some(format!(
2372 "the id of a {} entry must be in the {want}- series, not {}",
2373 link.entry_type, link.id
2374 )),
2375 (None, Some(owner)) => Some(format!(
2376 "{prefix}- ids are reserved for {owner} entries, but {} is a {}",
2377 link.id, link.entry_type
2378 )),
2379 _ => None,
2380 }
2381}
2382
2383fn amendment_target(
2385 amendment: &Amendment,
2386 seq: u64,
2387 seq_of: &HashMap<&str, u64>,
2388 links: &[&GovernanceChainLink],
2389) -> Result<u64, AmendmentError> {
2390 amendment.validate()?;
2391 let target = *seq_of.get(amendment.target.as_str()).ok_or_else(|| {
2392 AmendmentError::UnknownTarget(amendment.target.clone())
2393 })?;
2394 if target >= seq {
2395 return Err(AmendmentError::ForwardReference(amendment.target.clone()));
2396 }
2397 if links[target as usize - 1].attestation.entry_hash
2398 != amendment.target_entry_hash
2399 {
2400 return Err(AmendmentError::WrongTargetHash(amendment.target.clone()));
2401 }
2402 Ok(target)
2403}
2404
2405struct KeyWalk {
2407 history: Vec<(GovernanceKeyRecord, VerifyingKey)>,
2409 unanchored: Vec<PublicKeyHex>,
2410 seen: HashSet<PublicKeyHex>,
2414 repudiated: HashSet<u64>,
2416 genesis: PublicKeyHex,
2417 genesis_certified: bool,
2419}
2420
2421impl KeyWalk {
2422 fn new(genesis: &VerifyingKey, anchor: &KeyAnchor) -> Self {
2423 let public_key = PublicKeyHex::from(genesis);
2424 Self {
2425 genesis: public_key,
2426 genesis_certified: false,
2427 history: vec![(
2428 GovernanceKeyRecord {
2429 public_key,
2430 from_seq: 1,
2431 through_seq: None,
2432 status: KeyStatus::Active,
2433 introduced_by: None,
2434 retired_by: None,
2435 certified: false,
2436 },
2437 *genesis,
2438 )],
2439 unanchored: if anchor.contains(&public_key) {
2440 Vec::new()
2441 } else {
2442 vec![public_key]
2443 },
2444 seen: HashSet::from([public_key]),
2445 repudiated: HashSet::new(),
2446 }
2447 }
2448
2449 fn in_force(&self, seq: u64) -> (PublicKeyHex, VerifyingKey) {
2451 let (record, key) = self
2452 .history
2453 .iter()
2454 .rev()
2455 .find(|(r, _)| r.from_seq <= seq)
2456 .unwrap_or(&self.history[0]);
2457 (record.public_key, *key)
2458 }
2459
2460 fn active(&self) -> PublicKeyHex {
2462 self.history
2463 .last()
2464 .map(|(r, _)| r.public_key)
2465 .expect("the genesis key is always in the history")
2466 }
2467
2468 fn close(
2469 &mut self,
2470 through_seq: u64,
2471 status: KeyStatus,
2472 by: &GovernanceLogId,
2473 ) {
2474 if let Some((record, _)) = self.history.last_mut() {
2475 record.through_seq = Some(through_seq);
2476 record.status = status;
2477 record.retired_by = Some(by.clone());
2478 }
2479 }
2480
2481 fn open(
2482 &mut self,
2483 public_key: PublicKeyHex,
2484 key: VerifyingKey,
2485 from_seq: u64,
2486 by: &GovernanceLogId,
2487 ) {
2488 self.history.push((
2489 GovernanceKeyRecord {
2490 public_key,
2491 from_seq,
2492 through_seq: None,
2493 status: KeyStatus::Active,
2494 introduced_by: Some(by.clone()),
2495 retired_by: None,
2496 certified: true,
2497 },
2498 key,
2499 ));
2500 }
2501
2502 fn apply(
2504 &mut self,
2505 rotation: &KeyRotation,
2506 link: &GovernanceChainLink,
2507 seq: u64,
2508 links: &[&GovernanceChainLink],
2509 roots: &RootSet,
2510 ) -> Result<(), RotationError> {
2511 rotation.verify_certified(seq, link.attestation.prev_hash, roots)?;
2512 let new_key = rotation
2513 .new_key
2514 .to_verifying_key()
2515 .map_err(|_| RotationError::BadNewKey)?;
2516 if self.seen.contains(&rotation.new_key) {
2517 return Err(RotationError::ReusedKey);
2518 }
2519 match (&rotation.outgoing_certificate, self.genesis_certified) {
2524 (None, false) => {
2525 return Err(RotationError::MissingGenesisCertificate);
2526 }
2527 (Some(_), true) => {
2528 return Err(RotationError::UnexpectedOutgoingCertificate);
2529 }
2530 (Some(certificate), false) => certificate
2531 .verify_for(&KeyCertStatement::genesis(self.genesis), roots)
2532 .map_err(RotationError::OutgoingCertificate)?,
2533 (None, true) => {}
2534 }
2535 match rotation.reason {
2536 RotationReason::Routine => {
2537 if rotation.old_key != self.in_force(seq).0 {
2538 return Err(RotationError::WrongOldKey);
2539 }
2540 self.close(seq, KeyStatus::Retired, &link.id);
2541 self.open(rotation.new_key, new_key, seq + 1, &link.id);
2542 }
2543 RotationReason::Compromise => {
2544 let head = rotation
2545 .last_trusted()
2546 .ok_or(RotationError::MissingLastTrusted)?;
2547 let trusted_seq = head.chain_seq;
2548 let names_an_earlier_entry = trusted_seq >= 1
2549 && trusted_seq < seq
2550 && links[trusted_seq as usize - 1].id == head.id
2551 && links[trusted_seq as usize - 1].attestation.entry_hash
2552 == head.entry_hash;
2553 if !names_an_earlier_entry {
2554 return Err(RotationError::UnknownLastTrusted);
2555 }
2556 if self.repudiated.contains(&trusted_seq) {
2559 return Err(RotationError::RepudiatedLastTrusted);
2560 }
2561 if rotation.old_key != self.in_force(trusted_seq).0 {
2564 return Err(RotationError::WrongOldKey);
2565 }
2566 self.history.retain(|(r, _)| r.from_seq <= trusted_seq);
2567 self.close(trusted_seq, KeyStatus::Compromised, &link.id);
2568 self.open(rotation.new_key, new_key, seq, &link.id);
2569 self.repudiated.extend(trusted_seq + 1..seq);
2570 }
2571 }
2572 self.seen.insert(rotation.new_key);
2573 if !self.genesis_certified {
2574 self.genesis_certified = true;
2575 self.history[0].0.certified = true;
2576 self.unanchored.clear();
2577 }
2578 Ok(())
2579 }
2580}
2581
2582pub fn verify_chain(
2613 links: &[GovernanceChainLink],
2614 genesis_key: &VerifyingKey,
2615 anchor: &KeyAnchor,
2616 roots: &RootSet,
2617) -> GovernanceVerification {
2618 let mut links: Vec<&GovernanceChainLink> = links.iter().collect();
2619 links.sort_by_key(|l| l.attestation.chain_seq);
2620
2621 let mut seq_of: HashMap<&str, u64> = HashMap::new();
2622 let mut duplicates: HashSet<&str> = HashSet::new();
2623 for (i, link) in links.iter().enumerate() {
2624 if seq_of.insert(link.id.as_str(), i as u64 + 1).is_some() {
2625 duplicates.insert(link.id.as_str());
2626 }
2627 }
2628
2629 let mut walk = KeyWalk::new(genesis_key, anchor);
2630 let mut amendments: Vec<(u64, GovernanceLogId, Amendment, u64)> =
2632 Vec::new();
2633 let mut entries: Vec<EntryVerdict> = Vec::with_capacity(links.len());
2634 let mut prev: Option<&GovernanceChainLink> = None;
2635
2636 for (i, link) in links.iter().enumerate() {
2637 let a = &link.attestation;
2638 let expected_seq = i as u64 + 1;
2639 let mut problems: Vec<String> = Vec::new();
2640
2641 if let Some(problem) = prefix_problem(link) {
2642 problems.push(problem);
2643 }
2644 if duplicates.contains(link.id.as_str()) {
2645 problems.push(format!("{} appears more than once", link.id));
2646 }
2647
2648 let carries_meaning = matches!(
2652 link.entry_type,
2653 GovernanceLogEntryType::Amendment
2654 | GovernanceLogEntryType::KeyRotation
2655 );
2656 let mut amendment: Option<Amendment> = None;
2657 let mut rotation: Option<KeyRotation> = None;
2658 let mut content_matches: Option<bool> = None;
2659 match &link.data {
2660 Some(data) if non_integer_number(data).is_some() => {
2661 content_matches = Some(false);
2662 problems.push(format!(
2663 "`data` has a number that is not a 64-bit integer at {:?}; \
2664 governance data never contains one",
2665 non_integer_number(data).unwrap_or_default()
2666 ));
2667 }
2668 Some(data) => {
2669 let matched = data_hash(data) == a.data_hash;
2670 content_matches = Some(matched);
2671 if !matched {
2672 if carries_meaning {
2673 problems.push(
2674 "`data` does not hash to the attested data_hash"
2675 .into(),
2676 );
2677 }
2678 } else {
2679 match link.entry_type {
2680 GovernanceLogEntryType::Amendment => {
2681 match read_strictly(data) {
2682 Ok(v) => amendment = Some(v),
2683 Err(e) => problems.push(format!(
2684 "amendment `data` is malformed: {e}"
2685 )),
2686 }
2687 }
2688 GovernanceLogEntryType::KeyRotation => {
2689 match read_strictly(data) {
2690 Ok(v) => rotation = Some(v),
2691 Err(e) => problems.push(format!(
2692 "key_rotation `data` is malformed: {e}"
2693 )),
2694 }
2695 }
2696 _ => {}
2697 }
2698 }
2699 }
2700 None if carries_meaning => problems.push(format!(
2701 "a {} entry must carry its `data`",
2702 link.entry_type
2703 )),
2704 None => {}
2705 }
2706
2707 let declared = rotation
2711 .as_ref()
2712 .filter(|r| r.reason == RotationReason::Compromise)
2713 .map(|r| {
2714 if walk.seen.contains(&r.new_key) {
2715 return Err(RotationError::ReusedKey);
2716 }
2717 r.verify_certified(expected_seq, a.prev_hash, roots)?;
2718 r.new_key
2719 .to_verifying_key()
2720 .map_err(|_| RotationError::BadNewKey)
2721 });
2722 let (key_hex, key) = match &declared {
2723 Some(Ok(k)) => (PublicKeyHex::from(k), *k),
2724 _ => walk.in_force(expected_seq),
2725 };
2726 if let Some(Err(e)) = &declared {
2729 problems.push(e.to_string());
2730 }
2731
2732 let (hash_ok, signature_valid) = match verify_link(link, &key) {
2733 Ok(()) => (true, true),
2734 Err(LinkError::BadSignature) => {
2735 problems.push(LinkError::BadSignature.to_string());
2736 (true, false)
2737 }
2738 Err(e) => {
2739 problems.push(e.to_string());
2740 (false, false)
2741 }
2742 };
2743
2744 let mut link_valid = hash_ok;
2745 if a.chain_seq != expected_seq {
2746 link_valid = false;
2747 problems.push(format!(
2748 "chain_seq {} where {expected_seq} was expected",
2749 a.chain_seq
2750 ));
2751 }
2752 let expected_prev = prev.map(|p| p.attestation.entry_hash);
2753 if a.prev_hash != expected_prev {
2754 link_valid = false;
2755 problems.push(match (a.prev_hash, expected_prev) {
2756 (Some(_), None) => "first entry names a predecessor".into(),
2757 (None, Some(_)) => "prev_hash is null mid-chain".into(),
2758 _ => "prev_hash is not the previous entry's entry_hash".into(),
2759 });
2760 }
2761 let out_of_order = prev.is_some_and(|p| link.created_at < p.created_at);
2762
2763 let authentic = signature_valid && link_valid;
2767 if let Some(rotation) = rotation.as_ref().filter(|_| authentic)
2768 && let Err(e) =
2769 walk.apply(rotation, link, expected_seq, &links, roots)
2770 {
2771 let problem = e.to_string();
2772 if !problems.contains(&problem) {
2773 problems.push(problem);
2774 }
2775 }
2776 let mut texts = None;
2780 if let Some(amendment) = amendment
2781 .as_ref()
2782 .filter(|a| authentic && a.validate().is_ok())
2783 {
2784 match amendment.text_status(link.texts.as_ref()) {
2785 Ok(status) => texts = status,
2786 Err(e) => problems.push(e.to_string()),
2787 }
2788 } else if link.texts.as_ref().is_some_and(|t| !t.is_empty()) {
2789 problems.push(AmendmentError::UncommittedText.to_string());
2790 }
2791 if let Some(amendment) = amendment.filter(|_| authentic) {
2792 match amendment_target(&amendment, expected_seq, &seq_of, &links) {
2793 Ok(target) => amendments.push((
2794 expected_seq,
2795 link.id.clone(),
2796 amendment,
2797 target,
2798 )),
2799 Err(e) => problems.push(e.to_string()),
2800 }
2801 }
2802
2803 entries.push(EntryVerdict {
2804 id: link.id.clone(),
2805 chain_seq: a.chain_seq,
2806 signature_valid,
2807 link_valid,
2808 content_matches,
2809 retroactive: is_retroactive(link.created_at, a.signed_at),
2810 out_of_order,
2811 amended_by: Vec::new(),
2812 signed_by: Some(key_hex),
2813 redacted: false,
2814 redacted_data_hash: None,
2815 revisions: Vec::new(),
2816 latest_data_hash: None,
2817 superseded_revisions: Vec::new(),
2818 history: RevisionHistory::default(),
2819 repudiated: false,
2820 reattested_by: Vec::new(),
2821 texts,
2822 problem: (!problems.is_empty()).then(|| problems.join("; ")),
2823 });
2824 prev = Some(link);
2825 }
2826
2827 let mut applied = vec![false; amendments.len()];
2831 loop {
2832 let mut changed = false;
2833 for (i, (seq, id, amendment, target)) in amendments.iter().enumerate() {
2834 if applied[i]
2835 || amendment.kind != AmendmentKind::Reattested
2836 || walk.repudiated.contains(seq)
2837 {
2838 continue;
2839 }
2840 applied[i] = true;
2841 entries[*target as usize - 1].reattested_by.push(id.clone());
2842 walk.repudiated.remove(target);
2843 changed = true;
2844 }
2845 if !changed {
2846 break;
2847 }
2848 }
2849
2850 for (seq, id, amendment, target) in &amendments {
2851 if walk.repudiated.contains(seq) {
2852 continue;
2853 }
2854 let entry = &mut entries[*target as usize - 1];
2855 entry.amended_by.push(id.clone());
2856 let mut unrebased = false;
2857 if let Some(redaction) = &amendment.redaction {
2858 unrebased = !entry.revisions.is_empty()
2861 && redaction.resulting_latest_hash.is_none();
2862 entry.redacted = true;
2863 entry.redacted_data_hash = Some(redaction.resulting_data_hash);
2864 entry.history.rebased = entry.history.revisions.len();
2866 entry.history.rebased_hash = redaction.resulting_latest_hash;
2867 entry.latest_data_hash = redaction.resulting_latest_hash;
2868 entry.superseded_revisions = entry.revisions.clone();
2869 }
2870 if let Some(revision) = &amendment.revision {
2871 entry.revisions.push(id.clone());
2872 entry.latest_data_hash = Some(revision.resulting_data_hash);
2873 entry.history.revisions.push((id.clone(), revision.clone()));
2874 }
2875 if unrebased {
2876 let target = entry.id.clone();
2877 add_problem(
2878 &mut entries[*seq as usize - 1].problem,
2879 format!(
2880 "the redaction of {target}, which has revisions, names no \
2881 resulting_latest_hash"
2882 ),
2883 );
2884 }
2885 }
2886
2887 for (i, link) in links.iter().enumerate() {
2890 let entry = &mut entries[i];
2891 if let Some(data) = &link.data
2892 && entry.content_matches.is_some()
2893 && (entry.redacted || !entry.history.revisions.is_empty())
2894 {
2895 entry.content_matches =
2896 Some(entry.content_check(link.attestation.data_hash, data));
2897 }
2898 }
2899
2900 let mut seen = HashSet::new();
2901 walk.unanchored.retain(|key| seen.insert(*key));
2902
2903 let mut repudiated = Vec::new();
2904 for (i, entry) in entries.iter_mut().enumerate() {
2905 if walk.repudiated.contains(&(i as u64 + 1)) {
2906 entry.repudiated = true;
2907 repudiated.push(entry.id.clone());
2908 }
2909 }
2910
2911 GovernanceVerification {
2912 public_key: walk.active(),
2913 ok: false,
2914 head: prev.map(|p| p.id.clone()),
2915 entries,
2916 keys: walk.history.into_iter().map(|(record, _)| record).collect(),
2917 unanchored_keys: walk.unanchored,
2918 repudiated,
2919 }
2920 .settle()
2921}
2922
2923#[cfg(test)]
2924mod tests {
2925 use super::*;
2926 use crate::crypto::generate_keypair;
2927 use serde_json::json;
2928
2929 pub(super) fn gov(n: u32) -> GovernanceLogId {
2930 format!("GOV-2026-{n:04}").parse().unwrap()
2931 }
2932
2933 pub(super) fn amd(n: u32) -> GovernanceLogId {
2934 format!("AMD-2026-{n:04}").parse().unwrap()
2935 }
2936
2937 pub(super) fn key_id(n: u32) -> GovernanceLogId {
2938 format!("KEY-2026-{n:04}").parse().unwrap()
2939 }
2940
2941 pub(super) fn rec(n: u32) -> GovernanceLogId {
2942 format!("REC-2026-{n:04}").parse().unwrap()
2943 }
2944
2945 pub(super) fn at(secs: i64) -> DateTime<Utc> {
2946 DateTime::from_timestamp(1_700_000_000 + secs, 123_456_789).unwrap()
2947 }
2948
2949 fn anchored(key: &VerifyingKey) -> KeyAnchor {
2951 KeyAnchor::pinned(key.into())
2952 }
2953
2954 pub(super) fn resalted(draft: &AmendmentDraft, seq: u64) -> AmendmentDraft {
2957 let fix = |field: &str, t: &Option<CommittedText>| {
2958 t.as_ref().map(|t| {
2959 let salt = Sha256::digest(format!("{seq}/{field}/{}", t.text));
2960 CommittedText::with_salt(
2961 TextSalt::from(<[u8; 32]>::from(salt)),
2962 t.text.clone(),
2963 )
2964 })
2965 };
2966 let texts = AmendmentTexts {
2967 basis: fix("basis", &draft.texts.basis),
2968 note: fix("note", &draft.texts.note),
2969 rationale: fix("rationale", &draft.texts.rationale),
2970 };
2971 let commit = |t: &Option<CommittedText>| {
2972 t.as_ref().map(|t| AmendmentText::Committed(t.commitment()))
2973 };
2974 AmendmentDraft {
2975 amendment: Amendment {
2976 basis: commit(&texts.basis).unwrap(),
2977 note: commit(&texts.note).unwrap(),
2978 rationale: commit(&texts.rationale),
2979 ..draft.amendment.clone()
2980 },
2981 texts,
2982 }
2983 }
2984
2985 pub(super) fn v1(draft: AmendmentDraft) -> Amendment {
2987 let plain =
2988 |t: Option<CommittedText>| t.map(|t| AmendmentText::Plain(t.text));
2989 Amendment {
2990 agora_governance_amendment: 1,
2991 basis: plain(draft.texts.basis).unwrap(),
2992 note: plain(draft.texts.note).unwrap(),
2993 rationale: plain(draft.texts.rationale),
2994 ..draft.amendment
2995 }
2996 }
2997
2998 pub(super) fn root(n: u8) -> SigningKey {
3001 SigningKey::from_bytes(&[0xA0 + n; 32])
3002 }
3003
3004 pub(super) fn roots() -> RootSet {
3006 RootSet::new(
3007 [1, 2].map(|n| PublicKeyHex::from(&root(n).verifying_key())),
3008 1,
3009 )
3010 }
3011
3012 pub(super) fn certify(
3014 signers: &[&SigningKey],
3015 statement: KeyCertStatement,
3016 ) -> KeyCertificate {
3017 use ed25519_dalek::Signer;
3018 let message = statement.signed_bytes();
3019 signers.iter().fold(
3020 KeyCertificate::unsigned(statement),
3021 |certificate, signer| {
3022 certificate.with(RootSignature {
3023 root_key: (&signer.verifying_key()).into(),
3024 signature: signer.sign(&message).into(),
3025 })
3026 },
3027 )
3028 }
3029
3030 fn for_new_at(c: &Chain, key: &VerifyingKey) -> KeyCertificate {
3032 certify(
3033 &[&root(1)],
3034 KeyCertStatement::routine(key.into(), c.next_seq(), c.prev_hash()),
3035 )
3036 }
3037
3038 pub(super) fn link(
3039 key: &SigningKey,
3040 n: u32,
3041 prev: Option<&GovernanceChainLink>,
3042 data: &serde_json::Value,
3043 signed_at: DateTime<Utc>,
3044 ) -> GovernanceChainLink {
3045 let created_at = truncate_to_micros(at(n as i64 * 10));
3046 let envelope = Envelope::new(
3047 gov(n),
3048 GovernanceLogEntryType::CouncilDecision,
3049 created_at,
3050 prev.map(|p| p.attestation.entry_hash),
3051 data_hash(data),
3052 );
3053 let attestation = attest(
3054 key,
3055 &envelope,
3056 prev.map_or(1, |p| p.attestation.chain_seq + 1),
3057 signed_at,
3058 );
3059 GovernanceChainLink {
3060 id: gov(n),
3061 entry_type: GovernanceLogEntryType::CouncilDecision,
3062 created_at,
3063 attestation,
3064 data: None,
3065 texts: None,
3066 }
3067 }
3068
3069 pub(super) fn chain(key: &SigningKey, n: u32) -> Vec<GovernanceChainLink> {
3070 let mut out: Vec<GovernanceChainLink> = Vec::new();
3071 for i in 1..=n {
3072 let data = json!({"title": format!("Decision {i}"), "outcome": "approved"});
3073 let l = link(key, i, out.last(), &data, at(i as i64 * 10 + 1));
3074 out.push(l);
3075 }
3076 out
3077 }
3078
3079 pub(super) struct Chain {
3083 pub(super) links: Vec<GovernanceChainLink>,
3084 gov: u32,
3085 amd: u32,
3086 key: u32,
3087 genesis: Option<PublicKeyHex>,
3089 }
3090
3091 impl Chain {
3092 pub(super) fn new() -> Self {
3093 Self {
3094 links: Vec::new(),
3095 gov: 0,
3096 amd: 0,
3097 key: 0,
3098 genesis: None,
3099 }
3100 }
3101
3102 pub(super) fn prev_hash(&self) -> Option<Sha256Hex> {
3103 self.links.last().map(|l| l.attestation.entry_hash)
3104 }
3105
3106 pub(super) fn hash_at(&self, seq: usize) -> Sha256Hex {
3108 self.links[seq - 1].attestation.entry_hash
3109 }
3110
3111 pub(super) fn next_seq(&self) -> u64 {
3113 self.links.len() as u64 + 1
3114 }
3115
3116 pub(super) fn head(&self, seq: usize) -> TrustedHead {
3118 TrustedHead {
3119 id: self.links[seq - 1].id.clone(),
3120 chain_seq: seq as u64,
3121 entry_hash: self.hash_at(seq),
3122 }
3123 }
3124
3125 fn outgoing(&self, rotation: KeyRotation) -> KeyRotation {
3127 match (self.key, self.genesis) {
3128 (0, Some(genesis)) => rotation.with_outgoing(certify(
3129 &[&root(1)],
3130 KeyCertStatement::genesis(genesis),
3131 )),
3132 _ => rotation,
3133 }
3134 }
3135
3136 pub(super) fn routine(
3139 &self,
3140 old: &VerifyingKey,
3141 new: &SigningKey,
3142 ) -> KeyRotation {
3143 let statement = KeyCertStatement::routine(
3144 (&new.verifying_key()).into(),
3145 self.next_seq(),
3146 self.prev_hash(),
3147 );
3148 self.outgoing(KeyRotation::routine(
3149 old.into(),
3150 new,
3151 self.prev_hash(),
3152 at(self.next_seq() as i64 * 10 + 5),
3153 certify(&[&root(1)], statement),
3154 ))
3155 }
3156
3157 pub(super) fn compromise(
3160 &self,
3161 old: &VerifyingKey,
3162 new: &SigningKey,
3163 trusted: usize,
3164 ) -> KeyRotation {
3165 let statement = KeyCertStatement::compromise(
3166 (&new.verifying_key()).into(),
3167 self.next_seq(),
3168 self.prev_hash(),
3169 self.head(trusted),
3170 );
3171 self.outgoing(KeyRotation::compromise(
3172 old.into(),
3173 new,
3174 self.prev_hash(),
3175 at(self.next_seq() as i64 * 10 + 5),
3176 certify(&[&root(1)], statement),
3177 ))
3178 }
3179
3180 pub(super) fn next_amd(&self) -> GovernanceLogId {
3182 amd(self.amd + 1)
3183 }
3184
3185 pub(super) fn push(
3186 &mut self,
3187 signer: &SigningKey,
3188 id: GovernanceLogId,
3189 entry_type: GovernanceLogEntryType,
3190 data: serde_json::Value,
3191 carry: bool,
3192 ) -> GovernanceLogId {
3193 self.genesis
3194 .get_or_insert_with(|| (&signer.verifying_key()).into());
3195 let n = self.links.len() as i64 + 1;
3196 let created_at = truncate_to_micros(at(n * 10));
3197 let envelope = Envelope::new(
3198 id.clone(),
3199 entry_type,
3200 created_at,
3201 self.prev_hash(),
3202 data_hash(&data),
3203 );
3204 let attestation =
3205 attest(signer, &envelope, n as u64, at(n * 10 + 1));
3206 self.links.push(GovernanceChainLink {
3207 id: id.clone(),
3208 entry_type,
3209 created_at,
3210 attestation,
3211 data: carry.then_some(data),
3212 texts: None,
3213 });
3214 id
3215 }
3216
3217 pub(super) fn entry(
3220 &mut self,
3221 signer: &SigningKey,
3222 data: serde_json::Value,
3223 ) -> GovernanceLogId {
3224 self.gov += 1;
3225 let id = gov(self.gov);
3226 self.push(
3227 signer,
3228 id,
3229 GovernanceLogEntryType::CouncilDecision,
3230 data,
3231 false,
3232 )
3233 }
3234
3235 pub(super) fn decision(
3236 &mut self,
3237 signer: &SigningKey,
3238 ) -> GovernanceLogId {
3239 let data = json!({"title": format!("Decision {}", self.gov + 1)});
3240 self.entry(signer, data)
3241 }
3242
3243 pub(super) fn amend(
3245 &mut self,
3246 signer: &SigningKey,
3247 draft: &AmendmentDraft,
3248 ) -> GovernanceLogId {
3249 let draft = resalted(draft, self.next_seq());
3250 let id = self.amend_v1(signer, &draft.amendment);
3251 let link = self.links.last_mut().unwrap();
3252 link.texts = Some(draft.texts);
3253 id
3254 }
3255
3256 pub(super) fn amend_v1(
3259 &mut self,
3260 signer: &SigningKey,
3261 amendment: &Amendment,
3262 ) -> GovernanceLogId {
3263 self.amd += 1;
3264 let id = amd(self.amd);
3265 self.push(
3266 signer,
3267 id,
3268 GovernanceLogEntryType::Amendment,
3269 serde_json::to_value(amendment).unwrap(),
3270 true,
3271 )
3272 }
3273
3274 pub(super) fn rotate(
3275 &mut self,
3276 signer: &SigningKey,
3277 rotation: &KeyRotation,
3278 ) -> GovernanceLogId {
3279 self.key += 1;
3280 let id = key_id(self.key);
3281 self.push(
3282 signer,
3283 id,
3284 GovernanceLogEntryType::KeyRotation,
3285 serde_json::to_value(rotation).unwrap(),
3286 true,
3287 )
3288 }
3289 }
3290
3291 #[test]
3294 fn canonical_json_sorts_keys_at_every_level() {
3295 let v = json!({"b": {"z": 1, "a": [{"y": 2, "x": 3}]}, "a": null});
3296 assert_eq!(
3297 canonical_json(&v),
3298 br#"{"a":null,"b":{"a":[{"x":3,"y":2}],"z":1}}"#
3299 );
3300 }
3301
3302 #[test]
3303 fn canonical_json_is_compact_and_escapes_like_serde() {
3304 let v = json!({"s": "tab\there \"q\" ünïcode \u{1F600}", "n": [1, -2, 3.5, true, false]});
3305 let bytes = canonical_json(&v);
3306 let text = std::str::from_utf8(&bytes).unwrap();
3307 assert_eq!(
3308 text,
3309 r#"{"n":[1,-2,3.5,true,false],"s":"tab\there \"q\" ünïcode 😀"}"#
3310 );
3311 }
3312
3313 #[test]
3314 fn canonical_json_ignores_insertion_order() {
3315 let mut a = serde_json::Map::new();
3316 a.insert("z".into(), json!(1));
3317 a.insert("a".into(), json!(2));
3318 let mut b = serde_json::Map::new();
3319 b.insert("a".into(), json!(2));
3320 b.insert("z".into(), json!(1));
3321 assert_eq!(
3322 canonical_json(&serde_json::Value::Object(a)),
3323 canonical_json(&serde_json::Value::Object(b))
3324 );
3325 }
3326
3327 #[test]
3328 fn canonical_json_empty_containers() {
3329 assert_eq!(canonical_json(&json!({})), b"{}");
3330 assert_eq!(canonical_json(&json!([])), b"[]");
3331 assert_eq!(
3332 canonical_json(&json!({"a": {}, "b": []})),
3333 br#"{"a":{},"b":[]}"#
3334 );
3335 }
3336
3337 #[test]
3340 fn preimage_is_declaration_ordered_json() {
3341 let e = Envelope::new(
3342 gov(1),
3343 GovernanceLogEntryType::AppealsCourtDecision,
3344 at(0),
3345 None,
3346 data_hash(&json!({})),
3347 );
3348 let text = String::from_utf8(e.preimage()).unwrap();
3349 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}");
3350 }
3351
3352 #[test]
3353 fn every_envelope_field_changes_the_hash() {
3354 let base = Envelope::new(
3355 gov(1),
3356 GovernanceLogEntryType::CouncilDecision,
3357 at(0),
3358 None,
3359 data_hash(&json!({"a":1})),
3360 );
3361 let h = base.entry_hash();
3362 let mut e = base.clone();
3363 e.id = gov(2);
3364 assert_ne!(e.entry_hash(), h);
3365 let mut e = base.clone();
3366 e.entry_type = GovernanceLogEntryType::PolicyChange;
3367 assert_ne!(e.entry_hash(), h);
3368 let mut e = base.clone();
3369 e.created_at += 1;
3370 assert_ne!(e.entry_hash(), h);
3371 let mut e = base.clone();
3372 e.prev_hash = Some(h);
3373 assert_ne!(e.entry_hash(), h);
3374 let mut e = base.clone();
3375 e.data_hash = data_hash(&json!({"a":2}));
3376 assert_ne!(e.entry_hash(), h);
3377 assert_eq!(base.entry_hash(), h, "and it is deterministic");
3378 }
3379
3380 #[test]
3381 fn truncation_matches_what_the_envelope_carries() {
3382 let t = at(0);
3383 assert_eq!(truncate_to_micros(t).timestamp_subsec_nanos(), 123_456_000);
3384 assert_eq!(truncate_to_seconds(t).timestamp_subsec_nanos(), 0);
3385 assert_eq!(
3386 Envelope::new(
3387 gov(1),
3388 GovernanceLogEntryType::CouncilDecision,
3389 t,
3390 None,
3391 data_hash(&json!(null))
3392 )
3393 .created_at,
3394 truncate_to_micros(t).timestamp_micros()
3395 );
3396 }
3397
3398 #[test]
3401 fn hex_newtypes_round_trip_and_reject_wrong_lengths() {
3402 let h = data_hash(&json!(1));
3403 let s = serde_json::to_string(&h).unwrap();
3404 assert_eq!(s.len(), 66);
3405 let back: Sha256Hex = serde_json::from_str(&s).unwrap();
3406 assert_eq!(back, h);
3407 assert!(serde_json::from_str::<Sha256Hex>("\"abcd\"").is_err());
3408 assert!("zz".repeat(32).parse::<Sha256Hex>().is_err());
3409 assert!(Sha256Hex::try_from(vec![0u8; 31]).is_err());
3410 assert_eq!(format!("{h:?}"), format!("Sha256Hex({h})"));
3411 }
3412
3413 #[test]
3416 fn attest_then_verify_link() {
3417 let (key, pk) = generate_keypair();
3418 let l = link(&key, 1, None, &json!({"a": 1}), at(5));
3419 assert_eq!(verify_link(&l, &pk), Ok(()));
3420 assert!(verify_data(&l, &json!({"a": 1})));
3421 assert!(!verify_data(&l, &json!({"a": 2})));
3422 assert!(!l.attestation.retroactive);
3423 }
3424
3425 #[test]
3426 fn wrong_key_fails_signature_only() {
3427 let (key, _) = generate_keypair();
3428 let (_, other) = generate_keypair();
3429 let l = link(&key, 1, None, &json!({}), at(5));
3430 assert_eq!(verify_link(&l, &other), Err(LinkError::BadSignature));
3431 }
3432
3433 #[test]
3434 fn tampering_with_any_attested_field_is_detected() {
3435 let (key, pk) = generate_keypair();
3436 let l = link(&key, 1, None, &json!({"a": 1}), at(5));
3437
3438 let mut t = l.clone();
3439 t.created_at += chrono::Duration::microseconds(1);
3440 assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
3441
3442 let mut t = l.clone();
3443 t.entry_type = GovernanceLogEntryType::StewardVeto;
3444 assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
3445
3446 let mut t = l.clone();
3447 t.attestation.data_hash = data_hash(&json!({"a": 2}));
3448 assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
3449
3450 let mut t = l.clone();
3453 t.attestation.signed_at -= chrono::Duration::seconds(1);
3454 assert_eq!(verify_link(&t, &pk), Err(LinkError::BadSignature));
3455
3456 let mut t = l.clone();
3457 t.attestation.envelope_version = 2;
3458 assert_eq!(verify_link(&t, &pk), Err(LinkError::UnsupportedVersion(2)));
3459 }
3460
3461 #[test]
3462 fn a_good_chain_verifies_in_any_input_order() {
3463 let (key, pk) = generate_keypair();
3464 let mut c = chain(&key, 4);
3465 c.reverse();
3466 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
3467 assert!(v.ok, "{v:#?}");
3468 assert_eq!(v.head, Some(gov(4)));
3469 assert_eq!(
3470 v.entries.iter().map(|e| e.chain_seq).collect::<Vec<_>>(),
3471 [1, 2, 3, 4]
3472 );
3473 assert!(v.entries.iter().all(|e| e.content_matches.is_none()
3474 && e.problem.is_none()
3475 && !e.out_of_order));
3476 assert_eq!(v.public_key, PublicKeyHex::from(&pk));
3477 }
3478
3479 #[test]
3480 fn empty_chain_is_ok_with_no_head() {
3481 let (_, pk) = generate_keypair();
3482 let v = verify_chain(&[], &pk, &anchored(&pk), &roots());
3483 assert!(v.ok);
3484 assert!(v.head.is_none());
3485 assert!(v.entries.is_empty());
3486 }
3487
3488 #[test]
3489 fn a_changed_entry_breaks_its_signature_and_the_next_link() {
3490 let (key, pk) = generate_keypair();
3491 let mut c = chain(&key, 3);
3492 let rewritten = link(
3495 &key,
3496 2,
3497 Some(&c[0]),
3498 &json!({"title": "Decision 2", "outcome": "REJECTED"}),
3499 at(21),
3500 );
3501 c[1] = rewritten;
3502 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
3503 assert!(!v.ok);
3504 assert!(
3505 v.entries[1].signature_valid && v.entries[1].link_valid,
3506 "the rewrite itself is well-formed: {:#?}",
3507 v.entries[1]
3508 );
3509 assert!(
3510 !v.entries[2].link_valid,
3511 "but entry 3 no longer points at it: {:#?}",
3512 v.entries[2]
3513 );
3514 assert!(
3515 v.entries[2]
3516 .problem
3517 .as_deref()
3518 .unwrap()
3519 .contains("prev_hash")
3520 );
3521 }
3522
3523 #[test]
3524 fn a_removed_entry_is_a_gap_and_a_broken_link() {
3525 let (key, pk) = generate_keypair();
3526 let mut c = chain(&key, 3);
3527 c.remove(1);
3528 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
3529 assert!(!v.ok);
3530 assert!(v.entries[0].link_valid);
3531 let p = v.entries[1].problem.as_deref().unwrap();
3532 assert!(p.contains("chain_seq 3 where 2 was expected"), "{p}");
3533 assert!(p.contains("prev_hash"), "{p}");
3534 }
3535
3536 #[test]
3537 fn a_second_genesis_is_rejected() {
3538 let (key, pk) = generate_keypair();
3539 let mut c = chain(&key, 2);
3540 let rogue = link(&key, 2, None, &json!({}), at(21));
3541 c[1] = rogue;
3542 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
3543 assert!(!v.ok);
3544 assert!(
3545 v.entries[1]
3546 .problem
3547 .as_deref()
3548 .unwrap()
3549 .contains("null mid-chain")
3550 );
3551 }
3552
3553 #[test]
3554 fn retroactive_and_out_of_order_are_recomputed_not_copied() {
3555 let (key, pk) = generate_keypair();
3556 let first = link(&key, 1, None, &json!({}), at(10 + 3600));
3557 let created = truncate_to_micros(at(5));
3560 let envelope = Envelope::new(
3561 gov(2),
3562 GovernanceLogEntryType::CouncilDecision,
3563 created,
3564 Some(first.attestation.entry_hash),
3565 data_hash(&json!({})),
3566 );
3567 let attestation = attest(&key, &envelope, 2, at(6));
3568 let mut second = GovernanceChainLink {
3569 id: gov(2),
3570 entry_type: GovernanceLogEntryType::CouncilDecision,
3571 created_at: created,
3572 attestation,
3573 data: None,
3574 texts: None,
3575 };
3576 second.attestation.retroactive = true; let v = verify_chain(&[first, second], &pk, &anchored(&pk), &roots());
3578 assert!(v.ok, "{v:#?}");
3579 assert!(v.entries[0].retroactive);
3580 assert!(!v.entries[1].retroactive, "recomputed from timestamps");
3581 assert!(v.entries[1].out_of_order);
3582 }
3583
3584 #[test]
3585 fn content_mismatch_settles_to_not_ok() {
3586 let (key, pk) = generate_keypair();
3587 let c = chain(&key, 1);
3588 let mut v = verify_chain(&c, &pk, &anchored(&pk), &roots());
3589 v.entries[0].content_matches = Some(true);
3590 assert!(v.clone().settle().ok);
3591 v.entries[0].content_matches = Some(false);
3592 assert!(!v.settle().ok);
3593 }
3594
3595 #[test]
3598 fn an_amendment_fills_amended_by_on_its_target() {
3599 let (key, pk) = generate_keypair();
3600 let mut c = Chain::new();
3601 let target = c.decision(&key);
3602 c.decision(&key);
3603 let amendment = AmendmentDraft::new(
3604 target,
3605 c.hash_at(1),
3606 AmendmentKind::NonPrecedential,
3607 "§1 (Red Team Cases Recharacterized)",
3608 "diagnostic finding — not citable as moderation precedent",
3609 )
3610 .unwrap()
3611 .with_authority(gov(5))
3612 .with_rationale("§5 leaves the ruling itself standing");
3613 let id = c.amend(&key, &amendment);
3614
3615 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3616 assert!(v.ok, "{v:#?}");
3617 assert_eq!(v.entries[0].amended_by, vec![id]);
3618 assert!(v.entries[1].amended_by.is_empty());
3619 assert!(!v.entries[0].redacted);
3620 assert_eq!(v.head, Some(amd(1)));
3621 assert_eq!(v.entries[2].content_matches, Some(true));
3623 assert_eq!(v.entries[0].content_matches, None);
3624 assert_eq!(
3625 standing([amendment.amendment.kind]),
3626 Standing::NonPrecedential
3627 );
3628 }
3629
3630 #[test]
3631 fn an_amendment_must_name_an_earlier_entry_by_its_exact_hash() {
3632 let (key, pk) = generate_keypair();
3633 let problem = |c: &Chain, at: usize| -> String {
3634 verify_chain(&c.links, &pk, &anchored(&pk), &roots()).entries[at]
3635 .problem
3636 .clone()
3637 .unwrap_or_default()
3638 };
3639
3640 let mut c = Chain::new();
3641 c.decision(&key);
3642 let unknown = AmendmentDraft::new(
3643 gov(99),
3644 c.hash_at(1),
3645 AmendmentKind::Overruled,
3646 "b",
3647 "n",
3648 )
3649 .unwrap();
3650 c.amend(&key, &unknown);
3651 assert!(
3652 problem(&c, 1).contains("is not an entry of this chain"),
3653 "{}",
3654 problem(&c, 1)
3655 );
3656 assert!(!verify_chain(&c.links, &pk, &anchored(&pk), &roots()).ok);
3657
3658 let mut c = Chain::new();
3660 c.decision(&key);
3661 let forward = AmendmentDraft::new(
3662 gov(2),
3663 c.hash_at(1),
3664 AmendmentKind::Overruled,
3665 "b",
3666 "n",
3667 )
3668 .unwrap();
3669 c.amend(&key, &forward);
3670 c.decision(&key);
3671 assert!(
3672 problem(&c, 1).contains("not an earlier entry"),
3673 "{}",
3674 problem(&c, 1)
3675 );
3676
3677 let mut c = Chain::new();
3679 let target = c.decision(&key);
3680 let wrong_hash = AmendmentDraft::new(
3681 target,
3682 data_hash(&json!("some other entry")),
3683 AmendmentKind::Overruled,
3684 "b",
3685 "n",
3686 )
3687 .unwrap();
3688 c.amend(&key, &wrong_hash);
3689 assert!(problem(&c, 1).contains("entry_hash"), "{}", problem(&c, 1));
3690 assert!(
3691 verify_chain(&c.links, &pk, &anchored(&pk), &roots()).entries[0]
3692 .amended_by
3693 .is_empty()
3694 );
3695 }
3696
3697 #[test]
3698 fn redaction_shape_violations_fail_verification() {
3699 let (key, pk) = generate_keypair();
3700 let amend_with = |mutate: &dyn Fn(&mut Amendment)| -> String {
3701 let mut c = Chain::new();
3702 let target = c.decision(&key);
3703 let mut amendment = AmendmentDraft::new(
3704 target,
3705 c.hash_at(1),
3706 AmendmentKind::Correction,
3707 "b",
3708 "n",
3709 )
3710 .unwrap();
3711 mutate(&mut amendment.amendment);
3712 c.amend_v1(&key, &amendment.amendment);
3713 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3714 assert!(!v.ok, "{v:#?}");
3715 v.entries[1].problem.clone().unwrap_or_default()
3716 };
3717
3718 assert_eq!(
3721 AmendmentDraft::new(
3722 gov(1),
3723 data_hash(&json!(null)),
3724 AmendmentKind::Redaction,
3725 "b",
3726 "n"
3727 ),
3728 Err(AmendmentError::MissingRedaction)
3729 );
3730 let p = amend_with(&|a| a.kind = AmendmentKind::Redaction);
3731 assert!(p.contains("requires a `redaction`"), "{p}");
3732
3733 let p = amend_with(&|a| {
3734 a.redaction = Some(Redaction {
3735 fields: vec!["/x".into()],
3736 resulting_data_hash: data_hash(&json!({})),
3737 resulting_latest_hash: None,
3738 })
3739 });
3740 assert!(p.contains("only valid on kind"), "{p}");
3741
3742 let p = amend_with(&|a| a.agora_governance_amendment = 3);
3743 assert!(p.contains("agora_governance_amendment is 3"), "{p}");
3744
3745 let p = amend_with(&|a| a.agora_governance_amendment = 1);
3748 assert!(p.contains("does neither consistently"), "{p}");
3749 let p = amend_with(&|a| a.note = AmendmentText::Plain("n".into()));
3750 assert!(p.contains("does neither consistently"), "{p}");
3751 }
3752
3753 #[test]
3754 fn governance_data_holds_no_number_that_is_not_a_64_bit_integer() {
3755 let fine = json!({"a": [1, -2, u64::MAX, i64::MIN], "b": {"c": "0.5"}});
3756 assert_eq!(non_integer_number(&fine), None);
3757 assert!(blind_data(&fine, Blind::random()).is_ok());
3758
3759 for (text, pointer) in [
3760 (r#"{"a": {"b/c": [1, 0.5]}}"#, "/a/b~1c/1"),
3761 (r#"{"n": 1.0}"#, "/n"),
3762 (r#"{"n": 1e3}"#, "/n"),
3763 (r#"{"n": 18446744073709551616}"#, "/n"),
3764 (r#"{"n": -9223372036854775809}"#, "/n"),
3765 ] {
3766 let data: serde_json::Value = serde_json::from_str(text).unwrap();
3767 assert_eq!(non_integer_number(&data).as_deref(), Some(pointer));
3768 assert_eq!(
3769 blind_data(&data, Blind::random()),
3770 Err(BlindError::NonIntegerNumber(pointer.into())),
3771 "the writer refuses it"
3772 );
3773 assert_eq!(
3774 redact_data(&data, &["/n".into()], &amd(1), Blind::random()),
3775 Err(RedactError::NonIntegerNumber(pointer.into()))
3776 );
3777 }
3778 }
3779
3780 #[test]
3781 fn standing_is_the_last_amendment_that_changes_it() {
3782 use AmendmentKind::*;
3783 assert_eq!(standing([]), Standing::InForce);
3784 assert_eq!(standing([Correction, Redaction]), Standing::InForce);
3785 assert_eq!(
3786 standing([Correction, NonPrecedential, Redaction]),
3787 Standing::NonPrecedential
3788 );
3789 assert_eq!(standing([Overruled, Reinstated]), Standing::InForce);
3790 assert_eq!(standing([Reinstated, Superseded]), Standing::Superseded);
3791 assert_eq!(kind_standing(Reattested), None);
3792 assert_eq!(kind_standing(Reinstated), Some(Standing::InForce));
3793 }
3794
3795 #[test]
3796 fn a_redaction_verifies_against_the_amendment_and_nothing_else() {
3797 let (key, pk) = generate_keypair();
3798 let data = json!({
3799 "finding": "upheld",
3800 "subject": {"handle": "someone", "detail": "personal"},
3801 });
3802 let mut c = Chain::new();
3803 let target = c.entry(&key, data.clone());
3804 let amendment_id = c.next_amd();
3805 let (amendment, redacted) = AmendmentDraft::redaction(
3806 &amendment_id,
3807 target,
3808 c.hash_at(1),
3809 "GDPR Art. 17(1)(a)",
3810 "personal data removed on request",
3811 vec!["/subject/handle".into(), "/subject/detail".into()],
3812 &data,
3813 Blind::from([7; 32]),
3814 &[],
3815 )
3816 .unwrap();
3817 assert_eq!(c.amend(&key, &amendment), amendment_id);
3818 assert_eq!(redacted[BLIND_KEY], json!(Blind::from([7; 32])));
3819
3820 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3821 assert!(v.ok, "{v:#?}");
3822 assert!(v.entries[0].redacted);
3823 assert_eq!(v.entries[0].redacted_data_hash, Some(data_hash(&redacted)));
3824 assert_eq!(
3825 redacted["subject"]["handle"],
3826 json!(format!("[redacted by {amendment_id}]"))
3827 );
3828 assert_eq!(redacted["finding"], json!("upheld"), "and nothing else");
3829
3830 assert!(v.check_content(&c.links[0], &redacted));
3832 assert_eq!(v.entries[0].content_matches, Some(true));
3833 assert!(v.clone().settle().ok);
3834 assert!(v.check_content(&c.links[0], &data));
3836
3837 let mut tampered = redacted.clone();
3839 tampered["finding"] = json!("overturned");
3840 assert!(!v.check_content(&c.links[0], &tampered));
3841 assert_eq!(v.entries[0].content_matches, Some(false));
3842 assert!(!v.settle().ok);
3843 }
3844
3845 fn seats() -> serde_json::Value {
3848 json!({
3849 "title": "A motion",
3850 "rounds": [{
3851 "number": 1,
3852 "responses": [
3853 {"role": "lawyer", "rationale": "Because.", "raw_text": "Because.", "vote": "yes"},
3854 {"role": "artist", "rationale": "Why not.", "raw_text": "Why not?", "vote": "no"},
3855 ],
3856 }],
3857 "subject": {"handle": "someone"},
3858 BLIND_KEY: Blind::from([3; 32]),
3859 })
3860 }
3861
3862 const LAWYER: &str = "/rounds/0/responses/0";
3863
3864 fn patch(ops: serde_json::Value) -> json_patch::Patch {
3866 serde_json::from_value(ops).unwrap()
3867 }
3868
3869 fn dedup(data: &serde_json::Value) -> Edit {
3871 let (raw, rationale) =
3872 (format!("{LAWYER}/raw_text"), format!("{LAWYER}/rationale"));
3873 Revision::remove_duplicates(data, &[(&raw, &rationale)]).unwrap()
3874 }
3875
3876 fn revise(
3877 c: &Chain,
3878 target: &GovernanceLogId,
3879 latest: &serde_json::Value,
3880 patch: impl Into<Edit>,
3881 ) -> (AmendmentDraft, serde_json::Value) {
3882 AmendmentDraft::revision(
3883 target.clone(),
3884 GovernanceLogEntryType::CouncilDecision,
3885 c.hash_at(1),
3886 "Steward's record REC-2026-0001",
3887 "raw_text identical to the rationale removed",
3888 latest,
3889 patch,
3890 )
3891 .unwrap()
3892 }
3893
3894 fn revision_of(draft: &AmendmentDraft) -> &Revision {
3895 draft.amendment.revision.as_ref().unwrap()
3896 }
3897
3898 #[test]
3899 fn a_revision_overwrites_nothing_and_folds_to_the_latest() {
3900 let (key, pk) = generate_keypair();
3901 let data = seats();
3902 let mut c = Chain::new();
3903 let target = c.entry(&key, data.clone());
3904 let (first, v1) = revise(&c, &target, &data, dedup(&data));
3905 c.amend(&key, &first);
3906 let (second, v2) = revise(
3907 &c,
3908 &target,
3909 &v1,
3910 patch(json!([{"op": "move", "from": "/title", "path": "/motion"}])),
3911 );
3912 c.amend(&key, &second);
3913
3914 let seat = &v1["rounds"][0]["responses"];
3915 assert!(seat[0].get("raw_text").is_none(), "removed, no marker");
3916 assert_eq!(seat[1]["raw_text"], json!("Why not?"), "the other stays");
3917 assert_eq!(v2["motion"], json!("A motion"));
3918 assert_eq!(v2[BLIND_KEY], data[BLIND_KEY], "blind kept");
3919 assert_eq!(
3920 latest(&data, [revision_of(&first), revision_of(&second)]).unwrap(),
3921 v2
3922 );
3923
3924 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3925 assert!(v.ok, "{v:#?}");
3926 let entry = &v.entries[0];
3927 assert_eq!(entry.revisions, [amd(1), amd(2)]);
3928 assert_eq!(entry.latest_data_hash, Some(data_hash(&v2)));
3929 assert!(!entry.redacted && entry.redacted_data_hash.is_none());
3930
3931 assert!(v.check_content(&c.links[0], &data));
3933 assert!(v.check_content(&c.links[0], &v2));
3934 assert!(!v.check_content(&c.links[0], &v1), "not the latest");
3935 v.check_content(&c.links[0], &data);
3936 assert!(v.clone().settle().ok);
3937
3938 let wire = serde_json::to_value(&v).unwrap();
3941 assert!(wire["entries"][0].get("history").is_none());
3942 let mut old = wire.clone();
3943 for field in ["revisions", "latest_data_hash", "superseded_revisions"] {
3944 old["entries"][0].as_object_mut().unwrap().remove(field);
3945 }
3946 let old: GovernanceVerification = serde_json::from_value(old).unwrap();
3947 assert!(old.entries[0].revisions.is_empty());
3948 }
3949
3950 #[test]
3952 fn a_revision_that_does_not_produce_its_hash_fails() {
3953 let (key, pk) = generate_keypair();
3954 let data = seats();
3955 let mut c = Chain::new();
3956 let target = c.entry(&key, data.clone());
3957 let (mut draft, _) = revise(&c, &target, &data, dedup(&data));
3958 draft
3959 .amendment
3960 .revision
3961 .as_mut()
3962 .unwrap()
3963 .resulting_data_hash = data_hash(&json!({"something": "else"}));
3964 c.amend(&key, &draft);
3965
3966 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3967 assert!(v.ok, "the chain itself is sound: {v:#?}");
3968 assert!(v.check_content(&c.links[0], &data), "the stored data is");
3969 assert!(
3970 v.entries[0]
3971 .problem
3972 .as_deref()
3973 .is_some_and(|p| p.contains("does not produce")),
3974 "{:?}",
3975 v.entries[0].problem
3976 );
3977 assert!(!v.settle().ok);
3978
3979 c.links[0].data = Some(data);
3981 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3982 assert!(!v.ok);
3983 }
3984
3985 fn redact(
3986 c: &Chain,
3987 target: &GovernanceLogId,
3988 fields: &[&str],
3989 data: &serde_json::Value,
3990 revisions: &[&Revision],
3991 ) -> Result<(AmendmentDraft, serde_json::Value), RedactError> {
3992 AmendmentDraft::redaction(
3993 &c.next_amd(),
3994 target.clone(),
3995 c.hash_at(1),
3996 "GDPR Art. 17(1)(a)",
3997 "removed on request",
3998 fields.iter().map(|f| f.to_string()).collect(),
3999 data,
4000 Blind::from([9; 32]),
4001 revisions,
4002 )
4003 }
4004
4005 #[test]
4008 fn a_revision_then_a_redaction() {
4009 let (key, pk) = generate_keypair();
4010 let data = seats();
4011 let mut c = Chain::new();
4012 let target = c.entry(&key, data.clone());
4013 let (revision, _) = revise(&c, &target, &data, dedup(&data));
4014 c.amend(&key, &revision);
4015 let rationale = format!("{LAWYER}/rationale");
4016 let (redaction, redacted) = redact(
4017 &c,
4018 &target,
4019 &[&rationale],
4020 &data,
4021 &[revision_of(&revision)],
4022 )
4023 .unwrap();
4024 c.amend(&key, &redaction);
4025
4026 let fields = &redaction.amendment.redaction.as_ref().unwrap().fields;
4028 assert_eq!(fields, &[rationale, format!("{LAWYER}/raw_text")]);
4029 let seat = &redacted["rounds"][0]["responses"][0];
4030 assert_eq!(seat["raw_text"], seat["rationale"]);
4031 assert!(seat["raw_text"].as_str().unwrap().starts_with("[redacted"));
4032
4033 let rebased = latest(&redacted, [revision_of(&revision)]).unwrap();
4034 assert!(
4035 rebased["rounds"][0]["responses"][0]
4036 .get("raw_text")
4037 .is_none()
4038 );
4039 assert_eq!(
4040 redaction
4041 .amendment
4042 .redaction
4043 .as_ref()
4044 .unwrap()
4045 .resulting_latest_hash,
4046 Some(data_hash(&rebased))
4047 );
4048
4049 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4050 assert!(v.ok, "{v:#?}");
4051 let entry = &v.entries[0];
4052 assert_eq!(entry.latest_data_hash, Some(data_hash(&rebased)));
4053 assert_eq!(entry.superseded_revisions, [amd(1)]);
4054 assert!(v.check_content(&c.links[0], &redacted));
4055 assert!(v.check_content(&c.links[0], &rebased));
4056 assert!(
4057 v.check_content(&c.links[0], &data),
4058 "for whoever kept a copy"
4059 );
4060 v.check_content(&c.links[0], &redacted);
4061 assert!(v.clone().settle().ok, "{v:#?}");
4062
4063 let mut c2 = Chain::new();
4065 let target = c2.entry(&key, data.clone());
4066 c2.amend(&key, &revision);
4067 let (mut lying, _) = redact(
4068 &c2,
4069 &target,
4070 &["/subject/handle"],
4071 &data,
4072 &[revision_of(&revision)],
4073 )
4074 .unwrap();
4075 let (_, honest) = redact(
4076 &c2,
4077 &target,
4078 &["/subject/handle"],
4079 &data,
4080 &[revision_of(&revision)],
4081 )
4082 .unwrap();
4083 lying
4084 .amendment
4085 .redaction
4086 .as_mut()
4087 .unwrap()
4088 .resulting_latest_hash = Some(data_hash(&json!({})));
4089 c2.amend(&key, &lying);
4090 let mut v = verify_chain(&c2.links, &pk, &anchored(&pk), &roots());
4091 assert!(v.check_content(&c2.links[0], &honest));
4092 assert!(!v.settle().ok);
4093 }
4094
4095 #[test]
4098 fn a_redaction_of_a_revised_entry_must_name_the_latest_hash() {
4099 let (key, pk) = generate_keypair();
4100 let data = seats();
4101 let mut c = Chain::new();
4102 let target = c.entry(&key, data.clone());
4103 let (revision, _) = revise(&c, &target, &data, dedup(&data));
4104 c.amend(&key, &revision);
4105 let (redaction, _) =
4107 redact(&c, &target, &["/subject/handle"], &data, &[]).unwrap();
4108 c.amend(&key, &redaction);
4109 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4110 assert!(!v.ok);
4111 assert!(
4112 v.entries[2]
4113 .problem
4114 .as_deref()
4115 .is_some_and(|p| p.contains("names no resulting_latest_hash")),
4116 "{v:#?}"
4117 );
4118 }
4119
4120 #[test]
4123 fn a_false_duplicate_fails() {
4124 let (key, pk) = generate_keypair();
4125 let data = seats();
4126 let mut c = Chain::new();
4127 let target = c.entry(&key, data.clone());
4128 let artist = "/rounds/0/responses/1";
4129 let (raw, rationale) =
4130 (format!("{artist}/raw_text"), format!("{artist}/rationale"));
4131 let (mut lying, _) = revise(
4133 &c,
4134 &target,
4135 &data,
4136 patch(json!([{"op": "remove", "path": raw}])),
4137 );
4138 let revised = apply_patch(&data, &revision_of(&lying).patch).unwrap();
4139 let r = lying.amendment.revision.as_mut().unwrap();
4140 r.duplicates = vec![(raw, rationale)];
4141 r.resulting_data_hash = data_hash(&revised);
4142 c.amend(&key, &lying);
4143
4144 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4145 assert!(v.ok, "unread, the claim is unchecked: {v:#?}");
4146 assert!(v.check_content(&c.links[0], &data));
4147 assert!(
4148 v.entries[0]
4149 .problem
4150 .as_deref()
4151 .is_some_and(|p| p.contains("but they differ")),
4152 "{v:#?}"
4153 );
4154 assert!(!v.settle().ok);
4155 }
4156
4157 #[test]
4159 fn a_redaction_that_breaks_a_revision_is_refused() {
4160 let (key, _) = generate_keypair();
4161 let data = seats();
4162 let mut c = Chain::new();
4163 let target = c.entry(&key, data.clone());
4164 let (moved, _) = revise(
4165 &c,
4166 &target,
4167 &data,
4168 patch(
4169 json!([{"op": "move", "from": "/subject/handle", "path": "/handle"}]),
4170 ),
4171 );
4172 c.amend(&key, &moved);
4173 let err =
4174 redact(&c, &target, &["/subject"], &data, &[revision_of(&moved)])
4175 .unwrap_err();
4176 assert!(matches!(err, RedactError::Rebase(_)), "{err}");
4177 assert!(
4179 redact(
4180 &c,
4181 &target,
4182 &["/subject/handle"],
4183 &data,
4184 &[revision_of(&moved)]
4185 )
4186 .is_ok()
4187 );
4188 }
4189
4190 #[test]
4193 fn a_redaction_follows_every_duplicate_of_what_it_erases() {
4194 let revision = Revision {
4195 patch: patch(json!([{"op": "remove", "path": "/copy"}])),
4196 duplicates: vec![("/copy".into(), "/source/inner".into())],
4197 resulting_data_hash: data_hash(&json!(null)),
4198 };
4199 let extra = |fields: &[&str]| {
4200 let fields: Vec<String> =
4201 fields.iter().map(|f| f.to_string()).collect();
4202 duplicates_of(&fields, &[&revision])
4203 };
4204 assert_eq!(extra(&["/source/inner"]), ["/copy"]);
4205 assert_eq!(extra(&["/source/inner/name"]), ["/copy/name"]);
4206 assert_eq!(extra(&["/source"]), ["/copy"]);
4207 assert!(extra(&["/source/other"]).is_empty());
4208 assert!(extra(&["/source/inn"]).is_empty());
4209 }
4210
4211 #[test]
4214 fn a_redaction_then_a_revision() {
4215 let (key, pk) = generate_keypair();
4216 let data = seats();
4217 let mut c = Chain::new();
4218 let target = c.entry(&key, data.clone());
4219 let (redaction, redacted) =
4220 redact(&c, &target, &["/subject/handle"], &data, &[]).unwrap();
4221 assert_eq!(
4222 redaction
4223 .amendment
4224 .redaction
4225 .as_ref()
4226 .unwrap()
4227 .resulting_latest_hash,
4228 None,
4229 "nothing to rebase"
4230 );
4231 c.amend(&key, &redaction);
4232 let (revision, revised) =
4233 revise(&c, &target, &redacted, dedup(&redacted));
4234 c.amend(&key, &revision);
4235
4236 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4237 assert!(v.ok, "{v:#?}");
4238 assert_eq!(v.entries[0].latest_data_hash, Some(data_hash(&revised)));
4239 assert!(v.entries[0].superseded_revisions.is_empty());
4240 assert!(v.check_content(&c.links[0], &redacted));
4241 assert!(v.check_content(&c.links[0], &revised));
4242 assert!(v.settle().ok);
4243 }
4244
4245 #[test]
4246 fn a_revision_refuses_what_it_should() {
4247 let data = seats();
4248 let raw = &format!("{LAWYER}/raw_text");
4249 let rationale = &format!("{LAWYER}/rationale");
4250 let artist = "/rounds/0/responses/1";
4251 let dedup =
4252 |pairs: &[(&str, &str)]| Revision::remove_duplicates(&data, pairs);
4253
4254 assert_eq!(
4255 dedup(&[(
4256 &format!("{artist}/raw_text"),
4257 &format!("{artist}/rationale")
4258 )]),
4259 Err(ReviseError::NotIdentical {
4260 path: format!("{artist}/raw_text"),
4261 same_as: format!("{artist}/rationale"),
4262 })
4263 );
4264 assert_eq!(
4265 dedup(&[(raw, rationale), (rationale, raw)]),
4266 Err(ReviseError::SourceRemoved {
4267 path: raw.clone(),
4268 same_as: rationale.clone(),
4269 }),
4270 "a same_as the patch removes"
4271 );
4272 assert_eq!(
4273 dedup(&[("/nope", rationale)]),
4274 Err(ReviseError::Unresolved("/nope".into()))
4275 );
4276
4277 let make = |target_type, p: Edit| {
4278 AmendmentDraft::revision(
4279 gov(1),
4280 target_type,
4281 data_hash(&json!(null)),
4282 "b",
4283 "n",
4284 &data,
4285 p,
4286 )
4287 .map(|(_, v)| v)
4288 };
4289 let council = GovernanceLogEntryType::CouncilDecision;
4290 assert_eq!(
4291 make(council, patch(json!([])).into()),
4292 Err(ReviseError::EmptyPatch)
4293 );
4294 assert!(matches!(
4295 make(
4296 council,
4297 patch(json!([{"op": "remove", "path": "/nope"}])).into()
4298 ),
4299 Err(ReviseError::Patch(_))
4300 ));
4301 assert_eq!(
4302 make(
4303 council,
4304 patch(json!([{"op": "remove", "path": "/_blind"}])).into()
4305 ),
4306 Err(ReviseError::BlindPointer("/_blind".into()))
4307 );
4308 assert_eq!(
4309 make(
4310 council,
4311 patch(json!([{"op": "copy", "from": "/_blind", "path": "/b"}]))
4312 .into()
4313 ),
4314 Err(ReviseError::BlindPointer("/_blind".into()))
4315 );
4316 for entry_type in [
4317 GovernanceLogEntryType::Amendment,
4318 GovernanceLogEntryType::KeyRotation,
4319 GovernanceLogEntryType::StewardRecord,
4320 ] {
4321 assert!(!is_revisable(entry_type));
4322 assert_eq!(
4323 make(entry_type, dedup(&[(raw, rationale)]).unwrap()),
4324 Err(ReviseError::NotRevisable(entry_type))
4325 );
4326 }
4327 assert!(is_revisable(council));
4328 }
4329
4330 #[test]
4332 fn a_revision_that_adds_content_blinds_the_target() {
4333 let unblinded =
4334 json!({"title": "Old", "rationale": "r", "raw_text": "r"});
4335 let revise = |data: &serde_json::Value, p: serde_json::Value| {
4336 AmendmentDraft::revision(
4337 gov(1),
4338 GovernanceLogEntryType::CouncilDecision,
4339 data_hash(&json!(null)),
4340 "b",
4341 "n",
4342 data,
4343 patch(p),
4344 )
4345 .unwrap()
4346 };
4347 let add = json!([{"op": "add", "path": "/attachments", "value": []}]);
4348 let (draft, revised) = revise(&unblinded, add.clone());
4349 assert!(revised.get(BLIND_KEY).is_some());
4350 assert_eq!(revision_of(&draft).patch.len(), 2, "in the same patch");
4351 assert_eq!(
4352 latest(&unblinded, [revision_of(&draft)]).unwrap(),
4353 revised,
4354 "so the fold reproduces it"
4355 );
4356
4357 let (_, revised) =
4358 revise(&unblinded, json!([{"op": "remove", "path": "/raw_text"}]));
4359 assert!(revised.get(BLIND_KEY).is_none(), "a removal adds nothing");
4360
4361 let (draft, revised) = revise(&seats(), add);
4362 assert_eq!(revised[BLIND_KEY], seats()[BLIND_KEY], "never rotated");
4363 assert_eq!(revision_of(&draft).patch.len(), 1);
4364 }
4365
4366 #[test]
4367 fn revision_shape_violations_fail_verification() {
4368 let (key, pk) = generate_keypair();
4369 let amend_with = |mutate: &dyn Fn(&mut Amendment)| -> String {
4370 let mut c = Chain::new();
4371 let target = c.entry(&key, seats());
4372 let (mut draft, _) = revise(&c, &target, &seats(), dedup(&seats()));
4373 mutate(&mut draft.amendment);
4374 c.amend_v1(&key, &draft.amendment);
4375 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4376 assert!(!v.ok, "{v:#?}");
4377 v.entries[1].problem.clone().unwrap_or_default()
4378 };
4379 assert_eq!(
4380 AmendmentDraft::new(
4381 gov(1),
4382 data_hash(&json!(null)),
4383 AmendmentKind::Revision,
4384 "b",
4385 "n"
4386 ),
4387 Err(AmendmentError::MissingRevision)
4388 );
4389 let p = amend_with(&|a| a.revision = None);
4390 assert!(p.contains("requires a `revision`"), "{p}");
4391 let p = amend_with(&|a| a.kind = AmendmentKind::Correction);
4392 assert!(p.contains("only valid on kind `revision`"), "{p}");
4393 let p = amend_with(&|a| a.revision.as_mut().unwrap().patch.0.clear());
4394 assert!(p.contains("at least one op"), "{p}");
4395 }
4396
4397 #[cfg(feature = "schemars")]
4399 #[test]
4400 fn the_revision_schema_describes_a_patch() {
4401 let schema = crate::responses::inline_schema_for::<Amendment>();
4402 let revision = &schema["properties"]["revision"]["properties"];
4403 assert_eq!(revision["patch"]["type"], json!("array"), "{schema}");
4404 assert_eq!(
4405 revision["patch"]["items"]["properties"]["op"]["enum"],
4406 json!(["add", "remove", "replace", "move", "copy", "test"])
4407 );
4408 assert_eq!(revision["resulting_data_hash"]["type"], json!("string"));
4409 }
4410
4411 #[test]
4412 fn content_that_matches_nothing_fails_without_an_amendment() {
4413 let (key, pk) = generate_keypair();
4414 let mut c = Chain::new();
4415 c.entry(&key, json!({"a": 1}));
4416 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4417 assert!(!v.check_content(&c.links[0], &json!({"a": 2})));
4418 assert!(!v.clone().settle().ok);
4419 let other = link(&key, 9, None, &json!({}), at(9));
4421 assert!(!v.check_content(&other, &json!({})));
4422 }
4423
4424 #[test]
4425 fn tampering_with_an_amendments_data_is_caught_by_the_data_hash() {
4426 let (key, pk) = generate_keypair();
4427 let mut c = Chain::new();
4428 let target = c.decision(&key);
4429 let amendment = AmendmentDraft::new(
4430 target,
4431 c.hash_at(1),
4432 AmendmentKind::Overruled,
4433 "b",
4434 "overruled by a later decision",
4435 )
4436 .unwrap();
4437 c.amend(&key, &amendment);
4438 c.links[1].data.as_mut().unwrap()["note"] =
4439 json!("reinstated, actually");
4440
4441 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4442 assert!(!v.ok, "{v:#?}");
4443 let p = v.entries[1].problem.as_deref().unwrap();
4444 assert!(p.contains("does not hash to the attested data_hash"), "{p}");
4445 assert!(
4446 v.entries[0].amended_by.is_empty(),
4447 "an unreadable amendment has no effect"
4448 );
4449 assert!(
4450 v.entries[1].signature_valid && v.entries[1].link_valid,
4451 "the envelope is untouched — only the content is not what it \
4452 committed to"
4453 );
4454 }
4455
4456 #[test]
4457 fn an_amendment_or_rotation_must_carry_its_data() {
4458 let (key, pk) = generate_keypair();
4459 let mut c = Chain::new();
4460 let target = c.decision(&key);
4461 let amendment = AmendmentDraft::new(
4462 target,
4463 c.hash_at(1),
4464 AmendmentKind::Correction,
4465 "b",
4466 "n",
4467 )
4468 .unwrap();
4469 c.amend(&key, &amendment);
4470 let rotation = c.routine(&pk, &generate_keypair().0);
4471 c.rotate(&key, &rotation);
4472 c.links[1].data = None;
4473 c.links[2].data = None;
4474
4475 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4476 assert!(!v.ok, "{v:#?}");
4477 for (i, entry_type) in [(1, "amendment"), (2, "key_rotation")] {
4478 let p = v.entries[i].problem.as_deref().unwrap();
4479 assert!(p.contains(&format!("a {entry_type} entry")), "{p}");
4480 assert!(p.contains("must carry its `data`"), "{p}");
4481 }
4482 }
4483
4484 #[test]
4485 fn the_id_series_must_match_the_entry_type() {
4486 let (key, pk) = generate_keypair();
4487 let mut c = Chain::new();
4488 let target = c.decision(&key);
4489 let amendment = AmendmentDraft::new(
4490 target,
4491 c.hash_at(1),
4492 AmendmentKind::Correction,
4493 "b",
4494 "n",
4495 )
4496 .unwrap();
4497 c.push(
4498 &key,
4499 gov(7),
4500 GovernanceLogEntryType::Amendment,
4501 serde_json::to_value(&amendment.amendment).unwrap(),
4502 true,
4503 );
4504 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4505 assert!(!v.ok, "{v:#?}");
4506 let p = v.entries[1].problem.as_deref().unwrap();
4507 assert!(p.contains("must be in the AMD- series"), "{p}");
4508
4509 let mut c = Chain::new();
4510 c.push(
4511 &key,
4512 key_id(1),
4513 GovernanceLogEntryType::CouncilDecision,
4514 json!({}),
4515 false,
4516 );
4517 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4518 let p = v.entries[0].problem.as_deref().unwrap();
4519 assert!(p.contains("reserved"), "{p}");
4520 }
4521
4522 #[test]
4523 fn redact_data_replaces_whole_values_and_refuses_the_rest() {
4524 let id = amd(3);
4525 let blind = Blind::from([9; 32]);
4526 let data = json!({"a": {"b": [1, {"c": "secret"}]}, "d/e": "slash"});
4527 let out = redact_data(
4528 &data,
4529 &["/a/b/1/c".into(), "/d~1e".into()],
4530 &id,
4531 blind,
4532 )
4533 .unwrap();
4534 assert_eq!(out["a"]["b"][1]["c"], json!(redaction_marker(&id)));
4535 assert_eq!(out["d/e"], json!(redaction_marker(&id)));
4536 assert_eq!(out["a"]["b"][0], json!(1), "untouched");
4537 assert_eq!(out[BLIND_KEY], json!(blind), "a legacy entry gains one");
4538
4539 assert_eq!(
4540 redact_data(&data, &["/a/nope".into()], &id, blind),
4541 Err(RedactError::Unresolved("/a/nope".into()))
4542 );
4543 assert_eq!(
4544 redact_data(&data, &["".into()], &id, blind),
4545 Err(RedactError::WholeEntry)
4546 );
4547 assert_eq!(
4548 redact_data(&data, &[], &id, blind),
4549 Err(RedactError::NoFields)
4550 );
4551 assert_eq!(
4552 redact_data(&data, &["/_blind".into()], &id, blind),
4553 Err(RedactError::BlindPointer("/_blind".into()))
4554 );
4555 }
4556
4557 #[test]
4558 fn blind_data_is_for_objects_and_is_the_writers_to_supply() {
4559 let blind = Blind::from([1; 32]);
4560 let out = blind_data(&json!({"finding": "upheld"}), blind).unwrap();
4561 assert_eq!(out, json!({"finding": "upheld", "_blind": blind}));
4562 assert_eq!(
4563 blind_data(&json!([1]), blind),
4564 Err(BlindError::NotAnObject)
4565 );
4566 assert_eq!(blind_data(&out, blind), Err(BlindError::AlreadyBlinded));
4567 assert_ne!(Blind::random(), Blind::random());
4568
4569 use GovernanceLogEntryType::*;
4570 assert!(!is_redactable(Amendment) && !is_redactable(KeyRotation));
4571 assert!(
4572 is_redactable(CouncilDecision) && is_redactable(EmergencyAction)
4573 );
4574 }
4575
4576 #[test]
4580 fn a_removed_value_cannot_be_confirmed_by_guessing_it() {
4581 fn confirms(
4583 public: &serde_json::Value,
4584 pointer: &str,
4585 guess: &str,
4586 hash: Sha256Hex,
4587 ) -> bool {
4588 let mut attempt = public.clone();
4589 *attempt.pointer_mut(pointer).unwrap() = json!(guess);
4590 data_hash(&attempt) == hash
4591 }
4592 let legacy = json!({"finding": "upheld", "handle": "someone", "city": "Utrecht"});
4593
4594 let written = blind_data(&legacy, Blind::random()).unwrap();
4596 let first = redact_data(
4597 &written,
4598 &["/handle".into()],
4599 &amd(1),
4600 Blind::random(),
4601 )
4602 .unwrap();
4603 assert!(!confirms(&first, "/handle", "someone", data_hash(&written)));
4604
4605 let second =
4608 redact_data(&first, &["/city".into()], &amd(2), Blind::random())
4609 .unwrap();
4610 assert!(!confirms(&second, "/city", "Utrecht", data_hash(&first)));
4611
4612 let mut stripped =
4616 redact_data(&legacy, &["/handle".into()], &amd(3), Blind::random())
4617 .unwrap();
4618 let legacy_first = stripped.clone();
4619 stripped.as_object_mut().unwrap().remove(BLIND_KEY);
4620 assert!(confirms(
4621 &stripped,
4622 "/handle",
4623 "someone",
4624 data_hash(&legacy)
4625 ));
4626 let legacy_second = redact_data(
4628 &legacy_first,
4629 &["/city".into()],
4630 &amd(4),
4631 Blind::random(),
4632 )
4633 .unwrap();
4634 assert!(!confirms(
4635 &legacy_second,
4636 "/city",
4637 "Utrecht",
4638 data_hash(&legacy_first)
4639 ));
4640 }
4641
4642 #[test]
4645 fn a_routine_rotation_moves_the_chain_to_the_new_key() {
4646 let (old, old_pk) = generate_keypair();
4647 let (new, new_pk) = generate_keypair();
4648 let mut c = Chain::new();
4649 c.decision(&old);
4650 let rotation = c.routine(&old_pk, &new);
4651 let rotation_id = c.rotate(&old, &rotation);
4652 c.decision(&new);
4653
4654 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
4657 assert!(v.ok, "{v:#?}");
4658 assert_eq!(v.public_key, (&new_pk).into());
4659 assert_eq!(
4660 v.entries[1].signed_by,
4661 Some((&old_pk).into()),
4662 "the rotation itself is signed by the old key"
4663 );
4664 assert_eq!(v.entries[2].signed_by, Some((&new_pk).into()));
4665 assert!(v.unanchored_keys.is_empty());
4666 assert!(v.repudiated.is_empty());
4667 assert_eq!(v.keys.len(), 2);
4668 assert_eq!(v.keys[0].public_key, (&old_pk).into());
4669 assert_eq!(v.keys[0].from_seq, 1);
4670 assert_eq!(v.keys[0].through_seq, Some(2));
4671 assert_eq!(v.keys[0].status, KeyStatus::Retired);
4672 assert_eq!(v.keys[0].introduced_by, None);
4673 assert_eq!(v.keys[0].retired_by.as_ref(), Some(&rotation_id));
4674 assert!(v.keys[0].certified, "retroactively, by the rotation");
4675 assert_eq!(v.keys[1].from_seq, 3);
4676 assert_eq!(v.keys[1].through_seq, None);
4677 assert_eq!(v.keys[1].status, KeyStatus::Active);
4678 assert_eq!(v.keys[1].introduced_by.as_ref(), Some(&rotation_id));
4679 assert!(v.keys[1].certified);
4680 }
4681
4682 #[test]
4683 fn the_old_key_cannot_sign_after_a_routine_rotation() {
4684 let (old, old_pk) = generate_keypair();
4685 let (new, _) = generate_keypair();
4686 let mut c = Chain::new();
4687 c.decision(&old);
4688 let rotation = c.routine(&old_pk, &new);
4689 c.rotate(&old, &rotation);
4690 c.decision(&old);
4691
4692 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
4693 assert!(!v.ok, "{v:#?}");
4694 assert!(!v.entries[2].signature_valid);
4695 assert!(
4696 v.entries[2].link_valid,
4697 "the linkage is fine; the key is not"
4698 );
4699 }
4700
4701 #[test]
4704 fn the_online_key_cannot_certify_its_own_successor() {
4705 let (old, old_pk) = generate_keypair();
4706 let (thief, _) = generate_keypair();
4707 let mut c = Chain::new();
4708 c.decision(&old);
4709 let mut rotation = c.routine(&old_pk, &thief);
4710 let statement = rotation.certificate.statement.clone();
4712 rotation.certificate = certify(&[&old], statement);
4713 c.rotate(&old, &rotation);
4714 c.decision(&thief);
4715
4716 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
4717 assert!(!v.ok, "{v:#?}");
4718 let p = v.entries[1].problem.as_deref().unwrap();
4719 assert!(p.contains("0 valid root signature"), "{p}");
4720 assert_eq!(v.public_key, (&old_pk).into(), "the chain does not move");
4721 assert!(!v.entries[2].signature_valid);
4722 assert_eq!(v.keys.len(), 1);
4723 }
4724
4725 #[test]
4726 fn a_certificate_counts_distinct_known_roots_only() {
4727 let (old, old_pk) = generate_keypair();
4728 let (new, _) = generate_keypair();
4729 let (stranger, _) = generate_keypair();
4730 let two_of_two = RootSet::new(
4731 [
4732 (&root(1).verifying_key()).into(),
4733 (&root(2).verifying_key()).into(),
4734 ],
4735 2,
4736 );
4737 let verdict = |signers: &[&SigningKey], roots: &RootSet| {
4738 let mut c = Chain::new();
4739 c.decision(&old);
4740 let mut rotation = c.routine(&old_pk, &new);
4741 let statement = rotation.certificate.statement.clone();
4742 rotation.certificate = certify(signers, statement);
4743 let genesis = KeyCertStatement::genesis((&old_pk).into());
4745 rotation.outgoing_certificate =
4746 Some(certify(&[&root(1), &root(2)], genesis));
4747 c.rotate(&old, &rotation);
4748 verify_chain(&c.links, &old_pk, &anchored(&old_pk), roots)
4749 };
4750
4751 assert!(verdict(&[&root(1), &root(2)], &two_of_two).ok);
4752 assert!(verdict(&[&root(2)], &roots()).ok, "either root, 1-of-2");
4753
4754 for (signers, why) in [
4755 (vec![&root(1)], "below the threshold"),
4756 (vec![&root(1), &root(1)], "one root twice is one root"),
4757 (vec![&root(1), &stranger], "an unknown root is nobody"),
4758 (vec![], "unsigned"),
4759 ] {
4760 let v = verdict(&signers, &two_of_two);
4761 assert!(!v.ok, "{why}: {v:#?}");
4762 let p = v.entries[1].problem.as_deref().unwrap();
4763 assert!(p.contains("where 2 are needed"), "{why}: {p}");
4764 }
4765
4766 assert!(verdict(&[&stranger, &root(1)], &roots()).ok);
4768 }
4769
4770 #[test]
4771 fn a_certificate_is_good_for_one_statement_at_one_position() {
4772 let (old, old_pk) = generate_keypair();
4773 let (new, new_pk) = generate_keypair();
4774 let (other, other_pk) = generate_keypair();
4775 let anchor = anchored(&old_pk);
4776
4777 let mut c = Chain::new();
4781 c.decision(&old);
4782 let early = c.routine(&old_pk, &new);
4783 c.decision(&old);
4784 let mut rotation = c.routine(&old_pk, &new);
4785 rotation.certificate = early.certificate;
4786 c.rotate(&old, &rotation);
4787 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
4788 assert!(!v.ok, "{v:#?}");
4789 let p = v.entries[2].problem.as_deref().unwrap();
4790 assert!(
4791 p.contains("different key, purpose or chain position"),
4792 "{p}"
4793 );
4794 assert_eq!(v.public_key, (&old_pk).into());
4795
4796 let mut c = Chain::new();
4798 c.decision(&old);
4799 let for_new = c.routine(&old_pk, &new);
4800 let mut rotation = c.routine(&old_pk, &other);
4801 rotation.certificate = for_new.certificate;
4802 c.rotate(&old, &rotation);
4803 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
4804 let p = v.entries[1].problem.as_deref().unwrap();
4805 assert!(
4806 p.contains("different key, purpose or chain position"),
4807 "{p}"
4808 );
4809
4810 let mut c = Chain::new();
4812 c.decision(&old);
4813 let mut rotation = c.routine(&old_pk, &other);
4814 rotation.certificate = for_new_at(&c, &new_pk);
4815 rotation.certificate.statement.key = (&other_pk).into();
4816 c.rotate(&old, &rotation);
4817 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
4818 let p = v.entries[1].problem.as_deref().unwrap();
4819 assert!(p.contains("0 valid root signature"), "{p}");
4820
4821 let mut c = Chain::new();
4823 c.decision(&old);
4824 c.decision(&old);
4825 let mut rotation = c.compromise(&old_pk, &new, 1);
4826 rotation.certificate = for_new_at(&c, &new_pk);
4827 c.rotate(&new, &rotation);
4828 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
4829 assert!(!v.ok);
4830 assert!(v.repudiated.is_empty(), "{v:#?}");
4831 assert_eq!(v.public_key, (&old_pk).into());
4832 }
4833
4834 #[test]
4837 fn a_root_signature_without_the_domain_prefix_certifies_nothing() {
4838 use ed25519_dalek::Signer;
4839 let (old, old_pk) = generate_keypair();
4840 let (new, _) = generate_keypair();
4841 let mut c = Chain::new();
4842 c.decision(&old);
4843 let mut rotation = c.routine(&old_pk, &new);
4844 let statement = rotation.certificate.statement.clone();
4845 let bare = canonical_json(&serde_json::to_value(&statement).unwrap());
4846 assert_eq!(
4847 statement.signed_bytes(),
4848 [ROOT_DOMAIN, bare.as_slice()].concat()
4849 );
4850 rotation.certificate =
4851 KeyCertificate::unsigned(statement).with(RootSignature {
4852 root_key: (&root(1).verifying_key()).into(),
4853 signature: root(1).sign(&bare).into(),
4854 });
4855 c.rotate(&old, &rotation);
4856 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
4857 assert!(!v.ok, "{v:#?}");
4858 assert_eq!(v.public_key, (&old_pk).into());
4859 }
4860
4861 #[test]
4862 fn the_first_rotation_carries_the_genesis_certificate_and_only_it_does() {
4863 let (k1, k1_pk) = generate_keypair();
4864 let (k2, k2_pk) = generate_keypair();
4865 let (k3, _) = generate_keypair();
4866 let genesis =
4867 || certify(&[&root(1)], KeyCertStatement::genesis((&k1_pk).into()));
4868
4869 let mut c = Chain::new();
4871 c.decision(&k1);
4872 let mut rotation = c.routine(&k1_pk, &k2);
4873 rotation.outgoing_certificate = None;
4874 c.rotate(&k1, &rotation);
4875 let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
4876 assert!(!v.ok);
4877 let p = v.entries[1].problem.as_deref().unwrap();
4878 assert!(p.contains("genesis key's outgoing_certificate"), "{p}");
4879 assert_eq!(v.public_key, (&k1_pk).into());
4880
4881 let mut c = Chain::new();
4883 c.decision(&k1);
4884 let rotation = c.routine(&k1_pk, &k2).with_outgoing(certify(
4885 &[&root(1)],
4886 KeyCertStatement::genesis((&k2_pk).into()),
4887 ));
4888 c.rotate(&k1, &rotation);
4889 let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
4890 let p = v.entries[1].problem.as_deref().unwrap();
4891 assert!(p.starts_with("outgoing_certificate:"), "{p}");
4892
4893 let mut c = Chain::new();
4896 c.decision(&k1);
4897 let rotation = c.routine(&k1_pk, &k2);
4898 assert_eq!(rotation.outgoing_certificate, Some(genesis()));
4899 c.rotate(&k1, &rotation);
4900 let v = verify_chain(&c.links, &k1_pk, &KeyAnchor::default(), &roots());
4901 assert!(v.ok, "{v:#?}");
4902 assert!(v.unanchored_keys.is_empty(), "{v:#?}");
4903
4904 let again = c.routine(&k2_pk, &k3).with_outgoing(genesis());
4906 c.rotate(&k2, &again);
4907 let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
4908 assert!(!v.ok);
4909 let p = v.entries[2].problem.as_deref().unwrap();
4910 assert!(p.contains("nowhere else"), "{p}");
4911 }
4912
4913 #[test]
4914 fn a_forged_proof_of_possession_is_rejected() {
4915 let (old, old_pk) = generate_keypair();
4916 let (_, new_pk) = generate_keypair();
4917 let (thief, _) = generate_keypair();
4918 let mut c = Chain::new();
4919 c.decision(&old);
4920 let mut rotation = c.routine(&old_pk, &thief);
4923 rotation.new_key = (&new_pk).into();
4924 rotation.certificate = for_new_at(&c, &new_pk);
4925 let statement = rotation.statement(c.prev_hash());
4926 rotation.proof = crypto::sign(
4927 &old,
4928 statement.hash().as_bytes(),
4929 rotation.proof_signed_at,
4930 )
4931 .into();
4932 c.rotate(&old, &rotation);
4933
4934 assert_eq!(
4935 rotation.verify_proof(c.links[1].attestation.prev_hash),
4936 Err(RotationError::BadProof)
4937 );
4938 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
4939 assert!(!v.ok, "{v:#?}");
4940 assert!(
4941 v.entries[1]
4942 .problem
4943 .as_deref()
4944 .unwrap()
4945 .contains("proof of possession")
4946 );
4947 assert_eq!(v.public_key, (&old_pk).into(), "the chain does not move");
4948 }
4949
4950 #[test]
4951 fn a_compromise_repudiates_the_window_and_a_reattestation_restores_one() {
4952 let (old, old_pk) = generate_keypair();
4953 let (new, new_pk) = generate_keypair();
4954 let mut c = Chain::new();
4955 c.decision(&old); c.decision(&old); let reattested = c.decision(&old); let rotation = c.compromise(&old_pk, &new, 1);
4959 let rotation_id = c.rotate(&new, &rotation); let vouch = AmendmentDraft::new(
4961 reattested,
4962 c.hash_at(3),
4963 AmendmentKind::Reattested,
4964 "Art. VII",
4965 "independently verified; the Steward vouches for it",
4966 )
4967 .unwrap();
4968 let vouch_id = c.amend(&new, &vouch); let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
4971 assert!(
4972 v.ok,
4973 "repudiation is a declared state, not a defect: {v:#?}"
4974 );
4975 assert_eq!(v.repudiated, vec![gov(2)]);
4976 assert!(v.entries[1].repudiated);
4977 assert!(!v.entries[2].repudiated);
4978 assert_eq!(v.entries[2].reattested_by, vec![vouch_id.clone()]);
4979 assert_eq!(v.entries[2].amended_by, vec![vouch_id]);
4980 assert_eq!(v.entries[3].signed_by, Some((&new_pk).into()));
4981 assert_eq!(v.public_key, (&new_pk).into());
4982 assert_eq!(v.keys.len(), 2);
4983 assert_eq!(v.keys[0].status, KeyStatus::Compromised);
4984 assert_eq!(
4985 v.keys[0].through_seq,
4986 Some(1),
4987 "trusted through the last trusted entry, not through the \
4988 declaration"
4989 );
4990 assert_eq!(v.keys[0].retired_by.as_ref(), Some(&rotation_id));
4991 assert_eq!(v.keys[1].from_seq, 4, "the declaration is its own first");
4992 }
4993
4994 #[test]
4997 fn last_trusted_is_the_roots_to_say() {
4998 let (old, old_pk) = generate_keypair();
4999 let (new, _) = generate_keypair();
5000 let mut c = Chain::new();
5001 c.decision(&old); c.decision(&old); let mut rotation = c.compromise(&old_pk, &new, 1);
5004 rotation.certificate.statement.last_trusted = Some(c.head(2));
5005 c.rotate(&new, &rotation);
5006 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5007 assert!(!v.ok, "{v:#?}");
5008 assert!(v.repudiated.is_empty());
5009 assert_eq!(v.public_key, (&old_pk).into());
5010 }
5011
5012 #[test]
5013 fn a_stolen_key_cannot_be_rotated_back_in() {
5014 let (k1, k1_pk) = generate_keypair();
5018 let (k2, k2_pk) = generate_keypair();
5019 let mut c = Chain::new();
5020 c.decision(&k1); let real = c.compromise(&k1_pk, &k2, 1);
5022 c.rotate(&k2, &real); c.decision(&k2); let honest =
5025 verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5026 assert!(honest.ok, "{honest:#?}");
5027
5028 let hijack = c.compromise(&k2_pk, &k1, 3);
5029 c.rotate(&k1, &hijack); c.decision(&k1); let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5033 assert!(!v.ok);
5034 assert_eq!(v.public_key, (&k2_pk).into(), "the chain stays with K2");
5035 let p = v.entries[3].problem.as_deref().unwrap();
5036 assert!(p.contains("never brought back"), "{p}");
5037 assert!(!v.entries[3].signature_valid, "{:#?}", v.entries[3]);
5038 assert!(!v.entries[4].signature_valid, "K1 signs nothing again");
5039 assert_eq!(v.keys.len(), 2);
5040 assert_eq!(v.keys[1].status, KeyStatus::Active);
5041 }
5042
5043 #[test]
5044 fn a_routine_rotation_cannot_reuse_a_key_either() {
5045 let (k1, k1_pk) = generate_keypair();
5046 let (k2, k2_pk) = generate_keypair();
5047 let mut c = Chain::new();
5048 c.decision(&k1);
5049 let out = c.routine(&k1_pk, &k2);
5050 c.rotate(&k1, &out);
5051 let back = c.routine(&k2_pk, &k1);
5052 c.rotate(&k2, &back);
5053 let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5054 assert!(!v.ok);
5055 assert!(
5056 v.entries[2]
5057 .problem
5058 .as_deref()
5059 .unwrap()
5060 .contains("never brought back"),
5061 "{:#?}",
5062 v.entries[2]
5063 );
5064 assert_eq!(v.public_key, (&k2_pk).into());
5065 }
5066
5067 #[test]
5068 fn a_forged_entry_has_no_effects() {
5069 let (steward, steward_pk) = generate_keypair();
5070 let (forger, _) = generate_keypair();
5071 let anchor = anchored(&steward_pk);
5072 let mut c = Chain::new();
5073 let target = c.decision(&steward); let fake = AmendmentDraft::new(
5075 target,
5076 c.hash_at(1),
5077 AmendmentKind::Overruled,
5078 "none",
5079 "overruled, says nobody with the key",
5080 )
5081 .unwrap();
5082 c.amend(&forger, &fake); let grab = c.routine(&steward_pk, &forger);
5086 c.rotate(&forger, &grab); let v = verify_chain(&c.links, &steward_pk, &anchor, &roots());
5089 assert!(!v.ok);
5090 assert!(v.entries[0].amended_by.is_empty(), "{:#?}", v.entries[0]);
5091 assert_eq!(v.public_key, (&steward_pk).into());
5092 assert_eq!(v.keys.len(), 1);
5093 assert!(v.unanchored_keys.is_empty());
5094 }
5095
5096 #[test]
5097 fn a_repeated_id_is_a_problem() {
5098 let (key, pk) = generate_keypair();
5099 let mut c = Chain::new();
5100 c.decision(&key);
5101 c.gov = 0;
5102 c.decision(&key); let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
5104 assert!(!v.ok);
5105 assert!(v.entries.iter().all(|e| {
5106 e.problem
5107 .as_deref()
5108 .is_some_and(|p| p.contains("more than once"))
5109 }));
5110 }
5111
5112 #[test]
5113 fn a_second_compromise_cannot_anchor_inside_the_first_window() {
5114 let (k1, k1_pk) = generate_keypair();
5115 let (k2, _) = generate_keypair();
5116 let (k3, _) = generate_keypair();
5117 let mut c = Chain::new();
5118 c.decision(&k1); c.decision(&k1); let first = c.compromise(&k1_pk, &k2, 1);
5121 c.rotate(&k2, &first); let second = c.compromise(&k1_pk, &k3, 2);
5123 c.rotate(&k3, &second); let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5126 assert!(!v.ok);
5127 let p = v.entries[3].problem.as_deref().unwrap();
5128 assert!(p.contains("repudiated"), "{p}");
5129 assert_eq!(v.keys.len(), 2, "K1 and K2; K3 never took the chain");
5130 assert_eq!(v.keys[0].through_seq, Some(1));
5131 }
5132
5133 #[test]
5134 fn an_uncertified_compromise_fails_closed() {
5135 let (old, old_pk) = generate_keypair();
5136 let (new, new_pk) = generate_keypair();
5137 let mut c = Chain::new();
5138 c.decision(&old);
5139 c.decision(&old);
5140 let mut rotation = c.compromise(&old_pk, &new, 1);
5141 let statement = rotation.certificate.statement.clone();
5142 rotation.certificate = certify(&[&new], statement);
5143 c.rotate(&new, &rotation);
5144
5145 let anchor = anchored(&old_pk).with((&new_pk).into());
5147 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5148 assert!(!v.ok, "{v:#?}");
5149 let p = v.entries[2].problem.as_deref().unwrap();
5150 assert!(p.contains("0 valid root signature"), "{p}");
5151 assert!(!v.entries[2].signature_valid, "checked under the old key");
5152 assert!(v.repudiated.is_empty(), "and nothing is repudiated");
5153 assert_eq!(v.public_key, (&old_pk).into());
5154 }
5155
5156 #[test]
5161 fn a_rotation_inside_the_window_is_void_with_it() {
5162 let (steward, steward_pk) = generate_keypair();
5163 let (successor, _) = generate_keypair();
5164 let (recovery, recovery_pk) = generate_keypair();
5165
5166 let mut c = Chain::new();
5167 c.decision(&steward); c.decision(&steward); let routine = c.routine(&steward_pk, &successor);
5170 c.rotate(&steward, &routine); c.decision(&successor); let declaration = c.compromise(&steward_pk, &recovery, 1);
5174 c.rotate(&recovery, &declaration); let v = verify_chain(
5177 &c.links,
5178 &steward_pk,
5179 &anchored(&steward_pk),
5180 &roots(),
5181 );
5182 assert!(v.ok, "{v:#?}");
5183 assert_eq!(v.repudiated, vec![gov(2), key_id(1), gov(3)]);
5184 assert_eq!(v.public_key, (&recovery_pk).into());
5185 assert_eq!(v.keys.len(), 2, "the successor is not part of history");
5186 assert_eq!(v.keys[0].public_key, (&steward_pk).into());
5187 assert_eq!(v.keys[0].status, KeyStatus::Compromised);
5188 assert!(
5189 v.keys[0].certified,
5190 "the genesis certificate rode in on the voided rotation and \
5191 is the root's word all the same"
5192 );
5193 assert_eq!(v.keys[1].public_key, (&recovery_pk).into());
5194 }
5195
5196 #[test]
5197 fn a_compromise_must_name_a_real_head_and_the_key_that_held_it() {
5198 let (old, old_pk) = generate_keypair();
5199 let (new, new_pk) = generate_keypair();
5200 let anchor = anchored(&old_pk);
5201
5202 let mut c = Chain::new();
5205 c.decision(&old);
5206 let mut trusted = c.head(1);
5207 trusted.entry_hash = data_hash(&json!("nope"));
5208 let statement = KeyCertStatement::compromise(
5209 (&new_pk).into(),
5210 2,
5211 c.prev_hash(),
5212 trusted,
5213 );
5214 let mut rotation = c.compromise(&old_pk, &new, 1);
5215 rotation.certificate = certify(&[&root(1)], statement);
5216 c.rotate(&new, &rotation);
5217 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5218 let p = v.entries[1].problem.as_deref().unwrap();
5219 assert!(p.contains("last_trusted"), "{p}");
5220
5221 let (_, other_pk) = generate_keypair();
5223 let mut c = Chain::new();
5224 c.decision(&old);
5225 let rotation = c.compromise(&other_pk, &new, 1);
5226 c.rotate(&new, &rotation);
5227 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5228 let p = v.entries[1].problem.as_deref().unwrap();
5229 assert!(p.contains("old_key is not the key"), "{p}");
5230
5231 let mut c = Chain::new();
5233 c.decision(&old);
5234 let mut rotation = c.compromise(&old_pk, &new, 1);
5235 rotation.certificate.statement.last_trusted = None;
5236 c.rotate(&new, &rotation);
5237 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5238 let p = v.entries[1].problem.as_deref().unwrap();
5239 assert!(p.contains("must name last_trusted"), "{p}");
5240 }
5241
5242 #[test]
5243 fn a_rotation_carries_no_free_text() {
5244 let (old, old_pk) = generate_keypair();
5245 let (new, _) = generate_keypair();
5246 let mut c = Chain::new();
5247 c.decision(&old);
5248 let rotation = c.routine(&old_pk, &new);
5249 let mut value = serde_json::to_value(&rotation).unwrap();
5250 assert!(serde_json::from_value::<KeyRotation>(value.clone()).is_ok());
5251 value["note"] = json!("at the request of …");
5252 assert!(serde_json::from_value::<KeyRotation>(value).is_err());
5253
5254 let mut head = serde_json::to_value(c.head(1)).unwrap();
5255 head["comment"] = json!("the last one I remember signing");
5256 assert!(serde_json::from_value::<TrustedHead>(head).is_err());
5257
5258 let mut statement =
5259 serde_json::to_value(&rotation.certificate.statement).unwrap();
5260 statement["comment"] = json!("signed in the kitchen");
5261 assert!(serde_json::from_value::<KeyCertStatement>(statement).is_err());
5262 }
5263
5264 #[test]
5265 fn the_root_statement_bytes_are_pinned() {
5266 let key: PublicKeyHex = PUBLISHED_KEYS[0].parse().unwrap();
5267 assert_eq!(
5268 String::from_utf8(KeyCertStatement::genesis(key).signed_bytes())
5269 .unwrap(),
5270 "agora-governance-root-v1\n\
5271 {\"agora_governance_key_cert\":1,\"from_seq\":1,\
5272 \"key\":\"ebb3091dd328f1463362c171121921b2fe14628e3fc4c145deaccefb85c0e78a\",\
5273 \"last_trusted\":null,\"prev_hash\":null,\"purpose\":\"genesis\"}"
5274 );
5275
5276 let statement = KeyCertStatement::compromise(
5280 Sha256Hex::from([0xab; 32]).to_string().parse().unwrap(),
5281 15,
5282 Some([0xcd; 32].into()),
5283 TrustedHead {
5284 id: gov(10),
5285 chain_seq: 11,
5286 entry_hash: [0xef; 32].into(),
5287 },
5288 );
5289 assert_eq!(
5290 Sha256Hex::from(<[u8; 32]>::from(Sha256::digest(
5291 statement.signed_bytes()
5292 )))
5293 .to_string(),
5294 "633685771e08be126fd12ca4eb98c77120e5868236ff541ecba85ce6a21e6b68"
5295 );
5296 }
5297
5298 #[test]
5299 fn root_keys_are_curve_points_and_make_a_root_set() {
5300 let roots = RootSet::published();
5301 assert_eq!(roots.keys().count(), ROOT_KEYS.len());
5302 assert_eq!(roots.threshold(), ROOT_THRESHOLD);
5303 assert!(ROOT_THRESHOLD >= 1 && ROOT_THRESHOLD <= ROOT_KEYS.len());
5304 for root in ROOT_KEYS {
5305 let key: PublicKeyHex = root.parse().unwrap();
5306 assert!(roots.contains(&key));
5307 assert!(
5308 key.to_verifying_key().is_ok(),
5309 "{root} is not a valid Ed25519 public key"
5310 );
5311 assert!(
5312 !PUBLISHED_KEYS.contains(root),
5313 "a root key never signs entries"
5314 );
5315 }
5316 assert_eq!(RootSet::new([], 0).threshold(), 1);
5317 }
5318
5319 #[test]
5320 fn a_genesis_key_outside_the_anchor_is_reported_not_rejected() {
5321 let (key, pk) = generate_keypair();
5322 let c = chain(&key, 2);
5323 let v = verify_chain(&c, &pk, &KeyAnchor::default(), &roots());
5324 assert!(v.ok, "{v:#?}");
5325 assert_eq!(v.unanchored_keys, vec![PublicKeyHex::from(&pk)]);
5326 assert_eq!(v.keys.len(), 1);
5327 assert_eq!(v.keys[0].status, KeyStatus::Active);
5328 assert_eq!(v.keys[0].from_seq, 1);
5329 assert_eq!(v.keys[0].through_seq, None);
5330 }
5331
5332 #[test]
5333 fn published_keys_are_curve_points_and_make_an_anchor() {
5334 let anchor = KeyAnchor::published();
5335 assert!(!anchor.is_empty());
5336 assert_eq!(anchor.keys().count(), PUBLISHED_KEYS.len());
5337 for published in PUBLISHED_KEYS {
5338 let key: PublicKeyHex = published.parse().unwrap();
5339 assert!(anchor.contains(&key));
5340 assert!(
5341 key.to_verifying_key().is_ok(),
5342 "{published} is not a valid Ed25519 public key"
5343 );
5344 }
5345 let (_, other) = generate_keypair();
5346 assert!(!anchor.contains(&(&other).into()));
5347 assert!(
5348 anchor
5349 .clone()
5350 .with((&other).into())
5351 .contains(&(&other).into())
5352 );
5353 }
5354
5355 #[test]
5358 fn amendments_and_rotations_round_trip_as_entry_data() {
5359 let (key, pk) = generate_keypair();
5360 let amendment = AmendmentDraft::new(
5361 gov(1),
5362 data_hash(&json!("x")),
5363 AmendmentKind::Superseded,
5364 "Art. VI § 2",
5365 "superseded by GOV-2026-0009",
5366 )
5367 .unwrap()
5368 .with_authority(gov(9))
5369 .with_rationale("the later decision covers the same subject");
5370 let AmendmentDraft { amendment, texts } = amendment;
5371 let value = serde_json::to_value(&amendment).unwrap();
5372 assert_eq!(value["kind"], "superseded");
5373 assert_eq!(value["agora_governance_amendment"], 2);
5374 assert!(value.get("redaction").is_none(), "{value}");
5375 let text = value.to_string();
5376 assert!(!text.contains("Art. VI"), "no text in signed data: {text}");
5377 assert!(!text.contains("salt"), "and no salt: {text}");
5378 assert_eq!(
5379 value["note"]["commitment"],
5380 texts
5381 .note
5382 .as_ref()
5383 .unwrap()
5384 .commitment()
5385 .commitment
5386 .to_string()
5387 );
5388 assert_eq!(
5389 serde_json::from_value::<Amendment>(value).unwrap(),
5390 amendment
5391 );
5392 let beside = serde_json::to_value(&texts).unwrap();
5393 assert_eq!(beside["basis"]["text"], "Art. VI § 2");
5394 assert_eq!(
5395 serde_json::from_value::<AmendmentTexts>(beside).unwrap(),
5396 texts
5397 );
5398
5399 let legacy = json!({
5401 "agora_governance_amendment": 1,
5402 "target": "GOV-2026-0001",
5403 "target_entry_hash": data_hash(&json!("x")),
5404 "kind": "non_precedential",
5405 "authority": "GOV-2026-0005",
5406 "basis": "§1",
5407 "note": "diagnostic finding",
5408 });
5409 let legacy: Amendment = serde_json::from_value(legacy).unwrap();
5410 assert_eq!(legacy.validate(), Ok(()));
5411 assert_eq!(legacy.basis, AmendmentText::Plain("§1".into()));
5412
5413 let mut c = Chain::new();
5414 c.decision(&key);
5415 let rotation = c.compromise(&pk, &generate_keypair().0, 1);
5416 let value = serde_json::to_value(&rotation).unwrap();
5417 assert_eq!(value["agora_governance_key_rotation"], 2);
5418 assert_eq!(value["reason"], "compromise");
5419 let statement = &value["certificate"]["statement"];
5420 assert_eq!(statement["purpose"], "compromise");
5421 assert_eq!(statement["last_trusted"]["chain_seq"], 1);
5422 assert_eq!(
5423 value["outgoing_certificate"]["statement"]["purpose"],
5424 "genesis"
5425 );
5426 assert!(value.get("note").is_none(), "{value}");
5427 assert_eq!(
5428 serde_json::from_value::<KeyRotation>(value).unwrap(),
5429 rotation
5430 );
5431
5432 let notice =
5433 AmendmentNotice::new(amd(1), at(0), &amendment, Some(&texts));
5434 let value = serde_json::to_value(¬ice).unwrap();
5435 assert_eq!(value["id"], "AMD-2026-0001");
5436 assert_eq!(value["note"], "superseded by GOV-2026-0009");
5437 assert_eq!(
5438 serde_json::from_value::<AmendmentNotice>(value).unwrap(),
5439 notice
5440 );
5441
5442 let erased = AmendmentTexts {
5444 rationale: None,
5445 ..texts.clone()
5446 };
5447 let notice =
5448 AmendmentNotice::new(amd(1), at(0), &amendment, Some(&erased));
5449 assert_eq!(notice.note, "superseded by GOV-2026-0009");
5450 assert_eq!(notice.rationale.as_deref(), Some(WITHHELD_TEXT));
5451 let notice = AmendmentNotice::new(amd(1), at(0), &amendment, None);
5452 assert_eq!(notice.basis, WITHHELD_TEXT);
5453 let notice = AmendmentNotice::new(amd(1), at(0), &legacy, None);
5454 assert_eq!(notice.note, "diagnostic finding");
5455 }
5456
5457 #[test]
5461 fn an_amendment_verifies_with_or_without_its_optional_fields() {
5462 let (key, pk) = generate_keypair();
5463 let mut c = Chain::new();
5464 let target = c.decision(&key);
5465 let bare = AmendmentDraft::new(
5466 target.clone(),
5467 c.hash_at(1),
5468 AmendmentKind::Correction,
5469 "clerical",
5470 "typo in the citation",
5471 )
5472 .unwrap();
5473 assert!(
5474 serde_json::to_value(&bare.amendment)
5475 .unwrap()
5476 .get("rationale")
5477 .is_none()
5478 );
5479 c.amend(&key, &bare);
5480 let full = bare.clone().with_rationale("at length: …");
5481 c.amend(&key, &full);
5482 let mut future = serde_json::to_value(&full.amendment).unwrap();
5484 future["superseded_by_something_new"] = json!(["later"]);
5485 c.push(
5486 &key,
5487 amd(3),
5488 GovernanceLogEntryType::Amendment,
5489 future,
5490 true,
5491 );
5492
5493 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
5494 assert!(v.ok, "{v:#?}");
5495 assert_eq!(
5496 v.entries[0].amended_by,
5497 vec![amd(1), amd(2), amd(3)],
5498 "all three name the target"
5499 );
5500 }
5501
5502 #[test]
5505 fn pre_0_26_wire_still_deserializes() {
5506 let link: GovernanceChainLink = serde_json::from_value(json!({
5507 "id": "GOV-2026-0001",
5508 "entry_type": "council_decision",
5509 "created_at": "2023-11-14T22:13:20.123456Z",
5510 "attestation": {
5511 "envelope_version": 1,
5512 "chain_seq": 1,
5513 "prev_hash": null,
5514 "data_hash": "00".repeat(32),
5515 "entry_hash": "11".repeat(32),
5516 "signature": "22".repeat(64),
5517 "signed_at": "2023-11-14T22:13:21Z",
5518 "retroactive": false,
5519 },
5520 }))
5521 .unwrap();
5522 assert!(link.data.is_none());
5523 assert!(
5524 serde_json::to_value(&link).unwrap().get("data").is_none(),
5525 "and a link without data does not grow a null field"
5526 );
5527
5528 let verdict: EntryVerdict = serde_json::from_value(json!({
5529 "id": "GOV-2026-0001",
5530 "chain_seq": 1,
5531 "signature_valid": true,
5532 "link_valid": true,
5533 "content_matches": null,
5534 "retroactive": false,
5535 "out_of_order": false,
5536 "amended_by": [],
5537 }))
5538 .unwrap();
5539 assert!(!verdict.repudiated && !verdict.redacted);
5540 assert!(verdict.signed_by.is_none());
5541 assert!(verdict.reattested_by.is_empty());
5542
5543 let verification: GovernanceVerification =
5544 serde_json::from_value(json!({
5545 "public_key": "33".repeat(32),
5546 "ok": true,
5547 "head": "GOV-2026-0001",
5548 "entries": [],
5549 }))
5550 .unwrap();
5551 assert!(verification.keys.is_empty());
5552 assert!(verification.unanchored_keys.is_empty());
5553 assert!(verification.repudiated.is_empty());
5554 }
5555
5556 #[test]
5557 fn the_report_round_trips() {
5558 let (key, pk) = generate_keypair();
5559 let mut c = Chain::new();
5560 let target = c.decision(&key);
5561 let amendment = AmendmentDraft::new(
5562 target,
5563 c.hash_at(1),
5564 AmendmentKind::Overruled,
5565 "b",
5566 "n",
5567 )
5568 .unwrap();
5569 c.amend(&key, &amendment);
5570 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
5571 let text = serde_json::to_string(&v).unwrap();
5572 assert_eq!(
5573 serde_json::from_str::<GovernanceVerification>(&text).unwrap(),
5574 v
5575 );
5576
5577 let keys = GovernanceSigningKeys { keys: v.keys };
5578 let value = serde_json::to_value(&keys).unwrap();
5579 assert!(value["keys"].is_array(), "an object, not a bare array");
5580 assert_eq!(value["keys"][0]["status"], "active");
5581 assert_eq!(
5582 serde_json::from_value::<GovernanceSigningKeys>(value).unwrap(),
5583 keys
5584 );
5585 }
5586
5587 #[test]
5591 fn envelope_v1_preimage_and_hash_are_pinned() {
5592 let envelope = Envelope::new(
5593 gov(6),
5594 GovernanceLogEntryType::CouncilDecision,
5595 at(0),
5596 Some(Sha256Hex::from([0x11; 32])),
5597 data_hash(&json!({"outcome": "approved", "title": "Ratification"})),
5598 );
5599 assert_eq!(
5600 String::from_utf8(envelope.preimage()).unwrap(),
5601 "{\"agora_governance_log\":1,\"id\":\"GOV-2026-0006\",\
5602 \"entry_type\":\"council_decision\",\
5603 \"created_at\":1700000000123456,\
5604 \"prev_hash\":\"1111111111111111111111111111111111111111111111111111111111111111\",\
5605 \"data_hash\":\"a4adf645ae3f60c56484d01aea87d6d490321d7fc66b1607df14b023fe567c7b\"}"
5606 );
5607 assert_eq!(
5608 envelope.entry_hash().to_hex(),
5609 "ba27577432f81e415f1c01cc4cfabab6070e3ac50fd468fffe195ef19c0e9464"
5610 );
5611 }
5612
5613 #[cfg(feature = "agora-client")]
5627 #[tokio::test]
5628 #[ignore = "networked: hits the live platform"]
5629 async fn the_published_key_is_the_one_the_platform_serves() {
5630 let client = crate::client::Client::new(
5631 url::Url::parse("https://subliminal.technology").unwrap(),
5632 )
5633 .unwrap();
5634 let served = client.get_governance_signing_key().await.unwrap();
5635 assert_eq!(served.algorithm, "ed25519");
5636
5637 let genesis: PublicKeyHex = PUBLISHED_KEYS
5638 .first()
5639 .expect("PUBLISHED_KEYS is never empty")
5640 .parse()
5641 .unwrap();
5642 let links = client.get_governance_chain().await.unwrap();
5643 let report = verify_chain(
5644 &links,
5645 &genesis.to_verifying_key().unwrap(),
5646 &KeyAnchor::published(),
5647 &RootSet::published(),
5648 );
5649 let problems: Vec<_> = report
5650 .entries
5651 .iter()
5652 .filter_map(|e| {
5653 e.problem.as_ref().map(|p| (e.id.clone(), p.clone()))
5654 })
5655 .collect();
5656 assert!(
5657 report.ok,
5658 "the live chain does not verify under this build's genesis key \
5659 and roots: {problems:#?}"
5660 );
5661 assert!(report.unanchored_keys.is_empty(), "{report:#?}");
5662 assert_eq!(
5663 served.public_key, report.public_key,
5664 "the platform serves {} but the chain, followed under the \
5665 published roots, is held by {}",
5666 served.public_key, report.public_key
5667 );
5668 }
5669
5670 #[cfg(feature = "schemars")]
5671 #[test]
5672 fn wire_schemas_are_ref_free() {
5673 use crate::responses::inline_schema_for;
5674 for (name, schema) in [
5675 (
5676 "GovernanceAttestation",
5677 inline_schema_for::<GovernanceAttestation>(),
5678 ),
5679 (
5680 "GovernanceChainLink",
5681 inline_schema_for::<GovernanceChainLink>(),
5682 ),
5683 (
5684 "GovernanceSigningKey",
5685 inline_schema_for::<GovernanceSigningKey>(),
5686 ),
5687 (
5688 "GovernanceVerification",
5689 inline_schema_for::<GovernanceVerification>(),
5690 ),
5691 (
5692 "Vec<GovernanceChainLink>",
5693 inline_schema_for::<Vec<GovernanceChainLink>>(),
5694 ),
5695 ("Amendment", inline_schema_for::<Amendment>()),
5696 ("Redaction", inline_schema_for::<Redaction>()),
5697 ("Revision", inline_schema_for::<Revision>()),
5698 ("AmendmentNotice", inline_schema_for::<AmendmentNotice>()),
5699 ("KeyRotation", inline_schema_for::<KeyRotation>()),
5700 ("TrustedHead", inline_schema_for::<TrustedHead>()),
5701 (
5702 "GovernanceKeyRecord",
5703 inline_schema_for::<GovernanceKeyRecord>(),
5704 ),
5705 (
5706 "GovernanceSigningKeys",
5707 inline_schema_for::<GovernanceSigningKeys>(),
5708 ),
5709 ("AmendmentKind", inline_schema_for::<AmendmentKind>()),
5710 ("Standing", inline_schema_for::<Standing>()),
5711 ("KeyStatus", inline_schema_for::<KeyStatus>()),
5712 ("RotationReason", inline_schema_for::<RotationReason>()),
5713 ] {
5714 let text = serde_json::to_string(&schema).unwrap();
5715 assert!(!text.contains("$ref"), "{name} must be $ref-free: {text}");
5716 assert!(
5717 !text.contains("$defs"),
5718 "{name} must be $defs-free: {text}"
5719 );
5720 }
5721 let text =
5722 serde_json::to_string(&inline_schema_for::<Sha256Hex>()).unwrap();
5723 assert!(text.contains("^[0-9a-f]{64}$"), "{text}");
5724 let text = serde_json::to_string(&inline_schema_for::<SignatureHex>())
5725 .unwrap();
5726 assert!(text.contains("^[0-9a-f]{128}$"), "{text}");
5727 }
5728}