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