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