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