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
1645#[derive(Debug, thiserror::Error)]
1649#[error("the record's {relation:?} field does not parse: {error}")]
1650pub struct MalformedCitation {
1651 pub relation: CitationRelation,
1654 #[source]
1655 pub error: serde_json::Error,
1656}
1657
1658pub fn citations_in(
1663 entry_type: GovernanceLogEntryType,
1664 data: &serde_json::Value,
1665) -> Result<Vec<(GovernanceLogId, CitationRelation)>, MalformedCitation> {
1666 #[derive(Deserialize)]
1667 struct Concerns {
1668 #[serde(default)]
1669 concerns: Vec<GovernanceLogId>,
1670 }
1671 #[derive(Deserialize)]
1672 struct Authority {
1673 #[serde(default)]
1674 authority: Option<GovernanceLogId>,
1675 }
1676 #[derive(Deserialize)]
1677 struct Overrules {
1678 #[serde(default)]
1679 overrules: Vec<GovernanceLogId>,
1680 }
1681 fn each(
1682 ids: impl IntoIterator<Item = GovernanceLogId>,
1683 relation: CitationRelation,
1684 ) -> Vec<(GovernanceLogId, CitationRelation)> {
1685 ids.into_iter().map(|id| (id, relation)).collect()
1686 }
1687 let malformed =
1688 |relation| move |error| MalformedCitation { relation, error };
1689 match entry_type {
1690 GovernanceLogEntryType::StewardRecord => {
1691 serde_json::from_value::<Concerns>(data.clone())
1692 .map(|c| each(c.concerns, CitationRelation::Concerns))
1693 .map_err(malformed(CitationRelation::Concerns))
1694 }
1695 GovernanceLogEntryType::Amendment => {
1696 serde_json::from_value::<Authority>(data.clone())
1697 .map(|a| each(a.authority, CitationRelation::Authority))
1698 .map_err(malformed(CitationRelation::Authority))
1699 }
1700 GovernanceLogEntryType::CouncilDecision => {
1701 serde_json::from_value::<Overrules>(data.clone())
1702 .map(|o| each(o.overrules, CitationRelation::Overrules))
1703 .map_err(malformed(CitationRelation::Overrules))
1704 }
1705 _ => Ok(Vec::new()),
1706 }
1707}
1708
1709pub const KEY_ROTATION_VERSION: u32 = 2;
1715
1716pub const PUBLISHED_KEYS: &[&str] =
1723 &["ebb3091dd328f1463362c171121921b2fe14628e3fc4c145deaccefb85c0e78a"];
1724
1725#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1727#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1728#[cfg_attr(feature = "schemars", schemars(inline))]
1729#[serde(rename_all = "snake_case")]
1730pub enum RotationReason {
1731 Routine,
1733 Compromise,
1736}
1737
1738#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1740#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1741#[cfg_attr(feature = "schemars", schemars(inline))]
1742#[serde(deny_unknown_fields)]
1743pub struct TrustedHead {
1744 pub id: GovernanceLogId,
1745 pub chain_seq: u64,
1746 pub entry_hash: Sha256Hex,
1747}
1748
1749#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1751pub enum RotationError {
1752 #[error("agora_governance_key_rotation is {0}, not {KEY_ROTATION_VERSION}")]
1753 UnsupportedVersion(u32),
1754 #[error("new_key is not a valid Ed25519 public key")]
1755 BadNewKey,
1756 #[error(
1757 "the proof of possession does not verify for this rotation at this position"
1758 )]
1759 BadProof,
1760 #[error("a compromise certificate must name last_trusted")]
1761 MissingLastTrusted,
1762 #[error("certificate: {0}")]
1763 Certificate(#[from] CertificateError),
1764 #[error("outgoing_certificate: {0}")]
1765 OutgoingCertificate(CertificateError),
1766 #[error(
1767 "the chain's first rotation must carry the genesis key's \
1768 outgoing_certificate"
1769 )]
1770 MissingGenesisCertificate,
1771 #[error(
1772 "outgoing_certificate belongs on the chain's first rotation and \
1773 nowhere else"
1774 )]
1775 UnexpectedOutgoingCertificate,
1776 #[error("old_key is not the key that was in force")]
1777 WrongOldKey,
1778 #[error("last_trusted does not name an earlier entry of this chain")]
1779 UnknownLastTrusted,
1780 #[error("new_key has already held this chain; a key is never brought back")]
1781 ReusedKey,
1782 #[error("last_trusted names an entry an earlier compromise repudiated")]
1783 RepudiatedLastTrusted,
1784}
1785
1786#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1797#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1798#[cfg_attr(feature = "schemars", schemars(inline))]
1799#[serde(deny_unknown_fields)]
1800pub struct KeyRotation {
1801 pub agora_governance_key_rotation: u32,
1803 pub reason: RotationReason,
1804 pub old_key: PublicKeyHex,
1805 pub new_key: PublicKeyHex,
1806 pub proof: SignatureHex,
1810 pub proof_signed_at: i64,
1812 pub certificate: KeyCertificate,
1816 #[serde(default, skip_serializing_if = "Option::is_none")]
1820 pub outgoing_certificate: Option<KeyCertificate>,
1821}
1822
1823#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1830pub struct RotationStatement {
1831 pub agora_governance_key_rotation: u32,
1833 pub reason: RotationReason,
1834 pub old_key: PublicKeyHex,
1835 pub new_key: PublicKeyHex,
1836 pub prev_hash: Option<Sha256Hex>,
1837}
1838
1839impl RotationStatement {
1840 pub fn new(
1842 reason: RotationReason,
1843 old_key: PublicKeyHex,
1844 new_key: PublicKeyHex,
1845 prev_hash: Option<Sha256Hex>,
1846 ) -> Self {
1847 Self {
1848 agora_governance_key_rotation: KEY_ROTATION_VERSION,
1849 reason,
1850 old_key,
1851 new_key,
1852 prev_hash,
1853 }
1854 }
1855
1856 pub fn preimage(&self) -> Vec<u8> {
1858 serde_json::to_vec(self).expect("a RotationStatement always serializes")
1859 }
1860
1861 pub fn hash(&self) -> Sha256Hex {
1863 Sha256Hex(Sha256::digest(self.preimage()).into())
1864 }
1865}
1866
1867impl KeyRotation {
1868 pub fn routine(
1874 old_key: PublicKeyHex,
1875 new_signing_key: &SigningKey,
1876 prev_hash: Option<Sha256Hex>,
1877 now: DateTime<Utc>,
1878 certificate: KeyCertificate,
1879 ) -> Self {
1880 Self::build(
1881 RotationReason::Routine,
1882 old_key,
1883 new_signing_key,
1884 prev_hash,
1885 now,
1886 certificate,
1887 )
1888 }
1889
1890 pub fn compromise(
1898 old_key: PublicKeyHex,
1899 new_signing_key: &SigningKey,
1900 prev_hash: Option<Sha256Hex>,
1901 now: DateTime<Utc>,
1902 certificate: KeyCertificate,
1903 ) -> Self {
1904 Self::build(
1905 RotationReason::Compromise,
1906 old_key,
1907 new_signing_key,
1908 prev_hash,
1909 now,
1910 certificate,
1911 )
1912 }
1913
1914 fn build(
1915 reason: RotationReason,
1916 old_key: PublicKeyHex,
1917 new_signing_key: &SigningKey,
1918 prev_hash: Option<Sha256Hex>,
1919 now: DateTime<Utc>,
1920 certificate: KeyCertificate,
1921 ) -> Self {
1922 let new_key = PublicKeyHex::from(&new_signing_key.verifying_key());
1923 let proof_signed_at = truncate_to_seconds(now).timestamp();
1924 let statement =
1925 RotationStatement::new(reason, old_key, new_key, prev_hash);
1926 let proof = crypto::sign(
1927 new_signing_key,
1928 statement.hash().as_bytes(),
1929 proof_signed_at,
1930 );
1931 Self {
1932 agora_governance_key_rotation: KEY_ROTATION_VERSION,
1933 reason,
1934 old_key,
1935 new_key,
1936 proof: proof.into(),
1937 proof_signed_at,
1938 certificate,
1939 outgoing_certificate: None,
1940 }
1941 }
1942
1943 pub fn with_outgoing(mut self, certificate: KeyCertificate) -> Self {
1945 self.outgoing_certificate = Some(certificate);
1946 self
1947 }
1948
1949 pub fn statement(&self, prev_hash: Option<Sha256Hex>) -> RotationStatement {
1951 RotationStatement::new(
1952 self.reason,
1953 self.old_key,
1954 self.new_key,
1955 prev_hash,
1956 )
1957 }
1958
1959 pub fn last_trusted(&self) -> Option<&TrustedHead> {
1962 self.certificate.statement.last_trusted.as_ref()
1963 }
1964
1965 pub fn expected_statement(
1971 &self,
1972 seq: u64,
1973 prev_hash: Option<Sha256Hex>,
1974 ) -> Result<KeyCertStatement, RotationError> {
1975 match self.reason {
1976 RotationReason::Routine => {
1977 Ok(KeyCertStatement::routine(self.new_key, seq, prev_hash))
1978 }
1979 RotationReason::Compromise => Ok(KeyCertStatement::compromise(
1980 self.new_key,
1981 seq,
1982 prev_hash,
1983 self.last_trusted()
1984 .cloned()
1985 .ok_or(RotationError::MissingLastTrusted)?,
1986 )),
1987 }
1988 }
1989
1990 pub fn verify_proof(
1993 &self,
1994 prev_hash: Option<Sha256Hex>,
1995 ) -> Result<(), RotationError> {
1996 if self.agora_governance_key_rotation != KEY_ROTATION_VERSION {
1997 return Err(RotationError::UnsupportedVersion(
1998 self.agora_governance_key_rotation,
1999 ));
2000 }
2001 let new_key = self
2002 .new_key
2003 .to_verifying_key()
2004 .map_err(|_| RotationError::BadNewKey)?;
2005 crypto::verify(
2006 &new_key,
2007 self.statement(prev_hash).hash().as_bytes(),
2008 self.proof_signed_at,
2009 &Signature::from(&self.proof),
2010 )
2011 .then_some(())
2012 .ok_or(RotationError::BadProof)
2013 }
2014
2015 pub fn verify_certified(
2019 &self,
2020 seq: u64,
2021 prev_hash: Option<Sha256Hex>,
2022 roots: &RootSet,
2023 ) -> Result<(), RotationError> {
2024 self.verify_proof(prev_hash)?;
2025 let expected = self.expected_statement(seq, prev_hash)?;
2026 Ok(self.certificate.verify_for(&expected, roots)?)
2027 }
2028}
2029
2030#[derive(Debug, Clone, Default, PartialEq, Eq)]
2037pub struct KeyAnchor {
2038 keys: HashSet<PublicKeyHex>,
2039}
2040
2041impl KeyAnchor {
2042 pub fn published() -> Self {
2044 PUBLISHED_KEYS
2045 .iter()
2046 .map(|k| {
2047 k.parse()
2048 .expect("PUBLISHED_KEYS are valid 32-byte hex keys")
2049 })
2050 .collect()
2051 }
2052
2053 pub fn pinned(key: PublicKeyHex) -> Self {
2055 std::iter::once(key).collect()
2056 }
2057
2058 pub fn with(mut self, key: PublicKeyHex) -> Self {
2060 self.keys.insert(key);
2061 self
2062 }
2063
2064 pub fn contains(&self, key: &PublicKeyHex) -> bool {
2065 self.keys.contains(key)
2066 }
2067
2068 pub fn is_empty(&self) -> bool {
2069 self.keys.is_empty()
2070 }
2071
2072 pub fn keys(&self) -> impl Iterator<Item = &PublicKeyHex> {
2074 self.keys.iter()
2075 }
2076}
2077
2078impl FromIterator<PublicKeyHex> for KeyAnchor {
2079 fn from_iter<I: IntoIterator<Item = PublicKeyHex>>(iter: I) -> Self {
2080 Self {
2081 keys: iter.into_iter().collect(),
2082 }
2083 }
2084}
2085
2086#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2089#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2090#[cfg_attr(feature = "schemars", schemars(inline))]
2091pub struct GovernanceKeyRecord {
2092 pub public_key: PublicKeyHex,
2093 pub from_seq: u64,
2095 #[serde(default)]
2099 pub through_seq: Option<u64>,
2100 pub status: KeyStatus,
2101 #[serde(default)]
2104 pub introduced_by: Option<GovernanceLogId>,
2105 #[serde(default)]
2107 pub retired_by: Option<GovernanceLogId>,
2108 #[serde(default)]
2112 pub certified: bool,
2113}
2114
2115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2120#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2121#[cfg_attr(feature = "schemars", schemars(inline))]
2122pub struct GovernanceSigningKeys {
2123 pub keys: Vec<GovernanceKeyRecord>,
2125}
2126
2127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2129#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2130#[cfg_attr(feature = "schemars", schemars(inline))]
2131pub struct EntryVerdict {
2132 pub id: GovernanceLogId,
2133 pub chain_seq: u64,
2134 pub signature_valid: bool,
2137 pub link_valid: bool,
2140 #[serde(default)]
2147 pub content_matches: Option<bool>,
2148 pub retroactive: bool,
2150 pub out_of_order: bool,
2153 #[serde(default)]
2155 pub amended_by: Vec<GovernanceLogId>,
2156 #[serde(default, skip_serializing_if = "Option::is_none")]
2159 pub signed_by: Option<PublicKeyHex>,
2160 #[serde(default)]
2163 pub redacted: bool,
2164 #[serde(default, skip_serializing_if = "Option::is_none")]
2166 pub redacted_data_hash: Option<Sha256Hex>,
2167 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2170 pub revisions: Vec<GovernanceLogId>,
2171 #[serde(default, skip_serializing_if = "Option::is_none")]
2175 pub latest_data_hash: Option<Sha256Hex>,
2176 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2181 pub superseded_revisions: Vec<GovernanceLogId>,
2182 #[serde(skip)]
2185 pub history: RevisionHistory,
2186 #[serde(default)]
2189 pub repudiated: bool,
2190 #[serde(default)]
2193 pub reattested_by: Vec<GovernanceLogId>,
2194 #[serde(default, skip_serializing_if = "Option::is_none")]
2198 pub texts: Option<AmendmentTextStatus>,
2199 #[serde(default, skip_serializing_if = "Option::is_none")]
2201 pub problem: Option<String>,
2202}
2203
2204impl EntryVerdict {
2205 fn content_check(
2209 &mut self,
2210 attested: Sha256Hex,
2211 data: &serde_json::Value,
2212 ) -> bool {
2213 if non_integer_number(data).is_some() {
2215 return false;
2216 }
2217 let hash = data_hash(data);
2218 if hash == attested && self.redacted {
2219 return true;
2222 }
2223 if hash != attested && Some(hash) != self.redacted_data_hash {
2224 return Some(hash) == self.latest_data_hash;
2225 }
2226 let history = &self.history;
2227 let mut current = data.clone();
2228 let mut failures = Vec::new();
2229 for (i, (id, revision)) in history.revisions.iter().enumerate() {
2230 if i == history.rebased {
2231 break;
2232 }
2233 if let Some(failure) = false_duplicate(id, revision, ¤t) {
2234 failures.push(failure);
2235 break;
2236 }
2237 match apply_patch(¤t, &revision.patch) {
2238 Ok(next) => current = next,
2239 Err(_) => {
2240 failures.push(format!(
2241 "revision {id} does not apply to the redacted data"
2242 ));
2243 break;
2244 }
2245 }
2246 }
2247 if failures.is_empty()
2248 && let Some(expected) = history.rebased_hash
2249 && data_hash(¤t) != expected
2250 {
2251 failures.push(
2252 "the revisions rebased over the redacted data do not \
2253 produce the redaction's resulting_latest_hash"
2254 .to_string(),
2255 );
2256 }
2257 for (id, revision) in history.revisions.iter().skip(history.rebased) {
2258 if !failures.is_empty() {
2259 break;
2260 }
2261 if let Some(failure) = false_duplicate(id, revision, ¤t) {
2262 failures.push(failure);
2263 break;
2264 }
2265 match apply_patch(¤t, &revision.patch) {
2266 Ok(next)
2267 if data_hash(&next) == revision.resulting_data_hash =>
2268 {
2269 current = next;
2270 }
2271 Ok(_) => failures.push(format!(
2272 "revision {id} does not produce its resulting_data_hash"
2273 )),
2274 Err(_) => {
2275 failures.push(format!("revision {id} does not apply"))
2276 }
2277 }
2278 }
2279 for failure in failures {
2280 add_problem(&mut self.problem, failure);
2281 }
2282 true
2283 }
2284}
2285
2286fn add_problem(problems: &mut Option<String>, problem: String) {
2288 *problems = Some(match problems.take() {
2289 Some(p) if p.contains(&problem) => p,
2290 Some(p) => format!("{p}; {problem}"),
2291 None => problem,
2292 });
2293}
2294
2295fn false_duplicate(
2298 id: &GovernanceLogId,
2299 revision: &Revision,
2300 current: &serde_json::Value,
2301) -> Option<String> {
2302 revision.duplicates.iter().find_map(|(path, same_as)| {
2303 let same = match (current.pointer(path), current.pointer(same_as)) {
2304 (Some(a), Some(b)) => canonical_json(a) == canonical_json(b),
2305 _ => false,
2306 };
2307 (!same).then(|| {
2308 format!(
2309 "revision {id} removed {path} as a duplicate of {same_as}, \
2310 but they differ"
2311 )
2312 })
2313 })
2314}
2315
2316#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2318#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2319#[cfg_attr(feature = "schemars", schemars(inline))]
2320pub struct GovernanceVerification {
2321 pub public_key: PublicKeyHex,
2323 pub ok: bool,
2329 #[serde(default)]
2331 pub head: Option<GovernanceLogId>,
2332 pub entries: Vec<EntryVerdict>,
2334 #[serde(default)]
2336 pub keys: Vec<GovernanceKeyRecord>,
2337 #[serde(default)]
2342 pub unanchored_keys: Vec<PublicKeyHex>,
2343 #[serde(default)]
2345 pub repudiated: Vec<GovernanceLogId>,
2346}
2347
2348impl GovernanceVerification {
2349 pub fn settle(mut self) -> Self {
2351 self.ok = self.entries.iter().all(|e| {
2352 e.signature_valid
2353 && e.link_valid
2354 && e.content_matches != Some(false)
2355 && e.problem.is_none()
2356 });
2357 self
2358 }
2359
2360 pub fn check_content(
2371 &mut self,
2372 link: &GovernanceChainLink,
2373 data: &serde_json::Value,
2374 ) -> bool {
2375 let Some(entry) = self.entries.iter_mut().find(|e| e.id == link.id)
2376 else {
2377 return false;
2378 };
2379 let ok = entry.content_check(link.attestation.data_hash, data);
2380 entry.content_matches = Some(ok);
2381 ok
2382 }
2383}
2384
2385pub fn attest(
2396 key: &SigningKey,
2397 envelope: &Envelope,
2398 chain_seq: u64,
2399 signed_at: DateTime<Utc>,
2400) -> GovernanceAttestation {
2401 let signed_at = truncate_to_seconds(signed_at);
2402 let entry_hash = envelope.entry_hash();
2403 let signature =
2404 crypto::sign(key, entry_hash.as_bytes(), signed_at.timestamp());
2405 GovernanceAttestation {
2406 envelope_version: envelope.agora_governance_log,
2407 chain_seq,
2408 prev_hash: envelope.prev_hash,
2409 data_hash: envelope.data_hash,
2410 entry_hash,
2411 signature: signature.into(),
2412 signed_at,
2413 retroactive: is_retroactive(envelope.created_at(), signed_at),
2414 }
2415}
2416
2417#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
2423pub enum LinkError {
2424 #[error(
2425 "envelope version {0} is not supported (this verifier knows {ENVELOPE_VERSION})"
2426 )]
2427 UnsupportedVersion(u32),
2428 #[error("entry_hash does not recompute from the envelope fields")]
2429 HashMismatch,
2430 #[error("signature does not verify under the published key")]
2431 BadSignature,
2432}
2433
2434pub fn recompute_entry_hash(link: &GovernanceChainLink) -> Sha256Hex {
2436 Envelope::new(
2437 link.id.clone(),
2438 link.entry_type,
2439 link.created_at,
2440 link.attestation.prev_hash,
2441 link.attestation.data_hash,
2442 )
2443 .entry_hash()
2444}
2445
2446pub fn verify_link(
2448 link: &GovernanceChainLink,
2449 key: &VerifyingKey,
2450) -> Result<(), LinkError> {
2451 let a = &link.attestation;
2452 if a.envelope_version != ENVELOPE_VERSION {
2453 return Err(LinkError::UnsupportedVersion(a.envelope_version));
2454 }
2455 if recompute_entry_hash(link) != a.entry_hash {
2456 return Err(LinkError::HashMismatch);
2457 }
2458 if !crypto::verify(
2459 key,
2460 a.entry_hash.as_bytes(),
2461 a.signed_at.timestamp(),
2462 &Signature::from(&a.signature),
2463 ) {
2464 return Err(LinkError::BadSignature);
2465 }
2466 Ok(())
2467}
2468
2469pub fn verify_data(
2471 link: &GovernanceChainLink,
2472 data: &serde_json::Value,
2473) -> bool {
2474 data_hash(data) == link.attestation.data_hash
2475}
2476
2477fn same_shape(written: &serde_json::Value, read: &serde_json::Value) -> bool {
2487 use serde_json::Value::{Array, Null, Object};
2488 match (written, read) {
2489 (Object(w), Object(r)) => r.iter().all(|(k, r)| match w.get(k) {
2490 Some(w) => same_shape(w, r),
2491 None => r.is_null(),
2492 }),
2493 (Array(w), Array(r)) => {
2494 w.len() == r.len() && w.iter().zip(r).all(|(w, r)| same_shape(w, r))
2495 }
2496 (_, Object(_) | Array(_)) => false,
2497 (Object(_) | Array(_), Null) => false,
2498 _ => true,
2499 }
2500}
2501
2502fn read_strictly<T>(written: &serde_json::Value) -> Result<T, String>
2504where
2505 T: Serialize + serde::de::DeserializeOwned,
2506{
2507 let read: T =
2508 serde_json::from_value(written.clone()).map_err(|e| e.to_string())?;
2509 let again = serde_json::to_value(&read).map_err(|e| e.to_string())?;
2510 if same_shape(written, &again) {
2511 Ok(read)
2512 } else {
2513 Err("an array where an object belongs, or the reverse".into())
2514 }
2515}
2516
2517pub fn links_from_json(
2524 chain: &serde_json::Value,
2525) -> Result<Vec<GovernanceChainLink>, String> {
2526 chain
2527 .as_array()
2528 .ok_or("a chain is an array of links")?
2529 .iter()
2530 .map(read_strictly)
2531 .collect()
2532}
2533
2534fn read_as_written<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
2536where
2537 D: serde::Deserializer<'de>,
2538 T: Serialize + serde::de::DeserializeOwned,
2539{
2540 let written = serde_json::Value::deserialize(deserializer)?;
2541 if written.is_null() {
2542 return Ok(None);
2543 }
2544 read_strictly(&written)
2545 .map(Some)
2546 .map_err(serde::de::Error::custom)
2547}
2548
2549fn reserved_prefix(
2552 entry_type: GovernanceLogEntryType,
2553) -> Option<GovernanceLogPrefix> {
2554 match entry_type {
2555 GovernanceLogEntryType::Amendment => Some(GovernanceLogPrefix::Amd),
2556 GovernanceLogEntryType::KeyRotation => Some(GovernanceLogPrefix::Key),
2557 GovernanceLogEntryType::StewardRecord => Some(GovernanceLogPrefix::Rec),
2558 GovernanceLogEntryType::Obligation => Some(GovernanceLogPrefix::Obl),
2559 GovernanceLogEntryType::CouncilDecision
2560 | GovernanceLogEntryType::AppealsCourtDecision
2561 | GovernanceLogEntryType::EmergencyAction
2562 | GovernanceLogEntryType::PolicyChange
2563 | GovernanceLogEntryType::StewardVeto => None,
2564 }
2565}
2566
2567fn reserved_for(prefix: GovernanceLogPrefix) -> Option<GovernanceLogEntryType> {
2569 match prefix {
2570 GovernanceLogPrefix::Amd => Some(GovernanceLogEntryType::Amendment),
2571 GovernanceLogPrefix::Key => Some(GovernanceLogEntryType::KeyRotation),
2572 GovernanceLogPrefix::Rec => Some(GovernanceLogEntryType::StewardRecord),
2573 GovernanceLogPrefix::Obl => Some(GovernanceLogEntryType::Obligation),
2574 GovernanceLogPrefix::Gov | GovernanceLogPrefix::App => None,
2575 }
2576}
2577
2578fn prefix_problem(link: &GovernanceChainLink) -> Option<String> {
2580 let prefix = link.id.prefix();
2581 match (reserved_prefix(link.entry_type), reserved_for(prefix)) {
2582 (Some(want), _) if prefix != want => Some(format!(
2583 "the id of a {} entry must be in the {want}- series, not {}",
2584 link.entry_type, link.id
2585 )),
2586 (None, Some(owner)) => Some(format!(
2587 "{prefix}- ids are reserved for {owner} entries, but {} is a {}",
2588 link.id, link.entry_type
2589 )),
2590 _ => None,
2591 }
2592}
2593
2594pub const OBLIGATION_FIXED: [&str; 5] = [
2598 "/until",
2599 "/owed_by",
2600 "/kind",
2601 "/source",
2602 "/agora_obligation",
2603];
2604
2605pub fn revision_touches<'a>(
2608 revision: &Revision,
2609 pointers: &[&'a str],
2610) -> Option<&'a str> {
2611 revision.patch.iter().find_map(|op| {
2612 let from = match op {
2613 json_patch::PatchOperation::Move(m) => Some(m.from.as_str()),
2614 json_patch::PatchOperation::Copy(c) => Some(c.from.as_str()),
2615 _ => None,
2616 };
2617 [Some(op.path().as_str()), from]
2618 .into_iter()
2619 .flatten()
2620 .find_map(|p| pointers.iter().copied().find(|f| overlaps(p, f)))
2621 })
2622}
2623
2624fn amendment_target(
2626 amendment: &Amendment,
2627 seq: u64,
2628 seq_of: &HashMap<&str, u64>,
2629 links: &[&GovernanceChainLink],
2630) -> Result<u64, AmendmentError> {
2631 amendment.validate()?;
2632 let target = *seq_of.get(amendment.target.as_str()).ok_or_else(|| {
2633 AmendmentError::UnknownTarget(amendment.target.clone())
2634 })?;
2635 if target >= seq {
2636 return Err(AmendmentError::ForwardReference(amendment.target.clone()));
2637 }
2638 let target_link = links[target as usize - 1];
2639 if target_link.attestation.entry_hash != amendment.target_entry_hash {
2640 return Err(AmendmentError::WrongTargetHash(amendment.target.clone()));
2641 }
2642 let on_obligation =
2643 target_link.entry_type == GovernanceLogEntryType::Obligation;
2644 if amendment.kind.is_obligation() && !on_obligation {
2645 return Err(AmendmentError::NotAnObligation {
2646 kind: amendment.kind,
2647 target: amendment.target.clone(),
2648 });
2649 }
2650 if on_obligation && kind_standing(amendment.kind).is_some() {
2651 return Err(AmendmentError::PrecedentialOnObligation(amendment.kind));
2652 }
2653 if on_obligation
2654 && let Some(revision) = &amendment.revision
2655 && let Some(path) = revision_touches(revision, &OBLIGATION_FIXED)
2656 {
2657 return Err(AmendmentError::FixedObligationField(path.to_string()));
2658 }
2659 Ok(target)
2660}
2661
2662struct KeyWalk {
2664 history: Vec<(GovernanceKeyRecord, VerifyingKey)>,
2666 unanchored: Vec<PublicKeyHex>,
2667 seen: HashSet<PublicKeyHex>,
2671 repudiated: HashSet<u64>,
2673 genesis: PublicKeyHex,
2674 genesis_certified: bool,
2676}
2677
2678impl KeyWalk {
2679 fn new(genesis: &VerifyingKey, anchor: &KeyAnchor) -> Self {
2680 let public_key = PublicKeyHex::from(genesis);
2681 Self {
2682 genesis: public_key,
2683 genesis_certified: false,
2684 history: vec![(
2685 GovernanceKeyRecord {
2686 public_key,
2687 from_seq: 1,
2688 through_seq: None,
2689 status: KeyStatus::Active,
2690 introduced_by: None,
2691 retired_by: None,
2692 certified: false,
2693 },
2694 *genesis,
2695 )],
2696 unanchored: if anchor.contains(&public_key) {
2697 Vec::new()
2698 } else {
2699 vec![public_key]
2700 },
2701 seen: HashSet::from([public_key]),
2702 repudiated: HashSet::new(),
2703 }
2704 }
2705
2706 fn in_force(&self, seq: u64) -> (PublicKeyHex, VerifyingKey) {
2708 let (record, key) = self
2709 .history
2710 .iter()
2711 .rev()
2712 .find(|(r, _)| r.from_seq <= seq)
2713 .unwrap_or(&self.history[0]);
2714 (record.public_key, *key)
2715 }
2716
2717 fn active(&self) -> PublicKeyHex {
2719 self.history
2720 .last()
2721 .map(|(r, _)| r.public_key)
2722 .expect("the genesis key is always in the history")
2723 }
2724
2725 fn close(
2726 &mut self,
2727 through_seq: u64,
2728 status: KeyStatus,
2729 by: &GovernanceLogId,
2730 ) {
2731 if let Some((record, _)) = self.history.last_mut() {
2732 record.through_seq = Some(through_seq);
2733 record.status = status;
2734 record.retired_by = Some(by.clone());
2735 }
2736 }
2737
2738 fn open(
2739 &mut self,
2740 public_key: PublicKeyHex,
2741 key: VerifyingKey,
2742 from_seq: u64,
2743 by: &GovernanceLogId,
2744 ) {
2745 self.history.push((
2746 GovernanceKeyRecord {
2747 public_key,
2748 from_seq,
2749 through_seq: None,
2750 status: KeyStatus::Active,
2751 introduced_by: Some(by.clone()),
2752 retired_by: None,
2753 certified: true,
2754 },
2755 key,
2756 ));
2757 }
2758
2759 fn apply(
2761 &mut self,
2762 rotation: &KeyRotation,
2763 link: &GovernanceChainLink,
2764 seq: u64,
2765 links: &[&GovernanceChainLink],
2766 roots: &RootSet,
2767 ) -> Result<(), RotationError> {
2768 rotation.verify_certified(seq, link.attestation.prev_hash, roots)?;
2769 let new_key = rotation
2770 .new_key
2771 .to_verifying_key()
2772 .map_err(|_| RotationError::BadNewKey)?;
2773 if self.seen.contains(&rotation.new_key) {
2774 return Err(RotationError::ReusedKey);
2775 }
2776 match (&rotation.outgoing_certificate, self.genesis_certified) {
2781 (None, false) => {
2782 return Err(RotationError::MissingGenesisCertificate);
2783 }
2784 (Some(_), true) => {
2785 return Err(RotationError::UnexpectedOutgoingCertificate);
2786 }
2787 (Some(certificate), false) => certificate
2788 .verify_for(&KeyCertStatement::genesis(self.genesis), roots)
2789 .map_err(RotationError::OutgoingCertificate)?,
2790 (None, true) => {}
2791 }
2792 match rotation.reason {
2793 RotationReason::Routine => {
2794 if rotation.old_key != self.in_force(seq).0 {
2795 return Err(RotationError::WrongOldKey);
2796 }
2797 self.close(seq, KeyStatus::Retired, &link.id);
2798 self.open(rotation.new_key, new_key, seq + 1, &link.id);
2799 }
2800 RotationReason::Compromise => {
2801 let head = rotation
2802 .last_trusted()
2803 .ok_or(RotationError::MissingLastTrusted)?;
2804 let trusted_seq = head.chain_seq;
2805 let names_an_earlier_entry = trusted_seq >= 1
2806 && trusted_seq < seq
2807 && links[trusted_seq as usize - 1].id == head.id
2808 && links[trusted_seq as usize - 1].attestation.entry_hash
2809 == head.entry_hash;
2810 if !names_an_earlier_entry {
2811 return Err(RotationError::UnknownLastTrusted);
2812 }
2813 if self.repudiated.contains(&trusted_seq) {
2816 return Err(RotationError::RepudiatedLastTrusted);
2817 }
2818 if rotation.old_key != self.in_force(trusted_seq).0 {
2821 return Err(RotationError::WrongOldKey);
2822 }
2823 self.history.retain(|(r, _)| r.from_seq <= trusted_seq);
2824 self.close(trusted_seq, KeyStatus::Compromised, &link.id);
2825 self.open(rotation.new_key, new_key, seq, &link.id);
2826 self.repudiated.extend(trusted_seq + 1..seq);
2827 }
2828 }
2829 self.seen.insert(rotation.new_key);
2830 if !self.genesis_certified {
2831 self.genesis_certified = true;
2832 self.history[0].0.certified = true;
2833 self.unanchored.clear();
2834 }
2835 Ok(())
2836 }
2837}
2838
2839pub fn verify_chain(
2870 links: &[GovernanceChainLink],
2871 genesis_key: &VerifyingKey,
2872 anchor: &KeyAnchor,
2873 roots: &RootSet,
2874) -> GovernanceVerification {
2875 let mut links: Vec<&GovernanceChainLink> = links.iter().collect();
2876 links.sort_by_key(|l| l.attestation.chain_seq);
2877
2878 let mut seq_of: HashMap<&str, u64> = HashMap::new();
2879 let mut duplicates: HashSet<&str> = HashSet::new();
2880 for (i, link) in links.iter().enumerate() {
2881 if seq_of.insert(link.id.as_str(), i as u64 + 1).is_some() {
2882 duplicates.insert(link.id.as_str());
2883 }
2884 }
2885
2886 let mut walk = KeyWalk::new(genesis_key, anchor);
2887 let mut amendments: Vec<(u64, GovernanceLogId, Amendment, u64)> =
2889 Vec::new();
2890 let mut entries: Vec<EntryVerdict> = Vec::with_capacity(links.len());
2891 let mut prev: Option<&GovernanceChainLink> = None;
2892
2893 for (i, link) in links.iter().enumerate() {
2894 let a = &link.attestation;
2895 let expected_seq = i as u64 + 1;
2896 let mut problems: Vec<String> = Vec::new();
2897
2898 if let Some(problem) = prefix_problem(link) {
2899 problems.push(problem);
2900 }
2901 if duplicates.contains(link.id.as_str()) {
2902 problems.push(format!("{} appears more than once", link.id));
2903 }
2904
2905 let carries_meaning = matches!(
2909 link.entry_type,
2910 GovernanceLogEntryType::Amendment
2911 | GovernanceLogEntryType::KeyRotation
2912 );
2913 let mut amendment: Option<Amendment> = None;
2914 let mut rotation: Option<KeyRotation> = None;
2915 let mut content_matches: Option<bool> = None;
2916 match &link.data {
2917 Some(data) if non_integer_number(data).is_some() => {
2918 content_matches = Some(false);
2919 problems.push(format!(
2920 "`data` has a number that is not a 64-bit integer at {:?}; \
2921 governance data never contains one",
2922 non_integer_number(data).unwrap_or_default()
2923 ));
2924 }
2925 Some(data) => {
2926 let matched = data_hash(data) == a.data_hash;
2927 content_matches = Some(matched);
2928 if !matched {
2929 if carries_meaning {
2930 problems.push(
2931 "`data` does not hash to the attested data_hash"
2932 .into(),
2933 );
2934 }
2935 } else {
2936 match link.entry_type {
2937 GovernanceLogEntryType::Amendment => {
2938 match read_strictly(data) {
2939 Ok(v) => amendment = Some(v),
2940 Err(e) => problems.push(format!(
2941 "amendment `data` is malformed: {e}"
2942 )),
2943 }
2944 }
2945 GovernanceLogEntryType::KeyRotation => {
2946 match read_strictly(data) {
2947 Ok(v) => rotation = Some(v),
2948 Err(e) => problems.push(format!(
2949 "key_rotation `data` is malformed: {e}"
2950 )),
2951 }
2952 }
2953 _ => {}
2954 }
2955 }
2956 }
2957 None if carries_meaning => problems.push(format!(
2958 "a {} entry must carry its `data`",
2959 link.entry_type
2960 )),
2961 None => {}
2962 }
2963
2964 let declared = rotation
2968 .as_ref()
2969 .filter(|r| r.reason == RotationReason::Compromise)
2970 .map(|r| {
2971 if walk.seen.contains(&r.new_key) {
2972 return Err(RotationError::ReusedKey);
2973 }
2974 r.verify_certified(expected_seq, a.prev_hash, roots)?;
2975 r.new_key
2976 .to_verifying_key()
2977 .map_err(|_| RotationError::BadNewKey)
2978 });
2979 let (key_hex, key) = match &declared {
2980 Some(Ok(k)) => (PublicKeyHex::from(k), *k),
2981 _ => walk.in_force(expected_seq),
2982 };
2983 if let Some(Err(e)) = &declared {
2986 problems.push(e.to_string());
2987 }
2988
2989 let (hash_ok, signature_valid) = match verify_link(link, &key) {
2990 Ok(()) => (true, true),
2991 Err(LinkError::BadSignature) => {
2992 problems.push(LinkError::BadSignature.to_string());
2993 (true, false)
2994 }
2995 Err(e) => {
2996 problems.push(e.to_string());
2997 (false, false)
2998 }
2999 };
3000
3001 let mut link_valid = hash_ok;
3002 if a.chain_seq != expected_seq {
3003 link_valid = false;
3004 problems.push(format!(
3005 "chain_seq {} where {expected_seq} was expected",
3006 a.chain_seq
3007 ));
3008 }
3009 let expected_prev = prev.map(|p| p.attestation.entry_hash);
3010 if a.prev_hash != expected_prev {
3011 link_valid = false;
3012 problems.push(match (a.prev_hash, expected_prev) {
3013 (Some(_), None) => "first entry names a predecessor".into(),
3014 (None, Some(_)) => "prev_hash is null mid-chain".into(),
3015 _ => "prev_hash is not the previous entry's entry_hash".into(),
3016 });
3017 }
3018 let out_of_order = prev.is_some_and(|p| link.created_at < p.created_at);
3019
3020 let authentic = signature_valid && link_valid;
3024 if let Some(rotation) = rotation.as_ref().filter(|_| authentic)
3025 && let Err(e) =
3026 walk.apply(rotation, link, expected_seq, &links, roots)
3027 {
3028 let problem = e.to_string();
3029 if !problems.contains(&problem) {
3030 problems.push(problem);
3031 }
3032 }
3033 let mut texts = None;
3037 if let Some(amendment) = amendment
3038 .as_ref()
3039 .filter(|a| authentic && a.validate().is_ok())
3040 {
3041 match amendment.text_status(link.texts.as_ref()) {
3042 Ok(status) => texts = status,
3043 Err(e) => problems.push(e.to_string()),
3044 }
3045 } else if link.texts.as_ref().is_some_and(|t| !t.is_empty()) {
3046 problems.push(AmendmentError::UncommittedText.to_string());
3047 }
3048 if let Some(amendment) = amendment.filter(|_| authentic) {
3049 match amendment_target(&amendment, expected_seq, &seq_of, &links) {
3050 Ok(target) => amendments.push((
3051 expected_seq,
3052 link.id.clone(),
3053 amendment,
3054 target,
3055 )),
3056 Err(e) => problems.push(e.to_string()),
3057 }
3058 }
3059
3060 entries.push(EntryVerdict {
3061 id: link.id.clone(),
3062 chain_seq: a.chain_seq,
3063 signature_valid,
3064 link_valid,
3065 content_matches,
3066 retroactive: is_retroactive(link.created_at, a.signed_at),
3067 out_of_order,
3068 amended_by: Vec::new(),
3069 signed_by: Some(key_hex),
3070 redacted: false,
3071 redacted_data_hash: None,
3072 revisions: Vec::new(),
3073 latest_data_hash: None,
3074 superseded_revisions: Vec::new(),
3075 history: RevisionHistory::default(),
3076 repudiated: false,
3077 reattested_by: Vec::new(),
3078 texts,
3079 problem: (!problems.is_empty()).then(|| problems.join("; ")),
3080 });
3081 prev = Some(link);
3082 }
3083
3084 let mut applied = vec![false; amendments.len()];
3088 loop {
3089 let mut changed = false;
3090 for (i, (seq, id, amendment, target)) in amendments.iter().enumerate() {
3091 if applied[i]
3092 || amendment.kind != AmendmentKind::Reattested
3093 || walk.repudiated.contains(seq)
3094 {
3095 continue;
3096 }
3097 applied[i] = true;
3098 entries[*target as usize - 1].reattested_by.push(id.clone());
3099 walk.repudiated.remove(target);
3100 changed = true;
3101 }
3102 if !changed {
3103 break;
3104 }
3105 }
3106
3107 for (seq, id, amendment, target) in &amendments {
3108 if walk.repudiated.contains(seq) {
3109 continue;
3110 }
3111 let entry = &mut entries[*target as usize - 1];
3112 entry.amended_by.push(id.clone());
3113 let mut unrebased = false;
3114 if let Some(redaction) = &amendment.redaction {
3115 unrebased = !entry.revisions.is_empty()
3118 && redaction.resulting_latest_hash.is_none();
3119 entry.redacted = true;
3120 entry.redacted_data_hash = Some(redaction.resulting_data_hash);
3121 entry.history.rebased = entry.history.revisions.len();
3123 entry.history.rebased_hash = redaction.resulting_latest_hash;
3124 entry.latest_data_hash = redaction.resulting_latest_hash;
3125 entry.superseded_revisions = entry.revisions.clone();
3126 }
3127 if let Some(revision) = &amendment.revision {
3128 entry.revisions.push(id.clone());
3129 entry.latest_data_hash = Some(revision.resulting_data_hash);
3130 entry.history.revisions.push((id.clone(), revision.clone()));
3131 }
3132 if unrebased {
3133 let target = entry.id.clone();
3134 add_problem(
3135 &mut entries[*seq as usize - 1].problem,
3136 format!(
3137 "the redaction of {target}, which has revisions, names no \
3138 resulting_latest_hash"
3139 ),
3140 );
3141 }
3142 }
3143
3144 for (i, link) in links.iter().enumerate() {
3147 let entry = &mut entries[i];
3148 if let Some(data) = &link.data
3149 && entry.content_matches.is_some()
3150 && (entry.redacted || !entry.history.revisions.is_empty())
3151 {
3152 entry.content_matches =
3153 Some(entry.content_check(link.attestation.data_hash, data));
3154 }
3155 }
3156
3157 let mut seen = HashSet::new();
3158 walk.unanchored.retain(|key| seen.insert(*key));
3159
3160 let mut repudiated = Vec::new();
3161 for (i, entry) in entries.iter_mut().enumerate() {
3162 if walk.repudiated.contains(&(i as u64 + 1)) {
3163 entry.repudiated = true;
3164 repudiated.push(entry.id.clone());
3165 }
3166 }
3167
3168 GovernanceVerification {
3169 public_key: walk.active(),
3170 ok: false,
3171 head: prev.map(|p| p.id.clone()),
3172 entries,
3173 keys: walk.history.into_iter().map(|(record, _)| record).collect(),
3174 unanchored_keys: walk.unanchored,
3175 repudiated,
3176 }
3177 .settle()
3178}
3179
3180#[cfg(test)]
3181mod tests {
3182 use super::*;
3183
3184 #[test]
3185 fn pointer_tokens_escape_and_unescape() {
3186 assert_eq!(escape_pointer_token("a/b~c"), "a~1b~0c");
3187 assert_eq!(unescape_pointer_token("~01"), "~1");
3189 for token in ["", "plain", "a/b", "~", "~1", "/~0/"] {
3190 assert_eq!(
3191 unescape_pointer_token(&escape_pointer_token(token)),
3192 token
3193 );
3194 }
3195 assert_eq!(
3196 pointer_tokens("/rounds/0/a~1b~0c"),
3197 ["rounds", "0", "a/b~c"]
3198 );
3199 assert!(pointer_tokens("").is_empty());
3200 }
3201
3202 #[test]
3206 fn citations_are_read_from_each_records_own_field() {
3207 use serde_json::json;
3208 let id = |s: &str| s.parse::<GovernanceLogId>().unwrap();
3209 assert_eq!(
3210 citations_in(
3211 GovernanceLogEntryType::StewardRecord,
3212 &json!({"title": "t", "concerns": ["GOV-2026-0011", "AMD-2026-0004"]}),
3213 )
3214 .unwrap(),
3215 vec![
3216 (id("GOV-2026-0011"), CitationRelation::Concerns),
3217 (id("AMD-2026-0004"), CitationRelation::Concerns),
3218 ]
3219 );
3220 assert_eq!(
3221 citations_in(
3222 GovernanceLogEntryType::Amendment,
3223 &json!({"target": "APP-2026-0001", "authority": "GOV-2026-0005"}),
3224 )
3225 .unwrap(),
3226 vec![(id("GOV-2026-0005"), CitationRelation::Authority)],
3227 "the target is not a citation: it is listed under `amendments`"
3228 );
3229 assert_eq!(
3230 citations_in(
3231 GovernanceLogEntryType::CouncilDecision,
3232 &json!({"overrules": ["APP-2026-0002"]}),
3233 )
3234 .unwrap(),
3235 vec![(id("APP-2026-0002"), CitationRelation::Overrules)]
3236 );
3237 assert!(
3238 citations_in(
3239 GovernanceLogEntryType::Amendment,
3240 &json!({"authority": null})
3241 )
3242 .unwrap()
3243 .is_empty()
3244 );
3245 assert!(matches!(
3248 citations_in(
3249 GovernanceLogEntryType::StewardRecord,
3250 &json!({"concerns": "GOV-2026-0011"})
3251 ),
3252 Err(MalformedCitation {
3253 relation: CitationRelation::Concerns,
3254 ..
3255 })
3256 ));
3257 assert!(
3258 citations_in(
3259 GovernanceLogEntryType::AppealsCourtDecision,
3260 &json!({"concerns": ["GOV-2026-0011"]}),
3261 )
3262 .unwrap()
3263 .is_empty()
3264 );
3265 }
3266 use crate::crypto::generate_keypair;
3267 use serde_json::json;
3268
3269 pub(super) fn gov(n: u32) -> GovernanceLogId {
3270 format!("GOV-2026-{n:04}").parse().unwrap()
3271 }
3272
3273 pub(super) fn amd(n: u32) -> GovernanceLogId {
3274 format!("AMD-2026-{n:04}").parse().unwrap()
3275 }
3276
3277 pub(super) fn key_id(n: u32) -> GovernanceLogId {
3278 format!("KEY-2026-{n:04}").parse().unwrap()
3279 }
3280
3281 pub(super) fn rec(n: u32) -> GovernanceLogId {
3282 format!("REC-2026-{n:04}").parse().unwrap()
3283 }
3284
3285 pub(super) fn obl(n: u32) -> GovernanceLogId {
3286 format!("OBL-2026-{n:04}").parse().unwrap()
3287 }
3288
3289 pub(super) fn at(secs: i64) -> DateTime<Utc> {
3290 DateTime::from_timestamp(1_700_000_000 + secs, 123_456_789).unwrap()
3291 }
3292
3293 fn anchored(key: &VerifyingKey) -> KeyAnchor {
3295 KeyAnchor::pinned(key.into())
3296 }
3297
3298 pub(super) fn resalted(draft: &AmendmentDraft, seq: u64) -> AmendmentDraft {
3301 let fix = |field: &str, t: &Option<CommittedText>| {
3302 t.as_ref().map(|t| {
3303 let salt = Sha256::digest(format!("{seq}/{field}/{}", t.text));
3304 CommittedText::with_salt(
3305 TextSalt::from(<[u8; 32]>::from(salt)),
3306 t.text.clone(),
3307 )
3308 })
3309 };
3310 let texts = AmendmentTexts {
3311 basis: fix("basis", &draft.texts.basis),
3312 note: fix("note", &draft.texts.note),
3313 rationale: fix("rationale", &draft.texts.rationale),
3314 };
3315 let commit = |t: &Option<CommittedText>| {
3316 t.as_ref().map(|t| AmendmentText::Committed(t.commitment()))
3317 };
3318 AmendmentDraft {
3319 amendment: Amendment {
3320 basis: commit(&texts.basis).unwrap(),
3321 note: commit(&texts.note).unwrap(),
3322 rationale: commit(&texts.rationale),
3323 ..draft.amendment.clone()
3324 },
3325 texts,
3326 }
3327 }
3328
3329 pub(super) fn v1(draft: AmendmentDraft) -> Amendment {
3331 let plain =
3332 |t: Option<CommittedText>| t.map(|t| AmendmentText::Plain(t.text));
3333 Amendment {
3334 agora_governance_amendment: 1,
3335 basis: plain(draft.texts.basis).unwrap(),
3336 note: plain(draft.texts.note).unwrap(),
3337 rationale: plain(draft.texts.rationale),
3338 ..draft.amendment
3339 }
3340 }
3341
3342 pub(super) fn root(n: u8) -> SigningKey {
3345 SigningKey::from_bytes(&[0xA0 + n; 32])
3346 }
3347
3348 pub(super) fn roots() -> RootSet {
3350 RootSet::new(
3351 [1, 2].map(|n| PublicKeyHex::from(&root(n).verifying_key())),
3352 1,
3353 )
3354 }
3355
3356 pub(super) fn certify(
3358 signers: &[&SigningKey],
3359 statement: KeyCertStatement,
3360 ) -> KeyCertificate {
3361 use ed25519_dalek::Signer;
3362 let message = statement.signed_bytes();
3363 signers.iter().fold(
3364 KeyCertificate::unsigned(statement),
3365 |certificate, signer| {
3366 certificate.with(RootSignature {
3367 root_key: (&signer.verifying_key()).into(),
3368 signature: signer.sign(&message).into(),
3369 })
3370 },
3371 )
3372 }
3373
3374 fn for_new_at(c: &Chain, key: &VerifyingKey) -> KeyCertificate {
3376 certify(
3377 &[&root(1)],
3378 KeyCertStatement::routine(key.into(), c.next_seq(), c.prev_hash()),
3379 )
3380 }
3381
3382 pub(super) fn link(
3383 key: &SigningKey,
3384 n: u32,
3385 prev: Option<&GovernanceChainLink>,
3386 data: &serde_json::Value,
3387 signed_at: DateTime<Utc>,
3388 ) -> GovernanceChainLink {
3389 let created_at = truncate_to_micros(at(n as i64 * 10));
3390 let envelope = Envelope::new(
3391 gov(n),
3392 GovernanceLogEntryType::CouncilDecision,
3393 created_at,
3394 prev.map(|p| p.attestation.entry_hash),
3395 data_hash(data),
3396 );
3397 let attestation = attest(
3398 key,
3399 &envelope,
3400 prev.map_or(1, |p| p.attestation.chain_seq + 1),
3401 signed_at,
3402 );
3403 GovernanceChainLink {
3404 id: gov(n),
3405 entry_type: GovernanceLogEntryType::CouncilDecision,
3406 created_at,
3407 attestation,
3408 data: None,
3409 texts: None,
3410 }
3411 }
3412
3413 pub(super) fn chain(key: &SigningKey, n: u32) -> Vec<GovernanceChainLink> {
3414 let mut out: Vec<GovernanceChainLink> = Vec::new();
3415 for i in 1..=n {
3416 let data = json!({"title": format!("Decision {i}"), "outcome": "approved"});
3417 let l = link(key, i, out.last(), &data, at(i as i64 * 10 + 1));
3418 out.push(l);
3419 }
3420 out
3421 }
3422
3423 pub(super) struct Chain {
3427 pub(super) links: Vec<GovernanceChainLink>,
3428 gov: u32,
3429 amd: u32,
3430 key: u32,
3431 genesis: Option<PublicKeyHex>,
3433 }
3434
3435 impl Chain {
3436 pub(super) fn new() -> Self {
3437 Self {
3438 links: Vec::new(),
3439 gov: 0,
3440 amd: 0,
3441 key: 0,
3442 genesis: None,
3443 }
3444 }
3445
3446 pub(super) fn prev_hash(&self) -> Option<Sha256Hex> {
3447 self.links.last().map(|l| l.attestation.entry_hash)
3448 }
3449
3450 pub(super) fn hash_at(&self, seq: usize) -> Sha256Hex {
3452 self.links[seq - 1].attestation.entry_hash
3453 }
3454
3455 pub(super) fn next_seq(&self) -> u64 {
3457 self.links.len() as u64 + 1
3458 }
3459
3460 pub(super) fn head(&self, seq: usize) -> TrustedHead {
3462 TrustedHead {
3463 id: self.links[seq - 1].id.clone(),
3464 chain_seq: seq as u64,
3465 entry_hash: self.hash_at(seq),
3466 }
3467 }
3468
3469 fn outgoing(&self, rotation: KeyRotation) -> KeyRotation {
3471 match (self.key, self.genesis) {
3472 (0, Some(genesis)) => rotation.with_outgoing(certify(
3473 &[&root(1)],
3474 KeyCertStatement::genesis(genesis),
3475 )),
3476 _ => rotation,
3477 }
3478 }
3479
3480 pub(super) fn routine(
3483 &self,
3484 old: &VerifyingKey,
3485 new: &SigningKey,
3486 ) -> KeyRotation {
3487 let statement = KeyCertStatement::routine(
3488 (&new.verifying_key()).into(),
3489 self.next_seq(),
3490 self.prev_hash(),
3491 );
3492 self.outgoing(KeyRotation::routine(
3493 old.into(),
3494 new,
3495 self.prev_hash(),
3496 at(self.next_seq() as i64 * 10 + 5),
3497 certify(&[&root(1)], statement),
3498 ))
3499 }
3500
3501 pub(super) fn compromise(
3504 &self,
3505 old: &VerifyingKey,
3506 new: &SigningKey,
3507 trusted: usize,
3508 ) -> KeyRotation {
3509 let statement = KeyCertStatement::compromise(
3510 (&new.verifying_key()).into(),
3511 self.next_seq(),
3512 self.prev_hash(),
3513 self.head(trusted),
3514 );
3515 self.outgoing(KeyRotation::compromise(
3516 old.into(),
3517 new,
3518 self.prev_hash(),
3519 at(self.next_seq() as i64 * 10 + 5),
3520 certify(&[&root(1)], statement),
3521 ))
3522 }
3523
3524 pub(super) fn next_amd(&self) -> GovernanceLogId {
3526 amd(self.amd + 1)
3527 }
3528
3529 pub(super) fn push(
3530 &mut self,
3531 signer: &SigningKey,
3532 id: GovernanceLogId,
3533 entry_type: GovernanceLogEntryType,
3534 data: serde_json::Value,
3535 carry: bool,
3536 ) -> GovernanceLogId {
3537 self.genesis
3538 .get_or_insert_with(|| (&signer.verifying_key()).into());
3539 let n = self.links.len() as i64 + 1;
3540 let created_at = truncate_to_micros(at(n * 10));
3541 let envelope = Envelope::new(
3542 id.clone(),
3543 entry_type,
3544 created_at,
3545 self.prev_hash(),
3546 data_hash(&data),
3547 );
3548 let attestation =
3549 attest(signer, &envelope, n as u64, at(n * 10 + 1));
3550 self.links.push(GovernanceChainLink {
3551 id: id.clone(),
3552 entry_type,
3553 created_at,
3554 attestation,
3555 data: carry.then_some(data),
3556 texts: None,
3557 });
3558 id
3559 }
3560
3561 pub(super) fn entry(
3564 &mut self,
3565 signer: &SigningKey,
3566 data: serde_json::Value,
3567 ) -> GovernanceLogId {
3568 self.gov += 1;
3569 let id = gov(self.gov);
3570 self.push(
3571 signer,
3572 id,
3573 GovernanceLogEntryType::CouncilDecision,
3574 data,
3575 false,
3576 )
3577 }
3578
3579 pub(super) fn decision(
3580 &mut self,
3581 signer: &SigningKey,
3582 ) -> GovernanceLogId {
3583 let data = json!({"title": format!("Decision {}", self.gov + 1)});
3584 self.entry(signer, data)
3585 }
3586
3587 pub(super) fn amend(
3589 &mut self,
3590 signer: &SigningKey,
3591 draft: &AmendmentDraft,
3592 ) -> GovernanceLogId {
3593 let draft = resalted(draft, self.next_seq());
3594 let id = self.amend_v1(signer, &draft.amendment);
3595 let link = self.links.last_mut().unwrap();
3596 link.texts = Some(draft.texts);
3597 id
3598 }
3599
3600 pub(super) fn amend_v1(
3603 &mut self,
3604 signer: &SigningKey,
3605 amendment: &Amendment,
3606 ) -> GovernanceLogId {
3607 self.amd += 1;
3608 let id = amd(self.amd);
3609 self.push(
3610 signer,
3611 id,
3612 GovernanceLogEntryType::Amendment,
3613 serde_json::to_value(amendment).unwrap(),
3614 true,
3615 )
3616 }
3617
3618 pub(super) fn rotate(
3619 &mut self,
3620 signer: &SigningKey,
3621 rotation: &KeyRotation,
3622 ) -> GovernanceLogId {
3623 self.key += 1;
3624 let id = key_id(self.key);
3625 self.push(
3626 signer,
3627 id,
3628 GovernanceLogEntryType::KeyRotation,
3629 serde_json::to_value(rotation).unwrap(),
3630 true,
3631 )
3632 }
3633 }
3634
3635 #[test]
3638 fn canonical_json_sorts_keys_at_every_level() {
3639 let v = json!({"b": {"z": 1, "a": [{"y": 2, "x": 3}]}, "a": null});
3640 assert_eq!(
3641 canonical_json(&v),
3642 br#"{"a":null,"b":{"a":[{"x":3,"y":2}],"z":1}}"#
3643 );
3644 }
3645
3646 #[test]
3647 fn canonical_json_is_compact_and_escapes_like_serde() {
3648 let v = json!({"s": "tab\there \"q\" ünïcode \u{1F600}", "n": [1, -2, 3.5, true, false]});
3649 let bytes = canonical_json(&v);
3650 let text = std::str::from_utf8(&bytes).unwrap();
3651 assert_eq!(
3652 text,
3653 r#"{"n":[1,-2,3.5,true,false],"s":"tab\there \"q\" ünïcode 😀"}"#
3654 );
3655 }
3656
3657 #[test]
3658 fn canonical_json_ignores_insertion_order() {
3659 let mut a = serde_json::Map::new();
3660 a.insert("z".into(), json!(1));
3661 a.insert("a".into(), json!(2));
3662 let mut b = serde_json::Map::new();
3663 b.insert("a".into(), json!(2));
3664 b.insert("z".into(), json!(1));
3665 assert_eq!(
3666 canonical_json(&serde_json::Value::Object(a)),
3667 canonical_json(&serde_json::Value::Object(b))
3668 );
3669 }
3670
3671 #[test]
3672 fn canonical_json_empty_containers() {
3673 assert_eq!(canonical_json(&json!({})), b"{}");
3674 assert_eq!(canonical_json(&json!([])), b"[]");
3675 assert_eq!(
3676 canonical_json(&json!({"a": {}, "b": []})),
3677 br#"{"a":{},"b":[]}"#
3678 );
3679 }
3680
3681 #[test]
3684 fn preimage_is_declaration_ordered_json() {
3685 let e = Envelope::new(
3686 gov(1),
3687 GovernanceLogEntryType::AppealsCourtDecision,
3688 at(0),
3689 None,
3690 data_hash(&json!({})),
3691 );
3692 let text = String::from_utf8(e.preimage()).unwrap();
3693 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}");
3694 }
3695
3696 #[test]
3697 fn every_envelope_field_changes_the_hash() {
3698 let base = Envelope::new(
3699 gov(1),
3700 GovernanceLogEntryType::CouncilDecision,
3701 at(0),
3702 None,
3703 data_hash(&json!({"a":1})),
3704 );
3705 let h = base.entry_hash();
3706 let mut e = base.clone();
3707 e.id = gov(2);
3708 assert_ne!(e.entry_hash(), h);
3709 let mut e = base.clone();
3710 e.entry_type = GovernanceLogEntryType::PolicyChange;
3711 assert_ne!(e.entry_hash(), h);
3712 let mut e = base.clone();
3713 e.created_at += 1;
3714 assert_ne!(e.entry_hash(), h);
3715 let mut e = base.clone();
3716 e.prev_hash = Some(h);
3717 assert_ne!(e.entry_hash(), h);
3718 let mut e = base.clone();
3719 e.data_hash = data_hash(&json!({"a":2}));
3720 assert_ne!(e.entry_hash(), h);
3721 assert_eq!(base.entry_hash(), h, "and it is deterministic");
3722 }
3723
3724 #[test]
3725 fn truncation_matches_what_the_envelope_carries() {
3726 let t = at(0);
3727 assert_eq!(truncate_to_micros(t).timestamp_subsec_nanos(), 123_456_000);
3728 assert_eq!(truncate_to_seconds(t).timestamp_subsec_nanos(), 0);
3729 assert_eq!(
3730 Envelope::new(
3731 gov(1),
3732 GovernanceLogEntryType::CouncilDecision,
3733 t,
3734 None,
3735 data_hash(&json!(null))
3736 )
3737 .created_at,
3738 truncate_to_micros(t).timestamp_micros()
3739 );
3740 }
3741
3742 #[test]
3745 fn hex_newtypes_round_trip_and_reject_wrong_lengths() {
3746 let h = data_hash(&json!(1));
3747 let s = serde_json::to_string(&h).unwrap();
3748 assert_eq!(s.len(), 66);
3749 let back: Sha256Hex = serde_json::from_str(&s).unwrap();
3750 assert_eq!(back, h);
3751 assert!(serde_json::from_str::<Sha256Hex>("\"abcd\"").is_err());
3752 assert!("zz".repeat(32).parse::<Sha256Hex>().is_err());
3753 assert!(Sha256Hex::try_from(vec![0u8; 31]).is_err());
3754 assert_eq!(format!("{h:?}"), format!("Sha256Hex({h})"));
3755 }
3756
3757 #[test]
3760 fn attest_then_verify_link() {
3761 let (key, pk) = generate_keypair();
3762 let l = link(&key, 1, None, &json!({"a": 1}), at(5));
3763 assert_eq!(verify_link(&l, &pk), Ok(()));
3764 assert!(verify_data(&l, &json!({"a": 1})));
3765 assert!(!verify_data(&l, &json!({"a": 2})));
3766 assert!(!l.attestation.retroactive);
3767 }
3768
3769 #[test]
3770 fn wrong_key_fails_signature_only() {
3771 let (key, _) = generate_keypair();
3772 let (_, other) = generate_keypair();
3773 let l = link(&key, 1, None, &json!({}), at(5));
3774 assert_eq!(verify_link(&l, &other), Err(LinkError::BadSignature));
3775 }
3776
3777 #[test]
3778 fn tampering_with_any_attested_field_is_detected() {
3779 let (key, pk) = generate_keypair();
3780 let l = link(&key, 1, None, &json!({"a": 1}), at(5));
3781
3782 let mut t = l.clone();
3783 t.created_at += chrono::Duration::microseconds(1);
3784 assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
3785
3786 let mut t = l.clone();
3787 t.entry_type = GovernanceLogEntryType::StewardVeto;
3788 assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
3789
3790 let mut t = l.clone();
3791 t.attestation.data_hash = data_hash(&json!({"a": 2}));
3792 assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
3793
3794 let mut t = l.clone();
3797 t.attestation.signed_at -= chrono::Duration::seconds(1);
3798 assert_eq!(verify_link(&t, &pk), Err(LinkError::BadSignature));
3799
3800 let mut t = l.clone();
3801 t.attestation.envelope_version = 2;
3802 assert_eq!(verify_link(&t, &pk), Err(LinkError::UnsupportedVersion(2)));
3803 }
3804
3805 #[test]
3806 fn a_good_chain_verifies_in_any_input_order() {
3807 let (key, pk) = generate_keypair();
3808 let mut c = chain(&key, 4);
3809 c.reverse();
3810 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
3811 assert!(v.ok, "{v:#?}");
3812 assert_eq!(v.head, Some(gov(4)));
3813 assert_eq!(
3814 v.entries.iter().map(|e| e.chain_seq).collect::<Vec<_>>(),
3815 [1, 2, 3, 4]
3816 );
3817 assert!(v.entries.iter().all(|e| e.content_matches.is_none()
3818 && e.problem.is_none()
3819 && !e.out_of_order));
3820 assert_eq!(v.public_key, PublicKeyHex::from(&pk));
3821 }
3822
3823 #[test]
3824 fn empty_chain_is_ok_with_no_head() {
3825 let (_, pk) = generate_keypair();
3826 let v = verify_chain(&[], &pk, &anchored(&pk), &roots());
3827 assert!(v.ok);
3828 assert!(v.head.is_none());
3829 assert!(v.entries.is_empty());
3830 }
3831
3832 #[test]
3833 fn a_changed_entry_breaks_its_signature_and_the_next_link() {
3834 let (key, pk) = generate_keypair();
3835 let mut c = chain(&key, 3);
3836 let rewritten = link(
3839 &key,
3840 2,
3841 Some(&c[0]),
3842 &json!({"title": "Decision 2", "outcome": "REJECTED"}),
3843 at(21),
3844 );
3845 c[1] = rewritten;
3846 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
3847 assert!(!v.ok);
3848 assert!(
3849 v.entries[1].signature_valid && v.entries[1].link_valid,
3850 "the rewrite itself is well-formed: {:#?}",
3851 v.entries[1]
3852 );
3853 assert!(
3854 !v.entries[2].link_valid,
3855 "but entry 3 no longer points at it: {:#?}",
3856 v.entries[2]
3857 );
3858 assert!(
3859 v.entries[2]
3860 .problem
3861 .as_deref()
3862 .unwrap()
3863 .contains("prev_hash")
3864 );
3865 }
3866
3867 #[test]
3868 fn a_removed_entry_is_a_gap_and_a_broken_link() {
3869 let (key, pk) = generate_keypair();
3870 let mut c = chain(&key, 3);
3871 c.remove(1);
3872 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
3873 assert!(!v.ok);
3874 assert!(v.entries[0].link_valid);
3875 let p = v.entries[1].problem.as_deref().unwrap();
3876 assert!(p.contains("chain_seq 3 where 2 was expected"), "{p}");
3877 assert!(p.contains("prev_hash"), "{p}");
3878 }
3879
3880 #[test]
3881 fn a_second_genesis_is_rejected() {
3882 let (key, pk) = generate_keypair();
3883 let mut c = chain(&key, 2);
3884 let rogue = link(&key, 2, None, &json!({}), at(21));
3885 c[1] = rogue;
3886 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
3887 assert!(!v.ok);
3888 assert!(
3889 v.entries[1]
3890 .problem
3891 .as_deref()
3892 .unwrap()
3893 .contains("null mid-chain")
3894 );
3895 }
3896
3897 #[test]
3898 fn retroactive_and_out_of_order_are_recomputed_not_copied() {
3899 let (key, pk) = generate_keypair();
3900 let first = link(&key, 1, None, &json!({}), at(10 + 3600));
3901 let created = truncate_to_micros(at(5));
3904 let envelope = Envelope::new(
3905 gov(2),
3906 GovernanceLogEntryType::CouncilDecision,
3907 created,
3908 Some(first.attestation.entry_hash),
3909 data_hash(&json!({})),
3910 );
3911 let attestation = attest(&key, &envelope, 2, at(6));
3912 let mut second = GovernanceChainLink {
3913 id: gov(2),
3914 entry_type: GovernanceLogEntryType::CouncilDecision,
3915 created_at: created,
3916 attestation,
3917 data: None,
3918 texts: None,
3919 };
3920 second.attestation.retroactive = true; let v = verify_chain(&[first, second], &pk, &anchored(&pk), &roots());
3922 assert!(v.ok, "{v:#?}");
3923 assert!(v.entries[0].retroactive);
3924 assert!(!v.entries[1].retroactive, "recomputed from timestamps");
3925 assert!(v.entries[1].out_of_order);
3926 }
3927
3928 #[test]
3929 fn content_mismatch_settles_to_not_ok() {
3930 let (key, pk) = generate_keypair();
3931 let c = chain(&key, 1);
3932 let mut v = verify_chain(&c, &pk, &anchored(&pk), &roots());
3933 v.entries[0].content_matches = Some(true);
3934 assert!(v.clone().settle().ok);
3935 v.entries[0].content_matches = Some(false);
3936 assert!(!v.settle().ok);
3937 }
3938
3939 #[test]
3942 fn an_amendment_fills_amended_by_on_its_target() {
3943 let (key, pk) = generate_keypair();
3944 let mut c = Chain::new();
3945 let target = c.decision(&key);
3946 c.decision(&key);
3947 let amendment = AmendmentDraft::new(
3948 target,
3949 c.hash_at(1),
3950 AmendmentKind::NonPrecedential,
3951 "§1 (Red Team Cases Recharacterized)",
3952 "diagnostic finding — not citable as moderation precedent",
3953 )
3954 .unwrap()
3955 .with_authority(gov(5))
3956 .with_rationale("§5 leaves the ruling itself standing");
3957 let id = c.amend(&key, &amendment);
3958
3959 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3960 assert!(v.ok, "{v:#?}");
3961 assert_eq!(v.entries[0].amended_by, vec![id]);
3962 assert!(v.entries[1].amended_by.is_empty());
3963 assert!(!v.entries[0].redacted);
3964 assert_eq!(v.head, Some(amd(1)));
3965 assert_eq!(v.entries[2].content_matches, Some(true));
3967 assert_eq!(v.entries[0].content_matches, None);
3968 assert_eq!(
3969 standing([amendment.amendment.kind]),
3970 Standing::NonPrecedential
3971 );
3972 }
3973
3974 #[test]
3975 fn an_amendment_must_name_an_earlier_entry_by_its_exact_hash() {
3976 let (key, pk) = generate_keypair();
3977 let problem = |c: &Chain, at: usize| -> String {
3978 verify_chain(&c.links, &pk, &anchored(&pk), &roots()).entries[at]
3979 .problem
3980 .clone()
3981 .unwrap_or_default()
3982 };
3983
3984 let mut c = Chain::new();
3985 c.decision(&key);
3986 let unknown = AmendmentDraft::new(
3987 gov(99),
3988 c.hash_at(1),
3989 AmendmentKind::Overruled,
3990 "b",
3991 "n",
3992 )
3993 .unwrap();
3994 c.amend(&key, &unknown);
3995 assert!(
3996 problem(&c, 1).contains("is not an entry of this chain"),
3997 "{}",
3998 problem(&c, 1)
3999 );
4000 assert!(!verify_chain(&c.links, &pk, &anchored(&pk), &roots()).ok);
4001
4002 let mut c = Chain::new();
4004 c.decision(&key);
4005 let forward = AmendmentDraft::new(
4006 gov(2),
4007 c.hash_at(1),
4008 AmendmentKind::Overruled,
4009 "b",
4010 "n",
4011 )
4012 .unwrap();
4013 c.amend(&key, &forward);
4014 c.decision(&key);
4015 assert!(
4016 problem(&c, 1).contains("not an earlier entry"),
4017 "{}",
4018 problem(&c, 1)
4019 );
4020
4021 let mut c = Chain::new();
4023 let target = c.decision(&key);
4024 let wrong_hash = AmendmentDraft::new(
4025 target,
4026 data_hash(&json!("some other entry")),
4027 AmendmentKind::Overruled,
4028 "b",
4029 "n",
4030 )
4031 .unwrap();
4032 c.amend(&key, &wrong_hash);
4033 assert!(problem(&c, 1).contains("entry_hash"), "{}", problem(&c, 1));
4034 assert!(
4035 verify_chain(&c.links, &pk, &anchored(&pk), &roots()).entries[0]
4036 .amended_by
4037 .is_empty()
4038 );
4039 }
4040
4041 #[test]
4042 fn redaction_shape_violations_fail_verification() {
4043 let (key, pk) = generate_keypair();
4044 let amend_with = |mutate: &dyn Fn(&mut Amendment)| -> String {
4045 let mut c = Chain::new();
4046 let target = c.decision(&key);
4047 let mut amendment = AmendmentDraft::new(
4048 target,
4049 c.hash_at(1),
4050 AmendmentKind::Correction,
4051 "b",
4052 "n",
4053 )
4054 .unwrap();
4055 mutate(&mut amendment.amendment);
4056 c.amend_v1(&key, &amendment.amendment);
4057 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4058 assert!(!v.ok, "{v:#?}");
4059 v.entries[1].problem.clone().unwrap_or_default()
4060 };
4061
4062 assert_eq!(
4065 AmendmentDraft::new(
4066 gov(1),
4067 data_hash(&json!(null)),
4068 AmendmentKind::Redaction,
4069 "b",
4070 "n"
4071 ),
4072 Err(AmendmentError::MissingRedaction)
4073 );
4074 let p = amend_with(&|a| a.kind = AmendmentKind::Redaction);
4075 assert!(p.contains("requires a `redaction`"), "{p}");
4076
4077 let p = amend_with(&|a| {
4078 a.redaction = Some(Redaction {
4079 fields: vec!["/x".into()],
4080 resulting_data_hash: data_hash(&json!({})),
4081 resulting_latest_hash: None,
4082 })
4083 });
4084 assert!(p.contains("only valid on kind"), "{p}");
4085
4086 let p = amend_with(&|a| a.agora_governance_amendment = 3);
4087 assert!(p.contains("agora_governance_amendment is 3"), "{p}");
4088
4089 let p = amend_with(&|a| a.agora_governance_amendment = 1);
4092 assert!(p.contains("does neither consistently"), "{p}");
4093 let p = amend_with(&|a| a.note = AmendmentText::Plain("n".into()));
4094 assert!(p.contains("does neither consistently"), "{p}");
4095 }
4096
4097 #[test]
4098 fn governance_data_holds_no_number_that_is_not_a_64_bit_integer() {
4099 let fine = json!({"a": [1, -2, u64::MAX, i64::MIN], "b": {"c": "0.5"}});
4100 assert_eq!(non_integer_number(&fine), None);
4101 assert!(blind_data(&fine, Blind::random()).is_ok());
4102
4103 for (text, pointer) in [
4104 (r#"{"a": {"b/c": [1, 0.5]}}"#, "/a/b~1c/1"),
4105 (r#"{"n": 1.0}"#, "/n"),
4106 (r#"{"n": 1e3}"#, "/n"),
4107 (r#"{"n": 18446744073709551616}"#, "/n"),
4108 (r#"{"n": -9223372036854775809}"#, "/n"),
4109 ] {
4110 let data: serde_json::Value = serde_json::from_str(text).unwrap();
4111 assert_eq!(non_integer_number(&data).as_deref(), Some(pointer));
4112 assert_eq!(
4113 blind_data(&data, Blind::random()),
4114 Err(BlindError::NonIntegerNumber(pointer.into())),
4115 "the writer refuses it"
4116 );
4117 assert_eq!(
4118 redact_data(&data, &["/n".into()], &amd(1), Blind::random()),
4119 Err(RedactError::NonIntegerNumber(pointer.into()))
4120 );
4121 }
4122 }
4123
4124 #[test]
4125 fn standing_is_the_last_amendment_that_changes_it() {
4126 use AmendmentKind::*;
4127 assert_eq!(standing([]), Standing::InForce);
4128 assert_eq!(standing([Correction, Redaction]), Standing::InForce);
4129 assert_eq!(
4130 standing([Correction, NonPrecedential, Redaction]),
4131 Standing::NonPrecedential
4132 );
4133 assert_eq!(standing([Overruled, Reinstated]), Standing::InForce);
4134 assert_eq!(standing([Reinstated, Superseded]), Standing::Superseded);
4135 assert_eq!(kind_standing(Reattested), None);
4136 assert_eq!(kind_standing(Reinstated), Some(Standing::InForce));
4137 }
4138
4139 #[test]
4140 fn a_redaction_verifies_against_the_amendment_and_nothing_else() {
4141 let (key, pk) = generate_keypair();
4142 let data = json!({
4143 "finding": "upheld",
4144 "subject": {"handle": "someone", "detail": "personal"},
4145 });
4146 let mut c = Chain::new();
4147 let target = c.entry(&key, data.clone());
4148 let amendment_id = c.next_amd();
4149 let (amendment, redacted) = AmendmentDraft::redaction(
4150 &amendment_id,
4151 target,
4152 c.hash_at(1),
4153 "GDPR Art. 17(1)(a)",
4154 "personal data removed on request",
4155 vec!["/subject/handle".into(), "/subject/detail".into()],
4156 &data,
4157 Blind::from([7; 32]),
4158 &[],
4159 )
4160 .unwrap();
4161 assert_eq!(c.amend(&key, &amendment), amendment_id);
4162 assert_eq!(redacted[BLIND_KEY], json!(Blind::from([7; 32])));
4163
4164 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4165 assert!(v.ok, "{v:#?}");
4166 assert!(v.entries[0].redacted);
4167 assert_eq!(v.entries[0].redacted_data_hash, Some(data_hash(&redacted)));
4168 assert_eq!(
4169 redacted["subject"]["handle"],
4170 json!(format!("[redacted by {amendment_id}]"))
4171 );
4172 assert_eq!(redacted["finding"], json!("upheld"), "and nothing else");
4173
4174 assert!(v.check_content(&c.links[0], &redacted));
4176 assert_eq!(v.entries[0].content_matches, Some(true));
4177 assert!(v.clone().settle().ok);
4178 assert!(v.check_content(&c.links[0], &data));
4180
4181 let mut tampered = redacted.clone();
4183 tampered["finding"] = json!("overturned");
4184 assert!(!v.check_content(&c.links[0], &tampered));
4185 assert_eq!(v.entries[0].content_matches, Some(false));
4186 assert!(!v.settle().ok);
4187 }
4188
4189 fn seats() -> serde_json::Value {
4192 json!({
4193 "title": "A motion",
4194 "rounds": [{
4195 "number": 1,
4196 "responses": [
4197 {"role": "lawyer", "rationale": "Because.", "raw_text": "Because.", "vote": "yes"},
4198 {"role": "artist", "rationale": "Why not.", "raw_text": "Why not?", "vote": "no"},
4199 ],
4200 }],
4201 "subject": {"handle": "someone"},
4202 BLIND_KEY: Blind::from([3; 32]),
4203 })
4204 }
4205
4206 const LAWYER: &str = "/rounds/0/responses/0";
4207
4208 fn patch(ops: serde_json::Value) -> json_patch::Patch {
4210 serde_json::from_value(ops).unwrap()
4211 }
4212
4213 fn dedup(data: &serde_json::Value) -> Edit {
4215 let (raw, rationale) =
4216 (format!("{LAWYER}/raw_text"), format!("{LAWYER}/rationale"));
4217 Revision::remove_duplicates(data, &[(&raw, &rationale)]).unwrap()
4218 }
4219
4220 fn revise(
4221 c: &Chain,
4222 target: &GovernanceLogId,
4223 latest: &serde_json::Value,
4224 patch: impl Into<Edit>,
4225 ) -> (AmendmentDraft, serde_json::Value) {
4226 AmendmentDraft::revision(
4227 target.clone(),
4228 GovernanceLogEntryType::CouncilDecision,
4229 c.hash_at(1),
4230 "Steward's record REC-2026-0001",
4231 "raw_text identical to the rationale removed",
4232 latest,
4233 patch,
4234 )
4235 .unwrap()
4236 }
4237
4238 fn revision_of(draft: &AmendmentDraft) -> &Revision {
4239 draft.amendment.revision.as_ref().unwrap()
4240 }
4241
4242 #[test]
4243 fn a_revision_overwrites_nothing_and_folds_to_the_latest() {
4244 let (key, pk) = generate_keypair();
4245 let data = seats();
4246 let mut c = Chain::new();
4247 let target = c.entry(&key, data.clone());
4248 let (first, v1) = revise(&c, &target, &data, dedup(&data));
4249 c.amend(&key, &first);
4250 let (second, v2) = revise(
4251 &c,
4252 &target,
4253 &v1,
4254 patch(json!([{"op": "move", "from": "/title", "path": "/motion"}])),
4255 );
4256 c.amend(&key, &second);
4257
4258 let seat = &v1["rounds"][0]["responses"];
4259 assert!(seat[0].get("raw_text").is_none(), "removed, no marker");
4260 assert_eq!(seat[1]["raw_text"], json!("Why not?"), "the other stays");
4261 assert_eq!(v2["motion"], json!("A motion"));
4262 assert_eq!(v2[BLIND_KEY], data[BLIND_KEY], "blind kept");
4263 assert_eq!(
4264 latest(&data, [revision_of(&first), revision_of(&second)]).unwrap(),
4265 v2
4266 );
4267
4268 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4269 assert!(v.ok, "{v:#?}");
4270 let entry = &v.entries[0];
4271 assert_eq!(entry.revisions, [amd(1), amd(2)]);
4272 assert_eq!(entry.latest_data_hash, Some(data_hash(&v2)));
4273 assert!(!entry.redacted && entry.redacted_data_hash.is_none());
4274
4275 assert!(v.check_content(&c.links[0], &data));
4277 assert!(v.check_content(&c.links[0], &v2));
4278 assert!(!v.check_content(&c.links[0], &v1), "not the latest");
4279 v.check_content(&c.links[0], &data);
4280 assert!(v.clone().settle().ok);
4281
4282 let wire = serde_json::to_value(&v).unwrap();
4285 assert!(wire["entries"][0].get("history").is_none());
4286 let mut old = wire.clone();
4287 for field in ["revisions", "latest_data_hash", "superseded_revisions"] {
4288 old["entries"][0].as_object_mut().unwrap().remove(field);
4289 }
4290 let old: GovernanceVerification = serde_json::from_value(old).unwrap();
4291 assert!(old.entries[0].revisions.is_empty());
4292 }
4293
4294 #[test]
4296 fn a_revision_that_does_not_produce_its_hash_fails() {
4297 let (key, pk) = generate_keypair();
4298 let data = seats();
4299 let mut c = Chain::new();
4300 let target = c.entry(&key, data.clone());
4301 let (mut draft, _) = revise(&c, &target, &data, dedup(&data));
4302 draft
4303 .amendment
4304 .revision
4305 .as_mut()
4306 .unwrap()
4307 .resulting_data_hash = data_hash(&json!({"something": "else"}));
4308 c.amend(&key, &draft);
4309
4310 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4311 assert!(v.ok, "the chain itself is sound: {v:#?}");
4312 assert!(v.check_content(&c.links[0], &data), "the stored data is");
4313 assert!(
4314 v.entries[0]
4315 .problem
4316 .as_deref()
4317 .is_some_and(|p| p.contains("does not produce")),
4318 "{:?}",
4319 v.entries[0].problem
4320 );
4321 assert!(!v.settle().ok);
4322
4323 c.links[0].data = Some(data);
4325 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4326 assert!(!v.ok);
4327 }
4328
4329 fn redact(
4330 c: &Chain,
4331 target: &GovernanceLogId,
4332 fields: &[&str],
4333 data: &serde_json::Value,
4334 revisions: &[&Revision],
4335 ) -> Result<(AmendmentDraft, serde_json::Value), RedactError> {
4336 AmendmentDraft::redaction(
4337 &c.next_amd(),
4338 target.clone(),
4339 c.hash_at(1),
4340 "GDPR Art. 17(1)(a)",
4341 "removed on request",
4342 fields.iter().map(|f| f.to_string()).collect(),
4343 data,
4344 Blind::from([9; 32]),
4345 revisions,
4346 )
4347 }
4348
4349 #[test]
4352 fn a_revision_then_a_redaction() {
4353 let (key, pk) = generate_keypair();
4354 let data = seats();
4355 let mut c = Chain::new();
4356 let target = c.entry(&key, data.clone());
4357 let (revision, _) = revise(&c, &target, &data, dedup(&data));
4358 c.amend(&key, &revision);
4359 let rationale = format!("{LAWYER}/rationale");
4360 let (redaction, redacted) = redact(
4361 &c,
4362 &target,
4363 &[&rationale],
4364 &data,
4365 &[revision_of(&revision)],
4366 )
4367 .unwrap();
4368 c.amend(&key, &redaction);
4369
4370 let fields = &redaction.amendment.redaction.as_ref().unwrap().fields;
4372 assert_eq!(fields, &[rationale, format!("{LAWYER}/raw_text")]);
4373 let seat = &redacted["rounds"][0]["responses"][0];
4374 assert_eq!(seat["raw_text"], seat["rationale"]);
4375 assert!(seat["raw_text"].as_str().unwrap().starts_with("[redacted"));
4376
4377 let rebased = latest(&redacted, [revision_of(&revision)]).unwrap();
4378 assert!(
4379 rebased["rounds"][0]["responses"][0]
4380 .get("raw_text")
4381 .is_none()
4382 );
4383 assert_eq!(
4384 redaction
4385 .amendment
4386 .redaction
4387 .as_ref()
4388 .unwrap()
4389 .resulting_latest_hash,
4390 Some(data_hash(&rebased))
4391 );
4392
4393 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4394 assert!(v.ok, "{v:#?}");
4395 let entry = &v.entries[0];
4396 assert_eq!(entry.latest_data_hash, Some(data_hash(&rebased)));
4397 assert_eq!(entry.superseded_revisions, [amd(1)]);
4398 assert!(v.check_content(&c.links[0], &redacted));
4399 assert!(v.check_content(&c.links[0], &rebased));
4400 assert!(
4401 v.check_content(&c.links[0], &data),
4402 "for whoever kept a copy"
4403 );
4404 v.check_content(&c.links[0], &redacted);
4405 assert!(v.clone().settle().ok, "{v:#?}");
4406
4407 let mut c2 = Chain::new();
4409 let target = c2.entry(&key, data.clone());
4410 c2.amend(&key, &revision);
4411 let (mut lying, _) = redact(
4412 &c2,
4413 &target,
4414 &["/subject/handle"],
4415 &data,
4416 &[revision_of(&revision)],
4417 )
4418 .unwrap();
4419 let (_, honest) = redact(
4420 &c2,
4421 &target,
4422 &["/subject/handle"],
4423 &data,
4424 &[revision_of(&revision)],
4425 )
4426 .unwrap();
4427 lying
4428 .amendment
4429 .redaction
4430 .as_mut()
4431 .unwrap()
4432 .resulting_latest_hash = Some(data_hash(&json!({})));
4433 c2.amend(&key, &lying);
4434 let mut v = verify_chain(&c2.links, &pk, &anchored(&pk), &roots());
4435 assert!(v.check_content(&c2.links[0], &honest));
4436 assert!(!v.settle().ok);
4437 }
4438
4439 #[test]
4442 fn a_redaction_of_a_revised_entry_must_name_the_latest_hash() {
4443 let (key, pk) = generate_keypair();
4444 let data = seats();
4445 let mut c = Chain::new();
4446 let target = c.entry(&key, data.clone());
4447 let (revision, _) = revise(&c, &target, &data, dedup(&data));
4448 c.amend(&key, &revision);
4449 let (redaction, _) =
4451 redact(&c, &target, &["/subject/handle"], &data, &[]).unwrap();
4452 c.amend(&key, &redaction);
4453 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4454 assert!(!v.ok);
4455 assert!(
4456 v.entries[2]
4457 .problem
4458 .as_deref()
4459 .is_some_and(|p| p.contains("names no resulting_latest_hash")),
4460 "{v:#?}"
4461 );
4462 }
4463
4464 #[test]
4467 fn a_false_duplicate_fails() {
4468 let (key, pk) = generate_keypair();
4469 let data = seats();
4470 let mut c = Chain::new();
4471 let target = c.entry(&key, data.clone());
4472 let artist = "/rounds/0/responses/1";
4473 let (raw, rationale) =
4474 (format!("{artist}/raw_text"), format!("{artist}/rationale"));
4475 let (mut lying, _) = revise(
4477 &c,
4478 &target,
4479 &data,
4480 patch(json!([{"op": "remove", "path": raw}])),
4481 );
4482 let revised = apply_patch(&data, &revision_of(&lying).patch).unwrap();
4483 let r = lying.amendment.revision.as_mut().unwrap();
4484 r.duplicates = vec![(raw, rationale)];
4485 r.resulting_data_hash = data_hash(&revised);
4486 c.amend(&key, &lying);
4487
4488 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4489 assert!(v.ok, "unread, the claim is unchecked: {v:#?}");
4490 assert!(v.check_content(&c.links[0], &data));
4491 assert!(
4492 v.entries[0]
4493 .problem
4494 .as_deref()
4495 .is_some_and(|p| p.contains("but they differ")),
4496 "{v:#?}"
4497 );
4498 assert!(!v.settle().ok);
4499 }
4500
4501 #[test]
4503 fn a_redaction_that_breaks_a_revision_is_refused() {
4504 let (key, _) = generate_keypair();
4505 let data = seats();
4506 let mut c = Chain::new();
4507 let target = c.entry(&key, data.clone());
4508 let (moved, _) = revise(
4509 &c,
4510 &target,
4511 &data,
4512 patch(
4513 json!([{"op": "move", "from": "/subject/handle", "path": "/handle"}]),
4514 ),
4515 );
4516 c.amend(&key, &moved);
4517 let err =
4518 redact(&c, &target, &["/subject"], &data, &[revision_of(&moved)])
4519 .unwrap_err();
4520 assert!(matches!(err, RedactError::Rebase(_)), "{err}");
4521 assert!(
4523 redact(
4524 &c,
4525 &target,
4526 &["/subject/handle"],
4527 &data,
4528 &[revision_of(&moved)]
4529 )
4530 .is_ok()
4531 );
4532 }
4533
4534 #[test]
4537 fn a_redaction_follows_every_duplicate_of_what_it_erases() {
4538 let revision = Revision {
4539 patch: patch(json!([{"op": "remove", "path": "/copy"}])),
4540 duplicates: vec![("/copy".into(), "/source/inner".into())],
4541 resulting_data_hash: data_hash(&json!(null)),
4542 };
4543 let extra = |fields: &[&str]| {
4544 let fields: Vec<String> =
4545 fields.iter().map(|f| f.to_string()).collect();
4546 duplicates_of(&fields, &[&revision])
4547 };
4548 assert_eq!(extra(&["/source/inner"]), ["/copy"]);
4549 assert_eq!(extra(&["/source/inner/name"]), ["/copy/name"]);
4550 assert_eq!(extra(&["/source"]), ["/copy"]);
4551 assert!(extra(&["/source/other"]).is_empty());
4552 assert!(extra(&["/source/inn"]).is_empty());
4553 }
4554
4555 #[test]
4558 fn a_redaction_then_a_revision() {
4559 let (key, pk) = generate_keypair();
4560 let data = seats();
4561 let mut c = Chain::new();
4562 let target = c.entry(&key, data.clone());
4563 let (redaction, redacted) =
4564 redact(&c, &target, &["/subject/handle"], &data, &[]).unwrap();
4565 assert_eq!(
4566 redaction
4567 .amendment
4568 .redaction
4569 .as_ref()
4570 .unwrap()
4571 .resulting_latest_hash,
4572 None,
4573 "nothing to rebase"
4574 );
4575 c.amend(&key, &redaction);
4576 let (revision, revised) =
4577 revise(&c, &target, &redacted, dedup(&redacted));
4578 c.amend(&key, &revision);
4579
4580 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4581 assert!(v.ok, "{v:#?}");
4582 assert_eq!(v.entries[0].latest_data_hash, Some(data_hash(&revised)));
4583 assert!(v.entries[0].superseded_revisions.is_empty());
4584 assert!(v.check_content(&c.links[0], &redacted));
4585 assert!(v.check_content(&c.links[0], &revised));
4586 assert!(v.settle().ok);
4587 }
4588
4589 #[test]
4590 fn a_revision_refuses_what_it_should() {
4591 let data = seats();
4592 let raw = &format!("{LAWYER}/raw_text");
4593 let rationale = &format!("{LAWYER}/rationale");
4594 let artist = "/rounds/0/responses/1";
4595 let dedup =
4596 |pairs: &[(&str, &str)]| Revision::remove_duplicates(&data, pairs);
4597
4598 assert_eq!(
4599 dedup(&[(
4600 &format!("{artist}/raw_text"),
4601 &format!("{artist}/rationale")
4602 )]),
4603 Err(ReviseError::NotIdentical {
4604 path: format!("{artist}/raw_text"),
4605 same_as: format!("{artist}/rationale"),
4606 })
4607 );
4608 assert_eq!(
4609 dedup(&[(raw, rationale), (rationale, raw)]),
4610 Err(ReviseError::SourceRemoved {
4611 path: raw.clone(),
4612 same_as: rationale.clone(),
4613 }),
4614 "a same_as the patch removes"
4615 );
4616 assert_eq!(
4617 dedup(&[("/nope", rationale)]),
4618 Err(ReviseError::Unresolved("/nope".into()))
4619 );
4620
4621 let make = |target_type, p: Edit| {
4622 AmendmentDraft::revision(
4623 gov(1),
4624 target_type,
4625 data_hash(&json!(null)),
4626 "b",
4627 "n",
4628 &data,
4629 p,
4630 )
4631 .map(|(_, v)| v)
4632 };
4633 let council = GovernanceLogEntryType::CouncilDecision;
4634 assert_eq!(
4635 make(council, patch(json!([])).into()),
4636 Err(ReviseError::EmptyPatch)
4637 );
4638 assert!(matches!(
4639 make(
4640 council,
4641 patch(json!([{"op": "remove", "path": "/nope"}])).into()
4642 ),
4643 Err(ReviseError::Patch(_))
4644 ));
4645 assert_eq!(
4646 make(
4647 council,
4648 patch(json!([{"op": "remove", "path": "/_blind"}])).into()
4649 ),
4650 Err(ReviseError::BlindPointer("/_blind".into()))
4651 );
4652 assert_eq!(
4653 make(
4654 council,
4655 patch(json!([{"op": "copy", "from": "/_blind", "path": "/b"}]))
4656 .into()
4657 ),
4658 Err(ReviseError::BlindPointer("/_blind".into()))
4659 );
4660 for entry_type in [
4661 GovernanceLogEntryType::Amendment,
4662 GovernanceLogEntryType::KeyRotation,
4663 GovernanceLogEntryType::StewardRecord,
4664 ] {
4665 assert!(!is_revisable(entry_type));
4666 assert_eq!(
4667 make(entry_type, dedup(&[(raw, rationale)]).unwrap()),
4668 Err(ReviseError::NotRevisable(entry_type))
4669 );
4670 }
4671 assert!(is_revisable(council));
4672 }
4673
4674 #[test]
4676 fn a_revision_that_adds_content_blinds_the_target() {
4677 let unblinded =
4678 json!({"title": "Old", "rationale": "r", "raw_text": "r"});
4679 let revise = |data: &serde_json::Value, p: serde_json::Value| {
4680 AmendmentDraft::revision(
4681 gov(1),
4682 GovernanceLogEntryType::CouncilDecision,
4683 data_hash(&json!(null)),
4684 "b",
4685 "n",
4686 data,
4687 patch(p),
4688 )
4689 .unwrap()
4690 };
4691 let add = json!([{"op": "add", "path": "/attachments", "value": []}]);
4692 let (draft, revised) = revise(&unblinded, add.clone());
4693 assert!(revised.get(BLIND_KEY).is_some());
4694 assert_eq!(revision_of(&draft).patch.len(), 2, "in the same patch");
4695 assert_eq!(
4696 latest(&unblinded, [revision_of(&draft)]).unwrap(),
4697 revised,
4698 "so the fold reproduces it"
4699 );
4700
4701 let (_, revised) =
4702 revise(&unblinded, json!([{"op": "remove", "path": "/raw_text"}]));
4703 assert!(revised.get(BLIND_KEY).is_none(), "a removal adds nothing");
4704
4705 let (draft, revised) = revise(&seats(), add);
4706 assert_eq!(revised[BLIND_KEY], seats()[BLIND_KEY], "never rotated");
4707 assert_eq!(revision_of(&draft).patch.len(), 1);
4708 }
4709
4710 #[test]
4711 fn revision_shape_violations_fail_verification() {
4712 let (key, pk) = generate_keypair();
4713 let amend_with = |mutate: &dyn Fn(&mut Amendment)| -> String {
4714 let mut c = Chain::new();
4715 let target = c.entry(&key, seats());
4716 let (mut draft, _) = revise(&c, &target, &seats(), dedup(&seats()));
4717 mutate(&mut draft.amendment);
4718 c.amend_v1(&key, &draft.amendment);
4719 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4720 assert!(!v.ok, "{v:#?}");
4721 v.entries[1].problem.clone().unwrap_or_default()
4722 };
4723 assert_eq!(
4724 AmendmentDraft::new(
4725 gov(1),
4726 data_hash(&json!(null)),
4727 AmendmentKind::Revision,
4728 "b",
4729 "n"
4730 ),
4731 Err(AmendmentError::MissingRevision)
4732 );
4733 let p = amend_with(&|a| a.revision = None);
4734 assert!(p.contains("requires a `revision`"), "{p}");
4735 let p = amend_with(&|a| a.kind = AmendmentKind::Correction);
4736 assert!(p.contains("only valid on kind `revision`"), "{p}");
4737 let p = amend_with(&|a| a.revision.as_mut().unwrap().patch.0.clear());
4738 assert!(p.contains("at least one op"), "{p}");
4739 }
4740
4741 #[cfg(feature = "schemars")]
4743 #[test]
4744 fn the_revision_schema_describes_a_patch() {
4745 let schema = crate::responses::inline_schema_for::<Amendment>();
4746 let revision = &schema["properties"]["revision"]["properties"];
4747 assert_eq!(revision["patch"]["type"], json!("array"), "{schema}");
4748 assert_eq!(
4749 revision["patch"]["items"]["properties"]["op"]["enum"],
4750 json!(["add", "remove", "replace", "move", "copy", "test"])
4751 );
4752 assert_eq!(revision["resulting_data_hash"]["type"], json!("string"));
4753 }
4754
4755 #[test]
4756 fn content_that_matches_nothing_fails_without_an_amendment() {
4757 let (key, pk) = generate_keypair();
4758 let mut c = Chain::new();
4759 c.entry(&key, json!({"a": 1}));
4760 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4761 assert!(!v.check_content(&c.links[0], &json!({"a": 2})));
4762 assert!(!v.clone().settle().ok);
4763 let other = link(&key, 9, None, &json!({}), at(9));
4765 assert!(!v.check_content(&other, &json!({})));
4766 }
4767
4768 #[test]
4769 fn tampering_with_an_amendments_data_is_caught_by_the_data_hash() {
4770 let (key, pk) = generate_keypair();
4771 let mut c = Chain::new();
4772 let target = c.decision(&key);
4773 let amendment = AmendmentDraft::new(
4774 target,
4775 c.hash_at(1),
4776 AmendmentKind::Overruled,
4777 "b",
4778 "overruled by a later decision",
4779 )
4780 .unwrap();
4781 c.amend(&key, &amendment);
4782 c.links[1].data.as_mut().unwrap()["note"] =
4783 json!("reinstated, actually");
4784
4785 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4786 assert!(!v.ok, "{v:#?}");
4787 let p = v.entries[1].problem.as_deref().unwrap();
4788 assert!(p.contains("does not hash to the attested data_hash"), "{p}");
4789 assert!(
4790 v.entries[0].amended_by.is_empty(),
4791 "an unreadable amendment has no effect"
4792 );
4793 assert!(
4794 v.entries[1].signature_valid && v.entries[1].link_valid,
4795 "the envelope is untouched — only the content is not what it \
4796 committed to"
4797 );
4798 }
4799
4800 #[test]
4801 fn an_amendment_or_rotation_must_carry_its_data() {
4802 let (key, pk) = generate_keypair();
4803 let mut c = Chain::new();
4804 let target = c.decision(&key);
4805 let amendment = AmendmentDraft::new(
4806 target,
4807 c.hash_at(1),
4808 AmendmentKind::Correction,
4809 "b",
4810 "n",
4811 )
4812 .unwrap();
4813 c.amend(&key, &amendment);
4814 let rotation = c.routine(&pk, &generate_keypair().0);
4815 c.rotate(&key, &rotation);
4816 c.links[1].data = None;
4817 c.links[2].data = None;
4818
4819 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4820 assert!(!v.ok, "{v:#?}");
4821 for (i, entry_type) in [(1, "amendment"), (2, "key_rotation")] {
4822 let p = v.entries[i].problem.as_deref().unwrap();
4823 assert!(p.contains(&format!("a {entry_type} entry")), "{p}");
4824 assert!(p.contains("must carry its `data`"), "{p}");
4825 }
4826 }
4827
4828 #[test]
4829 fn the_id_series_must_match_the_entry_type() {
4830 let (key, pk) = generate_keypair();
4831 let mut c = Chain::new();
4832 let target = c.decision(&key);
4833 let amendment = AmendmentDraft::new(
4834 target,
4835 c.hash_at(1),
4836 AmendmentKind::Correction,
4837 "b",
4838 "n",
4839 )
4840 .unwrap();
4841 c.push(
4842 &key,
4843 gov(7),
4844 GovernanceLogEntryType::Amendment,
4845 serde_json::to_value(&amendment.amendment).unwrap(),
4846 true,
4847 );
4848 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4849 assert!(!v.ok, "{v:#?}");
4850 let p = v.entries[1].problem.as_deref().unwrap();
4851 assert!(p.contains("must be in the AMD- series"), "{p}");
4852
4853 let mut c = Chain::new();
4854 c.push(
4855 &key,
4856 key_id(1),
4857 GovernanceLogEntryType::CouncilDecision,
4858 json!({}),
4859 false,
4860 );
4861 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4862 let p = v.entries[0].problem.as_deref().unwrap();
4863 assert!(p.contains("reserved"), "{p}");
4864 }
4865
4866 #[test]
4867 fn redact_data_replaces_whole_values_and_refuses_the_rest() {
4868 let id = amd(3);
4869 let blind = Blind::from([9; 32]);
4870 let data = json!({"a": {"b": [1, {"c": "secret"}]}, "d/e": "slash"});
4871 let out = redact_data(
4872 &data,
4873 &["/a/b/1/c".into(), "/d~1e".into()],
4874 &id,
4875 blind,
4876 )
4877 .unwrap();
4878 assert_eq!(out["a"]["b"][1]["c"], json!(redaction_marker(&id)));
4879 assert_eq!(out["d/e"], json!(redaction_marker(&id)));
4880 assert_eq!(out["a"]["b"][0], json!(1), "untouched");
4881 assert_eq!(out[BLIND_KEY], json!(blind), "a legacy entry gains one");
4882
4883 assert_eq!(
4884 redact_data(&data, &["/a/nope".into()], &id, blind),
4885 Err(RedactError::Unresolved("/a/nope".into()))
4886 );
4887 assert_eq!(
4888 redact_data(&data, &["".into()], &id, blind),
4889 Err(RedactError::WholeEntry)
4890 );
4891 assert_eq!(
4892 redact_data(&data, &[], &id, blind),
4893 Err(RedactError::NoFields)
4894 );
4895 assert_eq!(
4896 redact_data(&data, &["/_blind".into()], &id, blind),
4897 Err(RedactError::BlindPointer("/_blind".into()))
4898 );
4899 }
4900
4901 #[test]
4902 fn blind_data_is_for_objects_and_is_the_writers_to_supply() {
4903 let blind = Blind::from([1; 32]);
4904 let out = blind_data(&json!({"finding": "upheld"}), blind).unwrap();
4905 assert_eq!(out, json!({"finding": "upheld", "_blind": blind}));
4906 assert_eq!(
4907 blind_data(&json!([1]), blind),
4908 Err(BlindError::NotAnObject)
4909 );
4910 assert_eq!(blind_data(&out, blind), Err(BlindError::AlreadyBlinded));
4911 assert_ne!(Blind::random(), Blind::random());
4912
4913 use GovernanceLogEntryType::*;
4914 assert!(!is_redactable(Amendment) && !is_redactable(KeyRotation));
4915 assert!(
4916 is_redactable(CouncilDecision) && is_redactable(EmergencyAction)
4917 );
4918 }
4919
4920 #[test]
4924 fn a_removed_value_cannot_be_confirmed_by_guessing_it() {
4925 fn confirms(
4927 public: &serde_json::Value,
4928 pointer: &str,
4929 guess: &str,
4930 hash: Sha256Hex,
4931 ) -> bool {
4932 let mut attempt = public.clone();
4933 *attempt.pointer_mut(pointer).unwrap() = json!(guess);
4934 data_hash(&attempt) == hash
4935 }
4936 let legacy = json!({"finding": "upheld", "handle": "someone", "city": "Utrecht"});
4937
4938 let written = blind_data(&legacy, Blind::random()).unwrap();
4940 let first = redact_data(
4941 &written,
4942 &["/handle".into()],
4943 &amd(1),
4944 Blind::random(),
4945 )
4946 .unwrap();
4947 assert!(!confirms(&first, "/handle", "someone", data_hash(&written)));
4948
4949 let second =
4952 redact_data(&first, &["/city".into()], &amd(2), Blind::random())
4953 .unwrap();
4954 assert!(!confirms(&second, "/city", "Utrecht", data_hash(&first)));
4955
4956 let mut stripped =
4960 redact_data(&legacy, &["/handle".into()], &amd(3), Blind::random())
4961 .unwrap();
4962 let legacy_first = stripped.clone();
4963 stripped.as_object_mut().unwrap().remove(BLIND_KEY);
4964 assert!(confirms(
4965 &stripped,
4966 "/handle",
4967 "someone",
4968 data_hash(&legacy)
4969 ));
4970 let legacy_second = redact_data(
4972 &legacy_first,
4973 &["/city".into()],
4974 &amd(4),
4975 Blind::random(),
4976 )
4977 .unwrap();
4978 assert!(!confirms(
4979 &legacy_second,
4980 "/city",
4981 "Utrecht",
4982 data_hash(&legacy_first)
4983 ));
4984 }
4985
4986 #[test]
4989 fn a_routine_rotation_moves_the_chain_to_the_new_key() {
4990 let (old, old_pk) = generate_keypair();
4991 let (new, new_pk) = generate_keypair();
4992 let mut c = Chain::new();
4993 c.decision(&old);
4994 let rotation = c.routine(&old_pk, &new);
4995 let rotation_id = c.rotate(&old, &rotation);
4996 c.decision(&new);
4997
4998 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5001 assert!(v.ok, "{v:#?}");
5002 assert_eq!(v.public_key, (&new_pk).into());
5003 assert_eq!(
5004 v.entries[1].signed_by,
5005 Some((&old_pk).into()),
5006 "the rotation itself is signed by the old key"
5007 );
5008 assert_eq!(v.entries[2].signed_by, Some((&new_pk).into()));
5009 assert!(v.unanchored_keys.is_empty());
5010 assert!(v.repudiated.is_empty());
5011 assert_eq!(v.keys.len(), 2);
5012 assert_eq!(v.keys[0].public_key, (&old_pk).into());
5013 assert_eq!(v.keys[0].from_seq, 1);
5014 assert_eq!(v.keys[0].through_seq, Some(2));
5015 assert_eq!(v.keys[0].status, KeyStatus::Retired);
5016 assert_eq!(v.keys[0].introduced_by, None);
5017 assert_eq!(v.keys[0].retired_by.as_ref(), Some(&rotation_id));
5018 assert!(v.keys[0].certified, "retroactively, by the rotation");
5019 assert_eq!(v.keys[1].from_seq, 3);
5020 assert_eq!(v.keys[1].through_seq, None);
5021 assert_eq!(v.keys[1].status, KeyStatus::Active);
5022 assert_eq!(v.keys[1].introduced_by.as_ref(), Some(&rotation_id));
5023 assert!(v.keys[1].certified);
5024 }
5025
5026 #[test]
5027 fn the_old_key_cannot_sign_after_a_routine_rotation() {
5028 let (old, old_pk) = generate_keypair();
5029 let (new, _) = generate_keypair();
5030 let mut c = Chain::new();
5031 c.decision(&old);
5032 let rotation = c.routine(&old_pk, &new);
5033 c.rotate(&old, &rotation);
5034 c.decision(&old);
5035
5036 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5037 assert!(!v.ok, "{v:#?}");
5038 assert!(!v.entries[2].signature_valid);
5039 assert!(
5040 v.entries[2].link_valid,
5041 "the linkage is fine; the key is not"
5042 );
5043 }
5044
5045 #[test]
5048 fn the_online_key_cannot_certify_its_own_successor() {
5049 let (old, old_pk) = generate_keypair();
5050 let (thief, _) = generate_keypair();
5051 let mut c = Chain::new();
5052 c.decision(&old);
5053 let mut rotation = c.routine(&old_pk, &thief);
5054 let statement = rotation.certificate.statement.clone();
5056 rotation.certificate = certify(&[&old], statement);
5057 c.rotate(&old, &rotation);
5058 c.decision(&thief);
5059
5060 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5061 assert!(!v.ok, "{v:#?}");
5062 let p = v.entries[1].problem.as_deref().unwrap();
5063 assert!(p.contains("0 valid root signature"), "{p}");
5064 assert_eq!(v.public_key, (&old_pk).into(), "the chain does not move");
5065 assert!(!v.entries[2].signature_valid);
5066 assert_eq!(v.keys.len(), 1);
5067 }
5068
5069 #[test]
5070 fn a_certificate_counts_distinct_known_roots_only() {
5071 let (old, old_pk) = generate_keypair();
5072 let (new, _) = generate_keypair();
5073 let (stranger, _) = generate_keypair();
5074 let two_of_two = RootSet::new(
5075 [
5076 (&root(1).verifying_key()).into(),
5077 (&root(2).verifying_key()).into(),
5078 ],
5079 2,
5080 );
5081 let verdict = |signers: &[&SigningKey], roots: &RootSet| {
5082 let mut c = Chain::new();
5083 c.decision(&old);
5084 let mut rotation = c.routine(&old_pk, &new);
5085 let statement = rotation.certificate.statement.clone();
5086 rotation.certificate = certify(signers, statement);
5087 let genesis = KeyCertStatement::genesis((&old_pk).into());
5089 rotation.outgoing_certificate =
5090 Some(certify(&[&root(1), &root(2)], genesis));
5091 c.rotate(&old, &rotation);
5092 verify_chain(&c.links, &old_pk, &anchored(&old_pk), roots)
5093 };
5094
5095 assert!(verdict(&[&root(1), &root(2)], &two_of_two).ok);
5096 assert!(verdict(&[&root(2)], &roots()).ok, "either root, 1-of-2");
5097
5098 for (signers, why) in [
5099 (vec![&root(1)], "below the threshold"),
5100 (vec![&root(1), &root(1)], "one root twice is one root"),
5101 (vec![&root(1), &stranger], "an unknown root is nobody"),
5102 (vec![], "unsigned"),
5103 ] {
5104 let v = verdict(&signers, &two_of_two);
5105 assert!(!v.ok, "{why}: {v:#?}");
5106 let p = v.entries[1].problem.as_deref().unwrap();
5107 assert!(p.contains("where 2 are needed"), "{why}: {p}");
5108 }
5109
5110 assert!(verdict(&[&stranger, &root(1)], &roots()).ok);
5112 }
5113
5114 #[test]
5115 fn a_certificate_is_good_for_one_statement_at_one_position() {
5116 let (old, old_pk) = generate_keypair();
5117 let (new, new_pk) = generate_keypair();
5118 let (other, other_pk) = generate_keypair();
5119 let anchor = anchored(&old_pk);
5120
5121 let mut c = Chain::new();
5125 c.decision(&old);
5126 let early = c.routine(&old_pk, &new);
5127 c.decision(&old);
5128 let mut rotation = c.routine(&old_pk, &new);
5129 rotation.certificate = early.certificate;
5130 c.rotate(&old, &rotation);
5131 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5132 assert!(!v.ok, "{v:#?}");
5133 let p = v.entries[2].problem.as_deref().unwrap();
5134 assert!(
5135 p.contains("different key, purpose or chain position"),
5136 "{p}"
5137 );
5138 assert_eq!(v.public_key, (&old_pk).into());
5139
5140 let mut c = Chain::new();
5142 c.decision(&old);
5143 let for_new = c.routine(&old_pk, &new);
5144 let mut rotation = c.routine(&old_pk, &other);
5145 rotation.certificate = for_new.certificate;
5146 c.rotate(&old, &rotation);
5147 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5148 let p = v.entries[1].problem.as_deref().unwrap();
5149 assert!(
5150 p.contains("different key, purpose or chain position"),
5151 "{p}"
5152 );
5153
5154 let mut c = Chain::new();
5156 c.decision(&old);
5157 let mut rotation = c.routine(&old_pk, &other);
5158 rotation.certificate = for_new_at(&c, &new_pk);
5159 rotation.certificate.statement.key = (&other_pk).into();
5160 c.rotate(&old, &rotation);
5161 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5162 let p = v.entries[1].problem.as_deref().unwrap();
5163 assert!(p.contains("0 valid root signature"), "{p}");
5164
5165 let mut c = Chain::new();
5167 c.decision(&old);
5168 c.decision(&old);
5169 let mut rotation = c.compromise(&old_pk, &new, 1);
5170 rotation.certificate = for_new_at(&c, &new_pk);
5171 c.rotate(&new, &rotation);
5172 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5173 assert!(!v.ok);
5174 assert!(v.repudiated.is_empty(), "{v:#?}");
5175 assert_eq!(v.public_key, (&old_pk).into());
5176 }
5177
5178 #[test]
5181 fn a_root_signature_without_the_domain_prefix_certifies_nothing() {
5182 use ed25519_dalek::Signer;
5183 let (old, old_pk) = generate_keypair();
5184 let (new, _) = generate_keypair();
5185 let mut c = Chain::new();
5186 c.decision(&old);
5187 let mut rotation = c.routine(&old_pk, &new);
5188 let statement = rotation.certificate.statement.clone();
5189 let bare = canonical_json(&serde_json::to_value(&statement).unwrap());
5190 assert_eq!(
5191 statement.signed_bytes(),
5192 [ROOT_DOMAIN, bare.as_slice()].concat()
5193 );
5194 rotation.certificate =
5195 KeyCertificate::unsigned(statement).with(RootSignature {
5196 root_key: (&root(1).verifying_key()).into(),
5197 signature: root(1).sign(&bare).into(),
5198 });
5199 c.rotate(&old, &rotation);
5200 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5201 assert!(!v.ok, "{v:#?}");
5202 assert_eq!(v.public_key, (&old_pk).into());
5203 }
5204
5205 #[test]
5206 fn the_first_rotation_carries_the_genesis_certificate_and_only_it_does() {
5207 let (k1, k1_pk) = generate_keypair();
5208 let (k2, k2_pk) = generate_keypair();
5209 let (k3, _) = generate_keypair();
5210 let genesis =
5211 || certify(&[&root(1)], KeyCertStatement::genesis((&k1_pk).into()));
5212
5213 let mut c = Chain::new();
5215 c.decision(&k1);
5216 let mut rotation = c.routine(&k1_pk, &k2);
5217 rotation.outgoing_certificate = None;
5218 c.rotate(&k1, &rotation);
5219 let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5220 assert!(!v.ok);
5221 let p = v.entries[1].problem.as_deref().unwrap();
5222 assert!(p.contains("genesis key's outgoing_certificate"), "{p}");
5223 assert_eq!(v.public_key, (&k1_pk).into());
5224
5225 let mut c = Chain::new();
5227 c.decision(&k1);
5228 let rotation = c.routine(&k1_pk, &k2).with_outgoing(certify(
5229 &[&root(1)],
5230 KeyCertStatement::genesis((&k2_pk).into()),
5231 ));
5232 c.rotate(&k1, &rotation);
5233 let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5234 let p = v.entries[1].problem.as_deref().unwrap();
5235 assert!(p.starts_with("outgoing_certificate:"), "{p}");
5236
5237 let mut c = Chain::new();
5240 c.decision(&k1);
5241 let rotation = c.routine(&k1_pk, &k2);
5242 assert_eq!(rotation.outgoing_certificate, Some(genesis()));
5243 c.rotate(&k1, &rotation);
5244 let v = verify_chain(&c.links, &k1_pk, &KeyAnchor::default(), &roots());
5245 assert!(v.ok, "{v:#?}");
5246 assert!(v.unanchored_keys.is_empty(), "{v:#?}");
5247
5248 let again = c.routine(&k2_pk, &k3).with_outgoing(genesis());
5250 c.rotate(&k2, &again);
5251 let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5252 assert!(!v.ok);
5253 let p = v.entries[2].problem.as_deref().unwrap();
5254 assert!(p.contains("nowhere else"), "{p}");
5255 }
5256
5257 #[test]
5258 fn a_forged_proof_of_possession_is_rejected() {
5259 let (old, old_pk) = generate_keypair();
5260 let (_, new_pk) = generate_keypair();
5261 let (thief, _) = generate_keypair();
5262 let mut c = Chain::new();
5263 c.decision(&old);
5264 let mut rotation = c.routine(&old_pk, &thief);
5267 rotation.new_key = (&new_pk).into();
5268 rotation.certificate = for_new_at(&c, &new_pk);
5269 let statement = rotation.statement(c.prev_hash());
5270 rotation.proof = crypto::sign(
5271 &old,
5272 statement.hash().as_bytes(),
5273 rotation.proof_signed_at,
5274 )
5275 .into();
5276 c.rotate(&old, &rotation);
5277
5278 assert_eq!(
5279 rotation.verify_proof(c.links[1].attestation.prev_hash),
5280 Err(RotationError::BadProof)
5281 );
5282 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5283 assert!(!v.ok, "{v:#?}");
5284 assert!(
5285 v.entries[1]
5286 .problem
5287 .as_deref()
5288 .unwrap()
5289 .contains("proof of possession")
5290 );
5291 assert_eq!(v.public_key, (&old_pk).into(), "the chain does not move");
5292 }
5293
5294 #[test]
5295 fn a_compromise_repudiates_the_window_and_a_reattestation_restores_one() {
5296 let (old, old_pk) = generate_keypair();
5297 let (new, new_pk) = generate_keypair();
5298 let mut c = Chain::new();
5299 c.decision(&old); c.decision(&old); let reattested = c.decision(&old); let rotation = c.compromise(&old_pk, &new, 1);
5303 let rotation_id = c.rotate(&new, &rotation); let vouch = AmendmentDraft::new(
5305 reattested,
5306 c.hash_at(3),
5307 AmendmentKind::Reattested,
5308 "Art. VII",
5309 "independently verified; the Steward vouches for it",
5310 )
5311 .unwrap();
5312 let vouch_id = c.amend(&new, &vouch); let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5315 assert!(
5316 v.ok,
5317 "repudiation is a declared state, not a defect: {v:#?}"
5318 );
5319 assert_eq!(v.repudiated, vec![gov(2)]);
5320 assert!(v.entries[1].repudiated);
5321 assert!(!v.entries[2].repudiated);
5322 assert_eq!(v.entries[2].reattested_by, vec![vouch_id.clone()]);
5323 assert_eq!(v.entries[2].amended_by, vec![vouch_id]);
5324 assert_eq!(v.entries[3].signed_by, Some((&new_pk).into()));
5325 assert_eq!(v.public_key, (&new_pk).into());
5326 assert_eq!(v.keys.len(), 2);
5327 assert_eq!(v.keys[0].status, KeyStatus::Compromised);
5328 assert_eq!(
5329 v.keys[0].through_seq,
5330 Some(1),
5331 "trusted through the last trusted entry, not through the \
5332 declaration"
5333 );
5334 assert_eq!(v.keys[0].retired_by.as_ref(), Some(&rotation_id));
5335 assert_eq!(v.keys[1].from_seq, 4, "the declaration is its own first");
5336 }
5337
5338 #[test]
5341 fn last_trusted_is_the_roots_to_say() {
5342 let (old, old_pk) = generate_keypair();
5343 let (new, _) = generate_keypair();
5344 let mut c = Chain::new();
5345 c.decision(&old); c.decision(&old); let mut rotation = c.compromise(&old_pk, &new, 1);
5348 rotation.certificate.statement.last_trusted = Some(c.head(2));
5349 c.rotate(&new, &rotation);
5350 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5351 assert!(!v.ok, "{v:#?}");
5352 assert!(v.repudiated.is_empty());
5353 assert_eq!(v.public_key, (&old_pk).into());
5354 }
5355
5356 #[test]
5357 fn a_stolen_key_cannot_be_rotated_back_in() {
5358 let (k1, k1_pk) = generate_keypair();
5362 let (k2, k2_pk) = generate_keypair();
5363 let mut c = Chain::new();
5364 c.decision(&k1); let real = c.compromise(&k1_pk, &k2, 1);
5366 c.rotate(&k2, &real); c.decision(&k2); let honest =
5369 verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5370 assert!(honest.ok, "{honest:#?}");
5371
5372 let hijack = c.compromise(&k2_pk, &k1, 3);
5373 c.rotate(&k1, &hijack); c.decision(&k1); let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5377 assert!(!v.ok);
5378 assert_eq!(v.public_key, (&k2_pk).into(), "the chain stays with K2");
5379 let p = v.entries[3].problem.as_deref().unwrap();
5380 assert!(p.contains("never brought back"), "{p}");
5381 assert!(!v.entries[3].signature_valid, "{:#?}", v.entries[3]);
5382 assert!(!v.entries[4].signature_valid, "K1 signs nothing again");
5383 assert_eq!(v.keys.len(), 2);
5384 assert_eq!(v.keys[1].status, KeyStatus::Active);
5385 }
5386
5387 #[test]
5388 fn a_routine_rotation_cannot_reuse_a_key_either() {
5389 let (k1, k1_pk) = generate_keypair();
5390 let (k2, k2_pk) = generate_keypair();
5391 let mut c = Chain::new();
5392 c.decision(&k1);
5393 let out = c.routine(&k1_pk, &k2);
5394 c.rotate(&k1, &out);
5395 let back = c.routine(&k2_pk, &k1);
5396 c.rotate(&k2, &back);
5397 let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5398 assert!(!v.ok);
5399 assert!(
5400 v.entries[2]
5401 .problem
5402 .as_deref()
5403 .unwrap()
5404 .contains("never brought back"),
5405 "{:#?}",
5406 v.entries[2]
5407 );
5408 assert_eq!(v.public_key, (&k2_pk).into());
5409 }
5410
5411 #[test]
5412 fn a_forged_entry_has_no_effects() {
5413 let (steward, steward_pk) = generate_keypair();
5414 let (forger, _) = generate_keypair();
5415 let anchor = anchored(&steward_pk);
5416 let mut c = Chain::new();
5417 let target = c.decision(&steward); let fake = AmendmentDraft::new(
5419 target,
5420 c.hash_at(1),
5421 AmendmentKind::Overruled,
5422 "none",
5423 "overruled, says nobody with the key",
5424 )
5425 .unwrap();
5426 c.amend(&forger, &fake); let grab = c.routine(&steward_pk, &forger);
5430 c.rotate(&forger, &grab); let v = verify_chain(&c.links, &steward_pk, &anchor, &roots());
5433 assert!(!v.ok);
5434 assert!(v.entries[0].amended_by.is_empty(), "{:#?}", v.entries[0]);
5435 assert_eq!(v.public_key, (&steward_pk).into());
5436 assert_eq!(v.keys.len(), 1);
5437 assert!(v.unanchored_keys.is_empty());
5438 }
5439
5440 #[test]
5441 fn a_repeated_id_is_a_problem() {
5442 let (key, pk) = generate_keypair();
5443 let mut c = Chain::new();
5444 c.decision(&key);
5445 c.gov = 0;
5446 c.decision(&key); let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
5448 assert!(!v.ok);
5449 assert!(v.entries.iter().all(|e| {
5450 e.problem
5451 .as_deref()
5452 .is_some_and(|p| p.contains("more than once"))
5453 }));
5454 }
5455
5456 #[test]
5457 fn a_second_compromise_cannot_anchor_inside_the_first_window() {
5458 let (k1, k1_pk) = generate_keypair();
5459 let (k2, _) = generate_keypair();
5460 let (k3, _) = generate_keypair();
5461 let mut c = Chain::new();
5462 c.decision(&k1); c.decision(&k1); let first = c.compromise(&k1_pk, &k2, 1);
5465 c.rotate(&k2, &first); let second = c.compromise(&k1_pk, &k3, 2);
5467 c.rotate(&k3, &second); let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5470 assert!(!v.ok);
5471 let p = v.entries[3].problem.as_deref().unwrap();
5472 assert!(p.contains("repudiated"), "{p}");
5473 assert_eq!(v.keys.len(), 2, "K1 and K2; K3 never took the chain");
5474 assert_eq!(v.keys[0].through_seq, Some(1));
5475 }
5476
5477 #[test]
5478 fn an_uncertified_compromise_fails_closed() {
5479 let (old, old_pk) = generate_keypair();
5480 let (new, new_pk) = generate_keypair();
5481 let mut c = Chain::new();
5482 c.decision(&old);
5483 c.decision(&old);
5484 let mut rotation = c.compromise(&old_pk, &new, 1);
5485 let statement = rotation.certificate.statement.clone();
5486 rotation.certificate = certify(&[&new], statement);
5487 c.rotate(&new, &rotation);
5488
5489 let anchor = anchored(&old_pk).with((&new_pk).into());
5491 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5492 assert!(!v.ok, "{v:#?}");
5493 let p = v.entries[2].problem.as_deref().unwrap();
5494 assert!(p.contains("0 valid root signature"), "{p}");
5495 assert!(!v.entries[2].signature_valid, "checked under the old key");
5496 assert!(v.repudiated.is_empty(), "and nothing is repudiated");
5497 assert_eq!(v.public_key, (&old_pk).into());
5498 }
5499
5500 #[test]
5505 fn a_rotation_inside_the_window_is_void_with_it() {
5506 let (steward, steward_pk) = generate_keypair();
5507 let (successor, _) = generate_keypair();
5508 let (recovery, recovery_pk) = generate_keypair();
5509
5510 let mut c = Chain::new();
5511 c.decision(&steward); c.decision(&steward); let routine = c.routine(&steward_pk, &successor);
5514 c.rotate(&steward, &routine); c.decision(&successor); let declaration = c.compromise(&steward_pk, &recovery, 1);
5518 c.rotate(&recovery, &declaration); let v = verify_chain(
5521 &c.links,
5522 &steward_pk,
5523 &anchored(&steward_pk),
5524 &roots(),
5525 );
5526 assert!(v.ok, "{v:#?}");
5527 assert_eq!(v.repudiated, vec![gov(2), key_id(1), gov(3)]);
5528 assert_eq!(v.public_key, (&recovery_pk).into());
5529 assert_eq!(v.keys.len(), 2, "the successor is not part of history");
5530 assert_eq!(v.keys[0].public_key, (&steward_pk).into());
5531 assert_eq!(v.keys[0].status, KeyStatus::Compromised);
5532 assert!(
5533 v.keys[0].certified,
5534 "the genesis certificate rode in on the voided rotation and \
5535 is the root's word all the same"
5536 );
5537 assert_eq!(v.keys[1].public_key, (&recovery_pk).into());
5538 }
5539
5540 #[test]
5541 fn a_compromise_must_name_a_real_head_and_the_key_that_held_it() {
5542 let (old, old_pk) = generate_keypair();
5543 let (new, new_pk) = generate_keypair();
5544 let anchor = anchored(&old_pk);
5545
5546 let mut c = Chain::new();
5549 c.decision(&old);
5550 let mut trusted = c.head(1);
5551 trusted.entry_hash = data_hash(&json!("nope"));
5552 let statement = KeyCertStatement::compromise(
5553 (&new_pk).into(),
5554 2,
5555 c.prev_hash(),
5556 trusted,
5557 );
5558 let mut rotation = c.compromise(&old_pk, &new, 1);
5559 rotation.certificate = certify(&[&root(1)], statement);
5560 c.rotate(&new, &rotation);
5561 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5562 let p = v.entries[1].problem.as_deref().unwrap();
5563 assert!(p.contains("last_trusted"), "{p}");
5564
5565 let (_, other_pk) = generate_keypair();
5567 let mut c = Chain::new();
5568 c.decision(&old);
5569 let rotation = c.compromise(&other_pk, &new, 1);
5570 c.rotate(&new, &rotation);
5571 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5572 let p = v.entries[1].problem.as_deref().unwrap();
5573 assert!(p.contains("old_key is not the key"), "{p}");
5574
5575 let mut c = Chain::new();
5577 c.decision(&old);
5578 let mut rotation = c.compromise(&old_pk, &new, 1);
5579 rotation.certificate.statement.last_trusted = None;
5580 c.rotate(&new, &rotation);
5581 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5582 let p = v.entries[1].problem.as_deref().unwrap();
5583 assert!(p.contains("must name last_trusted"), "{p}");
5584 }
5585
5586 #[test]
5587 fn a_rotation_carries_no_free_text() {
5588 let (old, old_pk) = generate_keypair();
5589 let (new, _) = generate_keypair();
5590 let mut c = Chain::new();
5591 c.decision(&old);
5592 let rotation = c.routine(&old_pk, &new);
5593 let mut value = serde_json::to_value(&rotation).unwrap();
5594 assert!(serde_json::from_value::<KeyRotation>(value.clone()).is_ok());
5595 value["note"] = json!("at the request of …");
5596 assert!(serde_json::from_value::<KeyRotation>(value).is_err());
5597
5598 let mut head = serde_json::to_value(c.head(1)).unwrap();
5599 head["comment"] = json!("the last one I remember signing");
5600 assert!(serde_json::from_value::<TrustedHead>(head).is_err());
5601
5602 let mut statement =
5603 serde_json::to_value(&rotation.certificate.statement).unwrap();
5604 statement["comment"] = json!("signed in the kitchen");
5605 assert!(serde_json::from_value::<KeyCertStatement>(statement).is_err());
5606 }
5607
5608 #[test]
5609 fn the_root_statement_bytes_are_pinned() {
5610 let key: PublicKeyHex = PUBLISHED_KEYS[0].parse().unwrap();
5611 assert_eq!(
5612 String::from_utf8(KeyCertStatement::genesis(key).signed_bytes())
5613 .unwrap(),
5614 "agora-governance-root-v1\n\
5615 {\"agora_governance_key_cert\":1,\"from_seq\":1,\
5616 \"key\":\"ebb3091dd328f1463362c171121921b2fe14628e3fc4c145deaccefb85c0e78a\",\
5617 \"last_trusted\":null,\"prev_hash\":null,\"purpose\":\"genesis\"}"
5618 );
5619
5620 let statement = KeyCertStatement::compromise(
5624 Sha256Hex::from([0xab; 32]).to_string().parse().unwrap(),
5625 15,
5626 Some([0xcd; 32].into()),
5627 TrustedHead {
5628 id: gov(10),
5629 chain_seq: 11,
5630 entry_hash: [0xef; 32].into(),
5631 },
5632 );
5633 assert_eq!(
5634 Sha256Hex::from(<[u8; 32]>::from(Sha256::digest(
5635 statement.signed_bytes()
5636 )))
5637 .to_string(),
5638 "633685771e08be126fd12ca4eb98c77120e5868236ff541ecba85ce6a21e6b68"
5639 );
5640 }
5641
5642 #[test]
5643 fn root_keys_are_curve_points_and_make_a_root_set() {
5644 let roots = RootSet::published();
5645 assert_eq!(roots.keys().count(), ROOT_KEYS.len());
5646 assert_eq!(roots.threshold(), ROOT_THRESHOLD);
5647 assert!(ROOT_THRESHOLD >= 1 && ROOT_THRESHOLD <= ROOT_KEYS.len());
5648 for root in ROOT_KEYS {
5649 let key: PublicKeyHex = root.parse().unwrap();
5650 assert!(roots.contains(&key));
5651 assert!(
5652 key.to_verifying_key().is_ok(),
5653 "{root} is not a valid Ed25519 public key"
5654 );
5655 assert!(
5656 !PUBLISHED_KEYS.contains(root),
5657 "a root key never signs entries"
5658 );
5659 }
5660 assert_eq!(RootSet::new([], 0).threshold(), 1);
5661 }
5662
5663 #[test]
5664 fn a_genesis_key_outside_the_anchor_is_reported_not_rejected() {
5665 let (key, pk) = generate_keypair();
5666 let c = chain(&key, 2);
5667 let v = verify_chain(&c, &pk, &KeyAnchor::default(), &roots());
5668 assert!(v.ok, "{v:#?}");
5669 assert_eq!(v.unanchored_keys, vec![PublicKeyHex::from(&pk)]);
5670 assert_eq!(v.keys.len(), 1);
5671 assert_eq!(v.keys[0].status, KeyStatus::Active);
5672 assert_eq!(v.keys[0].from_seq, 1);
5673 assert_eq!(v.keys[0].through_seq, None);
5674 }
5675
5676 #[test]
5677 fn published_keys_are_curve_points_and_make_an_anchor() {
5678 let anchor = KeyAnchor::published();
5679 assert!(!anchor.is_empty());
5680 assert_eq!(anchor.keys().count(), PUBLISHED_KEYS.len());
5681 for published in PUBLISHED_KEYS {
5682 let key: PublicKeyHex = published.parse().unwrap();
5683 assert!(anchor.contains(&key));
5684 assert!(
5685 key.to_verifying_key().is_ok(),
5686 "{published} is not a valid Ed25519 public key"
5687 );
5688 }
5689 let (_, other) = generate_keypair();
5690 assert!(!anchor.contains(&(&other).into()));
5691 assert!(
5692 anchor
5693 .clone()
5694 .with((&other).into())
5695 .contains(&(&other).into())
5696 );
5697 }
5698
5699 #[test]
5702 fn amendments_and_rotations_round_trip_as_entry_data() {
5703 let (key, pk) = generate_keypair();
5704 let amendment = AmendmentDraft::new(
5705 gov(1),
5706 data_hash(&json!("x")),
5707 AmendmentKind::Superseded,
5708 "Art. VI § 2",
5709 "superseded by GOV-2026-0009",
5710 )
5711 .unwrap()
5712 .with_authority(gov(9))
5713 .with_rationale("the later decision covers the same subject");
5714 let AmendmentDraft { amendment, texts } = amendment;
5715 let value = serde_json::to_value(&amendment).unwrap();
5716 assert_eq!(value["kind"], "superseded");
5717 assert_eq!(value["agora_governance_amendment"], 2);
5718 assert!(value.get("redaction").is_none(), "{value}");
5719 let text = value.to_string();
5720 assert!(!text.contains("Art. VI"), "no text in signed data: {text}");
5721 assert!(!text.contains("salt"), "and no salt: {text}");
5722 assert_eq!(
5723 value["note"]["commitment"],
5724 texts
5725 .note
5726 .as_ref()
5727 .unwrap()
5728 .commitment()
5729 .commitment
5730 .to_string()
5731 );
5732 assert_eq!(
5733 serde_json::from_value::<Amendment>(value).unwrap(),
5734 amendment
5735 );
5736 let beside = serde_json::to_value(&texts).unwrap();
5737 assert_eq!(beside["basis"]["text"], "Art. VI § 2");
5738 assert_eq!(
5739 serde_json::from_value::<AmendmentTexts>(beside).unwrap(),
5740 texts
5741 );
5742
5743 let legacy = json!({
5745 "agora_governance_amendment": 1,
5746 "target": "GOV-2026-0001",
5747 "target_entry_hash": data_hash(&json!("x")),
5748 "kind": "non_precedential",
5749 "authority": "GOV-2026-0005",
5750 "basis": "§1",
5751 "note": "diagnostic finding",
5752 });
5753 let legacy: Amendment = serde_json::from_value(legacy).unwrap();
5754 assert_eq!(legacy.validate(), Ok(()));
5755 assert_eq!(legacy.basis, AmendmentText::Plain("§1".into()));
5756
5757 let mut c = Chain::new();
5758 c.decision(&key);
5759 let rotation = c.compromise(&pk, &generate_keypair().0, 1);
5760 let value = serde_json::to_value(&rotation).unwrap();
5761 assert_eq!(value["agora_governance_key_rotation"], 2);
5762 assert_eq!(value["reason"], "compromise");
5763 let statement = &value["certificate"]["statement"];
5764 assert_eq!(statement["purpose"], "compromise");
5765 assert_eq!(statement["last_trusted"]["chain_seq"], 1);
5766 assert_eq!(
5767 value["outgoing_certificate"]["statement"]["purpose"],
5768 "genesis"
5769 );
5770 assert!(value.get("note").is_none(), "{value}");
5771 assert_eq!(
5772 serde_json::from_value::<KeyRotation>(value).unwrap(),
5773 rotation
5774 );
5775
5776 let notice =
5777 AmendmentNotice::new(amd(1), at(0), &amendment, Some(&texts));
5778 let value = serde_json::to_value(¬ice).unwrap();
5779 assert_eq!(value["id"], "AMD-2026-0001");
5780 assert_eq!(value["note"], "superseded by GOV-2026-0009");
5781 assert_eq!(
5782 serde_json::from_value::<AmendmentNotice>(value).unwrap(),
5783 notice
5784 );
5785
5786 let erased = AmendmentTexts {
5788 rationale: None,
5789 ..texts.clone()
5790 };
5791 let notice =
5792 AmendmentNotice::new(amd(1), at(0), &amendment, Some(&erased));
5793 assert_eq!(notice.note, "superseded by GOV-2026-0009");
5794 assert_eq!(notice.rationale.as_deref(), Some(WITHHELD_TEXT));
5795 let notice = AmendmentNotice::new(amd(1), at(0), &amendment, None);
5796 assert_eq!(notice.basis, WITHHELD_TEXT);
5797 let notice = AmendmentNotice::new(amd(1), at(0), &legacy, None);
5798 assert_eq!(notice.note, "diagnostic finding");
5799 }
5800
5801 #[test]
5805 fn an_amendment_verifies_with_or_without_its_optional_fields() {
5806 let (key, pk) = generate_keypair();
5807 let mut c = Chain::new();
5808 let target = c.decision(&key);
5809 let bare = AmendmentDraft::new(
5810 target.clone(),
5811 c.hash_at(1),
5812 AmendmentKind::Correction,
5813 "clerical",
5814 "typo in the citation",
5815 )
5816 .unwrap();
5817 assert!(
5818 serde_json::to_value(&bare.amendment)
5819 .unwrap()
5820 .get("rationale")
5821 .is_none()
5822 );
5823 c.amend(&key, &bare);
5824 let full = bare.clone().with_rationale("at length: …");
5825 c.amend(&key, &full);
5826 let mut future = serde_json::to_value(&full.amendment).unwrap();
5828 future["superseded_by_something_new"] = json!(["later"]);
5829 c.push(
5830 &key,
5831 amd(3),
5832 GovernanceLogEntryType::Amendment,
5833 future,
5834 true,
5835 );
5836
5837 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
5838 assert!(v.ok, "{v:#?}");
5839 assert_eq!(
5840 v.entries[0].amended_by,
5841 vec![amd(1), amd(2), amd(3)],
5842 "all three name the target"
5843 );
5844 }
5845
5846 #[test]
5849 fn pre_0_26_wire_still_deserializes() {
5850 let link: GovernanceChainLink = serde_json::from_value(json!({
5851 "id": "GOV-2026-0001",
5852 "entry_type": "council_decision",
5853 "created_at": "2023-11-14T22:13:20.123456Z",
5854 "attestation": {
5855 "envelope_version": 1,
5856 "chain_seq": 1,
5857 "prev_hash": null,
5858 "data_hash": "00".repeat(32),
5859 "entry_hash": "11".repeat(32),
5860 "signature": "22".repeat(64),
5861 "signed_at": "2023-11-14T22:13:21Z",
5862 "retroactive": false,
5863 },
5864 }))
5865 .unwrap();
5866 assert!(link.data.is_none());
5867 assert!(
5868 serde_json::to_value(&link).unwrap().get("data").is_none(),
5869 "and a link without data does not grow a null field"
5870 );
5871
5872 let verdict: EntryVerdict = serde_json::from_value(json!({
5873 "id": "GOV-2026-0001",
5874 "chain_seq": 1,
5875 "signature_valid": true,
5876 "link_valid": true,
5877 "content_matches": null,
5878 "retroactive": false,
5879 "out_of_order": false,
5880 "amended_by": [],
5881 }))
5882 .unwrap();
5883 assert!(!verdict.repudiated && !verdict.redacted);
5884 assert!(verdict.signed_by.is_none());
5885 assert!(verdict.reattested_by.is_empty());
5886
5887 let verification: GovernanceVerification =
5888 serde_json::from_value(json!({
5889 "public_key": "33".repeat(32),
5890 "ok": true,
5891 "head": "GOV-2026-0001",
5892 "entries": [],
5893 }))
5894 .unwrap();
5895 assert!(verification.keys.is_empty());
5896 assert!(verification.unanchored_keys.is_empty());
5897 assert!(verification.repudiated.is_empty());
5898 }
5899
5900 #[test]
5901 fn the_report_round_trips() {
5902 let (key, pk) = generate_keypair();
5903 let mut c = Chain::new();
5904 let target = c.decision(&key);
5905 let amendment = AmendmentDraft::new(
5906 target,
5907 c.hash_at(1),
5908 AmendmentKind::Overruled,
5909 "b",
5910 "n",
5911 )
5912 .unwrap();
5913 c.amend(&key, &amendment);
5914 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
5915 let text = serde_json::to_string(&v).unwrap();
5916 assert_eq!(
5917 serde_json::from_str::<GovernanceVerification>(&text).unwrap(),
5918 v
5919 );
5920
5921 let keys = GovernanceSigningKeys { keys: v.keys };
5922 let value = serde_json::to_value(&keys).unwrap();
5923 assert!(value["keys"].is_array(), "an object, not a bare array");
5924 assert_eq!(value["keys"][0]["status"], "active");
5925 assert_eq!(
5926 serde_json::from_value::<GovernanceSigningKeys>(value).unwrap(),
5927 keys
5928 );
5929 }
5930
5931 #[test]
5935 fn envelope_v1_preimage_and_hash_are_pinned() {
5936 let envelope = Envelope::new(
5937 gov(6),
5938 GovernanceLogEntryType::CouncilDecision,
5939 at(0),
5940 Some(Sha256Hex::from([0x11; 32])),
5941 data_hash(&json!({"outcome": "approved", "title": "Ratification"})),
5942 );
5943 assert_eq!(
5944 String::from_utf8(envelope.preimage()).unwrap(),
5945 "{\"agora_governance_log\":1,\"id\":\"GOV-2026-0006\",\
5946 \"entry_type\":\"council_decision\",\
5947 \"created_at\":1700000000123456,\
5948 \"prev_hash\":\"1111111111111111111111111111111111111111111111111111111111111111\",\
5949 \"data_hash\":\"a4adf645ae3f60c56484d01aea87d6d490321d7fc66b1607df14b023fe567c7b\"}"
5950 );
5951 assert_eq!(
5952 envelope.entry_hash().to_hex(),
5953 "ba27577432f81e415f1c01cc4cfabab6070e3ac50fd468fffe195ef19c0e9464"
5954 );
5955 }
5956
5957 #[cfg(feature = "agora-client")]
5971 #[tokio::test]
5972 #[ignore = "networked: hits the live platform"]
5973 async fn the_published_key_is_the_one_the_platform_serves() {
5974 let client = crate::client::Client::new(
5975 url::Url::parse("https://subliminal.technology").unwrap(),
5976 )
5977 .unwrap();
5978 let served = client.get_governance_signing_key().await.unwrap();
5979 assert_eq!(served.algorithm, "ed25519");
5980
5981 let genesis: PublicKeyHex = PUBLISHED_KEYS
5982 .first()
5983 .expect("PUBLISHED_KEYS is never empty")
5984 .parse()
5985 .unwrap();
5986 let links = client.get_governance_chain().await.unwrap();
5987 let report = verify_chain(
5988 &links,
5989 &genesis.to_verifying_key().unwrap(),
5990 &KeyAnchor::published(),
5991 &RootSet::published(),
5992 );
5993 let problems: Vec<_> = report
5994 .entries
5995 .iter()
5996 .filter_map(|e| {
5997 e.problem.as_ref().map(|p| (e.id.clone(), p.clone()))
5998 })
5999 .collect();
6000 assert!(
6001 report.ok,
6002 "the live chain does not verify under this build's genesis key \
6003 and roots: {problems:#?}"
6004 );
6005 assert!(report.unanchored_keys.is_empty(), "{report:#?}");
6006 assert_eq!(
6007 served.public_key, report.public_key,
6008 "the platform serves {} but the chain, followed under the \
6009 published roots, is held by {}",
6010 served.public_key, report.public_key
6011 );
6012 }
6013
6014 #[cfg(feature = "schemars")]
6015 #[test]
6016 fn wire_schemas_are_ref_free() {
6017 use crate::responses::inline_schema_for;
6018 for (name, schema) in [
6019 (
6020 "GovernanceAttestation",
6021 inline_schema_for::<GovernanceAttestation>(),
6022 ),
6023 (
6024 "GovernanceChainLink",
6025 inline_schema_for::<GovernanceChainLink>(),
6026 ),
6027 (
6028 "GovernanceSigningKey",
6029 inline_schema_for::<GovernanceSigningKey>(),
6030 ),
6031 (
6032 "GovernanceVerification",
6033 inline_schema_for::<GovernanceVerification>(),
6034 ),
6035 (
6036 "Vec<GovernanceChainLink>",
6037 inline_schema_for::<Vec<GovernanceChainLink>>(),
6038 ),
6039 ("Amendment", inline_schema_for::<Amendment>()),
6040 ("Redaction", inline_schema_for::<Redaction>()),
6041 ("Revision", inline_schema_for::<Revision>()),
6042 ("AmendmentNotice", inline_schema_for::<AmendmentNotice>()),
6043 ("KeyRotation", inline_schema_for::<KeyRotation>()),
6044 ("TrustedHead", inline_schema_for::<TrustedHead>()),
6045 (
6046 "GovernanceKeyRecord",
6047 inline_schema_for::<GovernanceKeyRecord>(),
6048 ),
6049 (
6050 "GovernanceSigningKeys",
6051 inline_schema_for::<GovernanceSigningKeys>(),
6052 ),
6053 ("AmendmentKind", inline_schema_for::<AmendmentKind>()),
6054 ("Standing", inline_schema_for::<Standing>()),
6055 ("KeyStatus", inline_schema_for::<KeyStatus>()),
6056 ("RotationReason", inline_schema_for::<RotationReason>()),
6057 ] {
6058 let text = serde_json::to_string(&schema).unwrap();
6059 assert!(!text.contains("$ref"), "{name} must be $ref-free: {text}");
6060 assert!(
6061 !text.contains("$defs"),
6062 "{name} must be $defs-free: {text}"
6063 );
6064 }
6065 let text =
6066 serde_json::to_string(&inline_schema_for::<Sha256Hex>()).unwrap();
6067 assert!(text.contains("^[0-9a-f]{64}$"), "{text}");
6068 let text = serde_json::to_string(&inline_schema_for::<SignatureHex>())
6069 .unwrap();
6070 assert!(text.contains("^[0-9a-f]{128}$"), "{text}");
6071 }
6072}