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