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