1use crate::crypto::{self, Signature, SigningKey, VerifyingKey};
64use crate::enums::GovernanceLogEntryType;
65use crate::ids::{GovernanceLogId, GovernanceLogPrefix};
66use chrono::{DateTime, Utc};
67use serde::{Deserialize, Serialize};
68use sha2::{Digest, Sha256};
69use std::collections::{HashMap, HashSet};
70
71pub use crate::enums::{AmendmentKind, CitationRelation, KeyStatus, Standing};
72
73mod texts;
74pub use texts::{
75 AmendmentText, AmendmentTextStatus, AmendmentTexts, CommittedText,
76 TextCommitment, TextStatus, WITHHELD_TEXT,
77};
78
79mod council;
80pub use council::{
81 Abstention, AgendaRanking, Ballot, BlankStatement, CouncilAttachment,
82 CouncilDecisionRecord, CouncilMember, CouncilRound, CouncilSeat,
83 CouncilVote, Decided, DecisionCategory, DecisionSummary, FinalRoundError,
84 FinalVotes, NotASeatChoice, PlacedProposal, RECUSED_ACCEPTORS,
85 RECUSED_DEFERRED_TAG, Rationale, RecordRuleError, RecusalVote,
86 STEWARD_RECUSED_TAG, SUMMARY_POINTER, SUMMARY_WRITER, SeatChoice,
87 SeatModels, SeatRanking, SeatResponse, SeatVote, SeatVotes, Statement,
88 StewardEmergency, StewardRecusal, StewardVote, SummaryAcceptance,
89 SummaryAuthorship, Vote, Votes, served_summary,
90};
91
92mod redactable;
93pub use redactable::{REDACTION_MARKER_PATTERN, Redactable};
94
95pub mod reading;
96
97pub use json_patch;
100
101mod obligation;
102pub use obligation::request;
103pub mod quote;
104pub use obligation::{
105 Evidence, KnownEntry, OBLIGATION_VERSION, Obligation, ObligationData,
106 ObligationError, ObligationRuleError, ObligationState, ObligationUpdate,
107 Until,
108};
109
110mod data_set;
111pub use data_set::{
112 DATA_SET_VERSION, DataFile, DataFileMismatch, DataFilePath,
113 DataFilePathError, DataSet, DataSetLookupError, DataSetLookupFailure,
114 DataSetTarget, MAX_DATA_FILE_BYTES,
115};
116
117mod record;
118pub use record::{
119 RecordAttachment, RecordParticipant, STEWARD_RECORD_VERSION, StewardRecord,
120};
121
122mod root;
123pub use root::{
124 CertPurpose, CertificateError, KEY_CERT_VERSION, KeyCertStatement,
125 KeyCertificate, ROOT_DOMAIN, ROOT_KEYS, ROOT_THRESHOLD, RootSet,
126 RootSignature,
127};
128
129#[cfg(test)]
131mod vectors;
132
133pub const ENVELOPE_VERSION: u32 = 1;
135
136pub const RETROACTIVE_AFTER: chrono::Duration = chrono::Duration::seconds(60);
139
140#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
147#[error("{type_name}: expected {expected} bytes of hex, got {got:?}")]
148pub struct HexLengthError {
149 pub type_name: &'static str,
150 pub expected: usize,
151 pub got: String,
152}
153
154macro_rules! hex_bytes {
155 ($(#[$meta:meta])* $name:ident, $len:expr) => {
156 $(#[$meta])*
157 #[derive(Clone, Copy, PartialEq, Eq, Hash)]
158 #[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
159 #[cfg_attr(feature = "sqlx", sqlx(transparent))]
160 pub struct $name([u8; $len]);
161
162 impl $name {
163 pub fn as_bytes(&self) -> &[u8; $len] {
165 &self.0
166 }
167
168 pub fn to_hex(&self) -> String {
170 hex::encode(self.0)
171 }
172 }
173
174 impl From<[u8; $len]> for $name {
175 fn from(bytes: [u8; $len]) -> Self {
176 Self(bytes)
177 }
178 }
179
180 impl TryFrom<&[u8]> for $name {
181 type Error = HexLengthError;
182
183 fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
184 <[u8; $len]>::try_from(bytes).map(Self).map_err(|_| {
185 HexLengthError {
186 type_name: stringify!($name),
187 expected: $len,
188 got: hex::encode(bytes),
189 }
190 })
191 }
192 }
193
194 impl TryFrom<Vec<u8>> for $name {
195 type Error = HexLengthError;
196
197 fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
198 Self::try_from(bytes.as_slice())
199 }
200 }
201
202 impl std::str::FromStr for $name {
203 type Err = HexLengthError;
204
205 fn from_str(s: &str) -> Result<Self, Self::Err> {
206 let bytes = hex::decode(s.trim()).map_err(|_| HexLengthError {
207 type_name: stringify!($name),
208 expected: $len,
209 got: s.to_string(),
210 })?;
211 Self::try_from(bytes.as_slice())
212 }
213 }
214
215 impl std::fmt::Display for $name {
216 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217 f.write_str(&self.to_hex())
218 }
219 }
220
221 impl std::fmt::Debug for $name {
222 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223 write!(f, "{}({})", stringify!($name), self.to_hex())
224 }
225 }
226
227 impl Serialize for $name {
228 fn serialize<S: serde::Serializer>(
229 &self,
230 s: S,
231 ) -> Result<S::Ok, S::Error> {
232 s.serialize_str(&self.to_hex())
233 }
234 }
235
236 impl<'de> Deserialize<'de> for $name {
237 fn deserialize<D: serde::Deserializer<'de>>(
238 d: D,
239 ) -> Result<Self, D::Error> {
240 let s = String::deserialize(d)?;
241 s.parse().map_err(serde::de::Error::custom)
242 }
243 }
244
245 #[cfg(feature = "schemars")]
249 impl schemars::JsonSchema for $name {
250 fn inline_schema() -> bool {
251 true
252 }
253
254 fn schema_name() -> std::borrow::Cow<'static, str> {
255 std::borrow::Cow::Borrowed(stringify!($name))
256 }
257
258 fn schema_id() -> std::borrow::Cow<'static, str> {
259 std::borrow::Cow::Borrowed(concat!(
260 module_path!(),
261 "::",
262 stringify!($name)
263 ))
264 }
265
266 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
267 schemars::json_schema!({
268 "type": "string",
269 "pattern": format!("^[0-9a-f]{{{}}}$", $len * 2),
270 "description": format!("{} bytes, lowercase hex", $len),
271 })
272 }
273 }
274 };
275}
276
277hex_bytes!(
278 Sha256Hex,
280 32
281);
282
283hex_bytes!(
284 SignatureHex,
286 64
287);
288
289hex_bytes!(
290 PublicKeyHex,
292 32
293);
294
295hex_bytes!(
296 GitCommitId,
299 20
300);
301
302hex_bytes!(
303 Blind,
306 32
307);
308
309hex_bytes!(
310 TextSalt,
313 32
314);
315
316impl Blind {
317 pub fn random() -> Self {
319 use rand::RngCore;
320 let mut bytes = [0u8; 32];
321 rand::rngs::OsRng.fill_bytes(&mut bytes);
322 Self(bytes)
323 }
324}
325
326impl TextSalt {
327 pub fn random() -> Self {
329 Self(*Blind::random().as_bytes())
330 }
331}
332
333impl From<Signature> for SignatureHex {
334 fn from(sig: Signature) -> Self {
335 Self(sig.to_bytes())
336 }
337}
338
339impl From<&SignatureHex> for Signature {
340 fn from(sig: &SignatureHex) -> Self {
341 Signature::from_bytes(&sig.0)
342 }
343}
344
345impl From<&VerifyingKey> for PublicKeyHex {
346 fn from(key: &VerifyingKey) -> Self {
347 Self(key.to_bytes())
348 }
349}
350
351impl PublicKeyHex {
352 pub fn to_verifying_key(
354 &self,
355 ) -> Result<VerifyingKey, ed25519_dalek::SignatureError> {
356 VerifyingKey::from_bytes(&self.0)
357 }
358}
359
360pub fn canonical_json(value: &serde_json::Value) -> Vec<u8> {
373 let mut out = Vec::new();
374 write_canonical(value, &mut out);
375 out
376}
377
378fn write_canonical(value: &serde_json::Value, out: &mut Vec<u8>) {
379 use serde_json::Value;
380 match value {
381 Value::Null => out.extend_from_slice(b"null"),
382 Value::Bool(b) => {
383 out.extend_from_slice(if *b { b"true" } else { b"false" })
384 }
385 Value::Number(n) => serde_json::to_writer(&mut *out, n)
386 .expect("a number always serializes"),
387 Value::String(s) => serde_json::to_writer(&mut *out, s)
388 .expect("a string always serializes"),
389 Value::Array(items) => {
390 out.push(b'[');
391 for (i, item) in items.iter().enumerate() {
392 if i > 0 {
393 out.push(b',');
394 }
395 write_canonical(item, out);
396 }
397 out.push(b']');
398 }
399 Value::Object(map) => {
400 let mut keys: Vec<&String> = map.keys().collect();
401 keys.sort_unstable();
402 out.push(b'{');
403 for (i, key) in keys.into_iter().enumerate() {
404 if i > 0 {
405 out.push(b',');
406 }
407 serde_json::to_writer(&mut *out, key)
408 .expect("a string always serializes");
409 out.push(b':');
410 write_canonical(&map[key], out);
411 }
412 out.push(b'}');
413 }
414 }
415}
416
417pub fn non_integer_number(data: &serde_json::Value) -> Option<String> {
428 fn find(value: &serde_json::Value, path: &mut String) -> bool {
429 use serde_json::Value::{Array, Number, Object};
430 let mark = path.len();
431 match value {
432 Number(n) => return n.is_f64(),
433 Array(items) => {
434 for (i, item) in items.iter().enumerate() {
435 path.push_str(&format!("/{i}"));
436 if find(item, path) {
437 return true;
438 }
439 path.truncate(mark);
440 }
441 }
442 Object(map) => {
443 for (key, item) in map {
444 path.push('/');
445 path.push_str(&escape_pointer_token(key));
446 if find(item, path) {
447 return true;
448 }
449 path.truncate(mark);
450 }
451 }
452 _ => {}
453 }
454 false
455 }
456 let mut path = String::new();
457 find(data, &mut path).then_some(path)
458}
459
460pub fn escape_pointer_token(token: &str) -> String {
464 token.replace('~', "~0").replace('/', "~1")
465}
466
467pub fn unescape_pointer_token(token: &str) -> String {
470 token.replace("~1", "/").replace("~0", "~")
471}
472
473pub fn pointer_tokens(pointer: &str) -> Vec<String> {
476 pointer
477 .split('/')
478 .skip(1)
479 .map(unescape_pointer_token)
480 .collect()
481}
482
483pub fn data_hash(data: &serde_json::Value) -> Sha256Hex {
485 Sha256Hex(Sha256::digest(canonical_json(data)).into())
486}
487
488#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
493pub struct Envelope {
494 pub agora_governance_log: u32,
496 pub id: GovernanceLogId,
497 pub entry_type: GovernanceLogEntryType,
498 pub created_at: i64,
500 pub prev_hash: Option<Sha256Hex>,
501 pub data_hash: Sha256Hex,
502}
503
504impl Envelope {
505 pub fn new(
507 id: GovernanceLogId,
508 entry_type: GovernanceLogEntryType,
509 created_at: DateTime<Utc>,
510 prev_hash: Option<Sha256Hex>,
511 data_hash: Sha256Hex,
512 ) -> Self {
513 Self {
514 agora_governance_log: ENVELOPE_VERSION,
515 id,
516 entry_type,
517 created_at: created_at.timestamp_micros(),
518 prev_hash,
519 data_hash,
520 }
521 }
522
523 pub fn created_at(&self) -> DateTime<Utc> {
525 DateTime::from_timestamp_micros(self.created_at)
526 .expect("an Envelope only ever holds an in-range timestamp")
527 }
528
529 pub fn preimage(&self) -> Vec<u8> {
531 serde_json::to_vec(self).expect("an Envelope always serializes")
532 }
533
534 pub fn entry_hash(&self) -> Sha256Hex {
536 Sha256Hex(Sha256::digest(self.preimage()).into())
537 }
538}
539
540pub fn truncate_to_micros(t: DateTime<Utc>) -> DateTime<Utc> {
543 DateTime::from_timestamp_micros(t.timestamp_micros())
544 .expect("a timestamp that came from a DateTime is in range")
545}
546
547pub fn truncate_to_seconds(t: DateTime<Utc>) -> DateTime<Utc> {
550 DateTime::from_timestamp(t.timestamp(), 0)
551 .expect("a timestamp that came from a DateTime is in range")
552}
553
554pub fn is_retroactive(
558 created_at: DateTime<Utc>,
559 signed_at: DateTime<Utc>,
560) -> bool {
561 signed_at - created_at > RETROACTIVE_AFTER
562}
563
564#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
570#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
571#[cfg_attr(feature = "schemars", schemars(inline))]
572pub struct GovernanceAttestation {
573 pub envelope_version: u32,
575 pub chain_seq: u64,
578 pub prev_hash: Option<Sha256Hex>,
580 pub data_hash: Sha256Hex,
582 pub entry_hash: Sha256Hex,
584 pub signature: SignatureHex,
587 pub signed_at: DateTime<Utc>,
590 pub retroactive: bool,
594}
595
596#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
600#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
601#[cfg_attr(feature = "schemars", schemars(inline))]
602pub struct GovernanceChainLink {
603 pub id: GovernanceLogId,
604 pub entry_type: GovernanceLogEntryType,
605 pub created_at: DateTime<Utc>,
606 pub attestation: GovernanceAttestation,
607 #[serde(default, skip_serializing_if = "Option::is_none")]
614 pub data: Option<serde_json::Value>,
615 #[serde(
619 default,
620 skip_serializing_if = "Option::is_none",
621 deserialize_with = "read_as_written"
622 )]
623 pub texts: Option<AmendmentTexts>,
624}
625
626#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
629#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
630#[cfg_attr(feature = "schemars", schemars(inline))]
631pub struct GovernanceSigningKey {
632 pub algorithm: String,
634 pub public_key: PublicKeyHex,
635 pub envelope_version: u32,
637}
638
639impl GovernanceSigningKey {
640 pub fn new(key: &VerifyingKey) -> Self {
642 Self {
643 algorithm: "ed25519".to_string(),
644 public_key: key.into(),
645 envelope_version: ENVELOPE_VERSION,
646 }
647 }
648}
649
650pub const AMENDMENT_VERSION: u32 = 2;
658
659#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
661pub enum AmendmentError {
662 #[error("agora_governance_amendment is {0}, not 1 or {AMENDMENT_VERSION}")]
663 UnsupportedVersion(u32),
664 #[error(
665 "a version 1 amendment carries its texts and a version \
666 {AMENDMENT_VERSION} one commits to them; this does neither \
667 consistently"
668 )]
669 TextShape,
670 #[error("`{0}` beside the entry is not the text the entry committed to")]
671 TextMismatch(&'static str),
672 #[error("texts beside an entry that commits to none")]
673 UncommittedText,
674 #[error("kind `redaction` requires a `redaction`")]
675 MissingRedaction,
676 #[error("`redaction` is only valid on kind `redaction`")]
677 UnexpectedRedaction,
678 #[error("kind `revision` requires a `revision`")]
679 MissingRevision,
680 #[error("`revision` is only valid on kind `revision`")]
681 UnexpectedRevision,
682 #[error("the `revision` is malformed: {0}")]
683 RevisionShape(ReviseError),
684 #[error("kind `{0}` requires an `update`")]
685 MissingUpdate(AmendmentKind),
686 #[error("`update` is only valid on the obligation kinds")]
687 UnexpectedUpdate,
688 #[error("an `update` of kind `{update}` on an amendment of kind `{kind}`")]
689 UpdateMismatch {
690 kind: AmendmentKind,
691 update: AmendmentKind,
692 },
693 #[error(
694 "kind `{kind}` is only valid on an obligation, and {target} is not one"
695 )]
696 NotAnObligation {
697 kind: AmendmentKind,
698 target: GovernanceLogId,
699 },
700 #[error(
701 "kind `{0}` changes precedential force, which an obligation does not have"
702 )]
703 PrecedentialOnObligation(AmendmentKind),
704 #[error(
705 "a revision of an obligation may not touch `{0}`; its status and \
706 deadline change only through the obligation kinds"
707 )]
708 FixedObligationField(String),
709 #[error("amendment target {0} is not an entry of this chain")]
710 UnknownTarget(GovernanceLogId),
711 #[error("amendment target {0} is not an earlier entry")]
712 ForwardReference(GovernanceLogId),
713 #[error("target_entry_hash is not {0}'s entry_hash")]
714 WrongTargetHash(GovernanceLogId),
715}
716
717#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
723#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
724#[cfg_attr(feature = "schemars", schemars(inline))]
725pub struct Amendment {
726 pub agora_governance_amendment: u32,
728 pub target: GovernanceLogId,
729 pub target_entry_hash: Sha256Hex,
731 pub kind: AmendmentKind,
732 #[serde(default)]
735 pub authority: Option<GovernanceLogId>,
736 pub basis: AmendmentText,
740 pub note: AmendmentText,
742 #[serde(default, skip_serializing_if = "Option::is_none")]
744 pub rationale: Option<AmendmentText>,
745 #[serde(default, skip_serializing_if = "Option::is_none")]
747 pub redaction: Option<Redaction>,
748 #[serde(default, skip_serializing_if = "Option::is_none")]
750 pub revision: Option<Revision>,
751 #[serde(default, skip_serializing_if = "Option::is_none")]
754 pub update: Option<ObligationUpdate>,
755}
756
757#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
759#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
760#[cfg_attr(feature = "schemars", schemars(inline))]
761pub struct Redaction {
762 pub fields: Vec<String>,
765 pub resulting_data_hash: Sha256Hex,
768 #[serde(default, skip_serializing_if = "Option::is_none")]
772 pub resulting_latest_hash: Option<Sha256Hex>,
773}
774
775#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
787pub struct Revision {
788 pub patch: json_patch::Patch,
790 #[serde(default, skip_serializing_if = "Vec::is_empty")]
794 pub duplicates: Vec<(String, String)>,
795 pub resulting_data_hash: Sha256Hex,
797}
798
799#[derive(Debug, Clone, PartialEq, Eq, Default)]
802pub struct Edit {
803 pub patch: json_patch::Patch,
804 pub duplicates: Vec<(String, String)>,
806}
807
808impl From<json_patch::Patch> for Edit {
809 fn from(patch: json_patch::Patch) -> Self {
810 Self {
811 patch,
812 duplicates: Vec::new(),
813 }
814 }
815}
816
817#[cfg(feature = "schemars")]
820impl schemars::JsonSchema for Revision {
821 fn inline_schema() -> bool {
822 true
823 }
824
825 fn schema_name() -> std::borrow::Cow<'static, str> {
826 "Revision".into()
827 }
828
829 fn json_schema(
830 generator: &mut schemars::SchemaGenerator,
831 ) -> schemars::Schema {
832 let hash = generator.subschema_for::<Sha256Hex>();
833 schemars::json_schema!({
834 "type": "object",
835 "properties": {
836 "patch": {
837 "description": "RFC 6902 JSON Patch from the previous version to this one",
838 "type": "array",
839 "items": {
840 "type": "object",
841 "properties": {
842 "op": {
843 "type": "string",
844 "enum": ["add", "remove", "replace", "move", "copy", "test"]
845 },
846 "path": {"type": "string"},
847 "from": {"type": "string"},
848 "value": true
849 },
850 "required": ["op", "path"]
851 }
852 },
853 "duplicates": {
854 "description": "(removed path, the path it duplicated) for each duplicate removed",
855 "type": "array",
856 "items": {
857 "type": "array",
858 "items": {"type": "string"},
859 "minItems": 2,
860 "maxItems": 2
861 }
862 },
863 "resulting_data_hash": hash
864 },
865 "required": ["patch", "resulting_data_hash"]
866 })
867 }
868}
869
870impl Revision {
871 pub fn validate(&self) -> Result<(), ReviseError> {
874 if self.patch.is_empty() {
875 return Err(ReviseError::EmptyPatch);
876 }
877 for (path, _) in &self.duplicates {
878 let removed = self.patch.iter().any(|op| {
879 matches!(op, json_patch::PatchOperation::Remove(r) if r.path.as_str() == path)
880 });
881 if !removed {
882 return Err(ReviseError::NotRemoved(path.clone()));
883 }
884 }
885 Ok(())
886 }
887
888 pub fn remove_duplicates(
892 data: &serde_json::Value,
893 pairs: &[(&str, &str)],
894 ) -> Result<Edit, ReviseError> {
895 let mut ops = Vec::with_capacity(pairs.len());
896 for (path, _) in pairs {
897 let path = json_patch::jsonptr::PointerBuf::parse(*path)
898 .map_err(|e| ReviseError::Patch(e.to_string()))?;
899 ops.push(json_patch::PatchOperation::Remove(
900 json_patch::RemoveOperation { path },
901 ));
902 }
903 let edit = Edit {
904 patch: json_patch::Patch(ops),
905 duplicates: pairs
906 .iter()
907 .map(|(p, s)| (p.to_string(), s.to_string()))
908 .collect(),
909 };
910 check_duplicates(data, &edit)?;
911 Ok(edit)
912 }
913}
914
915fn check_duplicates(
918 data: &serde_json::Value,
919 edit: &Edit,
920) -> Result<(), ReviseError> {
921 for (path, same_as) in &edit.duplicates {
922 let resolve = |pointer: &str| {
923 data.pointer(pointer)
924 .ok_or_else(|| ReviseError::Unresolved(pointer.to_string()))
925 };
926 let (removed, kept) = (resolve(path)?, resolve(same_as)?);
927 if canonical_json(removed) != canonical_json(kept) {
928 return Err(ReviseError::NotIdentical {
929 path: path.clone(),
930 same_as: same_as.clone(),
931 });
932 }
933 }
934 let revised = apply_patch(data, &edit.patch)?;
935 for (path, same_as) in &edit.duplicates {
936 let kept = data.pointer(same_as);
937 let removes = |op: &json_patch::PatchOperation| matches!(op, json_patch::PatchOperation::Remove(r) if r.path.as_str() == path);
938 if !edit.patch.iter().any(removes) {
939 return Err(ReviseError::NotRemoved(path.clone()));
940 }
941 let survives = revised.pointer(same_as).is_some_and(|v| {
942 kept.is_some_and(|k| canonical_json(v) == canonical_json(k))
943 });
944 if !survives {
945 return Err(ReviseError::SourceRemoved {
946 path: path.clone(),
947 same_as: same_as.clone(),
948 });
949 }
950 }
951 Ok(())
952}
953
954#[derive(Debug, Clone, PartialEq, Eq)]
957pub struct AmendmentDraft {
958 pub amendment: Amendment,
959 pub texts: AmendmentTexts,
960}
961
962impl AmendmentDraft {
963 pub fn new(
970 target: GovernanceLogId,
971 target_entry_hash: Sha256Hex,
972 kind: AmendmentKind,
973 basis: impl Into<CommittedText>,
974 note: impl Into<CommittedText>,
975 ) -> Result<Self, AmendmentError> {
976 match kind {
977 AmendmentKind::Redaction => {
978 return Err(AmendmentError::MissingRedaction);
979 }
980 AmendmentKind::Revision => {
981 return Err(AmendmentError::MissingRevision);
982 }
983 k if k.is_obligation() => {
984 return Err(AmendmentError::MissingUpdate(k));
985 }
986 _ => {}
987 }
988 let (basis, note) = (basis.into(), note.into());
989 Ok(Self {
990 amendment: Amendment {
991 agora_governance_amendment: AMENDMENT_VERSION,
992 target,
993 target_entry_hash,
994 kind,
995 authority: None,
996 basis: AmendmentText::Committed(basis.commitment()),
997 note: AmendmentText::Committed(note.commitment()),
998 rationale: None,
999 redaction: None,
1000 revision: None,
1001 update: None,
1002 },
1003 texts: AmendmentTexts {
1004 basis: Some(basis),
1005 note: Some(note),
1006 rationale: None,
1007 },
1008 })
1009 }
1010
1011 #[allow(clippy::too_many_arguments)]
1029 pub fn redaction(
1030 amendment_id: &GovernanceLogId,
1031 target: GovernanceLogId,
1032 target_entry_hash: Sha256Hex,
1033 basis: impl Into<CommittedText>,
1034 note: impl Into<CommittedText>,
1035 mut fields: Vec<String>,
1036 data: &serde_json::Value,
1037 blind: Blind,
1038 revisions: &[&Revision],
1039 ) -> Result<(Self, serde_json::Value), RedactError> {
1040 for extra in duplicates_of(&fields, revisions) {
1041 let covered = fields.iter().any(|f| {
1042 extra.strip_prefix(f.as_str()).is_some_and(|rest| {
1043 rest.is_empty() || rest.starts_with('/')
1044 })
1045 });
1046 if !covered && data.pointer(&extra).is_some() {
1047 fields.push(extra);
1048 }
1049 }
1050 let redacted = redact_data(data, &fields, amendment_id, blind)?;
1051 let resulting_latest_hash = match revisions {
1052 [] => None,
1053 _ => Some(data_hash(
1054 &latest(&redacted, revisions.iter().copied())
1055 .map_err(|e| RedactError::Rebase(e.to_string()))?,
1056 )),
1057 };
1058 let (basis, note) = (basis.into(), note.into());
1059 Ok((
1060 Self {
1061 amendment: Amendment {
1062 agora_governance_amendment: AMENDMENT_VERSION,
1063 target,
1064 target_entry_hash,
1065 kind: AmendmentKind::Redaction,
1066 authority: None,
1067 basis: AmendmentText::Committed(basis.commitment()),
1068 note: AmendmentText::Committed(note.commitment()),
1069 rationale: None,
1070 redaction: Some(Redaction {
1071 fields,
1072 resulting_data_hash: data_hash(&redacted),
1073 resulting_latest_hash,
1074 }),
1075 revision: None,
1076 update: None,
1077 },
1078 texts: AmendmentTexts {
1079 basis: Some(basis),
1080 note: Some(note),
1081 rationale: None,
1082 },
1083 },
1084 redacted,
1085 ))
1086 }
1087
1088 pub fn revision(
1099 target: GovernanceLogId,
1100 target_type: GovernanceLogEntryType,
1101 target_entry_hash: Sha256Hex,
1102 basis: impl Into<CommittedText>,
1103 note: impl Into<CommittedText>,
1104 latest: &serde_json::Value,
1105 edit: impl Into<Edit>,
1106 ) -> Result<(Self, serde_json::Value), ReviseError> {
1107 let Edit {
1108 mut patch,
1109 duplicates,
1110 } = edit.into();
1111 if !is_revisable(target_type) {
1112 return Err(ReviseError::NotRevisable(target_type));
1113 }
1114 if patch.is_empty() {
1115 return Err(ReviseError::EmptyPatch);
1116 }
1117 let blind_pointer = format!("/{BLIND_KEY}");
1118 for op in patch.iter() {
1119 let from = match op {
1120 json_patch::PatchOperation::Move(m) => Some(m.from.as_str()),
1121 json_patch::PatchOperation::Copy(c) => Some(c.from.as_str()),
1122 _ => None,
1123 };
1124 for pointer in
1125 [Some(op.path().as_str()), from].into_iter().flatten()
1126 {
1127 if overlaps(pointer, &blind_pointer) {
1128 return Err(ReviseError::BlindPointer(pointer.to_string()));
1129 }
1130 }
1131 }
1132 let adds = patch.iter().any(|op| {
1133 matches!(
1134 op,
1135 json_patch::PatchOperation::Add(_)
1136 | json_patch::PatchOperation::Replace(_)
1137 )
1138 });
1139 if adds && latest.is_object() && latest.get(BLIND_KEY).is_none() {
1140 patch.0.push(json_patch::PatchOperation::Add(
1141 json_patch::AddOperation {
1142 path: json_patch::jsonptr::PointerBuf::from_tokens([
1143 BLIND_KEY,
1144 ]),
1145 value: Blind::random().to_hex().into(),
1146 },
1147 ));
1148 }
1149 let edit = Edit { patch, duplicates };
1150 check_duplicates(latest, &edit)?;
1151 let Edit { patch, duplicates } = edit;
1152 let revised = apply_patch(latest, &patch)?;
1153 let (basis, note) = (basis.into(), note.into());
1154 Ok((
1155 Self {
1156 amendment: Amendment {
1157 agora_governance_amendment: AMENDMENT_VERSION,
1158 target,
1159 target_entry_hash,
1160 kind: AmendmentKind::Revision,
1161 authority: None,
1162 basis: AmendmentText::Committed(basis.commitment()),
1163 note: AmendmentText::Committed(note.commitment()),
1164 rationale: None,
1165 redaction: None,
1166 revision: Some(Revision {
1167 patch,
1168 duplicates,
1169 resulting_data_hash: data_hash(&revised),
1170 }),
1171 update: None,
1172 },
1173 texts: AmendmentTexts {
1174 basis: Some(basis),
1175 note: Some(note),
1176 rationale: None,
1177 },
1178 },
1179 revised,
1180 ))
1181 }
1182
1183 pub fn obligation(
1185 target: GovernanceLogId,
1186 target_entry_hash: Sha256Hex,
1187 update: ObligationUpdate,
1188 basis: impl Into<CommittedText>,
1189 note: impl Into<CommittedText>,
1190 ) -> Self {
1191 let (basis, note) = (basis.into(), note.into());
1192 Self {
1193 amendment: Amendment {
1194 agora_governance_amendment: AMENDMENT_VERSION,
1195 target,
1196 target_entry_hash,
1197 kind: update.kind(),
1198 authority: None,
1199 basis: AmendmentText::Committed(basis.commitment()),
1200 note: AmendmentText::Committed(note.commitment()),
1201 rationale: None,
1202 redaction: None,
1203 revision: None,
1204 update: Some(update),
1205 },
1206 texts: AmendmentTexts {
1207 basis: Some(basis),
1208 note: Some(note),
1209 rationale: None,
1210 },
1211 }
1212 }
1213
1214 pub fn with_authority(mut self, authority: GovernanceLogId) -> Self {
1216 self.amendment.authority = Some(authority);
1217 self
1218 }
1219
1220 pub fn with_rationale(
1222 mut self,
1223 rationale: impl Into<CommittedText>,
1224 ) -> Self {
1225 let rationale = rationale.into();
1226 self.amendment.rationale =
1227 Some(AmendmentText::Committed(rationale.commitment()));
1228 self.texts.rationale = Some(rationale);
1229 self
1230 }
1231}
1232
1233impl Amendment {
1234 pub fn validate(&self) -> Result<(), AmendmentError> {
1238 let plain = match self.agora_governance_amendment {
1239 1 => true,
1240 AMENDMENT_VERSION => false,
1241 other => return Err(AmendmentError::UnsupportedVersion(other)),
1242 };
1243 let texts =
1244 [Some(&self.basis), Some(&self.note), self.rationale.as_ref()];
1245 if texts.into_iter().flatten().any(|t| t.is_plain() != plain) {
1246 return Err(AmendmentError::TextShape);
1247 }
1248 match (self.kind, &self.redaction) {
1249 (AmendmentKind::Redaction, None) => {
1250 return Err(AmendmentError::MissingRedaction);
1251 }
1252 (k, Some(_)) if k != AmendmentKind::Redaction => {
1253 return Err(AmendmentError::UnexpectedRedaction);
1254 }
1255 _ => {}
1256 }
1257 match (self.kind, &self.revision) {
1258 (AmendmentKind::Revision, None) => {
1259 return Err(AmendmentError::MissingRevision);
1260 }
1261 (AmendmentKind::Revision, Some(r)) => {
1262 r.validate().map_err(AmendmentError::RevisionShape)?;
1263 }
1264 (_, Some(_)) => return Err(AmendmentError::UnexpectedRevision),
1265 _ => {}
1266 }
1267 match (self.kind, &self.update) {
1268 (kind, None) if kind.is_obligation() => {
1269 Err(AmendmentError::MissingUpdate(kind))
1270 }
1271 (kind, Some(_)) if !kind.is_obligation() => {
1272 Err(AmendmentError::UnexpectedUpdate)
1273 }
1274 (kind, Some(update)) if update.kind() != kind => {
1275 Err(AmendmentError::UpdateMismatch {
1276 kind,
1277 update: update.kind(),
1278 })
1279 }
1280 _ => Ok(()),
1281 }
1282 }
1283
1284 pub fn text_status(
1291 &self,
1292 beside: Option<&AmendmentTexts>,
1293 ) -> Result<Option<AmendmentTextStatus>, AmendmentError> {
1294 let empty = AmendmentTexts::default();
1295 let beside = beside.unwrap_or(&empty);
1296 let (Some(basis), Some(note)) = (
1297 self.basis.status(beside.basis.as_ref()),
1298 self.note.status(beside.note.as_ref()),
1299 ) else {
1300 return if beside.is_empty() {
1301 Ok(None)
1302 } else {
1303 Err(AmendmentError::UncommittedText)
1304 };
1305 };
1306 let rationale = match (&self.rationale, &beside.rationale) {
1307 (None, Some(_)) => return Err(AmendmentError::UncommittedText),
1308 (None, None) => None,
1309 (Some(text), beside) => text.status(beside.as_ref()),
1310 };
1311 for (name, status) in [
1312 ("basis", Some(basis)),
1313 ("note", Some(note)),
1314 ("rationale", rationale),
1315 ] {
1316 if status == Some(TextStatus::Mismatch) {
1317 return Err(AmendmentError::TextMismatch(name));
1318 }
1319 }
1320 Ok(Some(AmendmentTextStatus {
1321 basis,
1322 note,
1323 rationale,
1324 }))
1325 }
1326}
1327
1328pub fn kind_standing(kind: AmendmentKind) -> Option<Standing> {
1331 match kind {
1332 AmendmentKind::NonPrecedential => Some(Standing::NonPrecedential),
1333 AmendmentKind::Overruled => Some(Standing::Overruled),
1334 AmendmentKind::Superseded => Some(Standing::Superseded),
1335 AmendmentKind::Reinstated => Some(Standing::InForce),
1336 AmendmentKind::Correction
1337 | AmendmentKind::Redaction
1338 | AmendmentKind::Reattested
1339 | AmendmentKind::Revision
1340 | AmendmentKind::Planned
1341 | AmendmentKind::Discharged
1342 | AmendmentKind::Lapsed
1343 | AmendmentKind::Rescheduled
1344 | AmendmentKind::Withdrawn => None,
1345 }
1346}
1347
1348pub fn standing(kinds: impl IntoIterator<Item = AmendmentKind>) -> Standing {
1351 kinds
1352 .into_iter()
1353 .filter_map(kind_standing)
1354 .last()
1355 .unwrap_or_default()
1356}
1357
1358pub const BLIND_KEY: &str = "_blind";
1361
1362pub fn is_redactable(entry_type: GovernanceLogEntryType) -> bool {
1366 !matches!(
1367 entry_type,
1368 GovernanceLogEntryType::Amendment | GovernanceLogEntryType::KeyRotation
1369 )
1370}
1371
1372#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1374pub enum BlindError {
1375 #[error("a redactable entry's data must be a JSON object")]
1376 NotAnObject,
1377 #[error(
1378 "data already has a {BLIND_KEY:?} key; the writer supplies it, not the caller"
1379 )]
1380 AlreadyBlinded,
1381 #[error(
1382 "{0:?} is a number that is not a 64-bit integer; governance data \
1383 never contains one (put a fraction in a string)"
1384 )]
1385 NonIntegerNumber(String),
1386}
1387
1388pub fn blind_data(
1406 data: &serde_json::Value,
1407 blind: Blind,
1408) -> Result<serde_json::Value, BlindError> {
1409 if let Some(pointer) = non_integer_number(data) {
1410 return Err(BlindError::NonIntegerNumber(pointer));
1411 }
1412 let mut out = data.clone();
1413 let object = out.as_object_mut().ok_or(BlindError::NotAnObject)?;
1414 if object.contains_key(BLIND_KEY) {
1415 return Err(BlindError::AlreadyBlinded);
1416 }
1417 object.insert(BLIND_KEY.to_string(), blind.to_hex().into());
1418 Ok(out)
1419}
1420
1421#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1423pub enum RedactError {
1424 #[error("pointer {0:?} does not resolve in the entry's data")]
1425 Unresolved(String),
1426 #[error("the empty pointer would redact the whole entry")]
1427 WholeEntry,
1428 #[error("a redaction names at least one pointer")]
1429 NoFields,
1430 #[error("{0:?} is the entry's blind; every redaction replaces it already")]
1431 BlindPointer(String),
1432 #[error("a revision no longer applies to the redacted data: {0}")]
1433 Rebase(String),
1434 #[error(
1435 "{0:?} is a number that is not a 64-bit integer; governance data \
1436 never contains one"
1437 )]
1438 NonIntegerNumber(String),
1439}
1440
1441pub fn redaction_marker(amendment_id: &GovernanceLogId) -> String {
1443 format!("[redacted by {amendment_id}]")
1444}
1445
1446pub fn redact_data(
1462 data: &serde_json::Value,
1463 fields: &[String],
1464 amendment_id: &GovernanceLogId,
1465 blind: Blind,
1466) -> Result<serde_json::Value, RedactError> {
1467 if fields.is_empty() {
1468 return Err(RedactError::NoFields);
1469 }
1470 if let Some(pointer) = non_integer_number(data) {
1471 return Err(RedactError::NonIntegerNumber(pointer));
1472 }
1473 let blind_pointer = format!("/{BLIND_KEY}");
1474 let marker = serde_json::Value::String(redaction_marker(amendment_id));
1475 let mut out = data.clone();
1476 for pointer in fields {
1477 if pointer.is_empty() {
1478 return Err(RedactError::WholeEntry);
1479 }
1480 if *pointer == blind_pointer {
1481 return Err(RedactError::BlindPointer(pointer.clone()));
1482 }
1483 let slot = out
1484 .pointer_mut(pointer)
1485 .ok_or_else(|| RedactError::Unresolved(pointer.clone()))?;
1486 *slot = marker.clone();
1487 }
1488 if let Some(object) = out.as_object_mut() {
1491 object.insert(BLIND_KEY.to_string(), blind.to_hex().into());
1492 }
1493 Ok(out)
1494}
1495
1496pub fn is_revisable(entry_type: GovernanceLogEntryType) -> bool {
1500 is_redactable(entry_type)
1501 && entry_type != GovernanceLogEntryType::StewardRecord
1502}
1503
1504#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1506pub enum ReviseError {
1507 #[error("a revision's patch has at least one op")]
1508 EmptyPatch,
1509 #[error("the patch does not apply: {0}")]
1510 Patch(String),
1511 #[error("{0:?} is the entry's blind, which a revision never touches")]
1512 BlindPointer(String),
1513 #[error("pointer {0:?} does not resolve")]
1514 Unresolved(String),
1515 #[error("{path:?} is not byte-identical to {same_as:?}")]
1516 NotIdentical { path: String, same_as: String },
1517 #[error("{same_as:?}, which {path:?} duplicates, has to survive the patch")]
1518 SourceRemoved { path: String, same_as: String },
1519 #[error("{0:?} is listed as a duplicate the patch does not remove")]
1520 NotRemoved(String),
1521 #[error("{0} entries are never revised")]
1522 NotRevisable(GovernanceLogEntryType),
1523 #[error(
1524 "{0:?} is a number that is not a 64-bit integer; governance data \
1525 never contains one"
1526 )]
1527 NonIntegerNumber(String),
1528}
1529
1530fn overlaps(a: &str, b: &str) -> bool {
1532 let within = |inner: &str, outer: &str| {
1533 inner
1534 .strip_prefix(outer)
1535 .is_some_and(|rest| rest.is_empty() || rest.starts_with('/'))
1536 };
1537 within(a, b) || within(b, a)
1538}
1539
1540pub fn apply_patch(
1542 data: &serde_json::Value,
1543 patch: &json_patch::Patch,
1544) -> Result<serde_json::Value, ReviseError> {
1545 let mut out = data.clone();
1546 json_patch::patch(&mut out, patch)
1547 .map_err(|e| ReviseError::Patch(e.to_string()))?;
1548 if let Some(pointer) = non_integer_number(&out) {
1549 return Err(ReviseError::NonIntegerNumber(pointer));
1550 }
1551 Ok(out)
1552}
1553
1554pub fn latest<'a>(
1559 data: &serde_json::Value,
1560 revisions: impl IntoIterator<Item = &'a Revision>,
1561) -> Result<serde_json::Value, ReviseError> {
1562 let mut current = data.clone();
1563 for revision in revisions {
1564 current = apply_patch(¤t, &revision.patch)?;
1565 }
1566 Ok(current)
1567}
1568
1569fn duplicates_of(fields: &[String], revisions: &[&Revision]) -> Vec<String> {
1573 let mut extra = Vec::new();
1574 for (removed, same_as) in revisions.iter().flat_map(|r| &r.duplicates) {
1575 for field in fields {
1576 if let Some(rest) = field.strip_prefix(same_as.as_str())
1577 && (rest.is_empty() || rest.starts_with('/'))
1578 {
1579 extra.push(format!("{removed}{rest}"));
1581 } else if overlaps(field, same_as) {
1582 extra.push(removed.clone());
1584 }
1585 }
1586 }
1587 extra
1588}
1589
1590#[derive(Debug, Clone, PartialEq, Eq, Default)]
1593pub struct RevisionHistory {
1594 pub revisions: Vec<(GovernanceLogId, Revision)>,
1596 pub rebased: usize,
1599 pub rebased_hash: Option<Sha256Hex>,
1601}
1602
1603#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1605#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1606#[cfg_attr(feature = "schemars", schemars(inline))]
1607pub struct AmendmentNotice {
1608 pub id: GovernanceLogId,
1609 pub kind: AmendmentKind,
1610 #[serde(default)]
1611 pub authority: Option<GovernanceLogId>,
1612 pub basis: String,
1613 pub note: String,
1614 #[serde(default, skip_serializing_if = "Option::is_none")]
1616 pub rationale: Option<String>,
1617 #[serde(default, skip_serializing_if = "Option::is_none")]
1619 pub update: Option<ObligationUpdate>,
1620 pub created_at: DateTime<Utc>,
1621}
1622
1623impl AmendmentNotice {
1624 pub fn new(
1627 id: GovernanceLogId,
1628 created_at: DateTime<Utc>,
1629 amendment: &Amendment,
1630 beside: Option<&AmendmentTexts>,
1631 ) -> Self {
1632 let beside = beside.cloned().unwrap_or_default();
1633 Self {
1634 id,
1635 kind: amendment.kind,
1636 authority: amendment.authority.clone(),
1637 basis: amendment.basis.resolve(beside.basis.as_ref()).into(),
1638 note: amendment.note.resolve(beside.note.as_ref()).into(),
1639 rationale: amendment
1640 .rationale
1641 .as_ref()
1642 .map(|r| r.resolve(beside.rationale.as_ref()).into()),
1643 update: amendment.update.clone(),
1644 created_at,
1645 }
1646 }
1647}
1648
1649#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1655#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1656#[cfg_attr(feature = "schemars", schemars(inline))]
1657pub struct GovernanceCitation {
1658 pub id: GovernanceLogId,
1659 pub entry_type: GovernanceLogEntryType,
1660 pub title: String,
1661 pub relation: CitationRelation,
1662 pub created_at: DateTime<Utc>,
1663}
1664
1665#[derive(Debug, thiserror::Error)]
1669#[error("the record's {relation:?} field does not parse: {error}")]
1670pub struct MalformedCitation {
1671 pub relation: CitationRelation,
1674 #[source]
1675 pub error: serde_json::Error,
1676}
1677
1678pub fn citations_in(
1683 entry_type: GovernanceLogEntryType,
1684 data: &serde_json::Value,
1685) -> Result<Vec<(GovernanceLogId, CitationRelation)>, MalformedCitation> {
1686 #[derive(Deserialize)]
1687 struct Concerns {
1688 #[serde(default)]
1689 concerns: Vec<GovernanceLogId>,
1690 }
1691 #[derive(Deserialize)]
1692 struct Authority {
1693 #[serde(default)]
1694 authority: Option<GovernanceLogId>,
1695 }
1696 #[derive(Deserialize)]
1697 struct Overrules {
1698 #[serde(default)]
1699 overrules: Vec<GovernanceLogId>,
1700 }
1701 fn each(
1702 ids: impl IntoIterator<Item = GovernanceLogId>,
1703 relation: CitationRelation,
1704 ) -> Vec<(GovernanceLogId, CitationRelation)> {
1705 ids.into_iter().map(|id| (id, relation)).collect()
1706 }
1707 let malformed =
1708 |relation| move |error| MalformedCitation { relation, error };
1709 match entry_type {
1710 GovernanceLogEntryType::StewardRecord => {
1711 serde_json::from_value::<Concerns>(data.clone())
1712 .map(|c| each(c.concerns, CitationRelation::Concerns))
1713 .map_err(malformed(CitationRelation::Concerns))
1714 }
1715 GovernanceLogEntryType::Amendment => {
1716 serde_json::from_value::<Authority>(data.clone())
1717 .map(|a| each(a.authority, CitationRelation::Authority))
1718 .map_err(malformed(CitationRelation::Authority))
1719 }
1720 GovernanceLogEntryType::CouncilDecision => {
1721 serde_json::from_value::<Overrules>(data.clone())
1722 .map(|o| each(o.overrules, CitationRelation::Overrules))
1723 .map_err(malformed(CitationRelation::Overrules))
1724 }
1725 _ => Ok(Vec::new()),
1726 }
1727}
1728
1729pub const KEY_ROTATION_VERSION: u32 = 2;
1735
1736pub const PUBLISHED_KEYS: &[&str] =
1743 &["ebb3091dd328f1463362c171121921b2fe14628e3fc4c145deaccefb85c0e78a"];
1744
1745#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1747#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1748#[cfg_attr(feature = "schemars", schemars(inline))]
1749#[serde(rename_all = "snake_case")]
1750pub enum RotationReason {
1751 Routine,
1753 Compromise,
1756}
1757
1758#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1760#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1761#[cfg_attr(feature = "schemars", schemars(inline))]
1762#[serde(deny_unknown_fields)]
1763pub struct TrustedHead {
1764 pub id: GovernanceLogId,
1765 pub chain_seq: u64,
1766 pub entry_hash: Sha256Hex,
1767}
1768
1769#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1771pub enum RotationError {
1772 #[error("agora_governance_key_rotation is {0}, not {KEY_ROTATION_VERSION}")]
1773 UnsupportedVersion(u32),
1774 #[error("new_key is not a valid Ed25519 public key")]
1775 BadNewKey,
1776 #[error(
1777 "the proof of possession does not verify for this rotation at this position"
1778 )]
1779 BadProof,
1780 #[error("a compromise certificate must name last_trusted")]
1781 MissingLastTrusted,
1782 #[error("certificate: {0}")]
1783 Certificate(#[from] CertificateError),
1784 #[error("outgoing_certificate: {0}")]
1785 OutgoingCertificate(CertificateError),
1786 #[error(
1787 "the chain's first rotation must carry the genesis key's \
1788 outgoing_certificate"
1789 )]
1790 MissingGenesisCertificate,
1791 #[error(
1792 "outgoing_certificate belongs on the chain's first rotation and \
1793 nowhere else"
1794 )]
1795 UnexpectedOutgoingCertificate,
1796 #[error("old_key is not the key that was in force")]
1797 WrongOldKey,
1798 #[error("last_trusted does not name an earlier entry of this chain")]
1799 UnknownLastTrusted,
1800 #[error("new_key has already held this chain; a key is never brought back")]
1801 ReusedKey,
1802 #[error("last_trusted names an entry an earlier compromise repudiated")]
1803 RepudiatedLastTrusted,
1804}
1805
1806#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1817#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1818#[cfg_attr(feature = "schemars", schemars(inline))]
1819#[serde(deny_unknown_fields)]
1820pub struct KeyRotation {
1821 pub agora_governance_key_rotation: u32,
1823 pub reason: RotationReason,
1824 pub old_key: PublicKeyHex,
1825 pub new_key: PublicKeyHex,
1826 pub proof: SignatureHex,
1830 pub proof_signed_at: i64,
1832 pub certificate: KeyCertificate,
1836 #[serde(default, skip_serializing_if = "Option::is_none")]
1840 pub outgoing_certificate: Option<KeyCertificate>,
1841}
1842
1843#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1850pub struct RotationStatement {
1851 pub agora_governance_key_rotation: u32,
1853 pub reason: RotationReason,
1854 pub old_key: PublicKeyHex,
1855 pub new_key: PublicKeyHex,
1856 pub prev_hash: Option<Sha256Hex>,
1857}
1858
1859impl RotationStatement {
1860 pub fn new(
1862 reason: RotationReason,
1863 old_key: PublicKeyHex,
1864 new_key: PublicKeyHex,
1865 prev_hash: Option<Sha256Hex>,
1866 ) -> Self {
1867 Self {
1868 agora_governance_key_rotation: KEY_ROTATION_VERSION,
1869 reason,
1870 old_key,
1871 new_key,
1872 prev_hash,
1873 }
1874 }
1875
1876 pub fn preimage(&self) -> Vec<u8> {
1878 serde_json::to_vec(self).expect("a RotationStatement always serializes")
1879 }
1880
1881 pub fn hash(&self) -> Sha256Hex {
1883 Sha256Hex(Sha256::digest(self.preimage()).into())
1884 }
1885}
1886
1887impl KeyRotation {
1888 pub fn routine(
1894 old_key: PublicKeyHex,
1895 new_signing_key: &SigningKey,
1896 prev_hash: Option<Sha256Hex>,
1897 now: DateTime<Utc>,
1898 certificate: KeyCertificate,
1899 ) -> Self {
1900 Self::build(
1901 RotationReason::Routine,
1902 old_key,
1903 new_signing_key,
1904 prev_hash,
1905 now,
1906 certificate,
1907 )
1908 }
1909
1910 pub fn compromise(
1918 old_key: PublicKeyHex,
1919 new_signing_key: &SigningKey,
1920 prev_hash: Option<Sha256Hex>,
1921 now: DateTime<Utc>,
1922 certificate: KeyCertificate,
1923 ) -> Self {
1924 Self::build(
1925 RotationReason::Compromise,
1926 old_key,
1927 new_signing_key,
1928 prev_hash,
1929 now,
1930 certificate,
1931 )
1932 }
1933
1934 fn build(
1935 reason: RotationReason,
1936 old_key: PublicKeyHex,
1937 new_signing_key: &SigningKey,
1938 prev_hash: Option<Sha256Hex>,
1939 now: DateTime<Utc>,
1940 certificate: KeyCertificate,
1941 ) -> Self {
1942 let new_key = PublicKeyHex::from(&new_signing_key.verifying_key());
1943 let proof_signed_at = truncate_to_seconds(now).timestamp();
1944 let statement =
1945 RotationStatement::new(reason, old_key, new_key, prev_hash);
1946 let proof = crypto::sign(
1947 new_signing_key,
1948 statement.hash().as_bytes(),
1949 proof_signed_at,
1950 );
1951 Self {
1952 agora_governance_key_rotation: KEY_ROTATION_VERSION,
1953 reason,
1954 old_key,
1955 new_key,
1956 proof: proof.into(),
1957 proof_signed_at,
1958 certificate,
1959 outgoing_certificate: None,
1960 }
1961 }
1962
1963 pub fn with_outgoing(mut self, certificate: KeyCertificate) -> Self {
1965 self.outgoing_certificate = Some(certificate);
1966 self
1967 }
1968
1969 pub fn statement(&self, prev_hash: Option<Sha256Hex>) -> RotationStatement {
1971 RotationStatement::new(
1972 self.reason,
1973 self.old_key,
1974 self.new_key,
1975 prev_hash,
1976 )
1977 }
1978
1979 pub fn last_trusted(&self) -> Option<&TrustedHead> {
1982 self.certificate.statement.last_trusted.as_ref()
1983 }
1984
1985 pub fn expected_statement(
1991 &self,
1992 seq: u64,
1993 prev_hash: Option<Sha256Hex>,
1994 ) -> Result<KeyCertStatement, RotationError> {
1995 match self.reason {
1996 RotationReason::Routine => {
1997 Ok(KeyCertStatement::routine(self.new_key, seq, prev_hash))
1998 }
1999 RotationReason::Compromise => Ok(KeyCertStatement::compromise(
2000 self.new_key,
2001 seq,
2002 prev_hash,
2003 self.last_trusted()
2004 .cloned()
2005 .ok_or(RotationError::MissingLastTrusted)?,
2006 )),
2007 }
2008 }
2009
2010 pub fn verify_proof(
2013 &self,
2014 prev_hash: Option<Sha256Hex>,
2015 ) -> Result<(), RotationError> {
2016 if self.agora_governance_key_rotation != KEY_ROTATION_VERSION {
2017 return Err(RotationError::UnsupportedVersion(
2018 self.agora_governance_key_rotation,
2019 ));
2020 }
2021 let new_key = self
2022 .new_key
2023 .to_verifying_key()
2024 .map_err(|_| RotationError::BadNewKey)?;
2025 crypto::verify(
2026 &new_key,
2027 self.statement(prev_hash).hash().as_bytes(),
2028 self.proof_signed_at,
2029 &Signature::from(&self.proof),
2030 )
2031 .then_some(())
2032 .ok_or(RotationError::BadProof)
2033 }
2034
2035 pub fn verify_certified(
2039 &self,
2040 seq: u64,
2041 prev_hash: Option<Sha256Hex>,
2042 roots: &RootSet,
2043 ) -> Result<(), RotationError> {
2044 self.verify_proof(prev_hash)?;
2045 let expected = self.expected_statement(seq, prev_hash)?;
2046 Ok(self.certificate.verify_for(&expected, roots)?)
2047 }
2048}
2049
2050#[derive(Debug, Clone, Default, PartialEq, Eq)]
2057pub struct KeyAnchor {
2058 keys: HashSet<PublicKeyHex>,
2059}
2060
2061impl KeyAnchor {
2062 pub fn published() -> Self {
2064 PUBLISHED_KEYS
2065 .iter()
2066 .map(|k| {
2067 k.parse()
2068 .expect("PUBLISHED_KEYS are valid 32-byte hex keys")
2069 })
2070 .collect()
2071 }
2072
2073 pub fn pinned(key: PublicKeyHex) -> Self {
2075 std::iter::once(key).collect()
2076 }
2077
2078 pub fn with(mut self, key: PublicKeyHex) -> Self {
2080 self.keys.insert(key);
2081 self
2082 }
2083
2084 pub fn contains(&self, key: &PublicKeyHex) -> bool {
2085 self.keys.contains(key)
2086 }
2087
2088 pub fn is_empty(&self) -> bool {
2089 self.keys.is_empty()
2090 }
2091
2092 pub fn keys(&self) -> impl Iterator<Item = &PublicKeyHex> {
2094 self.keys.iter()
2095 }
2096}
2097
2098impl FromIterator<PublicKeyHex> for KeyAnchor {
2099 fn from_iter<I: IntoIterator<Item = PublicKeyHex>>(iter: I) -> Self {
2100 Self {
2101 keys: iter.into_iter().collect(),
2102 }
2103 }
2104}
2105
2106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2109#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2110#[cfg_attr(feature = "schemars", schemars(inline))]
2111pub struct GovernanceKeyRecord {
2112 pub public_key: PublicKeyHex,
2113 pub from_seq: u64,
2115 #[serde(default)]
2119 pub through_seq: Option<u64>,
2120 pub status: KeyStatus,
2121 #[serde(default)]
2124 pub introduced_by: Option<GovernanceLogId>,
2125 #[serde(default)]
2127 pub retired_by: Option<GovernanceLogId>,
2128 #[serde(default)]
2132 pub certified: bool,
2133}
2134
2135#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2140#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2141#[cfg_attr(feature = "schemars", schemars(inline))]
2142pub struct GovernanceSigningKeys {
2143 pub keys: Vec<GovernanceKeyRecord>,
2145}
2146
2147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2149#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2150#[cfg_attr(feature = "schemars", schemars(inline))]
2151pub struct EntryVerdict {
2152 pub id: GovernanceLogId,
2153 pub chain_seq: u64,
2154 pub signature_valid: bool,
2157 pub link_valid: bool,
2160 #[serde(default)]
2167 pub content_matches: Option<bool>,
2168 pub retroactive: bool,
2170 pub out_of_order: bool,
2173 #[serde(default)]
2175 pub amended_by: Vec<GovernanceLogId>,
2176 #[serde(default, skip_serializing_if = "Option::is_none")]
2179 pub signed_by: Option<PublicKeyHex>,
2180 #[serde(default)]
2183 pub redacted: bool,
2184 #[serde(default, skip_serializing_if = "Option::is_none")]
2186 pub redacted_data_hash: Option<Sha256Hex>,
2187 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2190 pub revisions: Vec<GovernanceLogId>,
2191 #[serde(default, skip_serializing_if = "Option::is_none")]
2195 pub latest_data_hash: Option<Sha256Hex>,
2196 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2201 pub superseded_revisions: Vec<GovernanceLogId>,
2202 #[serde(skip)]
2205 pub history: RevisionHistory,
2206 #[serde(default)]
2209 pub repudiated: bool,
2210 #[serde(default)]
2213 pub reattested_by: Vec<GovernanceLogId>,
2214 #[serde(default, skip_serializing_if = "Option::is_none")]
2218 pub texts: Option<AmendmentTextStatus>,
2219 #[serde(default, skip_serializing_if = "Option::is_none")]
2221 pub problem: Option<String>,
2222}
2223
2224impl EntryVerdict {
2225 fn content_check(
2229 &mut self,
2230 attested: Sha256Hex,
2231 data: &serde_json::Value,
2232 ) -> bool {
2233 if non_integer_number(data).is_some() {
2235 return false;
2236 }
2237 let hash = data_hash(data);
2238 if hash == attested && self.redacted {
2239 return true;
2242 }
2243 if hash != attested && Some(hash) != self.redacted_data_hash {
2244 return Some(hash) == self.latest_data_hash;
2245 }
2246 let history = &self.history;
2247 let mut current = data.clone();
2248 let mut failures = Vec::new();
2249 for (i, (id, revision)) in history.revisions.iter().enumerate() {
2250 if i == history.rebased {
2251 break;
2252 }
2253 if let Some(failure) = false_duplicate(id, revision, ¤t) {
2254 failures.push(failure);
2255 break;
2256 }
2257 match apply_patch(¤t, &revision.patch) {
2258 Ok(next) => current = next,
2259 Err(_) => {
2260 failures.push(format!(
2261 "revision {id} does not apply to the redacted data"
2262 ));
2263 break;
2264 }
2265 }
2266 }
2267 if failures.is_empty()
2268 && let Some(expected) = history.rebased_hash
2269 && data_hash(¤t) != expected
2270 {
2271 failures.push(
2272 "the revisions rebased over the redacted data do not \
2273 produce the redaction's resulting_latest_hash"
2274 .to_string(),
2275 );
2276 }
2277 for (id, revision) in history.revisions.iter().skip(history.rebased) {
2278 if !failures.is_empty() {
2279 break;
2280 }
2281 if let Some(failure) = false_duplicate(id, revision, ¤t) {
2282 failures.push(failure);
2283 break;
2284 }
2285 match apply_patch(¤t, &revision.patch) {
2286 Ok(next)
2287 if data_hash(&next) == revision.resulting_data_hash =>
2288 {
2289 current = next;
2290 }
2291 Ok(_) => failures.push(format!(
2292 "revision {id} does not produce its resulting_data_hash"
2293 )),
2294 Err(_) => {
2295 failures.push(format!("revision {id} does not apply"))
2296 }
2297 }
2298 }
2299 for failure in failures {
2300 add_problem(&mut self.problem, failure);
2301 }
2302 true
2303 }
2304}
2305
2306fn add_problem(problems: &mut Option<String>, problem: String) {
2308 *problems = Some(match problems.take() {
2309 Some(p) if p.contains(&problem) => p,
2310 Some(p) => format!("{p}; {problem}"),
2311 None => problem,
2312 });
2313}
2314
2315fn false_duplicate(
2318 id: &GovernanceLogId,
2319 revision: &Revision,
2320 current: &serde_json::Value,
2321) -> Option<String> {
2322 revision.duplicates.iter().find_map(|(path, same_as)| {
2323 let same = match (current.pointer(path), current.pointer(same_as)) {
2324 (Some(a), Some(b)) => canonical_json(a) == canonical_json(b),
2325 _ => false,
2326 };
2327 (!same).then(|| {
2328 format!(
2329 "revision {id} removed {path} as a duplicate of {same_as}, \
2330 but they differ"
2331 )
2332 })
2333 })
2334}
2335
2336#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2338#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2339#[cfg_attr(feature = "schemars", schemars(inline))]
2340pub struct GovernanceVerification {
2341 pub public_key: PublicKeyHex,
2343 pub ok: bool,
2349 #[serde(default)]
2351 pub head: Option<GovernanceLogId>,
2352 pub entries: Vec<EntryVerdict>,
2354 #[serde(default)]
2356 pub keys: Vec<GovernanceKeyRecord>,
2357 #[serde(default)]
2362 pub unanchored_keys: Vec<PublicKeyHex>,
2363 #[serde(default)]
2365 pub repudiated: Vec<GovernanceLogId>,
2366}
2367
2368#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2372#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2373#[cfg_attr(feature = "schemars", schemars(inline))]
2374pub struct GovernanceVerificationSummary {
2375 pub public_key: PublicKeyHex,
2377 pub ok: bool,
2379 #[serde(default)]
2381 pub head: Option<GovernanceLogId>,
2382 pub entries: u64,
2384 pub signature_failures: u64,
2386 pub link_failures: u64,
2388 pub content_mismatches: u64,
2390 pub problems: u64,
2392 pub retroactive: u64,
2394 pub redacted: u64,
2396 #[serde(default)]
2398 pub failed: Vec<GovernanceLogId>,
2399 #[serde(default, skip_serializing_if = "Option::is_none")]
2401 pub first_failure: Option<EntryVerdict>,
2402 #[serde(default)]
2404 pub keys: Vec<GovernanceKeyRecord>,
2405 #[serde(default)]
2407 pub unanchored_keys: Vec<PublicKeyHex>,
2408 #[serde(default)]
2410 pub repudiated: Vec<GovernanceLogId>,
2411 pub how_to_read: String,
2412}
2413
2414impl EntryVerdict {
2415 pub fn failed(&self) -> bool {
2418 !self.signature_valid
2419 || !self.link_valid
2420 || self.content_matches == Some(false)
2421 || self.problem.is_some()
2422 }
2423}
2424
2425impl GovernanceVerification {
2426 pub fn summary(&self) -> GovernanceVerificationSummary {
2428 let count = |f: &dyn Fn(&EntryVerdict) -> bool| -> u64 {
2429 self.entries.iter().filter(|e| f(e)).count() as u64
2430 };
2431 let failed: Vec<&EntryVerdict> =
2432 self.entries.iter().filter(|e| e.failed()).collect();
2433 GovernanceVerificationSummary {
2434 public_key: self.public_key,
2435 ok: self.ok,
2436 head: self.head.clone(),
2437 entries: self.entries.len() as u64,
2438 signature_failures: count(&|e| !e.signature_valid),
2439 link_failures: count(&|e| !e.link_valid),
2440 content_mismatches: count(&|e| e.content_matches == Some(false)),
2441 problems: count(&|e| e.problem.is_some()),
2442 retroactive: count(&|e| e.retroactive),
2443 redacted: count(&|e| e.redacted),
2444 failed: failed.iter().map(|e| e.id.clone()).collect(),
2445 first_failure: failed.first().map(|e| (*e).clone()),
2446 keys: self.keys.clone(),
2447 unanchored_keys: self.unanchored_keys.clone(),
2448 repudiated: self.repudiated.clone(),
2449 how_to_read: format!(
2450 "A summary of the whole check of {n} entries: verify_governance_log \
2451 with full=true returns one verdict per entry (signature, link, \
2452 content, redaction, revisions, signing key), about {kb} KB. \
2453 Either way this is the server checking itself, not independent \
2454 evidence; agora-agentkit's govlog::verify_chain is the \
2455 independent check.",
2456 n = self.entries.len(),
2457 kb = serde_json::to_string(self).map_or(0, |s| s.len() / 1024),
2458 ),
2459 }
2460 }
2461
2462 pub fn settle(mut self) -> Self {
2464 self.ok = self.entries.iter().all(|e| !e.failed());
2465 self
2466 }
2467
2468 pub fn check_content(
2479 &mut self,
2480 link: &GovernanceChainLink,
2481 data: &serde_json::Value,
2482 ) -> bool {
2483 let Some(entry) = self.entries.iter_mut().find(|e| e.id == link.id)
2484 else {
2485 return false;
2486 };
2487 let ok = entry.content_check(link.attestation.data_hash, data);
2488 entry.content_matches = Some(ok);
2489 ok
2490 }
2491}
2492
2493pub fn attest(
2504 key: &SigningKey,
2505 envelope: &Envelope,
2506 chain_seq: u64,
2507 signed_at: DateTime<Utc>,
2508) -> GovernanceAttestation {
2509 let signed_at = truncate_to_seconds(signed_at);
2510 let entry_hash = envelope.entry_hash();
2511 let signature =
2512 crypto::sign(key, entry_hash.as_bytes(), signed_at.timestamp());
2513 GovernanceAttestation {
2514 envelope_version: envelope.agora_governance_log,
2515 chain_seq,
2516 prev_hash: envelope.prev_hash,
2517 data_hash: envelope.data_hash,
2518 entry_hash,
2519 signature: signature.into(),
2520 signed_at,
2521 retroactive: is_retroactive(envelope.created_at(), signed_at),
2522 }
2523}
2524
2525#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
2531pub enum LinkError {
2532 #[error(
2533 "envelope version {0} is not supported (this verifier knows {ENVELOPE_VERSION})"
2534 )]
2535 UnsupportedVersion(u32),
2536 #[error("entry_hash does not recompute from the envelope fields")]
2537 HashMismatch,
2538 #[error("signature does not verify under the published key")]
2539 BadSignature,
2540 #[error(
2544 "this entry's type is newer than this verifier, which cannot check it; \
2545 upgrade agora-agentkit"
2546 )]
2547 UnknownEntryType,
2548}
2549
2550pub fn recompute_entry_hash(link: &GovernanceChainLink) -> Sha256Hex {
2552 Envelope::new(
2553 link.id.clone(),
2554 link.entry_type,
2555 link.created_at,
2556 link.attestation.prev_hash,
2557 link.attestation.data_hash,
2558 )
2559 .entry_hash()
2560}
2561
2562pub fn verify_link(
2564 link: &GovernanceChainLink,
2565 key: &VerifyingKey,
2566) -> Result<(), LinkError> {
2567 let a = &link.attestation;
2568 if a.envelope_version != ENVELOPE_VERSION {
2569 return Err(LinkError::UnsupportedVersion(a.envelope_version));
2570 }
2571 if link.entry_type == GovernanceLogEntryType::Unknown {
2572 return Err(LinkError::UnknownEntryType);
2573 }
2574 if recompute_entry_hash(link) != a.entry_hash {
2575 return Err(LinkError::HashMismatch);
2576 }
2577 if !crypto::verify(
2578 key,
2579 a.entry_hash.as_bytes(),
2580 a.signed_at.timestamp(),
2581 &Signature::from(&a.signature),
2582 ) {
2583 return Err(LinkError::BadSignature);
2584 }
2585 Ok(())
2586}
2587
2588pub fn verify_data(
2590 link: &GovernanceChainLink,
2591 data: &serde_json::Value,
2592) -> bool {
2593 data_hash(data) == link.attestation.data_hash
2594}
2595
2596fn same_shape(written: &serde_json::Value, read: &serde_json::Value) -> bool {
2606 use serde_json::Value::{Array, Null, Object};
2607 match (written, read) {
2608 (Object(w), Object(r)) => r.iter().all(|(k, r)| match w.get(k) {
2609 Some(w) => same_shape(w, r),
2610 None => r.is_null(),
2611 }),
2612 (Array(w), Array(r)) => {
2613 w.len() == r.len() && w.iter().zip(r).all(|(w, r)| same_shape(w, r))
2614 }
2615 (_, Object(_) | Array(_)) => false,
2616 (Object(_) | Array(_), Null) => false,
2617 _ => true,
2618 }
2619}
2620
2621fn read_strictly<T>(written: &serde_json::Value) -> Result<T, String>
2623where
2624 T: Serialize + serde::de::DeserializeOwned,
2625{
2626 let read: T =
2627 serde_json::from_value(written.clone()).map_err(|e| e.to_string())?;
2628 let again = serde_json::to_value(&read).map_err(|e| e.to_string())?;
2629 if same_shape(written, &again) {
2630 Ok(read)
2631 } else {
2632 Err("an array where an object belongs, or the reverse".into())
2633 }
2634}
2635
2636pub fn links_from_json(
2643 chain: &serde_json::Value,
2644) -> Result<Vec<GovernanceChainLink>, String> {
2645 chain
2646 .as_array()
2647 .ok_or("a chain is an array of links")?
2648 .iter()
2649 .map(read_strictly)
2650 .collect()
2651}
2652
2653fn read_as_written<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
2655where
2656 D: serde::Deserializer<'de>,
2657 T: Serialize + serde::de::DeserializeOwned,
2658{
2659 let written = serde_json::Value::deserialize(deserializer)?;
2660 if written.is_null() {
2661 return Ok(None);
2662 }
2663 read_strictly(&written)
2664 .map(Some)
2665 .map_err(serde::de::Error::custom)
2666}
2667
2668fn reserved_prefix(
2671 entry_type: GovernanceLogEntryType,
2672) -> Option<GovernanceLogPrefix> {
2673 match entry_type {
2674 GovernanceLogEntryType::Amendment => Some(GovernanceLogPrefix::Amd),
2675 GovernanceLogEntryType::KeyRotation => Some(GovernanceLogPrefix::Key),
2676 GovernanceLogEntryType::StewardRecord => Some(GovernanceLogPrefix::Rec),
2677 GovernanceLogEntryType::Obligation => Some(GovernanceLogPrefix::Obl),
2678 GovernanceLogEntryType::DataSet => Some(GovernanceLogPrefix::Data),
2679 GovernanceLogEntryType::CouncilDecision
2680 | GovernanceLogEntryType::AppealsCourtDecision
2681 | GovernanceLogEntryType::EmergencyAction
2682 | GovernanceLogEntryType::PolicyChange
2683 | GovernanceLogEntryType::StewardVeto
2684 | GovernanceLogEntryType::Unknown => None,
2685 }
2686}
2687
2688fn reserved_for(prefix: GovernanceLogPrefix) -> Option<GovernanceLogEntryType> {
2690 match prefix {
2691 GovernanceLogPrefix::Amd => Some(GovernanceLogEntryType::Amendment),
2692 GovernanceLogPrefix::Key => Some(GovernanceLogEntryType::KeyRotation),
2693 GovernanceLogPrefix::Rec => Some(GovernanceLogEntryType::StewardRecord),
2694 GovernanceLogPrefix::Obl => Some(GovernanceLogEntryType::Obligation),
2695 GovernanceLogPrefix::Data => Some(GovernanceLogEntryType::DataSet),
2696 GovernanceLogPrefix::Gov
2697 | GovernanceLogPrefix::App
2698 | GovernanceLogPrefix::Unknown => None,
2699 }
2700}
2701
2702fn prefix_problem(link: &GovernanceChainLink) -> Option<String> {
2704 let prefix = link.id.prefix();
2705 match (reserved_prefix(link.entry_type), reserved_for(prefix)) {
2706 (Some(want), _) if prefix != want => Some(format!(
2707 "the id of a {} entry must be in the {want}- series, not {}",
2708 link.entry_type, link.id
2709 )),
2710 (None, Some(owner)) => Some(format!(
2711 "{prefix}- ids are reserved for {owner} entries, but {} is a {}",
2712 link.id, link.entry_type
2713 )),
2714 _ => None,
2715 }
2716}
2717
2718pub const OBLIGATION_FIXED: [&str; 5] = [
2722 "/until",
2723 "/owed_by",
2724 "/kind",
2725 "/source",
2726 "/agora_obligation",
2727];
2728
2729pub fn revision_touches<'a>(
2732 revision: &Revision,
2733 pointers: &[&'a str],
2734) -> Option<&'a str> {
2735 revision.patch.iter().find_map(|op| {
2736 let from = match op {
2737 json_patch::PatchOperation::Move(m) => Some(m.from.as_str()),
2738 json_patch::PatchOperation::Copy(c) => Some(c.from.as_str()),
2739 _ => None,
2740 };
2741 [Some(op.path().as_str()), from]
2742 .into_iter()
2743 .flatten()
2744 .find_map(|p| pointers.iter().copied().find(|f| overlaps(p, f)))
2745 })
2746}
2747
2748fn amendment_target(
2750 amendment: &Amendment,
2751 seq: u64,
2752 seq_of: &HashMap<&str, u64>,
2753 links: &[&GovernanceChainLink],
2754) -> Result<u64, AmendmentError> {
2755 amendment.validate()?;
2756 let target = *seq_of.get(amendment.target.as_str()).ok_or_else(|| {
2757 AmendmentError::UnknownTarget(amendment.target.clone())
2758 })?;
2759 if target >= seq {
2760 return Err(AmendmentError::ForwardReference(amendment.target.clone()));
2761 }
2762 let target_link = links[target as usize - 1];
2763 if target_link.attestation.entry_hash != amendment.target_entry_hash {
2764 return Err(AmendmentError::WrongTargetHash(amendment.target.clone()));
2765 }
2766 let on_obligation =
2767 target_link.entry_type == GovernanceLogEntryType::Obligation;
2768 if amendment.kind.is_obligation() && !on_obligation {
2769 return Err(AmendmentError::NotAnObligation {
2770 kind: amendment.kind,
2771 target: amendment.target.clone(),
2772 });
2773 }
2774 if on_obligation && kind_standing(amendment.kind).is_some() {
2775 return Err(AmendmentError::PrecedentialOnObligation(amendment.kind));
2776 }
2777 if on_obligation
2778 && let Some(revision) = &amendment.revision
2779 && let Some(path) = revision_touches(revision, &OBLIGATION_FIXED)
2780 {
2781 return Err(AmendmentError::FixedObligationField(path.to_string()));
2782 }
2783 Ok(target)
2784}
2785
2786struct KeyWalk {
2788 history: Vec<(GovernanceKeyRecord, VerifyingKey)>,
2790 unanchored: Vec<PublicKeyHex>,
2791 seen: HashSet<PublicKeyHex>,
2795 repudiated: HashSet<u64>,
2797 genesis: PublicKeyHex,
2798 genesis_certified: bool,
2800}
2801
2802impl KeyWalk {
2803 fn new(genesis: &VerifyingKey, anchor: &KeyAnchor) -> Self {
2804 let public_key = PublicKeyHex::from(genesis);
2805 Self {
2806 genesis: public_key,
2807 genesis_certified: false,
2808 history: vec![(
2809 GovernanceKeyRecord {
2810 public_key,
2811 from_seq: 1,
2812 through_seq: None,
2813 status: KeyStatus::Active,
2814 introduced_by: None,
2815 retired_by: None,
2816 certified: false,
2817 },
2818 *genesis,
2819 )],
2820 unanchored: if anchor.contains(&public_key) {
2821 Vec::new()
2822 } else {
2823 vec![public_key]
2824 },
2825 seen: HashSet::from([public_key]),
2826 repudiated: HashSet::new(),
2827 }
2828 }
2829
2830 fn in_force(&self, seq: u64) -> (PublicKeyHex, VerifyingKey) {
2832 let (record, key) = self
2833 .history
2834 .iter()
2835 .rev()
2836 .find(|(r, _)| r.from_seq <= seq)
2837 .unwrap_or(&self.history[0]);
2838 (record.public_key, *key)
2839 }
2840
2841 fn active(&self) -> PublicKeyHex {
2843 self.history
2844 .last()
2845 .map(|(r, _)| r.public_key)
2846 .expect("the genesis key is always in the history")
2847 }
2848
2849 fn close(
2850 &mut self,
2851 through_seq: u64,
2852 status: KeyStatus,
2853 by: &GovernanceLogId,
2854 ) {
2855 if let Some((record, _)) = self.history.last_mut() {
2856 record.through_seq = Some(through_seq);
2857 record.status = status;
2858 record.retired_by = Some(by.clone());
2859 }
2860 }
2861
2862 fn open(
2863 &mut self,
2864 public_key: PublicKeyHex,
2865 key: VerifyingKey,
2866 from_seq: u64,
2867 by: &GovernanceLogId,
2868 ) {
2869 self.history.push((
2870 GovernanceKeyRecord {
2871 public_key,
2872 from_seq,
2873 through_seq: None,
2874 status: KeyStatus::Active,
2875 introduced_by: Some(by.clone()),
2876 retired_by: None,
2877 certified: true,
2878 },
2879 key,
2880 ));
2881 }
2882
2883 fn apply(
2885 &mut self,
2886 rotation: &KeyRotation,
2887 link: &GovernanceChainLink,
2888 seq: u64,
2889 links: &[&GovernanceChainLink],
2890 roots: &RootSet,
2891 ) -> Result<(), RotationError> {
2892 rotation.verify_certified(seq, link.attestation.prev_hash, roots)?;
2893 let new_key = rotation
2894 .new_key
2895 .to_verifying_key()
2896 .map_err(|_| RotationError::BadNewKey)?;
2897 if self.seen.contains(&rotation.new_key) {
2898 return Err(RotationError::ReusedKey);
2899 }
2900 match (&rotation.outgoing_certificate, self.genesis_certified) {
2905 (None, false) => {
2906 return Err(RotationError::MissingGenesisCertificate);
2907 }
2908 (Some(_), true) => {
2909 return Err(RotationError::UnexpectedOutgoingCertificate);
2910 }
2911 (Some(certificate), false) => certificate
2912 .verify_for(&KeyCertStatement::genesis(self.genesis), roots)
2913 .map_err(RotationError::OutgoingCertificate)?,
2914 (None, true) => {}
2915 }
2916 match rotation.reason {
2917 RotationReason::Routine => {
2918 if rotation.old_key != self.in_force(seq).0 {
2919 return Err(RotationError::WrongOldKey);
2920 }
2921 self.close(seq, KeyStatus::Retired, &link.id);
2922 self.open(rotation.new_key, new_key, seq + 1, &link.id);
2923 }
2924 RotationReason::Compromise => {
2925 let head = rotation
2926 .last_trusted()
2927 .ok_or(RotationError::MissingLastTrusted)?;
2928 let trusted_seq = head.chain_seq;
2929 let names_an_earlier_entry = trusted_seq >= 1
2930 && trusted_seq < seq
2931 && links[trusted_seq as usize - 1].id == head.id
2932 && links[trusted_seq as usize - 1].attestation.entry_hash
2933 == head.entry_hash;
2934 if !names_an_earlier_entry {
2935 return Err(RotationError::UnknownLastTrusted);
2936 }
2937 if self.repudiated.contains(&trusted_seq) {
2940 return Err(RotationError::RepudiatedLastTrusted);
2941 }
2942 if rotation.old_key != self.in_force(trusted_seq).0 {
2945 return Err(RotationError::WrongOldKey);
2946 }
2947 self.history.retain(|(r, _)| r.from_seq <= trusted_seq);
2948 self.close(trusted_seq, KeyStatus::Compromised, &link.id);
2949 self.open(rotation.new_key, new_key, seq, &link.id);
2950 self.repudiated.extend(trusted_seq + 1..seq);
2951 }
2952 }
2953 self.seen.insert(rotation.new_key);
2954 if !self.genesis_certified {
2955 self.genesis_certified = true;
2956 self.history[0].0.certified = true;
2957 self.unanchored.clear();
2958 }
2959 Ok(())
2960 }
2961}
2962
2963pub fn verify_chain(
2994 links: &[GovernanceChainLink],
2995 genesis_key: &VerifyingKey,
2996 anchor: &KeyAnchor,
2997 roots: &RootSet,
2998) -> GovernanceVerification {
2999 let mut links: Vec<&GovernanceChainLink> = links.iter().collect();
3000 links.sort_by_key(|l| l.attestation.chain_seq);
3001
3002 let mut seq_of: HashMap<&str, u64> = HashMap::new();
3003 let mut duplicates: HashSet<&str> = HashSet::new();
3004 for (i, link) in links.iter().enumerate() {
3005 if seq_of.insert(link.id.as_str(), i as u64 + 1).is_some() {
3006 duplicates.insert(link.id.as_str());
3007 }
3008 }
3009
3010 let mut walk = KeyWalk::new(genesis_key, anchor);
3011 let mut amendments: Vec<(u64, GovernanceLogId, Amendment, u64)> =
3013 Vec::new();
3014 let mut entries: Vec<EntryVerdict> = Vec::with_capacity(links.len());
3015 let mut prev: Option<&GovernanceChainLink> = None;
3016
3017 for (i, link) in links.iter().enumerate() {
3018 let a = &link.attestation;
3019 let expected_seq = i as u64 + 1;
3020 let mut problems: Vec<String> = Vec::new();
3021
3022 if let Some(problem) = prefix_problem(link) {
3023 problems.push(problem);
3024 }
3025 if duplicates.contains(link.id.as_str()) {
3026 problems.push(format!("{} appears more than once", link.id));
3027 }
3028
3029 let carries_meaning = matches!(
3033 link.entry_type,
3034 GovernanceLogEntryType::Amendment
3035 | GovernanceLogEntryType::KeyRotation
3036 );
3037 let mut amendment: Option<Amendment> = None;
3038 let mut rotation: Option<KeyRotation> = None;
3039 let mut content_matches: Option<bool> = None;
3040 match &link.data {
3041 Some(data) if non_integer_number(data).is_some() => {
3042 content_matches = Some(false);
3043 problems.push(format!(
3044 "`data` has a number that is not a 64-bit integer at {:?}; \
3045 governance data never contains one",
3046 non_integer_number(data).unwrap_or_default()
3047 ));
3048 }
3049 Some(data) => {
3050 let matched = data_hash(data) == a.data_hash;
3051 content_matches = Some(matched);
3052 if !matched {
3053 if carries_meaning {
3054 problems.push(
3055 "`data` does not hash to the attested data_hash"
3056 .into(),
3057 );
3058 }
3059 } else {
3060 match link.entry_type {
3061 GovernanceLogEntryType::Amendment => {
3062 match read_strictly(data) {
3063 Ok(v) => amendment = Some(v),
3064 Err(e) => problems.push(format!(
3065 "amendment `data` is malformed: {e}"
3066 )),
3067 }
3068 }
3069 GovernanceLogEntryType::KeyRotation => {
3070 match read_strictly(data) {
3071 Ok(v) => rotation = Some(v),
3072 Err(e) => problems.push(format!(
3073 "key_rotation `data` is malformed: {e}"
3074 )),
3075 }
3076 }
3077 _ => {}
3078 }
3079 }
3080 }
3081 None if carries_meaning => problems.push(format!(
3082 "a {} entry must carry its `data`",
3083 link.entry_type
3084 )),
3085 None => {}
3086 }
3087
3088 let declared = rotation
3092 .as_ref()
3093 .filter(|r| r.reason == RotationReason::Compromise)
3094 .map(|r| {
3095 if walk.seen.contains(&r.new_key) {
3096 return Err(RotationError::ReusedKey);
3097 }
3098 r.verify_certified(expected_seq, a.prev_hash, roots)?;
3099 r.new_key
3100 .to_verifying_key()
3101 .map_err(|_| RotationError::BadNewKey)
3102 });
3103 let (key_hex, key) = match &declared {
3104 Some(Ok(k)) => (PublicKeyHex::from(k), *k),
3105 _ => walk.in_force(expected_seq),
3106 };
3107 if let Some(Err(e)) = &declared {
3110 problems.push(e.to_string());
3111 }
3112
3113 let (hash_ok, signature_valid) = match verify_link(link, &key) {
3114 Ok(()) => (true, true),
3115 Err(LinkError::BadSignature) => {
3116 problems.push(LinkError::BadSignature.to_string());
3117 (true, false)
3118 }
3119 Err(e) => {
3120 problems.push(e.to_string());
3121 (false, false)
3122 }
3123 };
3124
3125 let mut link_valid = hash_ok;
3126 if a.chain_seq != expected_seq {
3127 link_valid = false;
3128 problems.push(format!(
3129 "chain_seq {} where {expected_seq} was expected",
3130 a.chain_seq
3131 ));
3132 }
3133 let expected_prev = prev.map(|p| p.attestation.entry_hash);
3134 if a.prev_hash != expected_prev {
3135 link_valid = false;
3136 problems.push(match (a.prev_hash, expected_prev) {
3137 (Some(_), None) => "first entry names a predecessor".into(),
3138 (None, Some(_)) => "prev_hash is null mid-chain".into(),
3139 _ => "prev_hash is not the previous entry's entry_hash".into(),
3140 });
3141 }
3142 let out_of_order = prev.is_some_and(|p| link.created_at < p.created_at);
3143
3144 let authentic = signature_valid && link_valid;
3148 if let Some(rotation) = rotation.as_ref().filter(|_| authentic)
3149 && let Err(e) =
3150 walk.apply(rotation, link, expected_seq, &links, roots)
3151 {
3152 let problem = e.to_string();
3153 if !problems.contains(&problem) {
3154 problems.push(problem);
3155 }
3156 }
3157 let mut texts = None;
3161 if let Some(amendment) = amendment
3162 .as_ref()
3163 .filter(|a| authentic && a.validate().is_ok())
3164 {
3165 match amendment.text_status(link.texts.as_ref()) {
3166 Ok(status) => texts = status,
3167 Err(e) => problems.push(e.to_string()),
3168 }
3169 } else if link.texts.as_ref().is_some_and(|t| !t.is_empty()) {
3170 problems.push(AmendmentError::UncommittedText.to_string());
3171 }
3172 if let Some(amendment) = amendment.filter(|_| authentic) {
3173 match amendment_target(&amendment, expected_seq, &seq_of, &links) {
3174 Ok(target) => amendments.push((
3175 expected_seq,
3176 link.id.clone(),
3177 amendment,
3178 target,
3179 )),
3180 Err(e) => problems.push(e.to_string()),
3181 }
3182 }
3183
3184 entries.push(EntryVerdict {
3185 id: link.id.clone(),
3186 chain_seq: a.chain_seq,
3187 signature_valid,
3188 link_valid,
3189 content_matches,
3190 retroactive: is_retroactive(link.created_at, a.signed_at),
3191 out_of_order,
3192 amended_by: Vec::new(),
3193 signed_by: Some(key_hex),
3194 redacted: false,
3195 redacted_data_hash: None,
3196 revisions: Vec::new(),
3197 latest_data_hash: None,
3198 superseded_revisions: Vec::new(),
3199 history: RevisionHistory::default(),
3200 repudiated: false,
3201 reattested_by: Vec::new(),
3202 texts,
3203 problem: (!problems.is_empty()).then(|| problems.join("; ")),
3204 });
3205 prev = Some(link);
3206 }
3207
3208 let mut applied = vec![false; amendments.len()];
3212 loop {
3213 let mut changed = false;
3214 for (i, (seq, id, amendment, target)) in amendments.iter().enumerate() {
3215 if applied[i]
3216 || amendment.kind != AmendmentKind::Reattested
3217 || walk.repudiated.contains(seq)
3218 {
3219 continue;
3220 }
3221 applied[i] = true;
3222 entries[*target as usize - 1].reattested_by.push(id.clone());
3223 walk.repudiated.remove(target);
3224 changed = true;
3225 }
3226 if !changed {
3227 break;
3228 }
3229 }
3230
3231 for (seq, id, amendment, target) in &amendments {
3232 if walk.repudiated.contains(seq) {
3233 continue;
3234 }
3235 let entry = &mut entries[*target as usize - 1];
3236 entry.amended_by.push(id.clone());
3237 let mut unrebased = false;
3238 if let Some(redaction) = &amendment.redaction {
3239 unrebased = !entry.revisions.is_empty()
3242 && redaction.resulting_latest_hash.is_none();
3243 entry.redacted = true;
3244 entry.redacted_data_hash = Some(redaction.resulting_data_hash);
3245 entry.history.rebased = entry.history.revisions.len();
3247 entry.history.rebased_hash = redaction.resulting_latest_hash;
3248 entry.latest_data_hash = redaction.resulting_latest_hash;
3249 entry.superseded_revisions = entry.revisions.clone();
3250 }
3251 if let Some(revision) = &amendment.revision {
3252 entry.revisions.push(id.clone());
3253 entry.latest_data_hash = Some(revision.resulting_data_hash);
3254 entry.history.revisions.push((id.clone(), revision.clone()));
3255 }
3256 if unrebased {
3257 let target = entry.id.clone();
3258 add_problem(
3259 &mut entries[*seq as usize - 1].problem,
3260 format!(
3261 "the redaction of {target}, which has revisions, names no \
3262 resulting_latest_hash"
3263 ),
3264 );
3265 }
3266 }
3267
3268 for (i, link) in links.iter().enumerate() {
3271 let entry = &mut entries[i];
3272 if let Some(data) = &link.data
3273 && entry.content_matches.is_some()
3274 && (entry.redacted || !entry.history.revisions.is_empty())
3275 {
3276 entry.content_matches =
3277 Some(entry.content_check(link.attestation.data_hash, data));
3278 }
3279 }
3280
3281 let mut seen = HashSet::new();
3282 walk.unanchored.retain(|key| seen.insert(*key));
3283
3284 let mut repudiated = Vec::new();
3285 for (i, entry) in entries.iter_mut().enumerate() {
3286 if walk.repudiated.contains(&(i as u64 + 1)) {
3287 entry.repudiated = true;
3288 repudiated.push(entry.id.clone());
3289 }
3290 }
3291
3292 GovernanceVerification {
3293 public_key: walk.active(),
3294 ok: false,
3295 head: prev.map(|p| p.id.clone()),
3296 entries,
3297 keys: walk.history.into_iter().map(|(record, _)| record).collect(),
3298 unanchored_keys: walk.unanchored,
3299 repudiated,
3300 }
3301 .settle()
3302}
3303
3304#[cfg(test)]
3305mod tests {
3306 use super::*;
3307
3308 #[test]
3309 fn pointer_tokens_escape_and_unescape() {
3310 assert_eq!(escape_pointer_token("a/b~c"), "a~1b~0c");
3311 assert_eq!(unescape_pointer_token("~01"), "~1");
3313 for token in ["", "plain", "a/b", "~", "~1", "/~0/"] {
3314 assert_eq!(
3315 unescape_pointer_token(&escape_pointer_token(token)),
3316 token
3317 );
3318 }
3319 assert_eq!(
3320 pointer_tokens("/rounds/0/a~1b~0c"),
3321 ["rounds", "0", "a/b~c"]
3322 );
3323 assert!(pointer_tokens("").is_empty());
3324 }
3325
3326 #[test]
3330 fn citations_are_read_from_each_records_own_field() {
3331 use serde_json::json;
3332 let id = |s: &str| s.parse::<GovernanceLogId>().unwrap();
3333 assert_eq!(
3334 citations_in(
3335 GovernanceLogEntryType::StewardRecord,
3336 &json!({"title": "t", "concerns": ["GOV-2026-0011", "AMD-2026-0004"]}),
3337 )
3338 .unwrap(),
3339 vec![
3340 (id("GOV-2026-0011"), CitationRelation::Concerns),
3341 (id("AMD-2026-0004"), CitationRelation::Concerns),
3342 ]
3343 );
3344 assert_eq!(
3345 citations_in(
3346 GovernanceLogEntryType::Amendment,
3347 &json!({"target": "APP-2026-0001", "authority": "GOV-2026-0005"}),
3348 )
3349 .unwrap(),
3350 vec![(id("GOV-2026-0005"), CitationRelation::Authority)],
3351 "the target is not a citation: it is listed under `amendments`"
3352 );
3353 assert_eq!(
3354 citations_in(
3355 GovernanceLogEntryType::CouncilDecision,
3356 &json!({"overrules": ["APP-2026-0002"]}),
3357 )
3358 .unwrap(),
3359 vec![(id("APP-2026-0002"), CitationRelation::Overrules)]
3360 );
3361 assert!(
3362 citations_in(
3363 GovernanceLogEntryType::Amendment,
3364 &json!({"authority": null})
3365 )
3366 .unwrap()
3367 .is_empty()
3368 );
3369 assert!(matches!(
3372 citations_in(
3373 GovernanceLogEntryType::StewardRecord,
3374 &json!({"concerns": "GOV-2026-0011"})
3375 ),
3376 Err(MalformedCitation {
3377 relation: CitationRelation::Concerns,
3378 ..
3379 })
3380 ));
3381 assert!(
3382 citations_in(
3383 GovernanceLogEntryType::AppealsCourtDecision,
3384 &json!({"concerns": ["GOV-2026-0011"]}),
3385 )
3386 .unwrap()
3387 .is_empty()
3388 );
3389 }
3390 use crate::crypto::generate_keypair;
3391 use serde_json::json;
3392
3393 pub(super) fn gov(n: u32) -> GovernanceLogId {
3394 format!("GOV-2026-{n:04}").parse().unwrap()
3395 }
3396
3397 pub(super) fn amd(n: u32) -> GovernanceLogId {
3398 format!("AMD-2026-{n:04}").parse().unwrap()
3399 }
3400
3401 pub(super) fn key_id(n: u32) -> GovernanceLogId {
3402 format!("KEY-2026-{n:04}").parse().unwrap()
3403 }
3404
3405 pub(super) fn rec(n: u32) -> GovernanceLogId {
3406 format!("REC-2026-{n:04}").parse().unwrap()
3407 }
3408
3409 pub(super) fn obl(n: u32) -> GovernanceLogId {
3410 format!("OBL-2026-{n:04}").parse().unwrap()
3411 }
3412
3413 pub(super) fn data_id(n: u32) -> GovernanceLogId {
3414 format!("DATA-2026-{n:04}").parse().unwrap()
3415 }
3416
3417 pub(super) fn at(secs: i64) -> DateTime<Utc> {
3418 DateTime::from_timestamp(1_700_000_000 + secs, 123_456_789).unwrap()
3419 }
3420
3421 fn anchored(key: &VerifyingKey) -> KeyAnchor {
3423 KeyAnchor::pinned(key.into())
3424 }
3425
3426 pub(super) fn resalted(draft: &AmendmentDraft, seq: u64) -> AmendmentDraft {
3429 let fix = |field: &str, t: &Option<CommittedText>| {
3430 t.as_ref().map(|t| {
3431 let salt = Sha256::digest(format!("{seq}/{field}/{}", t.text));
3432 CommittedText::with_salt(
3433 TextSalt::from(<[u8; 32]>::from(salt)),
3434 t.text.clone(),
3435 )
3436 })
3437 };
3438 let texts = AmendmentTexts {
3439 basis: fix("basis", &draft.texts.basis),
3440 note: fix("note", &draft.texts.note),
3441 rationale: fix("rationale", &draft.texts.rationale),
3442 };
3443 let commit = |t: &Option<CommittedText>| {
3444 t.as_ref().map(|t| AmendmentText::Committed(t.commitment()))
3445 };
3446 AmendmentDraft {
3447 amendment: Amendment {
3448 basis: commit(&texts.basis).unwrap(),
3449 note: commit(&texts.note).unwrap(),
3450 rationale: commit(&texts.rationale),
3451 ..draft.amendment.clone()
3452 },
3453 texts,
3454 }
3455 }
3456
3457 pub(super) fn v1(draft: AmendmentDraft) -> Amendment {
3459 let plain =
3460 |t: Option<CommittedText>| t.map(|t| AmendmentText::Plain(t.text));
3461 Amendment {
3462 agora_governance_amendment: 1,
3463 basis: plain(draft.texts.basis).unwrap(),
3464 note: plain(draft.texts.note).unwrap(),
3465 rationale: plain(draft.texts.rationale),
3466 ..draft.amendment
3467 }
3468 }
3469
3470 pub(super) fn root(n: u8) -> SigningKey {
3473 SigningKey::from_bytes(&[0xA0 + n; 32])
3474 }
3475
3476 pub(super) fn roots() -> RootSet {
3478 RootSet::new(
3479 [1, 2].map(|n| PublicKeyHex::from(&root(n).verifying_key())),
3480 1,
3481 )
3482 }
3483
3484 pub(super) fn certify(
3486 signers: &[&SigningKey],
3487 statement: KeyCertStatement,
3488 ) -> KeyCertificate {
3489 use ed25519_dalek::Signer;
3490 let message = statement.signed_bytes();
3491 signers.iter().fold(
3492 KeyCertificate::unsigned(statement),
3493 |certificate, signer| {
3494 certificate.with(RootSignature {
3495 root_key: (&signer.verifying_key()).into(),
3496 signature: signer.sign(&message).into(),
3497 })
3498 },
3499 )
3500 }
3501
3502 fn for_new_at(c: &Chain, key: &VerifyingKey) -> KeyCertificate {
3504 certify(
3505 &[&root(1)],
3506 KeyCertStatement::routine(key.into(), c.next_seq(), c.prev_hash()),
3507 )
3508 }
3509
3510 pub(super) fn link(
3511 key: &SigningKey,
3512 n: u32,
3513 prev: Option<&GovernanceChainLink>,
3514 data: &serde_json::Value,
3515 signed_at: DateTime<Utc>,
3516 ) -> GovernanceChainLink {
3517 let created_at = truncate_to_micros(at(n as i64 * 10));
3518 let envelope = Envelope::new(
3519 gov(n),
3520 GovernanceLogEntryType::CouncilDecision,
3521 created_at,
3522 prev.map(|p| p.attestation.entry_hash),
3523 data_hash(data),
3524 );
3525 let attestation = attest(
3526 key,
3527 &envelope,
3528 prev.map_or(1, |p| p.attestation.chain_seq + 1),
3529 signed_at,
3530 );
3531 GovernanceChainLink {
3532 id: gov(n),
3533 entry_type: GovernanceLogEntryType::CouncilDecision,
3534 created_at,
3535 attestation,
3536 data: None,
3537 texts: None,
3538 }
3539 }
3540
3541 pub(super) fn chain(key: &SigningKey, n: u32) -> Vec<GovernanceChainLink> {
3542 let mut out: Vec<GovernanceChainLink> = Vec::new();
3543 for i in 1..=n {
3544 let data = json!({"title": format!("Decision {i}"), "outcome": "approved"});
3545 let l = link(key, i, out.last(), &data, at(i as i64 * 10 + 1));
3546 out.push(l);
3547 }
3548 out
3549 }
3550
3551 pub(super) struct Chain {
3555 pub(super) links: Vec<GovernanceChainLink>,
3556 gov: u32,
3557 amd: u32,
3558 key: u32,
3559 genesis: Option<PublicKeyHex>,
3561 }
3562
3563 impl Chain {
3564 pub(super) fn new() -> Self {
3565 Self {
3566 links: Vec::new(),
3567 gov: 0,
3568 amd: 0,
3569 key: 0,
3570 genesis: None,
3571 }
3572 }
3573
3574 pub(super) fn prev_hash(&self) -> Option<Sha256Hex> {
3575 self.links.last().map(|l| l.attestation.entry_hash)
3576 }
3577
3578 pub(super) fn hash_at(&self, seq: usize) -> Sha256Hex {
3580 self.links[seq - 1].attestation.entry_hash
3581 }
3582
3583 pub(super) fn next_seq(&self) -> u64 {
3585 self.links.len() as u64 + 1
3586 }
3587
3588 pub(super) fn head(&self, seq: usize) -> TrustedHead {
3590 TrustedHead {
3591 id: self.links[seq - 1].id.clone(),
3592 chain_seq: seq as u64,
3593 entry_hash: self.hash_at(seq),
3594 }
3595 }
3596
3597 fn outgoing(&self, rotation: KeyRotation) -> KeyRotation {
3599 match (self.key, self.genesis) {
3600 (0, Some(genesis)) => rotation.with_outgoing(certify(
3601 &[&root(1)],
3602 KeyCertStatement::genesis(genesis),
3603 )),
3604 _ => rotation,
3605 }
3606 }
3607
3608 pub(super) fn routine(
3611 &self,
3612 old: &VerifyingKey,
3613 new: &SigningKey,
3614 ) -> KeyRotation {
3615 let statement = KeyCertStatement::routine(
3616 (&new.verifying_key()).into(),
3617 self.next_seq(),
3618 self.prev_hash(),
3619 );
3620 self.outgoing(KeyRotation::routine(
3621 old.into(),
3622 new,
3623 self.prev_hash(),
3624 at(self.next_seq() as i64 * 10 + 5),
3625 certify(&[&root(1)], statement),
3626 ))
3627 }
3628
3629 pub(super) fn compromise(
3632 &self,
3633 old: &VerifyingKey,
3634 new: &SigningKey,
3635 trusted: usize,
3636 ) -> KeyRotation {
3637 let statement = KeyCertStatement::compromise(
3638 (&new.verifying_key()).into(),
3639 self.next_seq(),
3640 self.prev_hash(),
3641 self.head(trusted),
3642 );
3643 self.outgoing(KeyRotation::compromise(
3644 old.into(),
3645 new,
3646 self.prev_hash(),
3647 at(self.next_seq() as i64 * 10 + 5),
3648 certify(&[&root(1)], statement),
3649 ))
3650 }
3651
3652 pub(super) fn next_amd(&self) -> GovernanceLogId {
3654 amd(self.amd + 1)
3655 }
3656
3657 pub(super) fn push(
3658 &mut self,
3659 signer: &SigningKey,
3660 id: GovernanceLogId,
3661 entry_type: GovernanceLogEntryType,
3662 data: serde_json::Value,
3663 carry: bool,
3664 ) -> GovernanceLogId {
3665 self.genesis
3666 .get_or_insert_with(|| (&signer.verifying_key()).into());
3667 let n = self.links.len() as i64 + 1;
3668 let created_at = truncate_to_micros(at(n * 10));
3669 let envelope = Envelope::new(
3670 id.clone(),
3671 entry_type,
3672 created_at,
3673 self.prev_hash(),
3674 data_hash(&data),
3675 );
3676 let attestation =
3677 attest(signer, &envelope, n as u64, at(n * 10 + 1));
3678 self.links.push(GovernanceChainLink {
3679 id: id.clone(),
3680 entry_type,
3681 created_at,
3682 attestation,
3683 data: carry.then_some(data),
3684 texts: None,
3685 });
3686 id
3687 }
3688
3689 pub(super) fn entry(
3692 &mut self,
3693 signer: &SigningKey,
3694 data: serde_json::Value,
3695 ) -> GovernanceLogId {
3696 self.gov += 1;
3697 let id = gov(self.gov);
3698 self.push(
3699 signer,
3700 id,
3701 GovernanceLogEntryType::CouncilDecision,
3702 data,
3703 false,
3704 )
3705 }
3706
3707 pub(super) fn decision(
3708 &mut self,
3709 signer: &SigningKey,
3710 ) -> GovernanceLogId {
3711 let data = json!({"title": format!("Decision {}", self.gov + 1)});
3712 self.entry(signer, data)
3713 }
3714
3715 pub(super) fn amend(
3717 &mut self,
3718 signer: &SigningKey,
3719 draft: &AmendmentDraft,
3720 ) -> GovernanceLogId {
3721 let draft = resalted(draft, self.next_seq());
3722 let id = self.amend_v1(signer, &draft.amendment);
3723 let link = self.links.last_mut().unwrap();
3724 link.texts = Some(draft.texts);
3725 id
3726 }
3727
3728 pub(super) fn amend_v1(
3731 &mut self,
3732 signer: &SigningKey,
3733 amendment: &Amendment,
3734 ) -> GovernanceLogId {
3735 self.amd += 1;
3736 let id = amd(self.amd);
3737 self.push(
3738 signer,
3739 id,
3740 GovernanceLogEntryType::Amendment,
3741 serde_json::to_value(amendment).unwrap(),
3742 true,
3743 )
3744 }
3745
3746 pub(super) fn rotate(
3747 &mut self,
3748 signer: &SigningKey,
3749 rotation: &KeyRotation,
3750 ) -> GovernanceLogId {
3751 self.key += 1;
3752 let id = key_id(self.key);
3753 self.push(
3754 signer,
3755 id,
3756 GovernanceLogEntryType::KeyRotation,
3757 serde_json::to_value(rotation).unwrap(),
3758 true,
3759 )
3760 }
3761 }
3762
3763 #[test]
3766 fn canonical_json_sorts_keys_at_every_level() {
3767 let v = json!({"b": {"z": 1, "a": [{"y": 2, "x": 3}]}, "a": null});
3768 assert_eq!(
3769 canonical_json(&v),
3770 br#"{"a":null,"b":{"a":[{"x":3,"y":2}],"z":1}}"#
3771 );
3772 }
3773
3774 #[test]
3775 fn canonical_json_is_compact_and_escapes_like_serde() {
3776 let v = json!({"s": "tab\there \"q\" ünïcode \u{1F600}", "n": [1, -2, 3.5, true, false]});
3777 let bytes = canonical_json(&v);
3778 let text = std::str::from_utf8(&bytes).unwrap();
3779 assert_eq!(
3780 text,
3781 r#"{"n":[1,-2,3.5,true,false],"s":"tab\there \"q\" ünïcode 😀"}"#
3782 );
3783 }
3784
3785 #[test]
3786 fn canonical_json_ignores_insertion_order() {
3787 let mut a = serde_json::Map::new();
3788 a.insert("z".into(), json!(1));
3789 a.insert("a".into(), json!(2));
3790 let mut b = serde_json::Map::new();
3791 b.insert("a".into(), json!(2));
3792 b.insert("z".into(), json!(1));
3793 assert_eq!(
3794 canonical_json(&serde_json::Value::Object(a)),
3795 canonical_json(&serde_json::Value::Object(b))
3796 );
3797 }
3798
3799 #[test]
3800 fn canonical_json_empty_containers() {
3801 assert_eq!(canonical_json(&json!({})), b"{}");
3802 assert_eq!(canonical_json(&json!([])), b"[]");
3803 assert_eq!(
3804 canonical_json(&json!({"a": {}, "b": []})),
3805 br#"{"a":{},"b":[]}"#
3806 );
3807 }
3808
3809 #[test]
3812 fn preimage_is_declaration_ordered_json() {
3813 let e = Envelope::new(
3814 gov(1),
3815 GovernanceLogEntryType::AppealsCourtDecision,
3816 at(0),
3817 None,
3818 data_hash(&json!({})),
3819 );
3820 let text = String::from_utf8(e.preimage()).unwrap();
3821 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}");
3822 }
3823
3824 #[test]
3825 fn every_envelope_field_changes_the_hash() {
3826 let base = Envelope::new(
3827 gov(1),
3828 GovernanceLogEntryType::CouncilDecision,
3829 at(0),
3830 None,
3831 data_hash(&json!({"a":1})),
3832 );
3833 let h = base.entry_hash();
3834 let mut e = base.clone();
3835 e.id = gov(2);
3836 assert_ne!(e.entry_hash(), h);
3837 let mut e = base.clone();
3838 e.entry_type = GovernanceLogEntryType::PolicyChange;
3839 assert_ne!(e.entry_hash(), h);
3840 let mut e = base.clone();
3841 e.created_at += 1;
3842 assert_ne!(e.entry_hash(), h);
3843 let mut e = base.clone();
3844 e.prev_hash = Some(h);
3845 assert_ne!(e.entry_hash(), h);
3846 let mut e = base.clone();
3847 e.data_hash = data_hash(&json!({"a":2}));
3848 assert_ne!(e.entry_hash(), h);
3849 assert_eq!(base.entry_hash(), h, "and it is deterministic");
3850 }
3851
3852 #[test]
3853 fn truncation_matches_what_the_envelope_carries() {
3854 let t = at(0);
3855 assert_eq!(truncate_to_micros(t).timestamp_subsec_nanos(), 123_456_000);
3856 assert_eq!(truncate_to_seconds(t).timestamp_subsec_nanos(), 0);
3857 assert_eq!(
3858 Envelope::new(
3859 gov(1),
3860 GovernanceLogEntryType::CouncilDecision,
3861 t,
3862 None,
3863 data_hash(&json!(null))
3864 )
3865 .created_at,
3866 truncate_to_micros(t).timestamp_micros()
3867 );
3868 }
3869
3870 #[test]
3873 fn hex_newtypes_round_trip_and_reject_wrong_lengths() {
3874 let h = data_hash(&json!(1));
3875 let s = serde_json::to_string(&h).unwrap();
3876 assert_eq!(s.len(), 66);
3877 let back: Sha256Hex = serde_json::from_str(&s).unwrap();
3878 assert_eq!(back, h);
3879 assert!(serde_json::from_str::<Sha256Hex>("\"abcd\"").is_err());
3880 assert!("zz".repeat(32).parse::<Sha256Hex>().is_err());
3881 assert!(Sha256Hex::try_from(vec![0u8; 31]).is_err());
3882 assert_eq!(format!("{h:?}"), format!("Sha256Hex({h})"));
3883 }
3884
3885 #[test]
3888 fn attest_then_verify_link() {
3889 let (key, pk) = generate_keypair();
3890 let l = link(&key, 1, None, &json!({"a": 1}), at(5));
3891 assert_eq!(verify_link(&l, &pk), Ok(()));
3892 assert!(verify_data(&l, &json!({"a": 1})));
3893 assert!(!verify_data(&l, &json!({"a": 2})));
3894 assert!(!l.attestation.retroactive);
3895 }
3896
3897 #[test]
3898 fn wrong_key_fails_signature_only() {
3899 let (key, _) = generate_keypair();
3900 let (_, other) = generate_keypair();
3901 let l = link(&key, 1, None, &json!({}), at(5));
3902 assert_eq!(verify_link(&l, &other), Err(LinkError::BadSignature));
3903 }
3904
3905 #[test]
3906 fn tampering_with_any_attested_field_is_detected() {
3907 let (key, pk) = generate_keypair();
3908 let l = link(&key, 1, None, &json!({"a": 1}), at(5));
3909
3910 let mut t = l.clone();
3911 t.created_at += chrono::Duration::microseconds(1);
3912 assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
3913
3914 let mut t = l.clone();
3915 t.entry_type = GovernanceLogEntryType::StewardVeto;
3916 assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
3917
3918 let mut t = l.clone();
3919 t.attestation.data_hash = data_hash(&json!({"a": 2}));
3920 assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
3921
3922 let mut t = l.clone();
3925 t.attestation.signed_at -= chrono::Duration::seconds(1);
3926 assert_eq!(verify_link(&t, &pk), Err(LinkError::BadSignature));
3927
3928 let mut t = l.clone();
3929 t.attestation.envelope_version = 2;
3930 assert_eq!(verify_link(&t, &pk), Err(LinkError::UnsupportedVersion(2)));
3931 }
3932
3933 #[test]
3934 fn a_good_chain_verifies_in_any_input_order() {
3935 let (key, pk) = generate_keypair();
3936 let mut c = chain(&key, 4);
3937 c.reverse();
3938 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
3939 assert!(v.ok, "{v:#?}");
3940 assert_eq!(v.head, Some(gov(4)));
3941 assert_eq!(
3942 v.entries.iter().map(|e| e.chain_seq).collect::<Vec<_>>(),
3943 [1, 2, 3, 4]
3944 );
3945 assert!(v.entries.iter().all(|e| e.content_matches.is_none()
3946 && e.problem.is_none()
3947 && !e.out_of_order));
3948 assert_eq!(v.public_key, PublicKeyHex::from(&pk));
3949 }
3950
3951 #[test]
3952 fn empty_chain_is_ok_with_no_head() {
3953 let (_, pk) = generate_keypair();
3954 let v = verify_chain(&[], &pk, &anchored(&pk), &roots());
3955 assert!(v.ok);
3956 assert!(v.head.is_none());
3957 assert!(v.entries.is_empty());
3958 }
3959
3960 #[test]
3961 fn a_changed_entry_breaks_its_signature_and_the_next_link() {
3962 let (key, pk) = generate_keypair();
3963 let mut c = chain(&key, 3);
3964 let rewritten = link(
3967 &key,
3968 2,
3969 Some(&c[0]),
3970 &json!({"title": "Decision 2", "outcome": "REJECTED"}),
3971 at(21),
3972 );
3973 c[1] = rewritten;
3974 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
3975 assert!(!v.ok);
3976 assert!(
3977 v.entries[1].signature_valid && v.entries[1].link_valid,
3978 "the rewrite itself is well-formed: {:#?}",
3979 v.entries[1]
3980 );
3981 assert!(
3982 !v.entries[2].link_valid,
3983 "but entry 3 no longer points at it: {:#?}",
3984 v.entries[2]
3985 );
3986 assert!(
3987 v.entries[2]
3988 .problem
3989 .as_deref()
3990 .unwrap()
3991 .contains("prev_hash")
3992 );
3993 }
3994
3995 #[test]
3996 fn a_removed_entry_is_a_gap_and_a_broken_link() {
3997 let (key, pk) = generate_keypair();
3998 let mut c = chain(&key, 3);
3999 c.remove(1);
4000 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
4001 assert!(!v.ok);
4002 assert!(v.entries[0].link_valid);
4003 let p = v.entries[1].problem.as_deref().unwrap();
4004 assert!(p.contains("chain_seq 3 where 2 was expected"), "{p}");
4005 assert!(p.contains("prev_hash"), "{p}");
4006 }
4007
4008 #[test]
4011 fn a_summary_counts_and_names_the_failures() {
4012 let (key, pk) = generate_keypair();
4013 let clean =
4014 verify_chain(&chain(&key, 3), &pk, &anchored(&pk), &roots());
4015 let summary = clean.summary();
4016 assert!(summary.ok);
4017 assert_eq!((summary.entries, summary.failed.len()), (3, 0));
4018 assert_eq!(summary.head, clean.head);
4019 assert!(summary.first_failure.is_none());
4020 assert!(summary.how_to_read.contains("full=true"));
4021
4022 let mut c = chain(&key, 3);
4023 c.remove(1);
4024 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
4025 let summary = v.summary();
4026 assert!(!summary.ok);
4027 assert_eq!(summary.link_failures, 1);
4028 assert_eq!(summary.failed, vec![v.entries[1].id.clone()]);
4029 assert_eq!(summary.first_failure.as_ref(), Some(&v.entries[1]));
4030 let json = serde_json::to_value(&summary).unwrap();
4031 assert!(json["entries"].is_u64(), "a count, not the rows: {json}");
4032 }
4033
4034 #[test]
4035 fn a_second_genesis_is_rejected() {
4036 let (key, pk) = generate_keypair();
4037 let mut c = chain(&key, 2);
4038 let rogue = link(&key, 2, None, &json!({}), at(21));
4039 c[1] = rogue;
4040 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
4041 assert!(!v.ok);
4042 assert!(
4043 v.entries[1]
4044 .problem
4045 .as_deref()
4046 .unwrap()
4047 .contains("null mid-chain")
4048 );
4049 }
4050
4051 #[test]
4052 fn retroactive_and_out_of_order_are_recomputed_not_copied() {
4053 let (key, pk) = generate_keypair();
4054 let first = link(&key, 1, None, &json!({}), at(10 + 3600));
4055 let created = truncate_to_micros(at(5));
4058 let envelope = Envelope::new(
4059 gov(2),
4060 GovernanceLogEntryType::CouncilDecision,
4061 created,
4062 Some(first.attestation.entry_hash),
4063 data_hash(&json!({})),
4064 );
4065 let attestation = attest(&key, &envelope, 2, at(6));
4066 let mut second = GovernanceChainLink {
4067 id: gov(2),
4068 entry_type: GovernanceLogEntryType::CouncilDecision,
4069 created_at: created,
4070 attestation,
4071 data: None,
4072 texts: None,
4073 };
4074 second.attestation.retroactive = true; let v = verify_chain(&[first, second], &pk, &anchored(&pk), &roots());
4076 assert!(v.ok, "{v:#?}");
4077 assert!(v.entries[0].retroactive);
4078 assert!(!v.entries[1].retroactive, "recomputed from timestamps");
4079 assert!(v.entries[1].out_of_order);
4080 }
4081
4082 #[test]
4083 fn content_mismatch_settles_to_not_ok() {
4084 let (key, pk) = generate_keypair();
4085 let c = chain(&key, 1);
4086 let mut v = verify_chain(&c, &pk, &anchored(&pk), &roots());
4087 v.entries[0].content_matches = Some(true);
4088 assert!(v.clone().settle().ok);
4089 v.entries[0].content_matches = Some(false);
4090 assert!(!v.settle().ok);
4091 }
4092
4093 #[test]
4096 fn an_amendment_fills_amended_by_on_its_target() {
4097 let (key, pk) = generate_keypair();
4098 let mut c = Chain::new();
4099 let target = c.decision(&key);
4100 c.decision(&key);
4101 let amendment = AmendmentDraft::new(
4102 target,
4103 c.hash_at(1),
4104 AmendmentKind::NonPrecedential,
4105 "§1 (Red Team Cases Recharacterized)",
4106 "diagnostic finding — not citable as moderation precedent",
4107 )
4108 .unwrap()
4109 .with_authority(gov(5))
4110 .with_rationale("§5 leaves the ruling itself standing");
4111 let id = c.amend(&key, &amendment);
4112
4113 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4114 assert!(v.ok, "{v:#?}");
4115 assert_eq!(v.entries[0].amended_by, vec![id]);
4116 assert!(v.entries[1].amended_by.is_empty());
4117 assert!(!v.entries[0].redacted);
4118 assert_eq!(v.head, Some(amd(1)));
4119 assert_eq!(v.entries[2].content_matches, Some(true));
4121 assert_eq!(v.entries[0].content_matches, None);
4122 assert_eq!(
4123 standing([amendment.amendment.kind]),
4124 Standing::NonPrecedential
4125 );
4126 }
4127
4128 #[test]
4129 fn an_amendment_must_name_an_earlier_entry_by_its_exact_hash() {
4130 let (key, pk) = generate_keypair();
4131 let problem = |c: &Chain, at: usize| -> String {
4132 verify_chain(&c.links, &pk, &anchored(&pk), &roots()).entries[at]
4133 .problem
4134 .clone()
4135 .unwrap_or_default()
4136 };
4137
4138 let mut c = Chain::new();
4139 c.decision(&key);
4140 let unknown = AmendmentDraft::new(
4141 gov(99),
4142 c.hash_at(1),
4143 AmendmentKind::Overruled,
4144 "b",
4145 "n",
4146 )
4147 .unwrap();
4148 c.amend(&key, &unknown);
4149 assert!(
4150 problem(&c, 1).contains("is not an entry of this chain"),
4151 "{}",
4152 problem(&c, 1)
4153 );
4154 assert!(!verify_chain(&c.links, &pk, &anchored(&pk), &roots()).ok);
4155
4156 let mut c = Chain::new();
4158 c.decision(&key);
4159 let forward = AmendmentDraft::new(
4160 gov(2),
4161 c.hash_at(1),
4162 AmendmentKind::Overruled,
4163 "b",
4164 "n",
4165 )
4166 .unwrap();
4167 c.amend(&key, &forward);
4168 c.decision(&key);
4169 assert!(
4170 problem(&c, 1).contains("not an earlier entry"),
4171 "{}",
4172 problem(&c, 1)
4173 );
4174
4175 let mut c = Chain::new();
4177 let target = c.decision(&key);
4178 let wrong_hash = AmendmentDraft::new(
4179 target,
4180 data_hash(&json!("some other entry")),
4181 AmendmentKind::Overruled,
4182 "b",
4183 "n",
4184 )
4185 .unwrap();
4186 c.amend(&key, &wrong_hash);
4187 assert!(problem(&c, 1).contains("entry_hash"), "{}", problem(&c, 1));
4188 assert!(
4189 verify_chain(&c.links, &pk, &anchored(&pk), &roots()).entries[0]
4190 .amended_by
4191 .is_empty()
4192 );
4193 }
4194
4195 #[test]
4196 fn redaction_shape_violations_fail_verification() {
4197 let (key, pk) = generate_keypair();
4198 let amend_with = |mutate: &dyn Fn(&mut Amendment)| -> String {
4199 let mut c = Chain::new();
4200 let target = c.decision(&key);
4201 let mut amendment = AmendmentDraft::new(
4202 target,
4203 c.hash_at(1),
4204 AmendmentKind::Correction,
4205 "b",
4206 "n",
4207 )
4208 .unwrap();
4209 mutate(&mut amendment.amendment);
4210 c.amend_v1(&key, &amendment.amendment);
4211 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4212 assert!(!v.ok, "{v:#?}");
4213 v.entries[1].problem.clone().unwrap_or_default()
4214 };
4215
4216 assert_eq!(
4219 AmendmentDraft::new(
4220 gov(1),
4221 data_hash(&json!(null)),
4222 AmendmentKind::Redaction,
4223 "b",
4224 "n"
4225 ),
4226 Err(AmendmentError::MissingRedaction)
4227 );
4228 let p = amend_with(&|a| a.kind = AmendmentKind::Redaction);
4229 assert!(p.contains("requires a `redaction`"), "{p}");
4230
4231 let p = amend_with(&|a| {
4232 a.redaction = Some(Redaction {
4233 fields: vec!["/x".into()],
4234 resulting_data_hash: data_hash(&json!({})),
4235 resulting_latest_hash: None,
4236 })
4237 });
4238 assert!(p.contains("only valid on kind"), "{p}");
4239
4240 let p = amend_with(&|a| a.agora_governance_amendment = 3);
4241 assert!(p.contains("agora_governance_amendment is 3"), "{p}");
4242
4243 let p = amend_with(&|a| a.agora_governance_amendment = 1);
4246 assert!(p.contains("does neither consistently"), "{p}");
4247 let p = amend_with(&|a| a.note = AmendmentText::Plain("n".into()));
4248 assert!(p.contains("does neither consistently"), "{p}");
4249 }
4250
4251 #[test]
4252 fn governance_data_holds_no_number_that_is_not_a_64_bit_integer() {
4253 let fine = json!({"a": [1, -2, u64::MAX, i64::MIN], "b": {"c": "0.5"}});
4254 assert_eq!(non_integer_number(&fine), None);
4255 assert!(blind_data(&fine, Blind::random()).is_ok());
4256
4257 for (text, pointer) in [
4258 (r#"{"a": {"b/c": [1, 0.5]}}"#, "/a/b~1c/1"),
4259 (r#"{"n": 1.0}"#, "/n"),
4260 (r#"{"n": 1e3}"#, "/n"),
4261 (r#"{"n": 18446744073709551616}"#, "/n"),
4262 (r#"{"n": -9223372036854775809}"#, "/n"),
4263 ] {
4264 let data: serde_json::Value = serde_json::from_str(text).unwrap();
4265 assert_eq!(non_integer_number(&data).as_deref(), Some(pointer));
4266 assert_eq!(
4267 blind_data(&data, Blind::random()),
4268 Err(BlindError::NonIntegerNumber(pointer.into())),
4269 "the writer refuses it"
4270 );
4271 assert_eq!(
4272 redact_data(&data, &["/n".into()], &amd(1), Blind::random()),
4273 Err(RedactError::NonIntegerNumber(pointer.into()))
4274 );
4275 }
4276 }
4277
4278 #[test]
4279 fn standing_is_the_last_amendment_that_changes_it() {
4280 use AmendmentKind::*;
4281 assert_eq!(standing([]), Standing::InForce);
4282 assert_eq!(standing([Correction, Redaction]), Standing::InForce);
4283 assert_eq!(
4284 standing([Correction, NonPrecedential, Redaction]),
4285 Standing::NonPrecedential
4286 );
4287 assert_eq!(standing([Overruled, Reinstated]), Standing::InForce);
4288 assert_eq!(standing([Reinstated, Superseded]), Standing::Superseded);
4289 assert_eq!(kind_standing(Reattested), None);
4290 assert_eq!(kind_standing(Reinstated), Some(Standing::InForce));
4291 }
4292
4293 #[test]
4294 fn a_redaction_verifies_against_the_amendment_and_nothing_else() {
4295 let (key, pk) = generate_keypair();
4296 let data = json!({
4297 "finding": "upheld",
4298 "subject": {"handle": "someone", "detail": "personal"},
4299 });
4300 let mut c = Chain::new();
4301 let target = c.entry(&key, data.clone());
4302 let amendment_id = c.next_amd();
4303 let (amendment, redacted) = AmendmentDraft::redaction(
4304 &amendment_id,
4305 target,
4306 c.hash_at(1),
4307 "GDPR Art. 17(1)(a)",
4308 "personal data removed on request",
4309 vec!["/subject/handle".into(), "/subject/detail".into()],
4310 &data,
4311 Blind::from([7; 32]),
4312 &[],
4313 )
4314 .unwrap();
4315 assert_eq!(c.amend(&key, &amendment), amendment_id);
4316 assert_eq!(redacted[BLIND_KEY], json!(Blind::from([7; 32])));
4317
4318 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4319 assert!(v.ok, "{v:#?}");
4320 assert!(v.entries[0].redacted);
4321 assert_eq!(v.entries[0].redacted_data_hash, Some(data_hash(&redacted)));
4322 assert_eq!(
4323 redacted["subject"]["handle"],
4324 json!(format!("[redacted by {amendment_id}]"))
4325 );
4326 assert_eq!(redacted["finding"], json!("upheld"), "and nothing else");
4327
4328 assert!(v.check_content(&c.links[0], &redacted));
4330 assert_eq!(v.entries[0].content_matches, Some(true));
4331 assert!(v.clone().settle().ok);
4332 assert!(v.check_content(&c.links[0], &data));
4334
4335 let mut tampered = redacted.clone();
4337 tampered["finding"] = json!("overturned");
4338 assert!(!v.check_content(&c.links[0], &tampered));
4339 assert_eq!(v.entries[0].content_matches, Some(false));
4340 assert!(!v.settle().ok);
4341 }
4342
4343 fn seats() -> serde_json::Value {
4346 json!({
4347 "title": "A motion",
4348 "rounds": [{
4349 "number": 1,
4350 "responses": [
4351 {"role": "lawyer", "rationale": "Because.", "raw_text": "Because.", "vote": "yes"},
4352 {"role": "artist", "rationale": "Why not.", "raw_text": "Why not?", "vote": "no"},
4353 ],
4354 }],
4355 "subject": {"handle": "someone"},
4356 BLIND_KEY: Blind::from([3; 32]),
4357 })
4358 }
4359
4360 const LAWYER: &str = "/rounds/0/responses/0";
4361
4362 fn patch(ops: serde_json::Value) -> json_patch::Patch {
4364 serde_json::from_value(ops).unwrap()
4365 }
4366
4367 fn dedup(data: &serde_json::Value) -> Edit {
4369 let (raw, rationale) =
4370 (format!("{LAWYER}/raw_text"), format!("{LAWYER}/rationale"));
4371 Revision::remove_duplicates(data, &[(&raw, &rationale)]).unwrap()
4372 }
4373
4374 fn revise(
4375 c: &Chain,
4376 target: &GovernanceLogId,
4377 latest: &serde_json::Value,
4378 patch: impl Into<Edit>,
4379 ) -> (AmendmentDraft, serde_json::Value) {
4380 AmendmentDraft::revision(
4381 target.clone(),
4382 GovernanceLogEntryType::CouncilDecision,
4383 c.hash_at(1),
4384 "Steward's record REC-2026-0001",
4385 "raw_text identical to the rationale removed",
4386 latest,
4387 patch,
4388 )
4389 .unwrap()
4390 }
4391
4392 fn revision_of(draft: &AmendmentDraft) -> &Revision {
4393 draft.amendment.revision.as_ref().unwrap()
4394 }
4395
4396 #[test]
4397 fn a_revision_overwrites_nothing_and_folds_to_the_latest() {
4398 let (key, pk) = generate_keypair();
4399 let data = seats();
4400 let mut c = Chain::new();
4401 let target = c.entry(&key, data.clone());
4402 let (first, v1) = revise(&c, &target, &data, dedup(&data));
4403 c.amend(&key, &first);
4404 let (second, v2) = revise(
4405 &c,
4406 &target,
4407 &v1,
4408 patch(json!([{"op": "move", "from": "/title", "path": "/motion"}])),
4409 );
4410 c.amend(&key, &second);
4411
4412 let seat = &v1["rounds"][0]["responses"];
4413 assert!(seat[0].get("raw_text").is_none(), "removed, no marker");
4414 assert_eq!(seat[1]["raw_text"], json!("Why not?"), "the other stays");
4415 assert_eq!(v2["motion"], json!("A motion"));
4416 assert_eq!(v2[BLIND_KEY], data[BLIND_KEY], "blind kept");
4417 assert_eq!(
4418 latest(&data, [revision_of(&first), revision_of(&second)]).unwrap(),
4419 v2
4420 );
4421
4422 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4423 assert!(v.ok, "{v:#?}");
4424 let entry = &v.entries[0];
4425 assert_eq!(entry.revisions, [amd(1), amd(2)]);
4426 assert_eq!(entry.latest_data_hash, Some(data_hash(&v2)));
4427 assert!(!entry.redacted && entry.redacted_data_hash.is_none());
4428
4429 assert!(v.check_content(&c.links[0], &data));
4431 assert!(v.check_content(&c.links[0], &v2));
4432 assert!(!v.check_content(&c.links[0], &v1), "not the latest");
4433 v.check_content(&c.links[0], &data);
4434 assert!(v.clone().settle().ok);
4435
4436 let wire = serde_json::to_value(&v).unwrap();
4439 assert!(wire["entries"][0].get("history").is_none());
4440 let mut old = wire.clone();
4441 for field in ["revisions", "latest_data_hash", "superseded_revisions"] {
4442 old["entries"][0].as_object_mut().unwrap().remove(field);
4443 }
4444 let old: GovernanceVerification = serde_json::from_value(old).unwrap();
4445 assert!(old.entries[0].revisions.is_empty());
4446 }
4447
4448 #[test]
4450 fn a_revision_that_does_not_produce_its_hash_fails() {
4451 let (key, pk) = generate_keypair();
4452 let data = seats();
4453 let mut c = Chain::new();
4454 let target = c.entry(&key, data.clone());
4455 let (mut draft, _) = revise(&c, &target, &data, dedup(&data));
4456 draft
4457 .amendment
4458 .revision
4459 .as_mut()
4460 .unwrap()
4461 .resulting_data_hash = data_hash(&json!({"something": "else"}));
4462 c.amend(&key, &draft);
4463
4464 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4465 assert!(v.ok, "the chain itself is sound: {v:#?}");
4466 assert!(v.check_content(&c.links[0], &data), "the stored data is");
4467 assert!(
4468 v.entries[0]
4469 .problem
4470 .as_deref()
4471 .is_some_and(|p| p.contains("does not produce")),
4472 "{:?}",
4473 v.entries[0].problem
4474 );
4475 assert!(!v.settle().ok);
4476
4477 c.links[0].data = Some(data);
4479 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4480 assert!(!v.ok);
4481 }
4482
4483 fn redact(
4484 c: &Chain,
4485 target: &GovernanceLogId,
4486 fields: &[&str],
4487 data: &serde_json::Value,
4488 revisions: &[&Revision],
4489 ) -> Result<(AmendmentDraft, serde_json::Value), RedactError> {
4490 AmendmentDraft::redaction(
4491 &c.next_amd(),
4492 target.clone(),
4493 c.hash_at(1),
4494 "GDPR Art. 17(1)(a)",
4495 "removed on request",
4496 fields.iter().map(|f| f.to_string()).collect(),
4497 data,
4498 Blind::from([9; 32]),
4499 revisions,
4500 )
4501 }
4502
4503 #[test]
4506 fn a_revision_then_a_redaction() {
4507 let (key, pk) = generate_keypair();
4508 let data = seats();
4509 let mut c = Chain::new();
4510 let target = c.entry(&key, data.clone());
4511 let (revision, _) = revise(&c, &target, &data, dedup(&data));
4512 c.amend(&key, &revision);
4513 let rationale = format!("{LAWYER}/rationale");
4514 let (redaction, redacted) = redact(
4515 &c,
4516 &target,
4517 &[&rationale],
4518 &data,
4519 &[revision_of(&revision)],
4520 )
4521 .unwrap();
4522 c.amend(&key, &redaction);
4523
4524 let fields = &redaction.amendment.redaction.as_ref().unwrap().fields;
4526 assert_eq!(fields, &[rationale, format!("{LAWYER}/raw_text")]);
4527 let seat = &redacted["rounds"][0]["responses"][0];
4528 assert_eq!(seat["raw_text"], seat["rationale"]);
4529 assert!(seat["raw_text"].as_str().unwrap().starts_with("[redacted"));
4530
4531 let rebased = latest(&redacted, [revision_of(&revision)]).unwrap();
4532 assert!(
4533 rebased["rounds"][0]["responses"][0]
4534 .get("raw_text")
4535 .is_none()
4536 );
4537 assert_eq!(
4538 redaction
4539 .amendment
4540 .redaction
4541 .as_ref()
4542 .unwrap()
4543 .resulting_latest_hash,
4544 Some(data_hash(&rebased))
4545 );
4546
4547 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4548 assert!(v.ok, "{v:#?}");
4549 let entry = &v.entries[0];
4550 assert_eq!(entry.latest_data_hash, Some(data_hash(&rebased)));
4551 assert_eq!(entry.superseded_revisions, [amd(1)]);
4552 assert!(v.check_content(&c.links[0], &redacted));
4553 assert!(v.check_content(&c.links[0], &rebased));
4554 assert!(
4555 v.check_content(&c.links[0], &data),
4556 "for whoever kept a copy"
4557 );
4558 v.check_content(&c.links[0], &redacted);
4559 assert!(v.clone().settle().ok, "{v:#?}");
4560
4561 let mut c2 = Chain::new();
4563 let target = c2.entry(&key, data.clone());
4564 c2.amend(&key, &revision);
4565 let (mut lying, _) = redact(
4566 &c2,
4567 &target,
4568 &["/subject/handle"],
4569 &data,
4570 &[revision_of(&revision)],
4571 )
4572 .unwrap();
4573 let (_, honest) = redact(
4574 &c2,
4575 &target,
4576 &["/subject/handle"],
4577 &data,
4578 &[revision_of(&revision)],
4579 )
4580 .unwrap();
4581 lying
4582 .amendment
4583 .redaction
4584 .as_mut()
4585 .unwrap()
4586 .resulting_latest_hash = Some(data_hash(&json!({})));
4587 c2.amend(&key, &lying);
4588 let mut v = verify_chain(&c2.links, &pk, &anchored(&pk), &roots());
4589 assert!(v.check_content(&c2.links[0], &honest));
4590 assert!(!v.settle().ok);
4591 }
4592
4593 #[test]
4596 fn a_redaction_of_a_revised_entry_must_name_the_latest_hash() {
4597 let (key, pk) = generate_keypair();
4598 let data = seats();
4599 let mut c = Chain::new();
4600 let target = c.entry(&key, data.clone());
4601 let (revision, _) = revise(&c, &target, &data, dedup(&data));
4602 c.amend(&key, &revision);
4603 let (redaction, _) =
4605 redact(&c, &target, &["/subject/handle"], &data, &[]).unwrap();
4606 c.amend(&key, &redaction);
4607 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4608 assert!(!v.ok);
4609 assert!(
4610 v.entries[2]
4611 .problem
4612 .as_deref()
4613 .is_some_and(|p| p.contains("names no resulting_latest_hash")),
4614 "{v:#?}"
4615 );
4616 }
4617
4618 #[test]
4621 fn a_false_duplicate_fails() {
4622 let (key, pk) = generate_keypair();
4623 let data = seats();
4624 let mut c = Chain::new();
4625 let target = c.entry(&key, data.clone());
4626 let artist = "/rounds/0/responses/1";
4627 let (raw, rationale) =
4628 (format!("{artist}/raw_text"), format!("{artist}/rationale"));
4629 let (mut lying, _) = revise(
4631 &c,
4632 &target,
4633 &data,
4634 patch(json!([{"op": "remove", "path": raw}])),
4635 );
4636 let revised = apply_patch(&data, &revision_of(&lying).patch).unwrap();
4637 let r = lying.amendment.revision.as_mut().unwrap();
4638 r.duplicates = vec![(raw, rationale)];
4639 r.resulting_data_hash = data_hash(&revised);
4640 c.amend(&key, &lying);
4641
4642 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4643 assert!(v.ok, "unread, the claim is unchecked: {v:#?}");
4644 assert!(v.check_content(&c.links[0], &data));
4645 assert!(
4646 v.entries[0]
4647 .problem
4648 .as_deref()
4649 .is_some_and(|p| p.contains("but they differ")),
4650 "{v:#?}"
4651 );
4652 assert!(!v.settle().ok);
4653 }
4654
4655 #[test]
4657 fn a_redaction_that_breaks_a_revision_is_refused() {
4658 let (key, _) = generate_keypair();
4659 let data = seats();
4660 let mut c = Chain::new();
4661 let target = c.entry(&key, data.clone());
4662 let (moved, _) = revise(
4663 &c,
4664 &target,
4665 &data,
4666 patch(
4667 json!([{"op": "move", "from": "/subject/handle", "path": "/handle"}]),
4668 ),
4669 );
4670 c.amend(&key, &moved);
4671 let err =
4672 redact(&c, &target, &["/subject"], &data, &[revision_of(&moved)])
4673 .unwrap_err();
4674 assert!(matches!(err, RedactError::Rebase(_)), "{err}");
4675 assert!(
4677 redact(
4678 &c,
4679 &target,
4680 &["/subject/handle"],
4681 &data,
4682 &[revision_of(&moved)]
4683 )
4684 .is_ok()
4685 );
4686 }
4687
4688 #[test]
4691 fn a_redaction_follows_every_duplicate_of_what_it_erases() {
4692 let revision = Revision {
4693 patch: patch(json!([{"op": "remove", "path": "/copy"}])),
4694 duplicates: vec![("/copy".into(), "/source/inner".into())],
4695 resulting_data_hash: data_hash(&json!(null)),
4696 };
4697 let extra = |fields: &[&str]| {
4698 let fields: Vec<String> =
4699 fields.iter().map(|f| f.to_string()).collect();
4700 duplicates_of(&fields, &[&revision])
4701 };
4702 assert_eq!(extra(&["/source/inner"]), ["/copy"]);
4703 assert_eq!(extra(&["/source/inner/name"]), ["/copy/name"]);
4704 assert_eq!(extra(&["/source"]), ["/copy"]);
4705 assert!(extra(&["/source/other"]).is_empty());
4706 assert!(extra(&["/source/inn"]).is_empty());
4707 }
4708
4709 #[test]
4712 fn a_redaction_then_a_revision() {
4713 let (key, pk) = generate_keypair();
4714 let data = seats();
4715 let mut c = Chain::new();
4716 let target = c.entry(&key, data.clone());
4717 let (redaction, redacted) =
4718 redact(&c, &target, &["/subject/handle"], &data, &[]).unwrap();
4719 assert_eq!(
4720 redaction
4721 .amendment
4722 .redaction
4723 .as_ref()
4724 .unwrap()
4725 .resulting_latest_hash,
4726 None,
4727 "nothing to rebase"
4728 );
4729 c.amend(&key, &redaction);
4730 let (revision, revised) =
4731 revise(&c, &target, &redacted, dedup(&redacted));
4732 c.amend(&key, &revision);
4733
4734 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4735 assert!(v.ok, "{v:#?}");
4736 assert_eq!(v.entries[0].latest_data_hash, Some(data_hash(&revised)));
4737 assert!(v.entries[0].superseded_revisions.is_empty());
4738 assert!(v.check_content(&c.links[0], &redacted));
4739 assert!(v.check_content(&c.links[0], &revised));
4740 assert!(v.settle().ok);
4741 }
4742
4743 #[test]
4744 fn a_revision_refuses_what_it_should() {
4745 let data = seats();
4746 let raw = &format!("{LAWYER}/raw_text");
4747 let rationale = &format!("{LAWYER}/rationale");
4748 let artist = "/rounds/0/responses/1";
4749 let dedup =
4750 |pairs: &[(&str, &str)]| Revision::remove_duplicates(&data, pairs);
4751
4752 assert_eq!(
4753 dedup(&[(
4754 &format!("{artist}/raw_text"),
4755 &format!("{artist}/rationale")
4756 )]),
4757 Err(ReviseError::NotIdentical {
4758 path: format!("{artist}/raw_text"),
4759 same_as: format!("{artist}/rationale"),
4760 })
4761 );
4762 assert_eq!(
4763 dedup(&[(raw, rationale), (rationale, raw)]),
4764 Err(ReviseError::SourceRemoved {
4765 path: raw.clone(),
4766 same_as: rationale.clone(),
4767 }),
4768 "a same_as the patch removes"
4769 );
4770 assert_eq!(
4771 dedup(&[("/nope", rationale)]),
4772 Err(ReviseError::Unresolved("/nope".into()))
4773 );
4774
4775 let make = |target_type, p: Edit| {
4776 AmendmentDraft::revision(
4777 gov(1),
4778 target_type,
4779 data_hash(&json!(null)),
4780 "b",
4781 "n",
4782 &data,
4783 p,
4784 )
4785 .map(|(_, v)| v)
4786 };
4787 let council = GovernanceLogEntryType::CouncilDecision;
4788 assert_eq!(
4789 make(council, patch(json!([])).into()),
4790 Err(ReviseError::EmptyPatch)
4791 );
4792 assert!(matches!(
4793 make(
4794 council,
4795 patch(json!([{"op": "remove", "path": "/nope"}])).into()
4796 ),
4797 Err(ReviseError::Patch(_))
4798 ));
4799 assert_eq!(
4800 make(
4801 council,
4802 patch(json!([{"op": "remove", "path": "/_blind"}])).into()
4803 ),
4804 Err(ReviseError::BlindPointer("/_blind".into()))
4805 );
4806 assert_eq!(
4807 make(
4808 council,
4809 patch(json!([{"op": "copy", "from": "/_blind", "path": "/b"}]))
4810 .into()
4811 ),
4812 Err(ReviseError::BlindPointer("/_blind".into()))
4813 );
4814 for entry_type in [
4815 GovernanceLogEntryType::Amendment,
4816 GovernanceLogEntryType::KeyRotation,
4817 GovernanceLogEntryType::StewardRecord,
4818 ] {
4819 assert!(!is_revisable(entry_type));
4820 assert_eq!(
4821 make(entry_type, dedup(&[(raw, rationale)]).unwrap()),
4822 Err(ReviseError::NotRevisable(entry_type))
4823 );
4824 }
4825 assert!(is_revisable(council));
4826 }
4827
4828 #[test]
4830 fn a_revision_that_adds_content_blinds_the_target() {
4831 let unblinded =
4832 json!({"title": "Old", "rationale": "r", "raw_text": "r"});
4833 let revise = |data: &serde_json::Value, p: serde_json::Value| {
4834 AmendmentDraft::revision(
4835 gov(1),
4836 GovernanceLogEntryType::CouncilDecision,
4837 data_hash(&json!(null)),
4838 "b",
4839 "n",
4840 data,
4841 patch(p),
4842 )
4843 .unwrap()
4844 };
4845 let add = json!([{"op": "add", "path": "/attachments", "value": []}]);
4846 let (draft, revised) = revise(&unblinded, add.clone());
4847 assert!(revised.get(BLIND_KEY).is_some());
4848 assert_eq!(revision_of(&draft).patch.len(), 2, "in the same patch");
4849 assert_eq!(
4850 latest(&unblinded, [revision_of(&draft)]).unwrap(),
4851 revised,
4852 "so the fold reproduces it"
4853 );
4854
4855 let (_, revised) =
4856 revise(&unblinded, json!([{"op": "remove", "path": "/raw_text"}]));
4857 assert!(revised.get(BLIND_KEY).is_none(), "a removal adds nothing");
4858
4859 let (draft, revised) = revise(&seats(), add);
4860 assert_eq!(revised[BLIND_KEY], seats()[BLIND_KEY], "never rotated");
4861 assert_eq!(revision_of(&draft).patch.len(), 1);
4862 }
4863
4864 #[test]
4865 fn revision_shape_violations_fail_verification() {
4866 let (key, pk) = generate_keypair();
4867 let amend_with = |mutate: &dyn Fn(&mut Amendment)| -> String {
4868 let mut c = Chain::new();
4869 let target = c.entry(&key, seats());
4870 let (mut draft, _) = revise(&c, &target, &seats(), dedup(&seats()));
4871 mutate(&mut draft.amendment);
4872 c.amend_v1(&key, &draft.amendment);
4873 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4874 assert!(!v.ok, "{v:#?}");
4875 v.entries[1].problem.clone().unwrap_or_default()
4876 };
4877 assert_eq!(
4878 AmendmentDraft::new(
4879 gov(1),
4880 data_hash(&json!(null)),
4881 AmendmentKind::Revision,
4882 "b",
4883 "n"
4884 ),
4885 Err(AmendmentError::MissingRevision)
4886 );
4887 let p = amend_with(&|a| a.revision = None);
4888 assert!(p.contains("requires a `revision`"), "{p}");
4889 let p = amend_with(&|a| a.kind = AmendmentKind::Correction);
4890 assert!(p.contains("only valid on kind `revision`"), "{p}");
4891 let p = amend_with(&|a| a.revision.as_mut().unwrap().patch.0.clear());
4892 assert!(p.contains("at least one op"), "{p}");
4893 }
4894
4895 #[cfg(feature = "schemars")]
4897 #[test]
4898 fn the_revision_schema_describes_a_patch() {
4899 let schema = crate::responses::inline_schema_for::<Amendment>();
4900 let revision = &schema["properties"]["revision"]["properties"];
4901 assert_eq!(revision["patch"]["type"], json!("array"), "{schema}");
4902 assert_eq!(
4903 revision["patch"]["items"]["properties"]["op"]["enum"],
4904 json!(["add", "remove", "replace", "move", "copy", "test"])
4905 );
4906 assert_eq!(revision["resulting_data_hash"]["type"], json!("string"));
4907 }
4908
4909 #[test]
4910 fn content_that_matches_nothing_fails_without_an_amendment() {
4911 let (key, pk) = generate_keypair();
4912 let mut c = Chain::new();
4913 c.entry(&key, json!({"a": 1}));
4914 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4915 assert!(!v.check_content(&c.links[0], &json!({"a": 2})));
4916 assert!(!v.clone().settle().ok);
4917 let other = link(&key, 9, None, &json!({}), at(9));
4919 assert!(!v.check_content(&other, &json!({})));
4920 }
4921
4922 #[test]
4923 fn tampering_with_an_amendments_data_is_caught_by_the_data_hash() {
4924 let (key, pk) = generate_keypair();
4925 let mut c = Chain::new();
4926 let target = c.decision(&key);
4927 let amendment = AmendmentDraft::new(
4928 target,
4929 c.hash_at(1),
4930 AmendmentKind::Overruled,
4931 "b",
4932 "overruled by a later decision",
4933 )
4934 .unwrap();
4935 c.amend(&key, &amendment);
4936 c.links[1].data.as_mut().unwrap()["note"] =
4937 json!("reinstated, actually");
4938
4939 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4940 assert!(!v.ok, "{v:#?}");
4941 let p = v.entries[1].problem.as_deref().unwrap();
4942 assert!(p.contains("does not hash to the attested data_hash"), "{p}");
4943 assert!(
4944 v.entries[0].amended_by.is_empty(),
4945 "an unreadable amendment has no effect"
4946 );
4947 assert!(
4948 v.entries[1].signature_valid && v.entries[1].link_valid,
4949 "the envelope is untouched — only the content is not what it \
4950 committed to"
4951 );
4952 }
4953
4954 #[test]
4955 fn an_amendment_or_rotation_must_carry_its_data() {
4956 let (key, pk) = generate_keypair();
4957 let mut c = Chain::new();
4958 let target = c.decision(&key);
4959 let amendment = AmendmentDraft::new(
4960 target,
4961 c.hash_at(1),
4962 AmendmentKind::Correction,
4963 "b",
4964 "n",
4965 )
4966 .unwrap();
4967 c.amend(&key, &amendment);
4968 let rotation = c.routine(&pk, &generate_keypair().0);
4969 c.rotate(&key, &rotation);
4970 c.links[1].data = None;
4971 c.links[2].data = None;
4972
4973 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4974 assert!(!v.ok, "{v:#?}");
4975 for (i, entry_type) in [(1, "amendment"), (2, "key_rotation")] {
4976 let p = v.entries[i].problem.as_deref().unwrap();
4977 assert!(p.contains(&format!("a {entry_type} entry")), "{p}");
4978 assert!(p.contains("must carry its `data`"), "{p}");
4979 }
4980 }
4981
4982 #[test]
4983 fn the_id_series_must_match_the_entry_type() {
4984 let (key, pk) = generate_keypair();
4985 let mut c = Chain::new();
4986 let target = c.decision(&key);
4987 let amendment = AmendmentDraft::new(
4988 target,
4989 c.hash_at(1),
4990 AmendmentKind::Correction,
4991 "b",
4992 "n",
4993 )
4994 .unwrap();
4995 c.push(
4996 &key,
4997 gov(7),
4998 GovernanceLogEntryType::Amendment,
4999 serde_json::to_value(&amendment.amendment).unwrap(),
5000 true,
5001 );
5002 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
5003 assert!(!v.ok, "{v:#?}");
5004 let p = v.entries[1].problem.as_deref().unwrap();
5005 assert!(p.contains("must be in the AMD- series"), "{p}");
5006
5007 let mut c = Chain::new();
5008 c.push(
5009 &key,
5010 key_id(1),
5011 GovernanceLogEntryType::CouncilDecision,
5012 json!({}),
5013 false,
5014 );
5015 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
5016 let p = v.entries[0].problem.as_deref().unwrap();
5017 assert!(p.contains("reserved"), "{p}");
5018 }
5019
5020 #[test]
5021 fn redact_data_replaces_whole_values_and_refuses_the_rest() {
5022 let id = amd(3);
5023 let blind = Blind::from([9; 32]);
5024 let data = json!({"a": {"b": [1, {"c": "secret"}]}, "d/e": "slash"});
5025 let out = redact_data(
5026 &data,
5027 &["/a/b/1/c".into(), "/d~1e".into()],
5028 &id,
5029 blind,
5030 )
5031 .unwrap();
5032 assert_eq!(out["a"]["b"][1]["c"], json!(redaction_marker(&id)));
5033 assert_eq!(out["d/e"], json!(redaction_marker(&id)));
5034 assert_eq!(out["a"]["b"][0], json!(1), "untouched");
5035 assert_eq!(out[BLIND_KEY], json!(blind), "a legacy entry gains one");
5036
5037 assert_eq!(
5038 redact_data(&data, &["/a/nope".into()], &id, blind),
5039 Err(RedactError::Unresolved("/a/nope".into()))
5040 );
5041 assert_eq!(
5042 redact_data(&data, &["".into()], &id, blind),
5043 Err(RedactError::WholeEntry)
5044 );
5045 assert_eq!(
5046 redact_data(&data, &[], &id, blind),
5047 Err(RedactError::NoFields)
5048 );
5049 assert_eq!(
5050 redact_data(&data, &["/_blind".into()], &id, blind),
5051 Err(RedactError::BlindPointer("/_blind".into()))
5052 );
5053 }
5054
5055 #[test]
5056 fn blind_data_is_for_objects_and_is_the_writers_to_supply() {
5057 let blind = Blind::from([1; 32]);
5058 let out = blind_data(&json!({"finding": "upheld"}), blind).unwrap();
5059 assert_eq!(out, json!({"finding": "upheld", "_blind": blind}));
5060 assert_eq!(
5061 blind_data(&json!([1]), blind),
5062 Err(BlindError::NotAnObject)
5063 );
5064 assert_eq!(blind_data(&out, blind), Err(BlindError::AlreadyBlinded));
5065 assert_ne!(Blind::random(), Blind::random());
5066
5067 use GovernanceLogEntryType::*;
5068 assert!(!is_redactable(Amendment) && !is_redactable(KeyRotation));
5069 assert!(
5070 is_redactable(CouncilDecision) && is_redactable(EmergencyAction)
5071 );
5072 }
5073
5074 #[test]
5078 fn a_removed_value_cannot_be_confirmed_by_guessing_it() {
5079 fn confirms(
5081 public: &serde_json::Value,
5082 pointer: &str,
5083 guess: &str,
5084 hash: Sha256Hex,
5085 ) -> bool {
5086 let mut attempt = public.clone();
5087 *attempt.pointer_mut(pointer).unwrap() = json!(guess);
5088 data_hash(&attempt) == hash
5089 }
5090 let legacy = json!({"finding": "upheld", "handle": "someone", "city": "Utrecht"});
5091
5092 let written = blind_data(&legacy, Blind::random()).unwrap();
5094 let first = redact_data(
5095 &written,
5096 &["/handle".into()],
5097 &amd(1),
5098 Blind::random(),
5099 )
5100 .unwrap();
5101 assert!(!confirms(&first, "/handle", "someone", data_hash(&written)));
5102
5103 let second =
5106 redact_data(&first, &["/city".into()], &amd(2), Blind::random())
5107 .unwrap();
5108 assert!(!confirms(&second, "/city", "Utrecht", data_hash(&first)));
5109
5110 let mut stripped =
5114 redact_data(&legacy, &["/handle".into()], &amd(3), Blind::random())
5115 .unwrap();
5116 let legacy_first = stripped.clone();
5117 stripped.as_object_mut().unwrap().remove(BLIND_KEY);
5118 assert!(confirms(
5119 &stripped,
5120 "/handle",
5121 "someone",
5122 data_hash(&legacy)
5123 ));
5124 let legacy_second = redact_data(
5126 &legacy_first,
5127 &["/city".into()],
5128 &amd(4),
5129 Blind::random(),
5130 )
5131 .unwrap();
5132 assert!(!confirms(
5133 &legacy_second,
5134 "/city",
5135 "Utrecht",
5136 data_hash(&legacy_first)
5137 ));
5138 }
5139
5140 #[test]
5143 fn a_routine_rotation_moves_the_chain_to_the_new_key() {
5144 let (old, old_pk) = generate_keypair();
5145 let (new, new_pk) = generate_keypair();
5146 let mut c = Chain::new();
5147 c.decision(&old);
5148 let rotation = c.routine(&old_pk, &new);
5149 let rotation_id = c.rotate(&old, &rotation);
5150 c.decision(&new);
5151
5152 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5155 assert!(v.ok, "{v:#?}");
5156 assert_eq!(v.public_key, (&new_pk).into());
5157 assert_eq!(
5158 v.entries[1].signed_by,
5159 Some((&old_pk).into()),
5160 "the rotation itself is signed by the old key"
5161 );
5162 assert_eq!(v.entries[2].signed_by, Some((&new_pk).into()));
5163 assert!(v.unanchored_keys.is_empty());
5164 assert!(v.repudiated.is_empty());
5165 assert_eq!(v.keys.len(), 2);
5166 assert_eq!(v.keys[0].public_key, (&old_pk).into());
5167 assert_eq!(v.keys[0].from_seq, 1);
5168 assert_eq!(v.keys[0].through_seq, Some(2));
5169 assert_eq!(v.keys[0].status, KeyStatus::Retired);
5170 assert_eq!(v.keys[0].introduced_by, None);
5171 assert_eq!(v.keys[0].retired_by.as_ref(), Some(&rotation_id));
5172 assert!(v.keys[0].certified, "retroactively, by the rotation");
5173 assert_eq!(v.keys[1].from_seq, 3);
5174 assert_eq!(v.keys[1].through_seq, None);
5175 assert_eq!(v.keys[1].status, KeyStatus::Active);
5176 assert_eq!(v.keys[1].introduced_by.as_ref(), Some(&rotation_id));
5177 assert!(v.keys[1].certified);
5178 }
5179
5180 #[test]
5181 fn the_old_key_cannot_sign_after_a_routine_rotation() {
5182 let (old, old_pk) = generate_keypair();
5183 let (new, _) = generate_keypair();
5184 let mut c = Chain::new();
5185 c.decision(&old);
5186 let rotation = c.routine(&old_pk, &new);
5187 c.rotate(&old, &rotation);
5188 c.decision(&old);
5189
5190 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5191 assert!(!v.ok, "{v:#?}");
5192 assert!(!v.entries[2].signature_valid);
5193 assert!(
5194 v.entries[2].link_valid,
5195 "the linkage is fine; the key is not"
5196 );
5197 }
5198
5199 #[test]
5202 fn the_online_key_cannot_certify_its_own_successor() {
5203 let (old, old_pk) = generate_keypair();
5204 let (thief, _) = generate_keypair();
5205 let mut c = Chain::new();
5206 c.decision(&old);
5207 let mut rotation = c.routine(&old_pk, &thief);
5208 let statement = rotation.certificate.statement.clone();
5210 rotation.certificate = certify(&[&old], statement);
5211 c.rotate(&old, &rotation);
5212 c.decision(&thief);
5213
5214 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5215 assert!(!v.ok, "{v:#?}");
5216 let p = v.entries[1].problem.as_deref().unwrap();
5217 assert!(p.contains("0 valid root signature"), "{p}");
5218 assert_eq!(v.public_key, (&old_pk).into(), "the chain does not move");
5219 assert!(!v.entries[2].signature_valid);
5220 assert_eq!(v.keys.len(), 1);
5221 }
5222
5223 #[test]
5224 fn a_certificate_counts_distinct_known_roots_only() {
5225 let (old, old_pk) = generate_keypair();
5226 let (new, _) = generate_keypair();
5227 let (stranger, _) = generate_keypair();
5228 let two_of_two = RootSet::new(
5229 [
5230 (&root(1).verifying_key()).into(),
5231 (&root(2).verifying_key()).into(),
5232 ],
5233 2,
5234 );
5235 let verdict = |signers: &[&SigningKey], roots: &RootSet| {
5236 let mut c = Chain::new();
5237 c.decision(&old);
5238 let mut rotation = c.routine(&old_pk, &new);
5239 let statement = rotation.certificate.statement.clone();
5240 rotation.certificate = certify(signers, statement);
5241 let genesis = KeyCertStatement::genesis((&old_pk).into());
5243 rotation.outgoing_certificate =
5244 Some(certify(&[&root(1), &root(2)], genesis));
5245 c.rotate(&old, &rotation);
5246 verify_chain(&c.links, &old_pk, &anchored(&old_pk), roots)
5247 };
5248
5249 assert!(verdict(&[&root(1), &root(2)], &two_of_two).ok);
5250 assert!(verdict(&[&root(2)], &roots()).ok, "either root, 1-of-2");
5251
5252 for (signers, why) in [
5253 (vec![&root(1)], "below the threshold"),
5254 (vec![&root(1), &root(1)], "one root twice is one root"),
5255 (vec![&root(1), &stranger], "an unknown root is nobody"),
5256 (vec![], "unsigned"),
5257 ] {
5258 let v = verdict(&signers, &two_of_two);
5259 assert!(!v.ok, "{why}: {v:#?}");
5260 let p = v.entries[1].problem.as_deref().unwrap();
5261 assert!(p.contains("where 2 are needed"), "{why}: {p}");
5262 }
5263
5264 assert!(verdict(&[&stranger, &root(1)], &roots()).ok);
5266 }
5267
5268 #[test]
5269 fn a_certificate_is_good_for_one_statement_at_one_position() {
5270 let (old, old_pk) = generate_keypair();
5271 let (new, new_pk) = generate_keypair();
5272 let (other, other_pk) = generate_keypair();
5273 let anchor = anchored(&old_pk);
5274
5275 let mut c = Chain::new();
5279 c.decision(&old);
5280 let early = c.routine(&old_pk, &new);
5281 c.decision(&old);
5282 let mut rotation = c.routine(&old_pk, &new);
5283 rotation.certificate = early.certificate;
5284 c.rotate(&old, &rotation);
5285 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5286 assert!(!v.ok, "{v:#?}");
5287 let p = v.entries[2].problem.as_deref().unwrap();
5288 assert!(
5289 p.contains("different key, purpose or chain position"),
5290 "{p}"
5291 );
5292 assert_eq!(v.public_key, (&old_pk).into());
5293
5294 let mut c = Chain::new();
5296 c.decision(&old);
5297 let for_new = c.routine(&old_pk, &new);
5298 let mut rotation = c.routine(&old_pk, &other);
5299 rotation.certificate = for_new.certificate;
5300 c.rotate(&old, &rotation);
5301 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5302 let p = v.entries[1].problem.as_deref().unwrap();
5303 assert!(
5304 p.contains("different key, purpose or chain position"),
5305 "{p}"
5306 );
5307
5308 let mut c = Chain::new();
5310 c.decision(&old);
5311 let mut rotation = c.routine(&old_pk, &other);
5312 rotation.certificate = for_new_at(&c, &new_pk);
5313 rotation.certificate.statement.key = (&other_pk).into();
5314 c.rotate(&old, &rotation);
5315 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5316 let p = v.entries[1].problem.as_deref().unwrap();
5317 assert!(p.contains("0 valid root signature"), "{p}");
5318
5319 let mut c = Chain::new();
5321 c.decision(&old);
5322 c.decision(&old);
5323 let mut rotation = c.compromise(&old_pk, &new, 1);
5324 rotation.certificate = for_new_at(&c, &new_pk);
5325 c.rotate(&new, &rotation);
5326 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5327 assert!(!v.ok);
5328 assert!(v.repudiated.is_empty(), "{v:#?}");
5329 assert_eq!(v.public_key, (&old_pk).into());
5330 }
5331
5332 #[test]
5335 fn a_root_signature_without_the_domain_prefix_certifies_nothing() {
5336 use ed25519_dalek::Signer;
5337 let (old, old_pk) = generate_keypair();
5338 let (new, _) = generate_keypair();
5339 let mut c = Chain::new();
5340 c.decision(&old);
5341 let mut rotation = c.routine(&old_pk, &new);
5342 let statement = rotation.certificate.statement.clone();
5343 let bare = canonical_json(&serde_json::to_value(&statement).unwrap());
5344 assert_eq!(
5345 statement.signed_bytes(),
5346 [ROOT_DOMAIN, bare.as_slice()].concat()
5347 );
5348 rotation.certificate =
5349 KeyCertificate::unsigned(statement).with(RootSignature {
5350 root_key: (&root(1).verifying_key()).into(),
5351 signature: root(1).sign(&bare).into(),
5352 });
5353 c.rotate(&old, &rotation);
5354 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5355 assert!(!v.ok, "{v:#?}");
5356 assert_eq!(v.public_key, (&old_pk).into());
5357 }
5358
5359 #[test]
5360 fn the_first_rotation_carries_the_genesis_certificate_and_only_it_does() {
5361 let (k1, k1_pk) = generate_keypair();
5362 let (k2, k2_pk) = generate_keypair();
5363 let (k3, _) = generate_keypair();
5364 let genesis =
5365 || certify(&[&root(1)], KeyCertStatement::genesis((&k1_pk).into()));
5366
5367 let mut c = Chain::new();
5369 c.decision(&k1);
5370 let mut rotation = c.routine(&k1_pk, &k2);
5371 rotation.outgoing_certificate = None;
5372 c.rotate(&k1, &rotation);
5373 let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5374 assert!(!v.ok);
5375 let p = v.entries[1].problem.as_deref().unwrap();
5376 assert!(p.contains("genesis key's outgoing_certificate"), "{p}");
5377 assert_eq!(v.public_key, (&k1_pk).into());
5378
5379 let mut c = Chain::new();
5381 c.decision(&k1);
5382 let rotation = c.routine(&k1_pk, &k2).with_outgoing(certify(
5383 &[&root(1)],
5384 KeyCertStatement::genesis((&k2_pk).into()),
5385 ));
5386 c.rotate(&k1, &rotation);
5387 let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5388 let p = v.entries[1].problem.as_deref().unwrap();
5389 assert!(p.starts_with("outgoing_certificate:"), "{p}");
5390
5391 let mut c = Chain::new();
5394 c.decision(&k1);
5395 let rotation = c.routine(&k1_pk, &k2);
5396 assert_eq!(rotation.outgoing_certificate, Some(genesis()));
5397 c.rotate(&k1, &rotation);
5398 let v = verify_chain(&c.links, &k1_pk, &KeyAnchor::default(), &roots());
5399 assert!(v.ok, "{v:#?}");
5400 assert!(v.unanchored_keys.is_empty(), "{v:#?}");
5401
5402 let again = c.routine(&k2_pk, &k3).with_outgoing(genesis());
5404 c.rotate(&k2, &again);
5405 let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5406 assert!(!v.ok);
5407 let p = v.entries[2].problem.as_deref().unwrap();
5408 assert!(p.contains("nowhere else"), "{p}");
5409 }
5410
5411 #[test]
5412 fn a_forged_proof_of_possession_is_rejected() {
5413 let (old, old_pk) = generate_keypair();
5414 let (_, new_pk) = generate_keypair();
5415 let (thief, _) = generate_keypair();
5416 let mut c = Chain::new();
5417 c.decision(&old);
5418 let mut rotation = c.routine(&old_pk, &thief);
5421 rotation.new_key = (&new_pk).into();
5422 rotation.certificate = for_new_at(&c, &new_pk);
5423 let statement = rotation.statement(c.prev_hash());
5424 rotation.proof = crypto::sign(
5425 &old,
5426 statement.hash().as_bytes(),
5427 rotation.proof_signed_at,
5428 )
5429 .into();
5430 c.rotate(&old, &rotation);
5431
5432 assert_eq!(
5433 rotation.verify_proof(c.links[1].attestation.prev_hash),
5434 Err(RotationError::BadProof)
5435 );
5436 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5437 assert!(!v.ok, "{v:#?}");
5438 assert!(
5439 v.entries[1]
5440 .problem
5441 .as_deref()
5442 .unwrap()
5443 .contains("proof of possession")
5444 );
5445 assert_eq!(v.public_key, (&old_pk).into(), "the chain does not move");
5446 }
5447
5448 #[test]
5449 fn a_compromise_repudiates_the_window_and_a_reattestation_restores_one() {
5450 let (old, old_pk) = generate_keypair();
5451 let (new, new_pk) = generate_keypair();
5452 let mut c = Chain::new();
5453 c.decision(&old); c.decision(&old); let reattested = c.decision(&old); let rotation = c.compromise(&old_pk, &new, 1);
5457 let rotation_id = c.rotate(&new, &rotation); let vouch = AmendmentDraft::new(
5459 reattested,
5460 c.hash_at(3),
5461 AmendmentKind::Reattested,
5462 "Art. VII",
5463 "independently verified; the Steward vouches for it",
5464 )
5465 .unwrap();
5466 let vouch_id = c.amend(&new, &vouch); let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5469 assert!(
5470 v.ok,
5471 "repudiation is a declared state, not a defect: {v:#?}"
5472 );
5473 assert_eq!(v.repudiated, vec![gov(2)]);
5474 assert!(v.entries[1].repudiated);
5475 assert!(!v.entries[2].repudiated);
5476 assert_eq!(v.entries[2].reattested_by, vec![vouch_id.clone()]);
5477 assert_eq!(v.entries[2].amended_by, vec![vouch_id]);
5478 assert_eq!(v.entries[3].signed_by, Some((&new_pk).into()));
5479 assert_eq!(v.public_key, (&new_pk).into());
5480 assert_eq!(v.keys.len(), 2);
5481 assert_eq!(v.keys[0].status, KeyStatus::Compromised);
5482 assert_eq!(
5483 v.keys[0].through_seq,
5484 Some(1),
5485 "trusted through the last trusted entry, not through the \
5486 declaration"
5487 );
5488 assert_eq!(v.keys[0].retired_by.as_ref(), Some(&rotation_id));
5489 assert_eq!(v.keys[1].from_seq, 4, "the declaration is its own first");
5490 }
5491
5492 #[test]
5495 fn last_trusted_is_the_roots_to_say() {
5496 let (old, old_pk) = generate_keypair();
5497 let (new, _) = generate_keypair();
5498 let mut c = Chain::new();
5499 c.decision(&old); c.decision(&old); let mut rotation = c.compromise(&old_pk, &new, 1);
5502 rotation.certificate.statement.last_trusted = Some(c.head(2));
5503 c.rotate(&new, &rotation);
5504 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5505 assert!(!v.ok, "{v:#?}");
5506 assert!(v.repudiated.is_empty());
5507 assert_eq!(v.public_key, (&old_pk).into());
5508 }
5509
5510 #[test]
5511 fn a_stolen_key_cannot_be_rotated_back_in() {
5512 let (k1, k1_pk) = generate_keypair();
5516 let (k2, k2_pk) = generate_keypair();
5517 let mut c = Chain::new();
5518 c.decision(&k1); let real = c.compromise(&k1_pk, &k2, 1);
5520 c.rotate(&k2, &real); c.decision(&k2); let honest =
5523 verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5524 assert!(honest.ok, "{honest:#?}");
5525
5526 let hijack = c.compromise(&k2_pk, &k1, 3);
5527 c.rotate(&k1, &hijack); c.decision(&k1); let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5531 assert!(!v.ok);
5532 assert_eq!(v.public_key, (&k2_pk).into(), "the chain stays with K2");
5533 let p = v.entries[3].problem.as_deref().unwrap();
5534 assert!(p.contains("never brought back"), "{p}");
5535 assert!(!v.entries[3].signature_valid, "{:#?}", v.entries[3]);
5536 assert!(!v.entries[4].signature_valid, "K1 signs nothing again");
5537 assert_eq!(v.keys.len(), 2);
5538 assert_eq!(v.keys[1].status, KeyStatus::Active);
5539 }
5540
5541 #[test]
5542 fn a_routine_rotation_cannot_reuse_a_key_either() {
5543 let (k1, k1_pk) = generate_keypair();
5544 let (k2, k2_pk) = generate_keypair();
5545 let mut c = Chain::new();
5546 c.decision(&k1);
5547 let out = c.routine(&k1_pk, &k2);
5548 c.rotate(&k1, &out);
5549 let back = c.routine(&k2_pk, &k1);
5550 c.rotate(&k2, &back);
5551 let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5552 assert!(!v.ok);
5553 assert!(
5554 v.entries[2]
5555 .problem
5556 .as_deref()
5557 .unwrap()
5558 .contains("never brought back"),
5559 "{:#?}",
5560 v.entries[2]
5561 );
5562 assert_eq!(v.public_key, (&k2_pk).into());
5563 }
5564
5565 #[test]
5566 fn a_forged_entry_has_no_effects() {
5567 let (steward, steward_pk) = generate_keypair();
5568 let (forger, _) = generate_keypair();
5569 let anchor = anchored(&steward_pk);
5570 let mut c = Chain::new();
5571 let target = c.decision(&steward); let fake = AmendmentDraft::new(
5573 target,
5574 c.hash_at(1),
5575 AmendmentKind::Overruled,
5576 "none",
5577 "overruled, says nobody with the key",
5578 )
5579 .unwrap();
5580 c.amend(&forger, &fake); let grab = c.routine(&steward_pk, &forger);
5584 c.rotate(&forger, &grab); let v = verify_chain(&c.links, &steward_pk, &anchor, &roots());
5587 assert!(!v.ok);
5588 assert!(v.entries[0].amended_by.is_empty(), "{:#?}", v.entries[0]);
5589 assert_eq!(v.public_key, (&steward_pk).into());
5590 assert_eq!(v.keys.len(), 1);
5591 assert!(v.unanchored_keys.is_empty());
5592 }
5593
5594 #[test]
5595 fn a_repeated_id_is_a_problem() {
5596 let (key, pk) = generate_keypair();
5597 let mut c = Chain::new();
5598 c.decision(&key);
5599 c.gov = 0;
5600 c.decision(&key); let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
5602 assert!(!v.ok);
5603 assert!(v.entries.iter().all(|e| {
5604 e.problem
5605 .as_deref()
5606 .is_some_and(|p| p.contains("more than once"))
5607 }));
5608 }
5609
5610 #[test]
5611 fn a_second_compromise_cannot_anchor_inside_the_first_window() {
5612 let (k1, k1_pk) = generate_keypair();
5613 let (k2, _) = generate_keypair();
5614 let (k3, _) = generate_keypair();
5615 let mut c = Chain::new();
5616 c.decision(&k1); c.decision(&k1); let first = c.compromise(&k1_pk, &k2, 1);
5619 c.rotate(&k2, &first); let second = c.compromise(&k1_pk, &k3, 2);
5621 c.rotate(&k3, &second); let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5624 assert!(!v.ok);
5625 let p = v.entries[3].problem.as_deref().unwrap();
5626 assert!(p.contains("repudiated"), "{p}");
5627 assert_eq!(v.keys.len(), 2, "K1 and K2; K3 never took the chain");
5628 assert_eq!(v.keys[0].through_seq, Some(1));
5629 }
5630
5631 #[test]
5632 fn an_uncertified_compromise_fails_closed() {
5633 let (old, old_pk) = generate_keypair();
5634 let (new, new_pk) = generate_keypair();
5635 let mut c = Chain::new();
5636 c.decision(&old);
5637 c.decision(&old);
5638 let mut rotation = c.compromise(&old_pk, &new, 1);
5639 let statement = rotation.certificate.statement.clone();
5640 rotation.certificate = certify(&[&new], statement);
5641 c.rotate(&new, &rotation);
5642
5643 let anchor = anchored(&old_pk).with((&new_pk).into());
5645 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5646 assert!(!v.ok, "{v:#?}");
5647 let p = v.entries[2].problem.as_deref().unwrap();
5648 assert!(p.contains("0 valid root signature"), "{p}");
5649 assert!(!v.entries[2].signature_valid, "checked under the old key");
5650 assert!(v.repudiated.is_empty(), "and nothing is repudiated");
5651 assert_eq!(v.public_key, (&old_pk).into());
5652 }
5653
5654 #[test]
5659 fn a_rotation_inside_the_window_is_void_with_it() {
5660 let (steward, steward_pk) = generate_keypair();
5661 let (successor, _) = generate_keypair();
5662 let (recovery, recovery_pk) = generate_keypair();
5663
5664 let mut c = Chain::new();
5665 c.decision(&steward); c.decision(&steward); let routine = c.routine(&steward_pk, &successor);
5668 c.rotate(&steward, &routine); c.decision(&successor); let declaration = c.compromise(&steward_pk, &recovery, 1);
5672 c.rotate(&recovery, &declaration); let v = verify_chain(
5675 &c.links,
5676 &steward_pk,
5677 &anchored(&steward_pk),
5678 &roots(),
5679 );
5680 assert!(v.ok, "{v:#?}");
5681 assert_eq!(v.repudiated, vec![gov(2), key_id(1), gov(3)]);
5682 assert_eq!(v.public_key, (&recovery_pk).into());
5683 assert_eq!(v.keys.len(), 2, "the successor is not part of history");
5684 assert_eq!(v.keys[0].public_key, (&steward_pk).into());
5685 assert_eq!(v.keys[0].status, KeyStatus::Compromised);
5686 assert!(
5687 v.keys[0].certified,
5688 "the genesis certificate rode in on the voided rotation and \
5689 is the root's word all the same"
5690 );
5691 assert_eq!(v.keys[1].public_key, (&recovery_pk).into());
5692 }
5693
5694 #[test]
5695 fn a_compromise_must_name_a_real_head_and_the_key_that_held_it() {
5696 let (old, old_pk) = generate_keypair();
5697 let (new, new_pk) = generate_keypair();
5698 let anchor = anchored(&old_pk);
5699
5700 let mut c = Chain::new();
5703 c.decision(&old);
5704 let mut trusted = c.head(1);
5705 trusted.entry_hash = data_hash(&json!("nope"));
5706 let statement = KeyCertStatement::compromise(
5707 (&new_pk).into(),
5708 2,
5709 c.prev_hash(),
5710 trusted,
5711 );
5712 let mut rotation = c.compromise(&old_pk, &new, 1);
5713 rotation.certificate = certify(&[&root(1)], statement);
5714 c.rotate(&new, &rotation);
5715 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5716 let p = v.entries[1].problem.as_deref().unwrap();
5717 assert!(p.contains("last_trusted"), "{p}");
5718
5719 let (_, other_pk) = generate_keypair();
5721 let mut c = Chain::new();
5722 c.decision(&old);
5723 let rotation = c.compromise(&other_pk, &new, 1);
5724 c.rotate(&new, &rotation);
5725 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5726 let p = v.entries[1].problem.as_deref().unwrap();
5727 assert!(p.contains("old_key is not the key"), "{p}");
5728
5729 let mut c = Chain::new();
5731 c.decision(&old);
5732 let mut rotation = c.compromise(&old_pk, &new, 1);
5733 rotation.certificate.statement.last_trusted = None;
5734 c.rotate(&new, &rotation);
5735 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5736 let p = v.entries[1].problem.as_deref().unwrap();
5737 assert!(p.contains("must name last_trusted"), "{p}");
5738 }
5739
5740 #[test]
5741 fn a_rotation_carries_no_free_text() {
5742 let (old, old_pk) = generate_keypair();
5743 let (new, _) = generate_keypair();
5744 let mut c = Chain::new();
5745 c.decision(&old);
5746 let rotation = c.routine(&old_pk, &new);
5747 let mut value = serde_json::to_value(&rotation).unwrap();
5748 assert!(serde_json::from_value::<KeyRotation>(value.clone()).is_ok());
5749 value["note"] = json!("at the request of …");
5750 assert!(serde_json::from_value::<KeyRotation>(value).is_err());
5751
5752 let mut head = serde_json::to_value(c.head(1)).unwrap();
5753 head["comment"] = json!("the last one I remember signing");
5754 assert!(serde_json::from_value::<TrustedHead>(head).is_err());
5755
5756 let mut statement =
5757 serde_json::to_value(&rotation.certificate.statement).unwrap();
5758 statement["comment"] = json!("signed in the kitchen");
5759 assert!(serde_json::from_value::<KeyCertStatement>(statement).is_err());
5760 }
5761
5762 #[test]
5763 fn the_root_statement_bytes_are_pinned() {
5764 let key: PublicKeyHex = PUBLISHED_KEYS[0].parse().unwrap();
5765 assert_eq!(
5766 String::from_utf8(KeyCertStatement::genesis(key).signed_bytes())
5767 .unwrap(),
5768 "agora-governance-root-v1\n\
5769 {\"agora_governance_key_cert\":1,\"from_seq\":1,\
5770 \"key\":\"ebb3091dd328f1463362c171121921b2fe14628e3fc4c145deaccefb85c0e78a\",\
5771 \"last_trusted\":null,\"prev_hash\":null,\"purpose\":\"genesis\"}"
5772 );
5773
5774 let statement = KeyCertStatement::compromise(
5778 Sha256Hex::from([0xab; 32]).to_string().parse().unwrap(),
5779 15,
5780 Some([0xcd; 32].into()),
5781 TrustedHead {
5782 id: gov(10),
5783 chain_seq: 11,
5784 entry_hash: [0xef; 32].into(),
5785 },
5786 );
5787 assert_eq!(
5788 Sha256Hex::from(<[u8; 32]>::from(Sha256::digest(
5789 statement.signed_bytes()
5790 )))
5791 .to_string(),
5792 "633685771e08be126fd12ca4eb98c77120e5868236ff541ecba85ce6a21e6b68"
5793 );
5794 }
5795
5796 #[test]
5797 fn root_keys_are_curve_points_and_make_a_root_set() {
5798 let roots = RootSet::published();
5799 assert_eq!(roots.keys().count(), ROOT_KEYS.len());
5800 assert_eq!(roots.threshold(), ROOT_THRESHOLD);
5801 assert!(ROOT_THRESHOLD >= 1 && ROOT_THRESHOLD <= ROOT_KEYS.len());
5802 for root in ROOT_KEYS {
5803 let key: PublicKeyHex = root.parse().unwrap();
5804 assert!(roots.contains(&key));
5805 assert!(
5806 key.to_verifying_key().is_ok(),
5807 "{root} is not a valid Ed25519 public key"
5808 );
5809 assert!(
5810 !PUBLISHED_KEYS.contains(root),
5811 "a root key never signs entries"
5812 );
5813 }
5814 assert_eq!(RootSet::new([], 0).threshold(), 1);
5815 }
5816
5817 #[test]
5818 fn a_genesis_key_outside_the_anchor_is_reported_not_rejected() {
5819 let (key, pk) = generate_keypair();
5820 let c = chain(&key, 2);
5821 let v = verify_chain(&c, &pk, &KeyAnchor::default(), &roots());
5822 assert!(v.ok, "{v:#?}");
5823 assert_eq!(v.unanchored_keys, vec![PublicKeyHex::from(&pk)]);
5824 assert_eq!(v.keys.len(), 1);
5825 assert_eq!(v.keys[0].status, KeyStatus::Active);
5826 assert_eq!(v.keys[0].from_seq, 1);
5827 assert_eq!(v.keys[0].through_seq, None);
5828 }
5829
5830 #[test]
5831 fn published_keys_are_curve_points_and_make_an_anchor() {
5832 let anchor = KeyAnchor::published();
5833 assert!(!anchor.is_empty());
5834 assert_eq!(anchor.keys().count(), PUBLISHED_KEYS.len());
5835 for published in PUBLISHED_KEYS {
5836 let key: PublicKeyHex = published.parse().unwrap();
5837 assert!(anchor.contains(&key));
5838 assert!(
5839 key.to_verifying_key().is_ok(),
5840 "{published} is not a valid Ed25519 public key"
5841 );
5842 }
5843 let (_, other) = generate_keypair();
5844 assert!(!anchor.contains(&(&other).into()));
5845 assert!(
5846 anchor
5847 .clone()
5848 .with((&other).into())
5849 .contains(&(&other).into())
5850 );
5851 }
5852
5853 #[test]
5856 fn amendments_and_rotations_round_trip_as_entry_data() {
5857 let (key, pk) = generate_keypair();
5858 let amendment = AmendmentDraft::new(
5859 gov(1),
5860 data_hash(&json!("x")),
5861 AmendmentKind::Superseded,
5862 "Art. VI § 2",
5863 "superseded by GOV-2026-0009",
5864 )
5865 .unwrap()
5866 .with_authority(gov(9))
5867 .with_rationale("the later decision covers the same subject");
5868 let AmendmentDraft { amendment, texts } = amendment;
5869 let value = serde_json::to_value(&amendment).unwrap();
5870 assert_eq!(value["kind"], "superseded");
5871 assert_eq!(value["agora_governance_amendment"], 2);
5872 assert!(value.get("redaction").is_none(), "{value}");
5873 let text = value.to_string();
5874 assert!(!text.contains("Art. VI"), "no text in signed data: {text}");
5875 assert!(!text.contains("salt"), "and no salt: {text}");
5876 assert_eq!(
5877 value["note"]["commitment"],
5878 texts
5879 .note
5880 .as_ref()
5881 .unwrap()
5882 .commitment()
5883 .commitment
5884 .to_string()
5885 );
5886 assert_eq!(
5887 serde_json::from_value::<Amendment>(value).unwrap(),
5888 amendment
5889 );
5890 let beside = serde_json::to_value(&texts).unwrap();
5891 assert_eq!(beside["basis"]["text"], "Art. VI § 2");
5892 assert_eq!(
5893 serde_json::from_value::<AmendmentTexts>(beside).unwrap(),
5894 texts
5895 );
5896
5897 let legacy = json!({
5899 "agora_governance_amendment": 1,
5900 "target": "GOV-2026-0001",
5901 "target_entry_hash": data_hash(&json!("x")),
5902 "kind": "non_precedential",
5903 "authority": "GOV-2026-0005",
5904 "basis": "§1",
5905 "note": "diagnostic finding",
5906 });
5907 let legacy: Amendment = serde_json::from_value(legacy).unwrap();
5908 assert_eq!(legacy.validate(), Ok(()));
5909 assert_eq!(legacy.basis, AmendmentText::Plain("§1".into()));
5910
5911 let mut c = Chain::new();
5912 c.decision(&key);
5913 let rotation = c.compromise(&pk, &generate_keypair().0, 1);
5914 let value = serde_json::to_value(&rotation).unwrap();
5915 assert_eq!(value["agora_governance_key_rotation"], 2);
5916 assert_eq!(value["reason"], "compromise");
5917 let statement = &value["certificate"]["statement"];
5918 assert_eq!(statement["purpose"], "compromise");
5919 assert_eq!(statement["last_trusted"]["chain_seq"], 1);
5920 assert_eq!(
5921 value["outgoing_certificate"]["statement"]["purpose"],
5922 "genesis"
5923 );
5924 assert!(value.get("note").is_none(), "{value}");
5925 assert_eq!(
5926 serde_json::from_value::<KeyRotation>(value).unwrap(),
5927 rotation
5928 );
5929
5930 let notice =
5931 AmendmentNotice::new(amd(1), at(0), &amendment, Some(&texts));
5932 let value = serde_json::to_value(¬ice).unwrap();
5933 assert_eq!(value["id"], "AMD-2026-0001");
5934 assert_eq!(value["note"], "superseded by GOV-2026-0009");
5935 assert_eq!(
5936 serde_json::from_value::<AmendmentNotice>(value).unwrap(),
5937 notice
5938 );
5939
5940 let erased = AmendmentTexts {
5942 rationale: None,
5943 ..texts.clone()
5944 };
5945 let notice =
5946 AmendmentNotice::new(amd(1), at(0), &amendment, Some(&erased));
5947 assert_eq!(notice.note, "superseded by GOV-2026-0009");
5948 assert_eq!(notice.rationale.as_deref(), Some(WITHHELD_TEXT));
5949 let notice = AmendmentNotice::new(amd(1), at(0), &amendment, None);
5950 assert_eq!(notice.basis, WITHHELD_TEXT);
5951 let notice = AmendmentNotice::new(amd(1), at(0), &legacy, None);
5952 assert_eq!(notice.note, "diagnostic finding");
5953 }
5954
5955 #[test]
5959 fn an_amendment_verifies_with_or_without_its_optional_fields() {
5960 let (key, pk) = generate_keypair();
5961 let mut c = Chain::new();
5962 let target = c.decision(&key);
5963 let bare = AmendmentDraft::new(
5964 target.clone(),
5965 c.hash_at(1),
5966 AmendmentKind::Correction,
5967 "clerical",
5968 "typo in the citation",
5969 )
5970 .unwrap();
5971 assert!(
5972 serde_json::to_value(&bare.amendment)
5973 .unwrap()
5974 .get("rationale")
5975 .is_none()
5976 );
5977 c.amend(&key, &bare);
5978 let full = bare.clone().with_rationale("at length: …");
5979 c.amend(&key, &full);
5980 let mut future = serde_json::to_value(&full.amendment).unwrap();
5982 future["superseded_by_something_new"] = json!(["later"]);
5983 c.push(
5984 &key,
5985 amd(3),
5986 GovernanceLogEntryType::Amendment,
5987 future,
5988 true,
5989 );
5990
5991 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
5992 assert!(v.ok, "{v:#?}");
5993 assert_eq!(
5994 v.entries[0].amended_by,
5995 vec![amd(1), amd(2), amd(3)],
5996 "all three name the target"
5997 );
5998 }
5999
6000 #[test]
6003 fn pre_0_26_wire_still_deserializes() {
6004 let link: GovernanceChainLink = serde_json::from_value(json!({
6005 "id": "GOV-2026-0001",
6006 "entry_type": "council_decision",
6007 "created_at": "2023-11-14T22:13:20.123456Z",
6008 "attestation": {
6009 "envelope_version": 1,
6010 "chain_seq": 1,
6011 "prev_hash": null,
6012 "data_hash": "00".repeat(32),
6013 "entry_hash": "11".repeat(32),
6014 "signature": "22".repeat(64),
6015 "signed_at": "2023-11-14T22:13:21Z",
6016 "retroactive": false,
6017 },
6018 }))
6019 .unwrap();
6020 assert!(link.data.is_none());
6021 assert!(
6022 serde_json::to_value(&link).unwrap().get("data").is_none(),
6023 "and a link without data does not grow a null field"
6024 );
6025
6026 let verdict: EntryVerdict = serde_json::from_value(json!({
6027 "id": "GOV-2026-0001",
6028 "chain_seq": 1,
6029 "signature_valid": true,
6030 "link_valid": true,
6031 "content_matches": null,
6032 "retroactive": false,
6033 "out_of_order": false,
6034 "amended_by": [],
6035 }))
6036 .unwrap();
6037 assert!(!verdict.repudiated && !verdict.redacted);
6038 assert!(verdict.signed_by.is_none());
6039 assert!(verdict.reattested_by.is_empty());
6040
6041 let verification: GovernanceVerification =
6042 serde_json::from_value(json!({
6043 "public_key": "33".repeat(32),
6044 "ok": true,
6045 "head": "GOV-2026-0001",
6046 "entries": [],
6047 }))
6048 .unwrap();
6049 assert!(verification.keys.is_empty());
6050 assert!(verification.unanchored_keys.is_empty());
6051 assert!(verification.repudiated.is_empty());
6052 }
6053
6054 #[test]
6055 fn the_report_round_trips() {
6056 let (key, pk) = generate_keypair();
6057 let mut c = Chain::new();
6058 let target = c.decision(&key);
6059 let amendment = AmendmentDraft::new(
6060 target,
6061 c.hash_at(1),
6062 AmendmentKind::Overruled,
6063 "b",
6064 "n",
6065 )
6066 .unwrap();
6067 c.amend(&key, &amendment);
6068 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
6069 let text = serde_json::to_string(&v).unwrap();
6070 assert_eq!(
6071 serde_json::from_str::<GovernanceVerification>(&text).unwrap(),
6072 v
6073 );
6074
6075 let keys = GovernanceSigningKeys { keys: v.keys };
6076 let value = serde_json::to_value(&keys).unwrap();
6077 assert!(value["keys"].is_array(), "an object, not a bare array");
6078 assert_eq!(value["keys"][0]["status"], "active");
6079 assert_eq!(
6080 serde_json::from_value::<GovernanceSigningKeys>(value).unwrap(),
6081 keys
6082 );
6083 }
6084
6085 #[test]
6089 fn envelope_v1_preimage_and_hash_are_pinned() {
6090 let envelope = Envelope::new(
6091 gov(6),
6092 GovernanceLogEntryType::CouncilDecision,
6093 at(0),
6094 Some(Sha256Hex::from([0x11; 32])),
6095 data_hash(&json!({"outcome": "approved", "title": "Ratification"})),
6096 );
6097 assert_eq!(
6098 String::from_utf8(envelope.preimage()).unwrap(),
6099 "{\"agora_governance_log\":1,\"id\":\"GOV-2026-0006\",\
6100 \"entry_type\":\"council_decision\",\
6101 \"created_at\":1700000000123456,\
6102 \"prev_hash\":\"1111111111111111111111111111111111111111111111111111111111111111\",\
6103 \"data_hash\":\"a4adf645ae3f60c56484d01aea87d6d490321d7fc66b1607df14b023fe567c7b\"}"
6104 );
6105 assert_eq!(
6106 envelope.entry_hash().to_hex(),
6107 "ba27577432f81e415f1c01cc4cfabab6070e3ac50fd468fffe195ef19c0e9464"
6108 );
6109 }
6110
6111 #[cfg(feature = "agora-client")]
6125 #[tokio::test]
6126 #[ignore = "networked: hits the live platform"]
6127 async fn the_published_key_is_the_one_the_platform_serves() {
6128 let client = crate::client::Client::new(
6129 url::Url::parse("https://subliminal.technology").unwrap(),
6130 )
6131 .unwrap();
6132 let served = client.get_governance_signing_key().await.unwrap();
6133 assert_eq!(served.algorithm, "ed25519");
6134
6135 let genesis: PublicKeyHex = PUBLISHED_KEYS
6136 .first()
6137 .expect("PUBLISHED_KEYS is never empty")
6138 .parse()
6139 .unwrap();
6140 let links = client.get_governance_chain().await.unwrap();
6141 let report = verify_chain(
6142 &links,
6143 &genesis.to_verifying_key().unwrap(),
6144 &KeyAnchor::published(),
6145 &RootSet::published(),
6146 );
6147 let problems: Vec<_> = report
6148 .entries
6149 .iter()
6150 .filter_map(|e| {
6151 e.problem.as_ref().map(|p| (e.id.clone(), p.clone()))
6152 })
6153 .collect();
6154 assert!(
6155 report.ok,
6156 "the live chain does not verify under this build's genesis key \
6157 and roots: {problems:#?}"
6158 );
6159 assert!(report.unanchored_keys.is_empty(), "{report:#?}");
6160 assert_eq!(
6161 served.public_key, report.public_key,
6162 "the platform serves {} but the chain, followed under the \
6163 published roots, is held by {}",
6164 served.public_key, report.public_key
6165 );
6166 }
6167
6168 #[cfg(feature = "schemars")]
6169 #[test]
6170 fn wire_schemas_are_ref_free() {
6171 use crate::responses::inline_schema_for;
6172 for (name, schema) in [
6173 (
6174 "GovernanceAttestation",
6175 inline_schema_for::<GovernanceAttestation>(),
6176 ),
6177 (
6178 "GovernanceChainLink",
6179 inline_schema_for::<GovernanceChainLink>(),
6180 ),
6181 (
6182 "GovernanceSigningKey",
6183 inline_schema_for::<GovernanceSigningKey>(),
6184 ),
6185 (
6186 "GovernanceVerification",
6187 inline_schema_for::<GovernanceVerification>(),
6188 ),
6189 (
6190 "Vec<GovernanceChainLink>",
6191 inline_schema_for::<Vec<GovernanceChainLink>>(),
6192 ),
6193 ("Amendment", inline_schema_for::<Amendment>()),
6194 ("Redaction", inline_schema_for::<Redaction>()),
6195 ("Revision", inline_schema_for::<Revision>()),
6196 ("AmendmentNotice", inline_schema_for::<AmendmentNotice>()),
6197 ("KeyRotation", inline_schema_for::<KeyRotation>()),
6198 ("TrustedHead", inline_schema_for::<TrustedHead>()),
6199 (
6200 "GovernanceKeyRecord",
6201 inline_schema_for::<GovernanceKeyRecord>(),
6202 ),
6203 (
6204 "GovernanceSigningKeys",
6205 inline_schema_for::<GovernanceSigningKeys>(),
6206 ),
6207 ("AmendmentKind", inline_schema_for::<AmendmentKind>()),
6208 ("Standing", inline_schema_for::<Standing>()),
6209 ("KeyStatus", inline_schema_for::<KeyStatus>()),
6210 ("RotationReason", inline_schema_for::<RotationReason>()),
6211 ] {
6212 let text = serde_json::to_string(&schema).unwrap();
6213 assert!(!text.contains("$ref"), "{name} must be $ref-free: {text}");
6214 assert!(
6215 !text.contains("$defs"),
6216 "{name} must be $defs-free: {text}"
6217 );
6218 }
6219 let text =
6220 serde_json::to_string(&inline_schema_for::<Sha256Hex>()).unwrap();
6221 assert!(text.contains("^[0-9a-f]{64}$"), "{text}");
6222 let text = serde_json::to_string(&inline_schema_for::<SignatureHex>())
6223 .unwrap();
6224 assert!(text.contains("^[0-9a-f]{128}$"), "{text}");
6225 }
6226}