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