1use crate::crypto::{self, Signature, SigningKey, VerifyingKey};
69use crate::enums::GovernanceLogEntryType;
70use crate::ids::{GovernanceLogId, GovernanceLogPrefix};
71use chrono::{DateTime, Utc};
72use serde::{Deserialize, Serialize};
73use sha2::{Digest, Sha256};
74use std::collections::{HashMap, HashSet};
75
76pub use crate::enums::{AmendmentKind, CitationRelation, KeyStatus, Standing};
77
78mod texts;
79pub use texts::{
80 AmendmentText, AmendmentTextStatus, AmendmentTexts, CommittedText,
81 TextCommitment, TextStatus, WITHHELD_TEXT,
82};
83
84mod council;
85pub use council::{
86 Abstention, AgendaRanking, AgoraAnswer, AgoraExchange, Asked, Ballot,
87 BatchedQuestion, BlankStatement, CouncilAttachment, CouncilDecisionRecord,
88 CouncilMember, CouncilOutcome, CouncilRound, CouncilSeat, CouncilVote,
89 DEFERRING_SEATS, Decided, DecisionCategory, DecisionSummary,
90 FinalRoundError, FinalVotes, NotASeatChoice, PlacedProposal, Question,
91 RECUSED_ACCEPTORS, RECUSED_DEFERRED_TAG, Rationale, RecordRuleError,
92 RecusalVote, STEWARD_RECUSED_TAG, SUMMARY_POINTER, SUMMARY_WRITER,
93 SeatChoice, SeatModels, SeatQuestions, SeatRanking, SeatResponse, SeatVote,
94 SeatVotes, Statement, StewardEmergency, StewardRecusal, StewardVote,
95 SummaryAcceptance, SummaryAuthorship, Testimony, TestimonyRecord,
96 Unanswered, Unheard, Vote, Votes, Witness, WitnessBatch, WitnessQuestion,
97 WitnessSource, part_label, served_summary, split_for_dms, test_labelled,
98 testimony_attachment_name,
99};
100
101mod redactable;
102pub use redactable::{REDACTION_MARKER_PATTERN, Redactable};
103
104pub mod reading;
105
106pub mod meeting_summary;
108
109pub use json_patch;
112
113mod obligation;
114pub use obligation::request;
115pub mod quote;
116pub use obligation::{
117 Evidence, KnownEntry, OBLIGATION_VERSION, Obligation, ObligationData,
118 ObligationError, ObligationRuleError, ObligationState, ObligationUpdate,
119 Until,
120};
121
122mod data_set;
123pub use data_set::{
124 DATA_SET_VERSION, DataFile, DataFileMismatch, DataFilePath,
125 DataFilePathError, DataRepoName, DataRepoNameError, DataSet,
126 DataSetLookupError, DataSetLookupFailure, DataSetTarget,
127 MAX_DATA_FILE_BYTES, MAX_WHOLE_READ_BYTES,
128};
129
130mod data_read;
131pub use data_read::{
132 DataCitation, DataCitationError, GitCommitPrefix, LineRange,
133 LineRangeError, LineSpan, PastEnd, count_lines,
134};
135
136mod test_entry;
137pub use test_entry::{
138 TEST_ENTRY_VERSION, TEST_INNER_POINTER, TEST_LABEL, TEST_MACHINE_FIELDS,
139 TestEntry, TestEntryError, decision_data, decision_type,
140};
141
142mod record;
143pub use record::{
144 RecordAttachment, RecordParticipant, STEWARD_RECORD_VERSION, StewardRecord,
145};
146
147mod root;
148pub use root::{
149 CertPurpose, CertificateError, KEY_CERT_VERSION, KeyCertStatement,
150 KeyCertificate, ROOT_DOMAIN, ROOT_KEYS, ROOT_THRESHOLD, RootSet,
151 RootSignature,
152};
153
154#[cfg(test)]
156mod vectors;
157
158pub const ENVELOPE_VERSION: u32 = 1;
160
161pub const RETROACTIVE_AFTER: chrono::Duration = chrono::Duration::seconds(60);
164
165#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
172#[error("{type_name}: expected {expected} bytes of hex, got {got:?}")]
173pub struct HexLengthError {
174 pub type_name: &'static str,
175 pub expected: usize,
176 pub got: String,
177}
178
179macro_rules! hex_bytes {
180 ($(#[$meta:meta])* $name:ident, $len:expr) => {
181 $(#[$meta])*
182 #[derive(Clone, Copy, PartialEq, Eq, Hash)]
183 #[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
184 #[cfg_attr(feature = "sqlx", sqlx(transparent))]
185 pub struct $name([u8; $len]);
186
187 impl $name {
188 pub fn as_bytes(&self) -> &[u8; $len] {
190 &self.0
191 }
192
193 pub fn to_hex(&self) -> String {
195 hex::encode(self.0)
196 }
197 }
198
199 impl From<[u8; $len]> for $name {
200 fn from(bytes: [u8; $len]) -> Self {
201 Self(bytes)
202 }
203 }
204
205 impl TryFrom<&[u8]> for $name {
206 type Error = HexLengthError;
207
208 fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
209 <[u8; $len]>::try_from(bytes).map(Self).map_err(|_| {
210 HexLengthError {
211 type_name: stringify!($name),
212 expected: $len,
213 got: hex::encode(bytes),
214 }
215 })
216 }
217 }
218
219 impl TryFrom<Vec<u8>> for $name {
220 type Error = HexLengthError;
221
222 fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
223 Self::try_from(bytes.as_slice())
224 }
225 }
226
227 impl std::str::FromStr for $name {
228 type Err = HexLengthError;
229
230 fn from_str(s: &str) -> Result<Self, Self::Err> {
231 let bytes = hex::decode(s.trim()).map_err(|_| HexLengthError {
232 type_name: stringify!($name),
233 expected: $len,
234 got: s.to_string(),
235 })?;
236 Self::try_from(bytes.as_slice())
237 }
238 }
239
240 impl std::fmt::Display for $name {
241 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
242 f.write_str(&self.to_hex())
243 }
244 }
245
246 impl std::fmt::Debug for $name {
247 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248 write!(f, "{}({})", stringify!($name), self.to_hex())
249 }
250 }
251
252 impl Serialize for $name {
253 fn serialize<S: serde::Serializer>(
254 &self,
255 s: S,
256 ) -> Result<S::Ok, S::Error> {
257 s.serialize_str(&self.to_hex())
258 }
259 }
260
261 impl<'de> Deserialize<'de> for $name {
262 fn deserialize<D: serde::Deserializer<'de>>(
263 d: D,
264 ) -> Result<Self, D::Error> {
265 let s = String::deserialize(d)?;
266 s.parse().map_err(serde::de::Error::custom)
267 }
268 }
269
270 #[cfg(feature = "schemars")]
274 impl schemars::JsonSchema for $name {
275 fn inline_schema() -> bool {
276 true
277 }
278
279 fn schema_name() -> std::borrow::Cow<'static, str> {
280 std::borrow::Cow::Borrowed(stringify!($name))
281 }
282
283 fn schema_id() -> std::borrow::Cow<'static, str> {
284 std::borrow::Cow::Borrowed(concat!(
285 module_path!(),
286 "::",
287 stringify!($name)
288 ))
289 }
290
291 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
292 schemars::json_schema!({
293 "type": "string",
294 "pattern": format!("^[0-9a-f]{{{}}}$", $len * 2),
295 "description": format!("{} bytes, lowercase hex", $len),
296 })
297 }
298 }
299 };
300}
301
302hex_bytes!(
303 Sha256Hex,
305 32
306);
307
308hex_bytes!(
309 SignatureHex,
311 64
312);
313
314hex_bytes!(
315 PublicKeyHex,
317 32
318);
319
320hex_bytes!(
321 GitCommitId,
324 20
325);
326
327hex_bytes!(
328 Blind,
331 32
332);
333
334hex_bytes!(
335 TextSalt,
338 32
339);
340
341impl Blind {
342 pub fn random() -> Self {
344 use rand::RngCore;
345 let mut bytes = [0u8; 32];
346 rand::rngs::OsRng.fill_bytes(&mut bytes);
347 Self(bytes)
348 }
349}
350
351impl TextSalt {
352 pub fn random() -> Self {
354 Self(*Blind::random().as_bytes())
355 }
356}
357
358impl From<Signature> for SignatureHex {
359 fn from(sig: Signature) -> Self {
360 Self(sig.to_bytes())
361 }
362}
363
364impl From<&SignatureHex> for Signature {
365 fn from(sig: &SignatureHex) -> Self {
366 Signature::from_bytes(&sig.0)
367 }
368}
369
370impl From<&VerifyingKey> for PublicKeyHex {
371 fn from(key: &VerifyingKey) -> Self {
372 Self(key.to_bytes())
373 }
374}
375
376impl PublicKeyHex {
377 pub fn to_verifying_key(
379 &self,
380 ) -> Result<VerifyingKey, ed25519_dalek::SignatureError> {
381 VerifyingKey::from_bytes(&self.0)
382 }
383}
384
385pub fn canonical_json(value: &serde_json::Value) -> Vec<u8> {
398 let mut out = Vec::new();
399 write_canonical(value, &mut out);
400 out
401}
402
403fn write_canonical(value: &serde_json::Value, out: &mut Vec<u8>) {
404 use serde_json::Value;
405 match value {
406 Value::Null => out.extend_from_slice(b"null"),
407 Value::Bool(b) => {
408 out.extend_from_slice(if *b { b"true" } else { b"false" })
409 }
410 Value::Number(n) => serde_json::to_writer(&mut *out, n)
411 .expect("a number always serializes"),
412 Value::String(s) => serde_json::to_writer(&mut *out, s)
413 .expect("a string always serializes"),
414 Value::Array(items) => {
415 out.push(b'[');
416 for (i, item) in items.iter().enumerate() {
417 if i > 0 {
418 out.push(b',');
419 }
420 write_canonical(item, out);
421 }
422 out.push(b']');
423 }
424 Value::Object(map) => {
425 let mut keys: Vec<&String> = map.keys().collect();
426 keys.sort_unstable();
427 out.push(b'{');
428 for (i, key) in keys.into_iter().enumerate() {
429 if i > 0 {
430 out.push(b',');
431 }
432 serde_json::to_writer(&mut *out, key)
433 .expect("a string always serializes");
434 out.push(b':');
435 write_canonical(&map[key], out);
436 }
437 out.push(b'}');
438 }
439 }
440}
441
442pub fn non_integer_number(data: &serde_json::Value) -> Option<String> {
453 fn find(value: &serde_json::Value, path: &mut String) -> bool {
454 use serde_json::Value::{Array, Number, Object};
455 let mark = path.len();
456 match value {
457 Number(n) => return n.is_f64(),
458 Array(items) => {
459 for (i, item) in items.iter().enumerate() {
460 path.push_str(&format!("/{i}"));
461 if find(item, path) {
462 return true;
463 }
464 path.truncate(mark);
465 }
466 }
467 Object(map) => {
468 for (key, item) in map {
469 path.push('/');
470 path.push_str(&escape_pointer_token(key));
471 if find(item, path) {
472 return true;
473 }
474 path.truncate(mark);
475 }
476 }
477 _ => {}
478 }
479 false
480 }
481 let mut path = String::new();
482 find(data, &mut path).then_some(path)
483}
484
485pub fn escape_pointer_token(token: &str) -> String {
489 token.replace('~', "~0").replace('/', "~1")
490}
491
492pub fn unescape_pointer_token(token: &str) -> String {
495 token.replace("~1", "/").replace("~0", "~")
496}
497
498pub fn pointer_tokens(pointer: &str) -> Vec<String> {
501 pointer
502 .split('/')
503 .skip(1)
504 .map(unescape_pointer_token)
505 .collect()
506}
507
508pub fn data_hash(data: &serde_json::Value) -> Sha256Hex {
510 Sha256Hex(Sha256::digest(canonical_json(data)).into())
511}
512
513#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
518pub struct Envelope {
519 pub agora_governance_log: u32,
521 pub id: GovernanceLogId,
522 pub entry_type: GovernanceLogEntryType,
523 pub created_at: i64,
525 pub prev_hash: Option<Sha256Hex>,
526 pub data_hash: Sha256Hex,
527}
528
529impl Envelope {
530 pub fn new(
532 id: GovernanceLogId,
533 entry_type: GovernanceLogEntryType,
534 created_at: DateTime<Utc>,
535 prev_hash: Option<Sha256Hex>,
536 data_hash: Sha256Hex,
537 ) -> Self {
538 Self {
539 agora_governance_log: ENVELOPE_VERSION,
540 id,
541 entry_type,
542 created_at: created_at.timestamp_micros(),
543 prev_hash,
544 data_hash,
545 }
546 }
547
548 pub fn created_at(&self) -> DateTime<Utc> {
550 DateTime::from_timestamp_micros(self.created_at)
551 .expect("an Envelope only ever holds an in-range timestamp")
552 }
553
554 pub fn preimage(&self) -> Vec<u8> {
556 serde_json::to_vec(self).expect("an Envelope always serializes")
557 }
558
559 pub fn entry_hash(&self) -> Sha256Hex {
561 Sha256Hex(Sha256::digest(self.preimage()).into())
562 }
563}
564
565pub fn truncate_to_micros(t: DateTime<Utc>) -> DateTime<Utc> {
568 DateTime::from_timestamp_micros(t.timestamp_micros())
569 .expect("a timestamp that came from a DateTime is in range")
570}
571
572pub fn truncate_to_seconds(t: DateTime<Utc>) -> DateTime<Utc> {
575 DateTime::from_timestamp(t.timestamp(), 0)
576 .expect("a timestamp that came from a DateTime is in range")
577}
578
579pub fn is_retroactive(
583 created_at: DateTime<Utc>,
584 signed_at: DateTime<Utc>,
585) -> bool {
586 signed_at - created_at > RETROACTIVE_AFTER
587}
588
589#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
595#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
596#[cfg_attr(feature = "schemars", schemars(inline))]
597pub struct GovernanceAttestation {
598 pub envelope_version: u32,
600 pub chain_seq: u64,
603 pub prev_hash: Option<Sha256Hex>,
605 pub data_hash: Sha256Hex,
607 pub entry_hash: Sha256Hex,
609 pub signature: SignatureHex,
612 pub signed_at: DateTime<Utc>,
615 pub retroactive: bool,
619}
620
621#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
625#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
626#[cfg_attr(feature = "schemars", schemars(inline))]
627pub struct GovernanceChainLink {
628 pub id: GovernanceLogId,
629 pub entry_type: GovernanceLogEntryType,
630 pub created_at: DateTime<Utc>,
631 pub attestation: GovernanceAttestation,
632 #[serde(default, skip_serializing_if = "Option::is_none")]
639 pub data: Option<serde_json::Value>,
640 #[serde(
644 default,
645 skip_serializing_if = "Option::is_none",
646 deserialize_with = "read_as_written"
647 )]
648 pub texts: Option<AmendmentTexts>,
649}
650
651#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
654#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
655#[cfg_attr(feature = "schemars", schemars(inline))]
656pub struct GovernanceSigningKey {
657 pub algorithm: String,
659 pub public_key: PublicKeyHex,
660 pub envelope_version: u32,
662}
663
664impl GovernanceSigningKey {
665 pub fn new(key: &VerifyingKey) -> Self {
667 Self {
668 algorithm: "ed25519".to_string(),
669 public_key: key.into(),
670 envelope_version: ENVELOPE_VERSION,
671 }
672 }
673}
674
675pub const AMENDMENT_VERSION: u32 = 2;
683
684#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
686pub enum AmendmentError {
687 #[error("agora_governance_amendment is {0}, not 1 or {AMENDMENT_VERSION}")]
688 UnsupportedVersion(u32),
689 #[error(
690 "a version 1 amendment carries its texts and a version \
691 {AMENDMENT_VERSION} one commits to them; this does neither \
692 consistently"
693 )]
694 TextShape,
695 #[error("`{0}` beside the entry is not the text the entry committed to")]
696 TextMismatch(&'static str),
697 #[error("texts beside an entry that commits to none")]
698 UncommittedText,
699 #[error("kind `redaction` requires a `redaction`")]
700 MissingRedaction,
701 #[error("`redaction` is only valid on kind `redaction`")]
702 UnexpectedRedaction,
703 #[error("kind `revision` requires a `revision`")]
704 MissingRevision,
705 #[error("`revision` is only valid on kind `revision`")]
706 UnexpectedRevision,
707 #[error("the `revision` is malformed: {0}")]
708 RevisionShape(ReviseError),
709 #[error("kind `{0}` requires an `update`")]
710 MissingUpdate(AmendmentKind),
711 #[error("`update` is only valid on the obligation kinds")]
712 UnexpectedUpdate,
713 #[error("an `update` of kind `{update}` on an amendment of kind `{kind}`")]
714 UpdateMismatch {
715 kind: AmendmentKind,
716 update: AmendmentKind,
717 },
718 #[error(
719 "kind `{kind}` is only valid on an obligation, and {target} is not one"
720 )]
721 NotAnObligation {
722 kind: AmendmentKind,
723 target: GovernanceLogId,
724 },
725 #[error(
726 "kind `{0}` changes precedential force, which an obligation does not have"
727 )]
728 PrecedentialOnObligation(AmendmentKind),
729 #[error(
730 "a revision of an obligation may not touch `{0}`; its status and \
731 deadline change only through the obligation kinds"
732 )]
733 FixedObligationField(String),
734 #[error("amendment target {0} is not an entry of this chain")]
735 UnknownTarget(GovernanceLogId),
736 #[error("amendment target {0} is not an earlier entry")]
737 ForwardReference(GovernanceLogId),
738 #[error("target_entry_hash is not {0}'s entry_hash")]
739 WrongTargetHash(GovernanceLogId),
740}
741
742#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
748#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
749#[cfg_attr(feature = "schemars", schemars(inline))]
750pub struct Amendment {
751 pub agora_governance_amendment: u32,
753 pub target: GovernanceLogId,
754 pub target_entry_hash: Sha256Hex,
756 pub kind: AmendmentKind,
757 #[serde(default)]
760 pub authority: Option<GovernanceLogId>,
761 pub basis: AmendmentText,
765 pub note: AmendmentText,
767 #[serde(default, skip_serializing_if = "Option::is_none")]
769 pub rationale: Option<AmendmentText>,
770 #[serde(default, skip_serializing_if = "Option::is_none")]
772 pub redaction: Option<Redaction>,
773 #[serde(default, skip_serializing_if = "Option::is_none")]
775 pub revision: Option<Revision>,
776 #[serde(default, skip_serializing_if = "Option::is_none")]
779 pub update: Option<ObligationUpdate>,
780}
781
782#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
784#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
785#[cfg_attr(feature = "schemars", schemars(inline))]
786pub struct Redaction {
787 pub fields: Vec<String>,
790 pub resulting_data_hash: Sha256Hex,
793 #[serde(default, skip_serializing_if = "Option::is_none")]
797 pub resulting_latest_hash: Option<Sha256Hex>,
798}
799
800#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
812pub struct Revision {
813 pub patch: json_patch::Patch,
815 #[serde(default, skip_serializing_if = "Vec::is_empty")]
819 pub duplicates: Vec<(String, String)>,
820 pub resulting_data_hash: Sha256Hex,
822}
823
824#[derive(Debug, Clone, PartialEq, Eq, Default)]
827pub struct Edit {
828 pub patch: json_patch::Patch,
829 pub duplicates: Vec<(String, String)>,
831}
832
833impl From<json_patch::Patch> for Edit {
834 fn from(patch: json_patch::Patch) -> Self {
835 Self {
836 patch,
837 duplicates: Vec::new(),
838 }
839 }
840}
841
842#[cfg(feature = "schemars")]
845impl schemars::JsonSchema for Revision {
846 fn inline_schema() -> bool {
847 true
848 }
849
850 fn schema_name() -> std::borrow::Cow<'static, str> {
851 "Revision".into()
852 }
853
854 fn json_schema(
855 generator: &mut schemars::SchemaGenerator,
856 ) -> schemars::Schema {
857 let hash = generator.subschema_for::<Sha256Hex>();
858 schemars::json_schema!({
859 "type": "object",
860 "properties": {
861 "patch": {
862 "description": "RFC 6902 JSON Patch from the previous version to this one",
863 "type": "array",
864 "items": {
865 "type": "object",
866 "properties": {
867 "op": {
868 "type": "string",
869 "enum": ["add", "remove", "replace", "move", "copy", "test"]
870 },
871 "path": {"type": "string"},
872 "from": {"type": "string"},
873 "value": true
874 },
875 "required": ["op", "path"]
876 }
877 },
878 "duplicates": {
879 "description": "(removed path, the path it duplicated) for each duplicate removed",
880 "type": "array",
881 "items": {
882 "type": "array",
883 "items": {"type": "string"},
884 "minItems": 2,
885 "maxItems": 2
886 }
887 },
888 "resulting_data_hash": hash
889 },
890 "required": ["patch", "resulting_data_hash"]
891 })
892 }
893}
894
895impl Revision {
896 pub fn validate(&self) -> Result<(), ReviseError> {
899 if self.patch.is_empty() {
900 return Err(ReviseError::EmptyPatch);
901 }
902 for (path, _) in &self.duplicates {
903 let removed = self.patch.iter().any(|op| {
904 matches!(op, json_patch::PatchOperation::Remove(r) if r.path.as_str() == path)
905 });
906 if !removed {
907 return Err(ReviseError::NotRemoved(path.clone()));
908 }
909 }
910 Ok(())
911 }
912
913 pub fn remove_duplicates(
917 data: &serde_json::Value,
918 pairs: &[(&str, &str)],
919 ) -> Result<Edit, ReviseError> {
920 let mut ops = Vec::with_capacity(pairs.len());
921 for (path, _) in pairs {
922 let path = json_patch::jsonptr::PointerBuf::parse(*path)
923 .map_err(|e| ReviseError::Patch(e.to_string()))?;
924 ops.push(json_patch::PatchOperation::Remove(
925 json_patch::RemoveOperation { path },
926 ));
927 }
928 let edit = Edit {
929 patch: json_patch::Patch(ops),
930 duplicates: pairs
931 .iter()
932 .map(|(p, s)| (p.to_string(), s.to_string()))
933 .collect(),
934 };
935 check_duplicates(data, &edit)?;
936 Ok(edit)
937 }
938}
939
940fn check_duplicates(
943 data: &serde_json::Value,
944 edit: &Edit,
945) -> Result<(), ReviseError> {
946 for (path, same_as) in &edit.duplicates {
947 let resolve = |pointer: &str| {
948 data.pointer(pointer)
949 .ok_or_else(|| ReviseError::Unresolved(pointer.to_string()))
950 };
951 let (removed, kept) = (resolve(path)?, resolve(same_as)?);
952 if canonical_json(removed) != canonical_json(kept) {
953 return Err(ReviseError::NotIdentical {
954 path: path.clone(),
955 same_as: same_as.clone(),
956 });
957 }
958 }
959 let revised = apply_patch(data, &edit.patch)?;
960 for (path, same_as) in &edit.duplicates {
961 let kept = data.pointer(same_as);
962 let removes = |op: &json_patch::PatchOperation| matches!(op, json_patch::PatchOperation::Remove(r) if r.path.as_str() == path);
963 if !edit.patch.iter().any(removes) {
964 return Err(ReviseError::NotRemoved(path.clone()));
965 }
966 let survives = revised.pointer(same_as).is_some_and(|v| {
967 kept.is_some_and(|k| canonical_json(v) == canonical_json(k))
968 });
969 if !survives {
970 return Err(ReviseError::SourceRemoved {
971 path: path.clone(),
972 same_as: same_as.clone(),
973 });
974 }
975 }
976 Ok(())
977}
978
979#[derive(Debug, Clone, PartialEq, Eq)]
982pub struct AmendmentDraft {
983 pub amendment: Amendment,
984 pub texts: AmendmentTexts,
985}
986
987impl AmendmentDraft {
988 pub fn new(
995 target: GovernanceLogId,
996 target_entry_hash: Sha256Hex,
997 kind: AmendmentKind,
998 basis: impl Into<CommittedText>,
999 note: impl Into<CommittedText>,
1000 ) -> Result<Self, AmendmentError> {
1001 match kind {
1002 AmendmentKind::Redaction => {
1003 return Err(AmendmentError::MissingRedaction);
1004 }
1005 AmendmentKind::Revision => {
1006 return Err(AmendmentError::MissingRevision);
1007 }
1008 k if k.is_obligation() => {
1009 return Err(AmendmentError::MissingUpdate(k));
1010 }
1011 _ => {}
1012 }
1013 let (basis, note) = (basis.into(), note.into());
1014 Ok(Self {
1015 amendment: Amendment {
1016 agora_governance_amendment: AMENDMENT_VERSION,
1017 target,
1018 target_entry_hash,
1019 kind,
1020 authority: None,
1021 basis: AmendmentText::Committed(basis.commitment()),
1022 note: AmendmentText::Committed(note.commitment()),
1023 rationale: None,
1024 redaction: None,
1025 revision: None,
1026 update: None,
1027 },
1028 texts: AmendmentTexts {
1029 basis: Some(basis),
1030 note: Some(note),
1031 rationale: None,
1032 },
1033 })
1034 }
1035
1036 #[allow(clippy::too_many_arguments)]
1054 pub fn redaction(
1055 amendment_id: &GovernanceLogId,
1056 target: GovernanceLogId,
1057 target_entry_hash: Sha256Hex,
1058 basis: impl Into<CommittedText>,
1059 note: impl Into<CommittedText>,
1060 mut fields: Vec<String>,
1061 data: &serde_json::Value,
1062 blind: Blind,
1063 revisions: &[&Revision],
1064 ) -> Result<(Self, serde_json::Value), RedactError> {
1065 for extra in duplicates_of(&fields, revisions) {
1066 let covered = fields.iter().any(|f| {
1067 extra.strip_prefix(f.as_str()).is_some_and(|rest| {
1068 rest.is_empty() || rest.starts_with('/')
1069 })
1070 });
1071 if !covered && data.pointer(&extra).is_some() {
1072 fields.push(extra);
1073 }
1074 }
1075 let redacted = redact_data(data, &fields, amendment_id, blind)?;
1076 let resulting_latest_hash = match revisions {
1077 [] => None,
1078 _ => Some(data_hash(
1079 &latest(&redacted, revisions.iter().copied())
1080 .map_err(|e| RedactError::Rebase(e.to_string()))?,
1081 )),
1082 };
1083 let (basis, note) = (basis.into(), note.into());
1084 Ok((
1085 Self {
1086 amendment: Amendment {
1087 agora_governance_amendment: AMENDMENT_VERSION,
1088 target,
1089 target_entry_hash,
1090 kind: AmendmentKind::Redaction,
1091 authority: None,
1092 basis: AmendmentText::Committed(basis.commitment()),
1093 note: AmendmentText::Committed(note.commitment()),
1094 rationale: None,
1095 redaction: Some(Redaction {
1096 fields,
1097 resulting_data_hash: data_hash(&redacted),
1098 resulting_latest_hash,
1099 }),
1100 revision: None,
1101 update: None,
1102 },
1103 texts: AmendmentTexts {
1104 basis: Some(basis),
1105 note: Some(note),
1106 rationale: None,
1107 },
1108 },
1109 redacted,
1110 ))
1111 }
1112
1113 pub fn revision(
1124 target: GovernanceLogId,
1125 target_type: GovernanceLogEntryType,
1126 target_entry_hash: Sha256Hex,
1127 basis: impl Into<CommittedText>,
1128 note: impl Into<CommittedText>,
1129 latest: &serde_json::Value,
1130 edit: impl Into<Edit>,
1131 ) -> Result<(Self, serde_json::Value), ReviseError> {
1132 let Edit {
1133 mut patch,
1134 duplicates,
1135 } = edit.into();
1136 if !is_revisable(target_type) {
1137 return Err(ReviseError::NotRevisable(target_type));
1138 }
1139 if patch.is_empty() {
1140 return Err(ReviseError::EmptyPatch);
1141 }
1142 let blind_pointer = format!("/{BLIND_KEY}");
1143 for op in patch.iter() {
1144 let from = match op {
1145 json_patch::PatchOperation::Move(m) => Some(m.from.as_str()),
1146 json_patch::PatchOperation::Copy(c) => Some(c.from.as_str()),
1147 _ => None,
1148 };
1149 for pointer in
1150 [Some(op.path().as_str()), from].into_iter().flatten()
1151 {
1152 if overlaps(pointer, &blind_pointer) {
1153 return Err(ReviseError::BlindPointer(pointer.to_string()));
1154 }
1155 }
1156 }
1157 let adds = patch.iter().any(|op| {
1158 matches!(
1159 op,
1160 json_patch::PatchOperation::Add(_)
1161 | json_patch::PatchOperation::Replace(_)
1162 )
1163 });
1164 if adds && latest.is_object() && latest.get(BLIND_KEY).is_none() {
1165 patch.0.push(json_patch::PatchOperation::Add(
1166 json_patch::AddOperation {
1167 path: json_patch::jsonptr::PointerBuf::from_tokens([
1168 BLIND_KEY,
1169 ]),
1170 value: Blind::random().to_hex().into(),
1171 },
1172 ));
1173 }
1174 let edit = Edit { patch, duplicates };
1175 check_duplicates(latest, &edit)?;
1176 let Edit { patch, duplicates } = edit;
1177 let revised = apply_patch(latest, &patch)?;
1178 let (basis, note) = (basis.into(), note.into());
1179 Ok((
1180 Self {
1181 amendment: Amendment {
1182 agora_governance_amendment: AMENDMENT_VERSION,
1183 target,
1184 target_entry_hash,
1185 kind: AmendmentKind::Revision,
1186 authority: None,
1187 basis: AmendmentText::Committed(basis.commitment()),
1188 note: AmendmentText::Committed(note.commitment()),
1189 rationale: None,
1190 redaction: None,
1191 revision: Some(Revision {
1192 patch,
1193 duplicates,
1194 resulting_data_hash: data_hash(&revised),
1195 }),
1196 update: None,
1197 },
1198 texts: AmendmentTexts {
1199 basis: Some(basis),
1200 note: Some(note),
1201 rationale: None,
1202 },
1203 },
1204 revised,
1205 ))
1206 }
1207
1208 pub fn obligation(
1210 target: GovernanceLogId,
1211 target_entry_hash: Sha256Hex,
1212 update: ObligationUpdate,
1213 basis: impl Into<CommittedText>,
1214 note: impl Into<CommittedText>,
1215 ) -> Self {
1216 let (basis, note) = (basis.into(), note.into());
1217 Self {
1218 amendment: Amendment {
1219 agora_governance_amendment: AMENDMENT_VERSION,
1220 target,
1221 target_entry_hash,
1222 kind: update.kind(),
1223 authority: None,
1224 basis: AmendmentText::Committed(basis.commitment()),
1225 note: AmendmentText::Committed(note.commitment()),
1226 rationale: None,
1227 redaction: None,
1228 revision: None,
1229 update: Some(update),
1230 },
1231 texts: AmendmentTexts {
1232 basis: Some(basis),
1233 note: Some(note),
1234 rationale: None,
1235 },
1236 }
1237 }
1238
1239 pub fn with_authority(mut self, authority: GovernanceLogId) -> Self {
1241 self.amendment.authority = Some(authority);
1242 self
1243 }
1244
1245 pub fn with_rationale(
1247 mut self,
1248 rationale: impl Into<CommittedText>,
1249 ) -> Self {
1250 let rationale = rationale.into();
1251 self.amendment.rationale =
1252 Some(AmendmentText::Committed(rationale.commitment()));
1253 self.texts.rationale = Some(rationale);
1254 self
1255 }
1256}
1257
1258impl Amendment {
1259 pub fn validate(&self) -> Result<(), AmendmentError> {
1263 let plain = match self.agora_governance_amendment {
1264 1 => true,
1265 AMENDMENT_VERSION => false,
1266 other => return Err(AmendmentError::UnsupportedVersion(other)),
1267 };
1268 let texts =
1269 [Some(&self.basis), Some(&self.note), self.rationale.as_ref()];
1270 if texts.into_iter().flatten().any(|t| t.is_plain() != plain) {
1271 return Err(AmendmentError::TextShape);
1272 }
1273 match (self.kind, &self.redaction) {
1274 (AmendmentKind::Redaction, None) => {
1275 return Err(AmendmentError::MissingRedaction);
1276 }
1277 (k, Some(_)) if k != AmendmentKind::Redaction => {
1278 return Err(AmendmentError::UnexpectedRedaction);
1279 }
1280 _ => {}
1281 }
1282 match (self.kind, &self.revision) {
1283 (AmendmentKind::Revision, None) => {
1284 return Err(AmendmentError::MissingRevision);
1285 }
1286 (AmendmentKind::Revision, Some(r)) => {
1287 r.validate().map_err(AmendmentError::RevisionShape)?;
1288 }
1289 (_, Some(_)) => return Err(AmendmentError::UnexpectedRevision),
1290 _ => {}
1291 }
1292 match (self.kind, &self.update) {
1293 (kind, None) if kind.is_obligation() => {
1294 Err(AmendmentError::MissingUpdate(kind))
1295 }
1296 (kind, Some(_)) if !kind.is_obligation() => {
1297 Err(AmendmentError::UnexpectedUpdate)
1298 }
1299 (kind, Some(update)) if update.kind() != kind => {
1300 Err(AmendmentError::UpdateMismatch {
1301 kind,
1302 update: update.kind(),
1303 })
1304 }
1305 _ => Ok(()),
1306 }
1307 }
1308
1309 pub fn text_status(
1316 &self,
1317 beside: Option<&AmendmentTexts>,
1318 ) -> Result<Option<AmendmentTextStatus>, AmendmentError> {
1319 let empty = AmendmentTexts::default();
1320 let beside = beside.unwrap_or(&empty);
1321 let (Some(basis), Some(note)) = (
1322 self.basis.status(beside.basis.as_ref()),
1323 self.note.status(beside.note.as_ref()),
1324 ) else {
1325 return if beside.is_empty() {
1326 Ok(None)
1327 } else {
1328 Err(AmendmentError::UncommittedText)
1329 };
1330 };
1331 let rationale = match (&self.rationale, &beside.rationale) {
1332 (None, Some(_)) => return Err(AmendmentError::UncommittedText),
1333 (None, None) => None,
1334 (Some(text), beside) => text.status(beside.as_ref()),
1335 };
1336 for (name, status) in [
1337 ("basis", Some(basis)),
1338 ("note", Some(note)),
1339 ("rationale", rationale),
1340 ] {
1341 if status == Some(TextStatus::Mismatch) {
1342 return Err(AmendmentError::TextMismatch(name));
1343 }
1344 }
1345 Ok(Some(AmendmentTextStatus {
1346 basis,
1347 note,
1348 rationale,
1349 }))
1350 }
1351}
1352
1353pub fn kind_standing(kind: AmendmentKind) -> Option<Standing> {
1356 match kind {
1357 AmendmentKind::NonPrecedential => Some(Standing::NonPrecedential),
1358 AmendmentKind::Overruled => Some(Standing::Overruled),
1359 AmendmentKind::Superseded => Some(Standing::Superseded),
1360 AmendmentKind::Reinstated => Some(Standing::InForce),
1361 AmendmentKind::Correction
1362 | AmendmentKind::Redaction
1363 | AmendmentKind::Reattested
1364 | AmendmentKind::Revision
1365 | AmendmentKind::Planned
1366 | AmendmentKind::Discharged
1367 | AmendmentKind::Lapsed
1368 | AmendmentKind::Rescheduled
1369 | AmendmentKind::Withdrawn => None,
1370 }
1371}
1372
1373pub fn standing(kinds: impl IntoIterator<Item = AmendmentKind>) -> Standing {
1376 kinds
1377 .into_iter()
1378 .filter_map(kind_standing)
1379 .last()
1380 .unwrap_or_default()
1381}
1382
1383pub const BLIND_KEY: &str = "_blind";
1386
1387pub fn is_redactable(entry_type: GovernanceLogEntryType) -> bool {
1391 !matches!(
1392 entry_type,
1393 GovernanceLogEntryType::Amendment | GovernanceLogEntryType::KeyRotation
1394 )
1395}
1396
1397#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1399pub enum BlindError {
1400 #[error("a redactable entry's data must be a JSON object")]
1401 NotAnObject,
1402 #[error(
1403 "data already has a {BLIND_KEY:?} key; the writer supplies it, not the caller"
1404 )]
1405 AlreadyBlinded,
1406 #[error(
1407 "{0:?} is a number that is not a 64-bit integer; governance data \
1408 never contains one (put a fraction in a string)"
1409 )]
1410 NonIntegerNumber(String),
1411}
1412
1413pub fn blind_data(
1431 data: &serde_json::Value,
1432 blind: Blind,
1433) -> Result<serde_json::Value, BlindError> {
1434 if let Some(pointer) = non_integer_number(data) {
1435 return Err(BlindError::NonIntegerNumber(pointer));
1436 }
1437 let mut out = data.clone();
1438 let object = out.as_object_mut().ok_or(BlindError::NotAnObject)?;
1439 if object.contains_key(BLIND_KEY) {
1440 return Err(BlindError::AlreadyBlinded);
1441 }
1442 object.insert(BLIND_KEY.to_string(), blind.to_hex().into());
1443 Ok(out)
1444}
1445
1446#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1448pub enum RedactError {
1449 #[error("pointer {0:?} does not resolve in the entry's data")]
1450 Unresolved(String),
1451 #[error("the empty pointer would redact the whole entry")]
1452 WholeEntry,
1453 #[error("a redaction names at least one pointer")]
1454 NoFields,
1455 #[error("{0:?} is the entry's blind; every redaction replaces it already")]
1456 BlindPointer(String),
1457 #[error("a revision no longer applies to the redacted data: {0}")]
1458 Rebase(String),
1459 #[error(
1460 "{0:?} is a number that is not a 64-bit integer; governance data \
1461 never contains one"
1462 )]
1463 NonIntegerNumber(String),
1464}
1465
1466pub fn redaction_marker(amendment_id: &GovernanceLogId) -> String {
1468 format!("[redacted by {amendment_id}]")
1469}
1470
1471pub fn redact_data(
1487 data: &serde_json::Value,
1488 fields: &[String],
1489 amendment_id: &GovernanceLogId,
1490 blind: Blind,
1491) -> Result<serde_json::Value, RedactError> {
1492 if fields.is_empty() {
1493 return Err(RedactError::NoFields);
1494 }
1495 if let Some(pointer) = non_integer_number(data) {
1496 return Err(RedactError::NonIntegerNumber(pointer));
1497 }
1498 let blind_pointer = format!("/{BLIND_KEY}");
1499 let marker = serde_json::Value::String(redaction_marker(amendment_id));
1500 let mut out = data.clone();
1501 for pointer in fields {
1502 if pointer.is_empty() {
1503 return Err(RedactError::WholeEntry);
1504 }
1505 if *pointer == blind_pointer {
1506 return Err(RedactError::BlindPointer(pointer.clone()));
1507 }
1508 let slot = out
1509 .pointer_mut(pointer)
1510 .ok_or_else(|| RedactError::Unresolved(pointer.clone()))?;
1511 *slot = marker.clone();
1512 }
1513 if let Some(object) = out.as_object_mut() {
1516 object.insert(BLIND_KEY.to_string(), blind.to_hex().into());
1517 }
1518 Ok(out)
1519}
1520
1521pub fn is_revisable(entry_type: GovernanceLogEntryType) -> bool {
1525 is_redactable(entry_type)
1526 && entry_type != GovernanceLogEntryType::StewardRecord
1527}
1528
1529#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1531pub enum ReviseError {
1532 #[error("a revision's patch has at least one op")]
1533 EmptyPatch,
1534 #[error("the patch does not apply: {0}")]
1535 Patch(String),
1536 #[error("{0:?} is the entry's blind, which a revision never touches")]
1537 BlindPointer(String),
1538 #[error("pointer {0:?} does not resolve")]
1539 Unresolved(String),
1540 #[error("{path:?} is not byte-identical to {same_as:?}")]
1541 NotIdentical { path: String, same_as: String },
1542 #[error("{same_as:?}, which {path:?} duplicates, has to survive the patch")]
1543 SourceRemoved { path: String, same_as: String },
1544 #[error("{0:?} is listed as a duplicate the patch does not remove")]
1545 NotRemoved(String),
1546 #[error("{0} entries are never revised")]
1547 NotRevisable(GovernanceLogEntryType),
1548 #[error(
1549 "{0:?} is a number that is not a 64-bit integer; governance data \
1550 never contains one"
1551 )]
1552 NonIntegerNumber(String),
1553}
1554
1555fn overlaps(a: &str, b: &str) -> bool {
1557 let within = |inner: &str, outer: &str| {
1558 inner
1559 .strip_prefix(outer)
1560 .is_some_and(|rest| rest.is_empty() || rest.starts_with('/'))
1561 };
1562 within(a, b) || within(b, a)
1563}
1564
1565pub fn apply_patch(
1567 data: &serde_json::Value,
1568 patch: &json_patch::Patch,
1569) -> Result<serde_json::Value, ReviseError> {
1570 let mut out = data.clone();
1571 json_patch::patch(&mut out, patch)
1572 .map_err(|e| ReviseError::Patch(e.to_string()))?;
1573 if let Some(pointer) = non_integer_number(&out) {
1574 return Err(ReviseError::NonIntegerNumber(pointer));
1575 }
1576 Ok(out)
1577}
1578
1579#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1582#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1583#[cfg_attr(feature = "schemars", schemars(inline))]
1584pub struct Change {
1585 pub kind: ChangeKind,
1586 pub path: String,
1588 #[serde(default, skip_serializing_if = "Option::is_none")]
1590 pub from: Option<String>,
1591 #[serde(default, skip_serializing_if = "Option::is_none")]
1593 pub before: Option<serde_json::Value>,
1594 #[serde(default, skip_serializing_if = "Option::is_none")]
1596 pub after: Option<serde_json::Value>,
1597 #[serde(default, skip_serializing_if = "Option::is_none")]
1601 pub omitted_bytes: Option<u64>,
1602}
1603
1604pub const MAX_CHANGE_BYTES: usize = 2_048;
1606pub const MAX_HISTORY_BYTES: usize = 16_384;
1608
1609#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
1614#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1615#[cfg_attr(feature = "schemars", schemars(inline))]
1616pub struct ChangeHistory {
1617 pub total: u64,
1619 pub first: u64,
1621 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1623 pub revisions: Vec<RevisionChanges>,
1624 #[serde(default, skip_serializing_if = "Option::is_none")]
1626 pub broken: Option<String>,
1627}
1628
1629#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1631#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1632#[cfg_attr(feature = "schemars", schemars(inline))]
1633pub struct RevisionChanges {
1634 pub id: GovernanceLogId,
1636 #[serde(default, skip_serializing_if = "Option::is_none")]
1637 pub created_at: Option<DateTime<Utc>>,
1638 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1639 pub changes: Vec<Change>,
1640 #[serde(default, skip_serializing_if = "Option::is_none")]
1643 pub omitted_bytes: Option<u64>,
1644 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1648 pub blind: bool,
1649}
1650
1651impl ChangeHistory {
1652 pub fn build(
1658 original: &serde_json::Value,
1659 revisions: &[(GovernanceLogId, Revision)],
1660 dated: impl Fn(&GovernanceLogId) -> Option<DateTime<Utc>>,
1661 shown: impl Fn(&GovernanceLogId) -> bool,
1662 history: crate::requests::History,
1663 ) -> Self {
1664 let total = revisions.len();
1665 let (changes, error) =
1666 revision_changes(original, revisions.iter().map(|(_, r)| r));
1667 let first = total - history.count(total);
1668 let broken = error.map(|e| {
1669 format!(
1670 "{} does not apply to the version before it ({e})",
1671 revisions[changes.len()].0
1672 )
1673 });
1674 let mut budget = MAX_HISTORY_BYTES;
1675 let listed = revisions
1676 .iter()
1677 .zip(changes)
1678 .skip(first)
1679 .map(|((id, _), changes)| {
1680 let blind_path = format!("/{BLIND_KEY}");
1681 let is_blind = |c: &Change| {
1682 c.path == blind_path
1683 || c.path.starts_with(&format!("{blind_path}/"))
1684 };
1685 let blind = changes.iter().any(is_blind);
1686 let changes: Vec<Change> = changes
1687 .into_iter()
1688 .filter(|c| !is_blind(c))
1689 .map(Change::capped)
1690 .collect();
1691 let bytes = serde_json::to_vec(&changes).map_or(0, |v| v.len());
1692 let fits = shown(id) || bytes <= budget;
1694 if fits && !shown(id) {
1695 budget -= bytes;
1696 }
1697 RevisionChanges {
1698 id: id.clone(),
1699 created_at: dated(id),
1700 changes: if fits { changes } else { Vec::new() },
1701 omitted_bytes: (!fits).then_some(bytes as u64),
1702 blind,
1703 }
1704 })
1705 .collect();
1706 Self {
1707 total: total as u64,
1708 first: first as u64,
1709 revisions: listed,
1710 broken,
1711 }
1712 }
1713
1714 pub fn unreadable(total: usize, why: String) -> Self {
1716 Self {
1717 total: total as u64,
1718 broken: Some(why),
1719 ..Self::default()
1720 }
1721 }
1722}
1723
1724impl Change {
1725 fn capped(self) -> Self {
1729 let bytes = |v: &Option<serde_json::Value>| {
1730 v.as_ref()
1731 .and_then(|v| serde_json::to_vec(v).ok())
1732 .map_or(0, |v| v.len())
1733 };
1734 let total = bytes(&self.before) + bytes(&self.after);
1735 if total <= MAX_CHANGE_BYTES && self.kind != ChangeKind::Removed {
1736 return self;
1737 }
1738 Self {
1739 before: None,
1740 after: None,
1741 omitted_bytes: Some(total as u64),
1742 ..self
1743 }
1744 }
1745}
1746
1747#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1749#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1750#[cfg_attr(feature = "schemars", schemars(inline))]
1751#[serde(rename_all = "snake_case")]
1752pub enum ChangeKind {
1753 Added,
1754 Replaced,
1755 Removed,
1756 Moved,
1757 Copied,
1758}
1759
1760pub fn revision_changes<'a>(
1764 original: &serde_json::Value,
1765 revisions: impl IntoIterator<Item = &'a Revision>,
1766) -> (Vec<Vec<Change>>, Option<ReviseError>) {
1767 use json_patch::PatchOperation as Op;
1768 let mut current = original.clone();
1769 let mut out = Vec::new();
1770 for revision in revisions {
1771 let mut changes = Vec::new();
1772 for op in &revision.patch.0 {
1773 let at = |p: &json_patch::jsonptr::Pointer| {
1774 current.pointer(p.as_str()).cloned()
1775 };
1776 let replaced = |p: &json_patch::jsonptr::Pointer| {
1779 let parent = p.parent().map_or("", |q| q.as_str());
1780 current
1781 .pointer(parent)
1782 .filter(|v| v.is_object())
1783 .and_then(|_| at(p))
1784 };
1785 let change = match op {
1786 Op::Test(_) => None,
1787 Op::Add(a) => Some(Change {
1788 kind: ChangeKind::Added,
1789 path: a.path.to_string(),
1790 from: None,
1791 before: replaced(&a.path),
1792 after: Some(a.value.clone()),
1793 omitted_bytes: None,
1794 }),
1795 Op::Replace(r) => Some(Change {
1796 kind: ChangeKind::Replaced,
1797 path: r.path.to_string(),
1798 from: None,
1799 before: at(&r.path),
1800 after: Some(r.value.clone()),
1801 omitted_bytes: None,
1802 }),
1803 Op::Remove(r) => Some(Change {
1804 kind: ChangeKind::Removed,
1805 path: r.path.to_string(),
1806 from: None,
1807 before: at(&r.path),
1808 after: None,
1809 omitted_bytes: None,
1810 }),
1811 Op::Move(m) => Some(Change {
1812 kind: ChangeKind::Moved,
1813 path: m.path.to_string(),
1814 from: Some(m.from.to_string()),
1815 before: replaced(&m.path),
1816 after: at(&m.from),
1817 omitted_bytes: None,
1818 }),
1819 Op::Copy(c) => Some(Change {
1820 kind: ChangeKind::Copied,
1821 path: c.path.to_string(),
1822 from: Some(c.from.to_string()),
1823 before: replaced(&c.path),
1824 after: at(&c.from),
1825 omitted_bytes: None,
1826 }),
1827 };
1828 match apply_patch(¤t, &json_patch::Patch(vec![op.clone()])) {
1829 Ok(next) => current = next,
1830 Err(e) => return (out, Some(e)),
1831 }
1832 changes.extend(change);
1833 }
1834 out.push(changes);
1835 }
1836 (out, None)
1837}
1838
1839pub fn latest<'a>(
1844 data: &serde_json::Value,
1845 revisions: impl IntoIterator<Item = &'a Revision>,
1846) -> Result<serde_json::Value, ReviseError> {
1847 let mut current = data.clone();
1848 for revision in revisions {
1849 current = apply_patch(¤t, &revision.patch)?;
1850 }
1851 Ok(current)
1852}
1853
1854fn duplicates_of(fields: &[String], revisions: &[&Revision]) -> Vec<String> {
1858 let mut extra = Vec::new();
1859 for (removed, same_as) in revisions.iter().flat_map(|r| &r.duplicates) {
1860 for field in fields {
1861 if let Some(rest) = field.strip_prefix(same_as.as_str())
1862 && (rest.is_empty() || rest.starts_with('/'))
1863 {
1864 extra.push(format!("{removed}{rest}"));
1866 } else if overlaps(field, same_as) {
1867 extra.push(removed.clone());
1869 }
1870 }
1871 }
1872 extra
1873}
1874
1875#[derive(Debug, Clone, PartialEq, Eq, Default)]
1878pub struct RevisionHistory {
1879 pub revisions: Vec<(GovernanceLogId, Revision)>,
1881 pub rebased: usize,
1884 pub rebased_hash: Option<Sha256Hex>,
1886}
1887
1888#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1890#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1891#[cfg_attr(feature = "schemars", schemars(inline))]
1892pub struct AmendmentNotice {
1893 pub id: GovernanceLogId,
1894 pub kind: AmendmentKind,
1895 #[serde(default)]
1896 pub authority: Option<GovernanceLogId>,
1897 pub basis: String,
1898 pub note: String,
1899 #[serde(default, skip_serializing_if = "Option::is_none")]
1901 pub rationale: Option<String>,
1902 #[serde(default, skip_serializing_if = "Option::is_none")]
1904 pub update: Option<ObligationUpdate>,
1905 pub created_at: DateTime<Utc>,
1906}
1907
1908impl AmendmentNotice {
1909 pub fn new(
1912 id: GovernanceLogId,
1913 created_at: DateTime<Utc>,
1914 amendment: &Amendment,
1915 beside: Option<&AmendmentTexts>,
1916 ) -> Self {
1917 let beside = beside.cloned().unwrap_or_default();
1918 Self {
1919 id,
1920 kind: amendment.kind,
1921 authority: amendment.authority.clone(),
1922 basis: amendment.basis.resolve(beside.basis.as_ref()).into(),
1923 note: amendment.note.resolve(beside.note.as_ref()).into(),
1924 rationale: amendment
1925 .rationale
1926 .as_ref()
1927 .map(|r| r.resolve(beside.rationale.as_ref()).into()),
1928 update: amendment.update.clone(),
1929 created_at,
1930 }
1931 }
1932}
1933
1934#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1940#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1941#[cfg_attr(feature = "schemars", schemars(inline))]
1942pub struct GovernanceCitation {
1943 pub id: GovernanceLogId,
1944 pub entry_type: GovernanceLogEntryType,
1945 pub title: String,
1946 pub relation: CitationRelation,
1947 pub created_at: DateTime<Utc>,
1948}
1949
1950#[derive(Debug, thiserror::Error)]
1954#[error("the record's {relation:?} field does not parse: {error}")]
1955pub struct MalformedCitation {
1956 pub relation: CitationRelation,
1959 #[source]
1960 pub error: serde_json::Error,
1961}
1962
1963pub fn citations_in(
1968 entry_type: GovernanceLogEntryType,
1969 data: &serde_json::Value,
1970) -> Result<Vec<(GovernanceLogId, CitationRelation)>, MalformedCitation> {
1971 #[derive(Deserialize)]
1972 struct Concerns {
1973 #[serde(default)]
1974 concerns: Vec<GovernanceLogId>,
1975 }
1976 #[derive(Deserialize)]
1977 struct Authority {
1978 #[serde(default)]
1979 authority: Option<GovernanceLogId>,
1980 }
1981 #[derive(Deserialize)]
1982 struct Overrules {
1983 #[serde(default)]
1984 overrules: Vec<GovernanceLogId>,
1985 }
1986 fn each(
1987 ids: impl IntoIterator<Item = GovernanceLogId>,
1988 relation: CitationRelation,
1989 ) -> Vec<(GovernanceLogId, CitationRelation)> {
1990 ids.into_iter().map(|id| (id, relation)).collect()
1991 }
1992 let malformed =
1993 |relation| move |error| MalformedCitation { relation, error };
1994 match entry_type {
1995 GovernanceLogEntryType::StewardRecord => {
1996 serde_json::from_value::<Concerns>(data.clone())
1997 .map(|c| each(c.concerns, CitationRelation::Concerns))
1998 .map_err(malformed(CitationRelation::Concerns))
1999 }
2000 GovernanceLogEntryType::Amendment => {
2001 serde_json::from_value::<Authority>(data.clone())
2002 .map(|a| each(a.authority, CitationRelation::Authority))
2003 .map_err(malformed(CitationRelation::Authority))
2004 }
2005 GovernanceLogEntryType::CouncilDecision => {
2006 serde_json::from_value::<Overrules>(data.clone())
2007 .map(|o| each(o.overrules, CitationRelation::Overrules))
2008 .map_err(malformed(CitationRelation::Overrules))
2009 }
2010 _ => Ok(Vec::new()),
2011 }
2012}
2013
2014pub const KEY_ROTATION_VERSION: u32 = 2;
2020
2021pub const PUBLISHED_KEYS: &[&str] =
2028 &["ebb3091dd328f1463362c171121921b2fe14628e3fc4c145deaccefb85c0e78a"];
2029
2030#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2032#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2033#[cfg_attr(feature = "schemars", schemars(inline))]
2034#[serde(rename_all = "snake_case")]
2035pub enum RotationReason {
2036 Routine,
2038 Compromise,
2041}
2042
2043#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2045#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2046#[cfg_attr(feature = "schemars", schemars(inline))]
2047#[serde(deny_unknown_fields)]
2048pub struct TrustedHead {
2049 pub id: GovernanceLogId,
2050 pub chain_seq: u64,
2051 pub entry_hash: Sha256Hex,
2052}
2053
2054#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
2056pub enum RotationError {
2057 #[error("agora_governance_key_rotation is {0}, not {KEY_ROTATION_VERSION}")]
2058 UnsupportedVersion(u32),
2059 #[error("new_key is not a valid Ed25519 public key")]
2060 BadNewKey,
2061 #[error(
2062 "the proof of possession does not verify for this rotation at this position"
2063 )]
2064 BadProof,
2065 #[error("a compromise certificate must name last_trusted")]
2066 MissingLastTrusted,
2067 #[error("certificate: {0}")]
2068 Certificate(#[from] CertificateError),
2069 #[error("outgoing_certificate: {0}")]
2070 OutgoingCertificate(CertificateError),
2071 #[error(
2072 "the chain's first rotation must carry the genesis key's \
2073 outgoing_certificate"
2074 )]
2075 MissingGenesisCertificate,
2076 #[error(
2077 "outgoing_certificate belongs on the chain's first rotation and \
2078 nowhere else"
2079 )]
2080 UnexpectedOutgoingCertificate,
2081 #[error("old_key is not the key that was in force")]
2082 WrongOldKey,
2083 #[error("last_trusted does not name an earlier entry of this chain")]
2084 UnknownLastTrusted,
2085 #[error("new_key has already held this chain; a key is never brought back")]
2086 ReusedKey,
2087 #[error("last_trusted names an entry an earlier compromise repudiated")]
2088 RepudiatedLastTrusted,
2089}
2090
2091#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2102#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2103#[cfg_attr(feature = "schemars", schemars(inline))]
2104#[serde(deny_unknown_fields)]
2105pub struct KeyRotation {
2106 pub agora_governance_key_rotation: u32,
2108 pub reason: RotationReason,
2109 pub old_key: PublicKeyHex,
2110 pub new_key: PublicKeyHex,
2111 pub proof: SignatureHex,
2115 pub proof_signed_at: i64,
2117 pub certificate: KeyCertificate,
2121 #[serde(default, skip_serializing_if = "Option::is_none")]
2125 pub outgoing_certificate: Option<KeyCertificate>,
2126}
2127
2128#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2135pub struct RotationStatement {
2136 pub agora_governance_key_rotation: u32,
2138 pub reason: RotationReason,
2139 pub old_key: PublicKeyHex,
2140 pub new_key: PublicKeyHex,
2141 pub prev_hash: Option<Sha256Hex>,
2142}
2143
2144impl RotationStatement {
2145 pub fn new(
2147 reason: RotationReason,
2148 old_key: PublicKeyHex,
2149 new_key: PublicKeyHex,
2150 prev_hash: Option<Sha256Hex>,
2151 ) -> Self {
2152 Self {
2153 agora_governance_key_rotation: KEY_ROTATION_VERSION,
2154 reason,
2155 old_key,
2156 new_key,
2157 prev_hash,
2158 }
2159 }
2160
2161 pub fn preimage(&self) -> Vec<u8> {
2163 serde_json::to_vec(self).expect("a RotationStatement always serializes")
2164 }
2165
2166 pub fn hash(&self) -> Sha256Hex {
2168 Sha256Hex(Sha256::digest(self.preimage()).into())
2169 }
2170}
2171
2172impl KeyRotation {
2173 pub fn routine(
2179 old_key: PublicKeyHex,
2180 new_signing_key: &SigningKey,
2181 prev_hash: Option<Sha256Hex>,
2182 now: DateTime<Utc>,
2183 certificate: KeyCertificate,
2184 ) -> Self {
2185 Self::build(
2186 RotationReason::Routine,
2187 old_key,
2188 new_signing_key,
2189 prev_hash,
2190 now,
2191 certificate,
2192 )
2193 }
2194
2195 pub fn compromise(
2203 old_key: PublicKeyHex,
2204 new_signing_key: &SigningKey,
2205 prev_hash: Option<Sha256Hex>,
2206 now: DateTime<Utc>,
2207 certificate: KeyCertificate,
2208 ) -> Self {
2209 Self::build(
2210 RotationReason::Compromise,
2211 old_key,
2212 new_signing_key,
2213 prev_hash,
2214 now,
2215 certificate,
2216 )
2217 }
2218
2219 fn build(
2220 reason: RotationReason,
2221 old_key: PublicKeyHex,
2222 new_signing_key: &SigningKey,
2223 prev_hash: Option<Sha256Hex>,
2224 now: DateTime<Utc>,
2225 certificate: KeyCertificate,
2226 ) -> Self {
2227 let new_key = PublicKeyHex::from(&new_signing_key.verifying_key());
2228 let proof_signed_at = truncate_to_seconds(now).timestamp();
2229 let statement =
2230 RotationStatement::new(reason, old_key, new_key, prev_hash);
2231 let proof = crypto::sign(
2232 new_signing_key,
2233 statement.hash().as_bytes(),
2234 proof_signed_at,
2235 );
2236 Self {
2237 agora_governance_key_rotation: KEY_ROTATION_VERSION,
2238 reason,
2239 old_key,
2240 new_key,
2241 proof: proof.into(),
2242 proof_signed_at,
2243 certificate,
2244 outgoing_certificate: None,
2245 }
2246 }
2247
2248 pub fn with_outgoing(mut self, certificate: KeyCertificate) -> Self {
2250 self.outgoing_certificate = Some(certificate);
2251 self
2252 }
2253
2254 pub fn statement(&self, prev_hash: Option<Sha256Hex>) -> RotationStatement {
2256 RotationStatement::new(
2257 self.reason,
2258 self.old_key,
2259 self.new_key,
2260 prev_hash,
2261 )
2262 }
2263
2264 pub fn last_trusted(&self) -> Option<&TrustedHead> {
2267 self.certificate.statement.last_trusted.as_ref()
2268 }
2269
2270 pub fn expected_statement(
2276 &self,
2277 seq: u64,
2278 prev_hash: Option<Sha256Hex>,
2279 ) -> Result<KeyCertStatement, RotationError> {
2280 match self.reason {
2281 RotationReason::Routine => {
2282 Ok(KeyCertStatement::routine(self.new_key, seq, prev_hash))
2283 }
2284 RotationReason::Compromise => Ok(KeyCertStatement::compromise(
2285 self.new_key,
2286 seq,
2287 prev_hash,
2288 self.last_trusted()
2289 .cloned()
2290 .ok_or(RotationError::MissingLastTrusted)?,
2291 )),
2292 }
2293 }
2294
2295 pub fn verify_proof(
2298 &self,
2299 prev_hash: Option<Sha256Hex>,
2300 ) -> Result<(), RotationError> {
2301 if self.agora_governance_key_rotation != KEY_ROTATION_VERSION {
2302 return Err(RotationError::UnsupportedVersion(
2303 self.agora_governance_key_rotation,
2304 ));
2305 }
2306 let new_key = self
2307 .new_key
2308 .to_verifying_key()
2309 .map_err(|_| RotationError::BadNewKey)?;
2310 crypto::verify(
2311 &new_key,
2312 self.statement(prev_hash).hash().as_bytes(),
2313 self.proof_signed_at,
2314 &Signature::from(&self.proof),
2315 )
2316 .then_some(())
2317 .ok_or(RotationError::BadProof)
2318 }
2319
2320 pub fn verify_certified(
2324 &self,
2325 seq: u64,
2326 prev_hash: Option<Sha256Hex>,
2327 roots: &RootSet,
2328 ) -> Result<(), RotationError> {
2329 self.verify_proof(prev_hash)?;
2330 let expected = self.expected_statement(seq, prev_hash)?;
2331 Ok(self.certificate.verify_for(&expected, roots)?)
2332 }
2333}
2334
2335#[derive(Debug, Clone, Default, PartialEq, Eq)]
2342pub struct KeyAnchor {
2343 keys: HashSet<PublicKeyHex>,
2344}
2345
2346impl KeyAnchor {
2347 pub fn published() -> Self {
2349 PUBLISHED_KEYS
2350 .iter()
2351 .map(|k| {
2352 k.parse()
2353 .expect("PUBLISHED_KEYS are valid 32-byte hex keys")
2354 })
2355 .collect()
2356 }
2357
2358 pub fn pinned(key: PublicKeyHex) -> Self {
2360 std::iter::once(key).collect()
2361 }
2362
2363 pub fn with(mut self, key: PublicKeyHex) -> Self {
2365 self.keys.insert(key);
2366 self
2367 }
2368
2369 pub fn contains(&self, key: &PublicKeyHex) -> bool {
2370 self.keys.contains(key)
2371 }
2372
2373 pub fn is_empty(&self) -> bool {
2374 self.keys.is_empty()
2375 }
2376
2377 pub fn keys(&self) -> impl Iterator<Item = &PublicKeyHex> {
2379 self.keys.iter()
2380 }
2381}
2382
2383impl FromIterator<PublicKeyHex> for KeyAnchor {
2384 fn from_iter<I: IntoIterator<Item = PublicKeyHex>>(iter: I) -> Self {
2385 Self {
2386 keys: iter.into_iter().collect(),
2387 }
2388 }
2389}
2390
2391#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2394#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2395#[cfg_attr(feature = "schemars", schemars(inline))]
2396pub struct GovernanceKeyRecord {
2397 pub public_key: PublicKeyHex,
2398 pub from_seq: u64,
2400 #[serde(default)]
2404 pub through_seq: Option<u64>,
2405 pub status: KeyStatus,
2406 #[serde(default)]
2409 pub introduced_by: Option<GovernanceLogId>,
2410 #[serde(default)]
2412 pub retired_by: Option<GovernanceLogId>,
2413 #[serde(default)]
2417 pub certified: bool,
2418}
2419
2420#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2425#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2426#[cfg_attr(feature = "schemars", schemars(inline))]
2427pub struct GovernanceSigningKeys {
2428 pub keys: Vec<GovernanceKeyRecord>,
2430}
2431
2432#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2434#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2435#[cfg_attr(feature = "schemars", schemars(inline))]
2436pub struct EntryVerdict {
2437 pub id: GovernanceLogId,
2438 pub chain_seq: u64,
2439 pub signature_valid: bool,
2442 pub link_valid: bool,
2445 #[serde(default)]
2452 pub content_matches: Option<bool>,
2453 pub retroactive: bool,
2455 pub out_of_order: bool,
2458 #[serde(default)]
2460 pub amended_by: Vec<GovernanceLogId>,
2461 #[serde(default, skip_serializing_if = "Option::is_none")]
2464 pub signed_by: Option<PublicKeyHex>,
2465 #[serde(default)]
2468 pub redacted: bool,
2469 #[serde(default, skip_serializing_if = "Option::is_none")]
2471 pub redacted_data_hash: Option<Sha256Hex>,
2472 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2475 pub revisions: Vec<GovernanceLogId>,
2476 #[serde(default, skip_serializing_if = "Option::is_none")]
2480 pub latest_data_hash: Option<Sha256Hex>,
2481 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2486 pub superseded_revisions: Vec<GovernanceLogId>,
2487 #[serde(skip)]
2490 pub history: RevisionHistory,
2491 #[serde(default)]
2494 pub repudiated: bool,
2495 #[serde(default)]
2498 pub reattested_by: Vec<GovernanceLogId>,
2499 #[serde(default, skip_serializing_if = "Option::is_none")]
2503 pub texts: Option<AmendmentTextStatus>,
2504 #[serde(default, skip_serializing_if = "Option::is_none")]
2506 pub problem: Option<String>,
2507}
2508
2509impl EntryVerdict {
2510 fn content_check(
2514 &mut self,
2515 attested: Sha256Hex,
2516 data: &serde_json::Value,
2517 ) -> bool {
2518 if non_integer_number(data).is_some() {
2520 return false;
2521 }
2522 let hash = data_hash(data);
2523 if hash == attested && self.redacted {
2524 return true;
2527 }
2528 if hash != attested && Some(hash) != self.redacted_data_hash {
2529 return Some(hash) == self.latest_data_hash;
2530 }
2531 let history = &self.history;
2532 let mut current = data.clone();
2533 let mut failures = Vec::new();
2534 for (i, (id, revision)) in history.revisions.iter().enumerate() {
2535 if i == history.rebased {
2536 break;
2537 }
2538 if let Some(failure) = false_duplicate(id, revision, ¤t) {
2539 failures.push(failure);
2540 break;
2541 }
2542 match apply_patch(¤t, &revision.patch) {
2543 Ok(next) => current = next,
2544 Err(_) => {
2545 failures.push(format!(
2546 "revision {id} does not apply to the redacted data"
2547 ));
2548 break;
2549 }
2550 }
2551 }
2552 if failures.is_empty()
2553 && let Some(expected) = history.rebased_hash
2554 && data_hash(¤t) != expected
2555 {
2556 failures.push(
2557 "the revisions rebased over the redacted data do not \
2558 produce the redaction's resulting_latest_hash"
2559 .to_string(),
2560 );
2561 }
2562 for (id, revision) in history.revisions.iter().skip(history.rebased) {
2563 if !failures.is_empty() {
2564 break;
2565 }
2566 if let Some(failure) = false_duplicate(id, revision, ¤t) {
2567 failures.push(failure);
2568 break;
2569 }
2570 match apply_patch(¤t, &revision.patch) {
2571 Ok(next)
2572 if data_hash(&next) == revision.resulting_data_hash =>
2573 {
2574 current = next;
2575 }
2576 Ok(_) => failures.push(format!(
2577 "revision {id} does not produce its resulting_data_hash"
2578 )),
2579 Err(_) => {
2580 failures.push(format!("revision {id} does not apply"))
2581 }
2582 }
2583 }
2584 for failure in failures {
2585 add_problem(&mut self.problem, failure);
2586 }
2587 true
2588 }
2589}
2590
2591fn add_problem(problems: &mut Option<String>, problem: String) {
2593 *problems = Some(match problems.take() {
2594 Some(p) if p.contains(&problem) => p,
2595 Some(p) => format!("{p}; {problem}"),
2596 None => problem,
2597 });
2598}
2599
2600fn false_duplicate(
2603 id: &GovernanceLogId,
2604 revision: &Revision,
2605 current: &serde_json::Value,
2606) -> Option<String> {
2607 revision.duplicates.iter().find_map(|(path, same_as)| {
2608 let same = match (current.pointer(path), current.pointer(same_as)) {
2609 (Some(a), Some(b)) => canonical_json(a) == canonical_json(b),
2610 _ => false,
2611 };
2612 (!same).then(|| {
2613 format!(
2614 "revision {id} removed {path} as a duplicate of {same_as}, \
2615 but they differ"
2616 )
2617 })
2618 })
2619}
2620
2621#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2623#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2624#[cfg_attr(feature = "schemars", schemars(inline))]
2625pub struct GovernanceVerification {
2626 pub public_key: PublicKeyHex,
2628 pub ok: bool,
2634 #[serde(default)]
2636 pub head: Option<GovernanceLogId>,
2637 pub entries: Vec<EntryVerdict>,
2639 #[serde(default)]
2641 pub keys: Vec<GovernanceKeyRecord>,
2642 #[serde(default)]
2647 pub unanchored_keys: Vec<PublicKeyHex>,
2648 #[serde(default)]
2650 pub repudiated: Vec<GovernanceLogId>,
2651}
2652
2653#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2657#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
2658#[cfg_attr(feature = "schemars", schemars(inline))]
2659pub struct GovernanceVerificationSummary {
2660 pub public_key: PublicKeyHex,
2662 pub ok: bool,
2664 #[serde(default)]
2666 pub head: Option<GovernanceLogId>,
2667 pub entries: u64,
2669 pub signature_failures: u64,
2671 pub link_failures: u64,
2673 pub content_mismatches: u64,
2675 pub problems: u64,
2677 pub retroactive: u64,
2679 pub redacted: u64,
2681 #[serde(default)]
2683 pub failed: Vec<GovernanceLogId>,
2684 #[serde(default, skip_serializing_if = "Option::is_none")]
2686 pub first_failure: Option<EntryVerdict>,
2687 #[serde(default)]
2689 pub keys: Vec<GovernanceKeyRecord>,
2690 #[serde(default)]
2692 pub unanchored_keys: Vec<PublicKeyHex>,
2693 #[serde(default)]
2695 pub repudiated: Vec<GovernanceLogId>,
2696 pub how_to_read: String,
2697}
2698
2699impl EntryVerdict {
2700 pub fn failed(&self) -> bool {
2703 !self.signature_valid
2704 || !self.link_valid
2705 || self.content_matches == Some(false)
2706 || self.problem.is_some()
2707 }
2708}
2709
2710impl GovernanceVerification {
2711 pub fn summary(&self) -> GovernanceVerificationSummary {
2713 let count = |f: &dyn Fn(&EntryVerdict) -> bool| -> u64 {
2714 self.entries.iter().filter(|e| f(e)).count() as u64
2715 };
2716 let failed: Vec<&EntryVerdict> =
2717 self.entries.iter().filter(|e| e.failed()).collect();
2718 GovernanceVerificationSummary {
2719 public_key: self.public_key,
2720 ok: self.ok,
2721 head: self.head.clone(),
2722 entries: self.entries.len() as u64,
2723 signature_failures: count(&|e| !e.signature_valid),
2724 link_failures: count(&|e| !e.link_valid),
2725 content_mismatches: count(&|e| e.content_matches == Some(false)),
2726 problems: count(&|e| e.problem.is_some()),
2727 retroactive: count(&|e| e.retroactive),
2728 redacted: count(&|e| e.redacted),
2729 failed: failed.iter().map(|e| e.id.clone()).collect(),
2730 first_failure: failed.first().map(|e| (*e).clone()),
2731 keys: self.keys.clone(),
2732 unanchored_keys: self.unanchored_keys.clone(),
2733 repudiated: self.repudiated.clone(),
2734 how_to_read: format!(
2735 "A summary of the whole check of {n} entries: verify_governance_log \
2736 with full=true returns one verdict per entry (signature, link, \
2737 content, redaction, revisions, signing key), about {kb} KB. \
2738 Either way this is the server checking itself, not independent \
2739 evidence; agora-agentkit's govlog::verify_chain is the \
2740 independent check.",
2741 n = self.entries.len(),
2742 kb = serde_json::to_string(self).map_or(0, |s| s.len() / 1024),
2743 ),
2744 }
2745 }
2746
2747 pub fn settle(mut self) -> Self {
2749 self.ok = self.entries.iter().all(|e| !e.failed());
2750 self
2751 }
2752
2753 pub fn check_content(
2764 &mut self,
2765 link: &GovernanceChainLink,
2766 data: &serde_json::Value,
2767 ) -> bool {
2768 let Some(entry) = self.entries.iter_mut().find(|e| e.id == link.id)
2769 else {
2770 return false;
2771 };
2772 let ok = entry.content_check(link.attestation.data_hash, data);
2773 entry.content_matches = Some(ok);
2774 ok
2775 }
2776}
2777
2778pub fn attest(
2789 key: &SigningKey,
2790 envelope: &Envelope,
2791 chain_seq: u64,
2792 signed_at: DateTime<Utc>,
2793) -> GovernanceAttestation {
2794 let signed_at = truncate_to_seconds(signed_at);
2795 let entry_hash = envelope.entry_hash();
2796 let signature =
2797 crypto::sign(key, entry_hash.as_bytes(), signed_at.timestamp());
2798 GovernanceAttestation {
2799 envelope_version: envelope.agora_governance_log,
2800 chain_seq,
2801 prev_hash: envelope.prev_hash,
2802 data_hash: envelope.data_hash,
2803 entry_hash,
2804 signature: signature.into(),
2805 signed_at,
2806 retroactive: is_retroactive(envelope.created_at(), signed_at),
2807 }
2808}
2809
2810#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
2816pub enum LinkError {
2817 #[error(
2818 "envelope version {0} is not supported (this verifier knows {ENVELOPE_VERSION})"
2819 )]
2820 UnsupportedVersion(u32),
2821 #[error("entry_hash does not recompute from the envelope fields")]
2822 HashMismatch,
2823 #[error("signature does not verify under the published key")]
2824 BadSignature,
2825 #[error(
2829 "this entry's type is newer than this verifier, which cannot check it; \
2830 upgrade agora-agentkit"
2831 )]
2832 UnknownEntryType,
2833}
2834
2835pub fn recompute_entry_hash(link: &GovernanceChainLink) -> Sha256Hex {
2837 Envelope::new(
2838 link.id.clone(),
2839 link.entry_type,
2840 link.created_at,
2841 link.attestation.prev_hash,
2842 link.attestation.data_hash,
2843 )
2844 .entry_hash()
2845}
2846
2847pub fn verify_link(
2849 link: &GovernanceChainLink,
2850 key: &VerifyingKey,
2851) -> Result<(), LinkError> {
2852 let a = &link.attestation;
2853 if a.envelope_version != ENVELOPE_VERSION {
2854 return Err(LinkError::UnsupportedVersion(a.envelope_version));
2855 }
2856 if link.entry_type == GovernanceLogEntryType::Unknown {
2857 return Err(LinkError::UnknownEntryType);
2858 }
2859 if recompute_entry_hash(link) != a.entry_hash {
2860 return Err(LinkError::HashMismatch);
2861 }
2862 if !crypto::verify(
2863 key,
2864 a.entry_hash.as_bytes(),
2865 a.signed_at.timestamp(),
2866 &Signature::from(&a.signature),
2867 ) {
2868 return Err(LinkError::BadSignature);
2869 }
2870 Ok(())
2871}
2872
2873pub fn verify_data(
2875 link: &GovernanceChainLink,
2876 data: &serde_json::Value,
2877) -> bool {
2878 data_hash(data) == link.attestation.data_hash
2879}
2880
2881fn same_shape(written: &serde_json::Value, read: &serde_json::Value) -> bool {
2891 use serde_json::Value::{Array, Null, Object};
2892 match (written, read) {
2893 (Object(w), Object(r)) => r.iter().all(|(k, r)| match w.get(k) {
2894 Some(w) => same_shape(w, r),
2895 None => r.is_null(),
2896 }),
2897 (Array(w), Array(r)) => {
2898 w.len() == r.len() && w.iter().zip(r).all(|(w, r)| same_shape(w, r))
2899 }
2900 (_, Object(_) | Array(_)) => false,
2901 (Object(_) | Array(_), Null) => false,
2902 _ => true,
2903 }
2904}
2905
2906fn read_strictly<T>(written: &serde_json::Value) -> Result<T, String>
2908where
2909 T: Serialize + serde::de::DeserializeOwned,
2910{
2911 let read: T =
2912 serde_json::from_value(written.clone()).map_err(|e| e.to_string())?;
2913 let again = serde_json::to_value(&read).map_err(|e| e.to_string())?;
2914 if same_shape(written, &again) {
2915 Ok(read)
2916 } else {
2917 Err("an array where an object belongs, or the reverse".into())
2918 }
2919}
2920
2921pub fn links_from_json(
2928 chain: &serde_json::Value,
2929) -> Result<Vec<GovernanceChainLink>, String> {
2930 chain
2931 .as_array()
2932 .ok_or("a chain is an array of links")?
2933 .iter()
2934 .map(read_strictly)
2935 .collect()
2936}
2937
2938fn read_as_written<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
2940where
2941 D: serde::Deserializer<'de>,
2942 T: Serialize + serde::de::DeserializeOwned,
2943{
2944 let written = serde_json::Value::deserialize(deserializer)?;
2945 if written.is_null() {
2946 return Ok(None);
2947 }
2948 read_strictly(&written)
2949 .map(Some)
2950 .map_err(serde::de::Error::custom)
2951}
2952
2953fn reserved_prefix(
2956 entry_type: GovernanceLogEntryType,
2957) -> Option<GovernanceLogPrefix> {
2958 match entry_type {
2959 GovernanceLogEntryType::Amendment => Some(GovernanceLogPrefix::Amd),
2960 GovernanceLogEntryType::KeyRotation => Some(GovernanceLogPrefix::Key),
2961 GovernanceLogEntryType::StewardRecord => Some(GovernanceLogPrefix::Rec),
2962 GovernanceLogEntryType::Obligation => Some(GovernanceLogPrefix::Obl),
2963 GovernanceLogEntryType::DataSet => Some(GovernanceLogPrefix::Data),
2964 GovernanceLogEntryType::Test => Some(GovernanceLogPrefix::Test),
2965 GovernanceLogEntryType::CouncilDecision
2966 | GovernanceLogEntryType::AppealsCourtDecision
2967 | GovernanceLogEntryType::EmergencyAction
2968 | GovernanceLogEntryType::PolicyChange
2969 | GovernanceLogEntryType::StewardVeto
2970 | GovernanceLogEntryType::Unknown => None,
2971 }
2972}
2973
2974fn reserved_for(prefix: GovernanceLogPrefix) -> Option<GovernanceLogEntryType> {
2976 match prefix {
2977 GovernanceLogPrefix::Amd => Some(GovernanceLogEntryType::Amendment),
2978 GovernanceLogPrefix::Key => Some(GovernanceLogEntryType::KeyRotation),
2979 GovernanceLogPrefix::Rec => Some(GovernanceLogEntryType::StewardRecord),
2980 GovernanceLogPrefix::Obl => Some(GovernanceLogEntryType::Obligation),
2981 GovernanceLogPrefix::Data => Some(GovernanceLogEntryType::DataSet),
2982 GovernanceLogPrefix::Test => Some(GovernanceLogEntryType::Test),
2983 GovernanceLogPrefix::Gov
2984 | GovernanceLogPrefix::App
2985 | GovernanceLogPrefix::Unknown => None,
2986 }
2987}
2988
2989fn prefix_problem(link: &GovernanceChainLink) -> Option<String> {
2991 let prefix = link.id.prefix();
2992 match (reserved_prefix(link.entry_type), reserved_for(prefix)) {
2993 (Some(want), _) if prefix != want => Some(format!(
2994 "the id of a {} entry must be in the {want}- series, not {}",
2995 link.entry_type, link.id
2996 )),
2997 (None, Some(owner)) => Some(format!(
2998 "{prefix}- ids are reserved for {owner} entries, but {} is a {}",
2999 link.id, link.entry_type
3000 )),
3001 _ => None,
3002 }
3003}
3004
3005pub const OBLIGATION_FIXED: [&str; 5] = [
3009 "/until",
3010 "/owed_by",
3011 "/kind",
3012 "/source",
3013 "/agora_obligation",
3014];
3015
3016pub fn revision_touches<'a>(
3019 revision: &Revision,
3020 pointers: &[&'a str],
3021) -> Option<&'a str> {
3022 revision.patch.iter().find_map(|op| {
3023 let from = match op {
3024 json_patch::PatchOperation::Move(m) => Some(m.from.as_str()),
3025 json_patch::PatchOperation::Copy(c) => Some(c.from.as_str()),
3026 _ => None,
3027 };
3028 [Some(op.path().as_str()), from]
3029 .into_iter()
3030 .flatten()
3031 .find_map(|p| pointers.iter().copied().find(|f| overlaps(p, f)))
3032 })
3033}
3034
3035fn amendment_target(
3037 amendment: &Amendment,
3038 seq: u64,
3039 seq_of: &HashMap<&str, u64>,
3040 links: &[&GovernanceChainLink],
3041) -> Result<u64, AmendmentError> {
3042 amendment.validate()?;
3043 let target = *seq_of.get(amendment.target.as_str()).ok_or_else(|| {
3044 AmendmentError::UnknownTarget(amendment.target.clone())
3045 })?;
3046 if target >= seq {
3047 return Err(AmendmentError::ForwardReference(amendment.target.clone()));
3048 }
3049 let target_link = links[target as usize - 1];
3050 if target_link.attestation.entry_hash != amendment.target_entry_hash {
3051 return Err(AmendmentError::WrongTargetHash(amendment.target.clone()));
3052 }
3053 let on_obligation =
3054 target_link.entry_type == GovernanceLogEntryType::Obligation;
3055 if amendment.kind.is_obligation() && !on_obligation {
3056 return Err(AmendmentError::NotAnObligation {
3057 kind: amendment.kind,
3058 target: amendment.target.clone(),
3059 });
3060 }
3061 if on_obligation && kind_standing(amendment.kind).is_some() {
3062 return Err(AmendmentError::PrecedentialOnObligation(amendment.kind));
3063 }
3064 if on_obligation
3065 && let Some(revision) = &amendment.revision
3066 && let Some(path) = revision_touches(revision, &OBLIGATION_FIXED)
3067 {
3068 return Err(AmendmentError::FixedObligationField(path.to_string()));
3069 }
3070 Ok(target)
3071}
3072
3073struct KeyWalk {
3075 history: Vec<(GovernanceKeyRecord, VerifyingKey)>,
3077 unanchored: Vec<PublicKeyHex>,
3078 seen: HashSet<PublicKeyHex>,
3082 repudiated: HashSet<u64>,
3084 genesis: PublicKeyHex,
3085 genesis_certified: bool,
3087}
3088
3089impl KeyWalk {
3090 fn new(genesis: &VerifyingKey, anchor: &KeyAnchor) -> Self {
3091 let public_key = PublicKeyHex::from(genesis);
3092 Self {
3093 genesis: public_key,
3094 genesis_certified: false,
3095 history: vec![(
3096 GovernanceKeyRecord {
3097 public_key,
3098 from_seq: 1,
3099 through_seq: None,
3100 status: KeyStatus::Active,
3101 introduced_by: None,
3102 retired_by: None,
3103 certified: false,
3104 },
3105 *genesis,
3106 )],
3107 unanchored: if anchor.contains(&public_key) {
3108 Vec::new()
3109 } else {
3110 vec![public_key]
3111 },
3112 seen: HashSet::from([public_key]),
3113 repudiated: HashSet::new(),
3114 }
3115 }
3116
3117 fn in_force(&self, seq: u64) -> (PublicKeyHex, VerifyingKey) {
3119 let (record, key) = self
3120 .history
3121 .iter()
3122 .rev()
3123 .find(|(r, _)| r.from_seq <= seq)
3124 .unwrap_or(&self.history[0]);
3125 (record.public_key, *key)
3126 }
3127
3128 fn active(&self) -> PublicKeyHex {
3130 self.history
3131 .last()
3132 .map(|(r, _)| r.public_key)
3133 .expect("the genesis key is always in the history")
3134 }
3135
3136 fn close(
3137 &mut self,
3138 through_seq: u64,
3139 status: KeyStatus,
3140 by: &GovernanceLogId,
3141 ) {
3142 if let Some((record, _)) = self.history.last_mut() {
3143 record.through_seq = Some(through_seq);
3144 record.status = status;
3145 record.retired_by = Some(by.clone());
3146 }
3147 }
3148
3149 fn open(
3150 &mut self,
3151 public_key: PublicKeyHex,
3152 key: VerifyingKey,
3153 from_seq: u64,
3154 by: &GovernanceLogId,
3155 ) {
3156 self.history.push((
3157 GovernanceKeyRecord {
3158 public_key,
3159 from_seq,
3160 through_seq: None,
3161 status: KeyStatus::Active,
3162 introduced_by: Some(by.clone()),
3163 retired_by: None,
3164 certified: true,
3165 },
3166 key,
3167 ));
3168 }
3169
3170 fn apply(
3172 &mut self,
3173 rotation: &KeyRotation,
3174 link: &GovernanceChainLink,
3175 seq: u64,
3176 links: &[&GovernanceChainLink],
3177 roots: &RootSet,
3178 ) -> Result<(), RotationError> {
3179 rotation.verify_certified(seq, link.attestation.prev_hash, roots)?;
3180 let new_key = rotation
3181 .new_key
3182 .to_verifying_key()
3183 .map_err(|_| RotationError::BadNewKey)?;
3184 if self.seen.contains(&rotation.new_key) {
3185 return Err(RotationError::ReusedKey);
3186 }
3187 match (&rotation.outgoing_certificate, self.genesis_certified) {
3192 (None, false) => {
3193 return Err(RotationError::MissingGenesisCertificate);
3194 }
3195 (Some(_), true) => {
3196 return Err(RotationError::UnexpectedOutgoingCertificate);
3197 }
3198 (Some(certificate), false) => certificate
3199 .verify_for(&KeyCertStatement::genesis(self.genesis), roots)
3200 .map_err(RotationError::OutgoingCertificate)?,
3201 (None, true) => {}
3202 }
3203 match rotation.reason {
3204 RotationReason::Routine => {
3205 if rotation.old_key != self.in_force(seq).0 {
3206 return Err(RotationError::WrongOldKey);
3207 }
3208 self.close(seq, KeyStatus::Retired, &link.id);
3209 self.open(rotation.new_key, new_key, seq + 1, &link.id);
3210 }
3211 RotationReason::Compromise => {
3212 let head = rotation
3213 .last_trusted()
3214 .ok_or(RotationError::MissingLastTrusted)?;
3215 let trusted_seq = head.chain_seq;
3216 let names_an_earlier_entry = trusted_seq >= 1
3217 && trusted_seq < seq
3218 && links[trusted_seq as usize - 1].id == head.id
3219 && links[trusted_seq as usize - 1].attestation.entry_hash
3220 == head.entry_hash;
3221 if !names_an_earlier_entry {
3222 return Err(RotationError::UnknownLastTrusted);
3223 }
3224 if self.repudiated.contains(&trusted_seq) {
3227 return Err(RotationError::RepudiatedLastTrusted);
3228 }
3229 if rotation.old_key != self.in_force(trusted_seq).0 {
3232 return Err(RotationError::WrongOldKey);
3233 }
3234 self.history.retain(|(r, _)| r.from_seq <= trusted_seq);
3235 self.close(trusted_seq, KeyStatus::Compromised, &link.id);
3236 self.open(rotation.new_key, new_key, seq, &link.id);
3237 self.repudiated.extend(trusted_seq + 1..seq);
3238 }
3239 }
3240 self.seen.insert(rotation.new_key);
3241 if !self.genesis_certified {
3242 self.genesis_certified = true;
3243 self.history[0].0.certified = true;
3244 self.unanchored.clear();
3245 }
3246 Ok(())
3247 }
3248}
3249
3250pub fn verify_chain(
3281 links: &[GovernanceChainLink],
3282 genesis_key: &VerifyingKey,
3283 anchor: &KeyAnchor,
3284 roots: &RootSet,
3285) -> GovernanceVerification {
3286 let mut links: Vec<&GovernanceChainLink> = links.iter().collect();
3287 links.sort_by_key(|l| l.attestation.chain_seq);
3288
3289 let mut seq_of: HashMap<&str, u64> = HashMap::new();
3290 let mut duplicates: HashSet<&str> = HashSet::new();
3291 for (i, link) in links.iter().enumerate() {
3292 if seq_of.insert(link.id.as_str(), i as u64 + 1).is_some() {
3293 duplicates.insert(link.id.as_str());
3294 }
3295 }
3296
3297 let mut walk = KeyWalk::new(genesis_key, anchor);
3298 let mut amendments: Vec<(u64, GovernanceLogId, Amendment, u64)> =
3300 Vec::new();
3301 let mut entries: Vec<EntryVerdict> = Vec::with_capacity(links.len());
3302 let mut prev: Option<&GovernanceChainLink> = None;
3303
3304 for (i, link) in links.iter().enumerate() {
3305 let a = &link.attestation;
3306 let expected_seq = i as u64 + 1;
3307 let mut problems: Vec<String> = Vec::new();
3308
3309 if let Some(problem) = prefix_problem(link) {
3310 problems.push(problem);
3311 }
3312 if duplicates.contains(link.id.as_str()) {
3313 problems.push(format!("{} appears more than once", link.id));
3314 }
3315
3316 let carries_meaning = matches!(
3320 link.entry_type,
3321 GovernanceLogEntryType::Amendment
3322 | GovernanceLogEntryType::KeyRotation
3323 );
3324 let mut amendment: Option<Amendment> = None;
3325 let mut rotation: Option<KeyRotation> = None;
3326 let mut content_matches: Option<bool> = None;
3327 match &link.data {
3328 Some(data) if non_integer_number(data).is_some() => {
3329 content_matches = Some(false);
3330 problems.push(format!(
3331 "`data` has a number that is not a 64-bit integer at {:?}; \
3332 governance data never contains one",
3333 non_integer_number(data).unwrap_or_default()
3334 ));
3335 }
3336 Some(data) => {
3337 let matched = data_hash(data) == a.data_hash;
3338 content_matches = Some(matched);
3339 if !matched {
3340 if carries_meaning {
3341 problems.push(
3342 "`data` does not hash to the attested data_hash"
3343 .into(),
3344 );
3345 }
3346 } else {
3347 match link.entry_type {
3348 GovernanceLogEntryType::Amendment => {
3349 match read_strictly(data) {
3350 Ok(v) => amendment = Some(v),
3351 Err(e) => problems.push(format!(
3352 "amendment `data` is malformed: {e}"
3353 )),
3354 }
3355 }
3356 GovernanceLogEntryType::KeyRotation => {
3357 match read_strictly(data) {
3358 Ok(v) => rotation = Some(v),
3359 Err(e) => problems.push(format!(
3360 "key_rotation `data` is malformed: {e}"
3361 )),
3362 }
3363 }
3364 _ => {}
3365 }
3366 }
3367 }
3368 None if carries_meaning => problems.push(format!(
3369 "a {} entry must carry its `data`",
3370 link.entry_type
3371 )),
3372 None => {}
3373 }
3374
3375 let declared = rotation
3379 .as_ref()
3380 .filter(|r| r.reason == RotationReason::Compromise)
3381 .map(|r| {
3382 if walk.seen.contains(&r.new_key) {
3383 return Err(RotationError::ReusedKey);
3384 }
3385 r.verify_certified(expected_seq, a.prev_hash, roots)?;
3386 r.new_key
3387 .to_verifying_key()
3388 .map_err(|_| RotationError::BadNewKey)
3389 });
3390 let (key_hex, key) = match &declared {
3391 Some(Ok(k)) => (PublicKeyHex::from(k), *k),
3392 _ => walk.in_force(expected_seq),
3393 };
3394 if let Some(Err(e)) = &declared {
3397 problems.push(e.to_string());
3398 }
3399
3400 let (hash_ok, signature_valid) = match verify_link(link, &key) {
3401 Ok(()) => (true, true),
3402 Err(LinkError::BadSignature) => {
3403 problems.push(LinkError::BadSignature.to_string());
3404 (true, false)
3405 }
3406 Err(e) => {
3407 problems.push(e.to_string());
3408 (false, false)
3409 }
3410 };
3411
3412 let mut link_valid = hash_ok;
3413 if a.chain_seq != expected_seq {
3414 link_valid = false;
3415 problems.push(format!(
3416 "chain_seq {} where {expected_seq} was expected",
3417 a.chain_seq
3418 ));
3419 }
3420 let expected_prev = prev.map(|p| p.attestation.entry_hash);
3421 if a.prev_hash != expected_prev {
3422 link_valid = false;
3423 problems.push(match (a.prev_hash, expected_prev) {
3424 (Some(_), None) => "first entry names a predecessor".into(),
3425 (None, Some(_)) => "prev_hash is null mid-chain".into(),
3426 _ => "prev_hash is not the previous entry's entry_hash".into(),
3427 });
3428 }
3429 let out_of_order = prev.is_some_and(|p| link.created_at < p.created_at);
3430
3431 let authentic = signature_valid && link_valid;
3435 if let Some(rotation) = rotation.as_ref().filter(|_| authentic)
3436 && let Err(e) =
3437 walk.apply(rotation, link, expected_seq, &links, roots)
3438 {
3439 let problem = e.to_string();
3440 if !problems.contains(&problem) {
3441 problems.push(problem);
3442 }
3443 }
3444 let mut texts = None;
3448 if let Some(amendment) = amendment
3449 .as_ref()
3450 .filter(|a| authentic && a.validate().is_ok())
3451 {
3452 match amendment.text_status(link.texts.as_ref()) {
3453 Ok(status) => texts = status,
3454 Err(e) => problems.push(e.to_string()),
3455 }
3456 } else if link.texts.as_ref().is_some_and(|t| !t.is_empty()) {
3457 problems.push(AmendmentError::UncommittedText.to_string());
3458 }
3459 if let Some(amendment) = amendment.filter(|_| authentic) {
3460 match amendment_target(&amendment, expected_seq, &seq_of, &links) {
3461 Ok(target) => amendments.push((
3462 expected_seq,
3463 link.id.clone(),
3464 amendment,
3465 target,
3466 )),
3467 Err(e) => problems.push(e.to_string()),
3468 }
3469 }
3470
3471 entries.push(EntryVerdict {
3472 id: link.id.clone(),
3473 chain_seq: a.chain_seq,
3474 signature_valid,
3475 link_valid,
3476 content_matches,
3477 retroactive: is_retroactive(link.created_at, a.signed_at),
3478 out_of_order,
3479 amended_by: Vec::new(),
3480 signed_by: Some(key_hex),
3481 redacted: false,
3482 redacted_data_hash: None,
3483 revisions: Vec::new(),
3484 latest_data_hash: None,
3485 superseded_revisions: Vec::new(),
3486 history: RevisionHistory::default(),
3487 repudiated: false,
3488 reattested_by: Vec::new(),
3489 texts,
3490 problem: (!problems.is_empty()).then(|| problems.join("; ")),
3491 });
3492 prev = Some(link);
3493 }
3494
3495 let mut applied = vec![false; amendments.len()];
3499 loop {
3500 let mut changed = false;
3501 for (i, (seq, id, amendment, target)) in amendments.iter().enumerate() {
3502 if applied[i]
3503 || amendment.kind != AmendmentKind::Reattested
3504 || walk.repudiated.contains(seq)
3505 {
3506 continue;
3507 }
3508 applied[i] = true;
3509 entries[*target as usize - 1].reattested_by.push(id.clone());
3510 walk.repudiated.remove(target);
3511 changed = true;
3512 }
3513 if !changed {
3514 break;
3515 }
3516 }
3517
3518 for (seq, id, amendment, target) in &amendments {
3519 if walk.repudiated.contains(seq) {
3520 continue;
3521 }
3522 let entry = &mut entries[*target as usize - 1];
3523 entry.amended_by.push(id.clone());
3524 let mut unrebased = false;
3525 if let Some(redaction) = &amendment.redaction {
3526 unrebased = !entry.revisions.is_empty()
3529 && redaction.resulting_latest_hash.is_none();
3530 entry.redacted = true;
3531 entry.redacted_data_hash = Some(redaction.resulting_data_hash);
3532 entry.history.rebased = entry.history.revisions.len();
3534 entry.history.rebased_hash = redaction.resulting_latest_hash;
3535 entry.latest_data_hash = redaction.resulting_latest_hash;
3536 entry.superseded_revisions = entry.revisions.clone();
3537 }
3538 if let Some(revision) = &amendment.revision {
3539 entry.revisions.push(id.clone());
3540 entry.latest_data_hash = Some(revision.resulting_data_hash);
3541 entry.history.revisions.push((id.clone(), revision.clone()));
3542 }
3543 if unrebased {
3544 let target = entry.id.clone();
3545 add_problem(
3546 &mut entries[*seq as usize - 1].problem,
3547 format!(
3548 "the redaction of {target}, which has revisions, names no \
3549 resulting_latest_hash"
3550 ),
3551 );
3552 }
3553 }
3554
3555 for (i, link) in links.iter().enumerate() {
3558 let entry = &mut entries[i];
3559 if let Some(data) = &link.data
3560 && entry.content_matches.is_some()
3561 && (entry.redacted || !entry.history.revisions.is_empty())
3562 {
3563 entry.content_matches =
3564 Some(entry.content_check(link.attestation.data_hash, data));
3565 }
3566 }
3567
3568 let mut seen = HashSet::new();
3569 walk.unanchored.retain(|key| seen.insert(*key));
3570
3571 let mut repudiated = Vec::new();
3572 for (i, entry) in entries.iter_mut().enumerate() {
3573 if walk.repudiated.contains(&(i as u64 + 1)) {
3574 entry.repudiated = true;
3575 repudiated.push(entry.id.clone());
3576 }
3577 }
3578
3579 GovernanceVerification {
3580 public_key: walk.active(),
3581 ok: false,
3582 head: prev.map(|p| p.id.clone()),
3583 entries,
3584 keys: walk.history.into_iter().map(|(record, _)| record).collect(),
3585 unanchored_keys: walk.unanchored,
3586 repudiated,
3587 }
3588 .settle()
3589}
3590
3591#[cfg(test)]
3592mod tests {
3593 use super::*;
3594
3595 fn revision(patch: serde_json::Value) -> Revision {
3596 Revision {
3597 patch: serde_json::from_value(patch).unwrap(),
3598 duplicates: Vec::new(),
3599 resulting_data_hash: "0".repeat(64).parse().unwrap(),
3600 }
3601 }
3602
3603 #[test]
3606 fn revision_changes_replay_in_order() {
3607 let original = serde_json::json!({
3608 "landmark": "no", "tags": ["a"], "old": 3, "obj": {"k": 1},
3609 });
3610 let revisions = [
3611 revision(serde_json::json!([
3612 {"op": "test", "path": "/landmark", "value": "no"},
3613 {"op": "replace", "path": "/landmark", "value": "yes"},
3614 {"op": "add", "path": "/tags/1", "value": "b"},
3615 {"op": "add", "path": "/obj/k", "value": 2},
3616 ])),
3617 revision(serde_json::json!([
3618 {"op": "replace", "path": "/landmark", "value": "maybe"},
3619 {"op": "remove", "path": "/old"},
3620 {"op": "move", "from": "/tags/0", "path": "/first"},
3621 ])),
3622 ];
3623 let (changes, error) = revision_changes(&original, &revisions);
3624 assert!(error.is_none());
3625 let c = |kind,
3626 path: &str,
3627 before: Option<serde_json::Value>,
3628 after: Option<serde_json::Value>| Change {
3629 kind,
3630 path: path.into(),
3631 from: None,
3632 before,
3633 after,
3634 omitted_bytes: None,
3635 };
3636 use ChangeKind as K;
3637 assert_eq!(
3638 changes[0],
3639 vec![
3640 c(
3641 K::Replaced,
3642 "/landmark",
3643 Some("no".into()),
3644 Some("yes".into())
3645 ),
3646 c(K::Added, "/tags/1", None, Some("b".into())),
3648 c(K::Added, "/obj/k", Some(1.into()), Some(2.into())),
3650 ]
3651 );
3652 assert_eq!(
3653 changes[1],
3654 vec![
3655 c(
3656 K::Replaced,
3657 "/landmark",
3658 Some("yes".into()),
3659 Some("maybe".into())
3660 ),
3661 c(K::Removed, "/old", Some(3.into()), None),
3662 Change {
3663 from: Some("/tags/0".into()),
3664 ..c(K::Moved, "/first", None, Some("a".into()))
3665 },
3666 ]
3667 );
3668 let broken = [
3670 revisions[0].clone(),
3671 revision(serde_json::json!([
3672 {"op": "replace", "path": "/nowhere", "value": 1},
3673 ])),
3674 ];
3675 let (changes, error) = revision_changes(&original, &broken);
3676 assert_eq!(changes.len(), 1);
3677 assert!(error.is_some());
3678 }
3679
3680 #[test]
3683 fn a_shown_revision_costs_the_budget_nothing() {
3684 let big = |n: usize| {
3685 revision(serde_json::json!([
3686 {"op": "add", "path": format!("/r{n}"), "value": "x".repeat(1_900)}
3687 ]))
3688 };
3689 let revisions: Vec<_> = (0..12)
3690 .map(|n| {
3691 (format!("AMD-2026-{:04}", n + 1).parse().unwrap(), big(n))
3692 })
3693 .collect();
3694 let build = |shown: &dyn Fn(&GovernanceLogId) -> bool| {
3695 ChangeHistory::build(
3696 &serde_json::json!({}),
3697 &revisions,
3698 |_| None,
3699 shown,
3700 crate::requests::History::All,
3701 )
3702 };
3703 let left_out = |h: &ChangeHistory| {
3704 h.revisions
3705 .iter()
3706 .filter(|r| r.omitted_bytes.is_some())
3707 .count()
3708 };
3709 assert!(left_out(&build(&|_| false)) > 0);
3710 let first_four: Vec<GovernanceLogId> =
3711 revisions[..4].iter().map(|(id, _)| id.clone()).collect();
3712 assert_eq!(left_out(&build(&|id| first_four.contains(id))), 0);
3713 }
3714
3715 #[test]
3716 fn pointer_tokens_escape_and_unescape() {
3717 assert_eq!(escape_pointer_token("a/b~c"), "a~1b~0c");
3718 assert_eq!(unescape_pointer_token("~01"), "~1");
3720 for token in ["", "plain", "a/b", "~", "~1", "/~0/"] {
3721 assert_eq!(
3722 unescape_pointer_token(&escape_pointer_token(token)),
3723 token
3724 );
3725 }
3726 assert_eq!(
3727 pointer_tokens("/rounds/0/a~1b~0c"),
3728 ["rounds", "0", "a/b~c"]
3729 );
3730 assert!(pointer_tokens("").is_empty());
3731 }
3732
3733 #[test]
3737 fn citations_are_read_from_each_records_own_field() {
3738 use serde_json::json;
3739 let id = |s: &str| s.parse::<GovernanceLogId>().unwrap();
3740 assert_eq!(
3741 citations_in(
3742 GovernanceLogEntryType::StewardRecord,
3743 &json!({"title": "t", "concerns": ["GOV-2026-0011", "AMD-2026-0004"]}),
3744 )
3745 .unwrap(),
3746 vec![
3747 (id("GOV-2026-0011"), CitationRelation::Concerns),
3748 (id("AMD-2026-0004"), CitationRelation::Concerns),
3749 ]
3750 );
3751 assert_eq!(
3752 citations_in(
3753 GovernanceLogEntryType::Amendment,
3754 &json!({"target": "APP-2026-0001", "authority": "GOV-2026-0005"}),
3755 )
3756 .unwrap(),
3757 vec![(id("GOV-2026-0005"), CitationRelation::Authority)],
3758 "the target is not a citation: it is listed under `amendments`"
3759 );
3760 assert_eq!(
3761 citations_in(
3762 GovernanceLogEntryType::CouncilDecision,
3763 &json!({"overrules": ["APP-2026-0002"]}),
3764 )
3765 .unwrap(),
3766 vec![(id("APP-2026-0002"), CitationRelation::Overrules)]
3767 );
3768 assert!(
3769 citations_in(
3770 GovernanceLogEntryType::Amendment,
3771 &json!({"authority": null})
3772 )
3773 .unwrap()
3774 .is_empty()
3775 );
3776 assert!(matches!(
3779 citations_in(
3780 GovernanceLogEntryType::StewardRecord,
3781 &json!({"concerns": "GOV-2026-0011"})
3782 ),
3783 Err(MalformedCitation {
3784 relation: CitationRelation::Concerns,
3785 ..
3786 })
3787 ));
3788 assert!(
3789 citations_in(
3790 GovernanceLogEntryType::AppealsCourtDecision,
3791 &json!({"concerns": ["GOV-2026-0011"]}),
3792 )
3793 .unwrap()
3794 .is_empty()
3795 );
3796 }
3797 use crate::crypto::generate_keypair;
3798 use serde_json::json;
3799
3800 pub(super) fn gov(n: u32) -> GovernanceLogId {
3801 format!("GOV-2026-{n:04}").parse().unwrap()
3802 }
3803
3804 pub(super) fn amd(n: u32) -> GovernanceLogId {
3805 format!("AMD-2026-{n:04}").parse().unwrap()
3806 }
3807
3808 pub(super) fn key_id(n: u32) -> GovernanceLogId {
3809 format!("KEY-2026-{n:04}").parse().unwrap()
3810 }
3811
3812 pub(super) fn rec(n: u32) -> GovernanceLogId {
3813 format!("REC-2026-{n:04}").parse().unwrap()
3814 }
3815
3816 pub(super) fn obl(n: u32) -> GovernanceLogId {
3817 format!("OBL-2026-{n:04}").parse().unwrap()
3818 }
3819
3820 pub(super) fn data_id(n: u32) -> GovernanceLogId {
3821 format!("DATA-2026-{n:04}").parse().unwrap()
3822 }
3823
3824 pub(super) fn test_id(n: u32) -> GovernanceLogId {
3825 format!("TEST-2026-{n:04}").parse().unwrap()
3826 }
3827
3828 pub(super) fn at(secs: i64) -> DateTime<Utc> {
3829 DateTime::from_timestamp(1_700_000_000 + secs, 123_456_789).unwrap()
3830 }
3831
3832 fn anchored(key: &VerifyingKey) -> KeyAnchor {
3834 KeyAnchor::pinned(key.into())
3835 }
3836
3837 pub(super) fn resalted(draft: &AmendmentDraft, seq: u64) -> AmendmentDraft {
3840 let fix = |field: &str, t: &Option<CommittedText>| {
3841 t.as_ref().map(|t| {
3842 let salt = Sha256::digest(format!("{seq}/{field}/{}", t.text));
3843 CommittedText::with_salt(
3844 TextSalt::from(<[u8; 32]>::from(salt)),
3845 t.text.clone(),
3846 )
3847 })
3848 };
3849 let texts = AmendmentTexts {
3850 basis: fix("basis", &draft.texts.basis),
3851 note: fix("note", &draft.texts.note),
3852 rationale: fix("rationale", &draft.texts.rationale),
3853 };
3854 let commit = |t: &Option<CommittedText>| {
3855 t.as_ref().map(|t| AmendmentText::Committed(t.commitment()))
3856 };
3857 AmendmentDraft {
3858 amendment: Amendment {
3859 basis: commit(&texts.basis).unwrap(),
3860 note: commit(&texts.note).unwrap(),
3861 rationale: commit(&texts.rationale),
3862 ..draft.amendment.clone()
3863 },
3864 texts,
3865 }
3866 }
3867
3868 pub(super) fn v1(draft: AmendmentDraft) -> Amendment {
3870 let plain =
3871 |t: Option<CommittedText>| t.map(|t| AmendmentText::Plain(t.text));
3872 Amendment {
3873 agora_governance_amendment: 1,
3874 basis: plain(draft.texts.basis).unwrap(),
3875 note: plain(draft.texts.note).unwrap(),
3876 rationale: plain(draft.texts.rationale),
3877 ..draft.amendment
3878 }
3879 }
3880
3881 pub(super) fn root(n: u8) -> SigningKey {
3884 SigningKey::from_bytes(&[0xA0 + n; 32])
3885 }
3886
3887 pub(super) fn roots() -> RootSet {
3889 RootSet::new(
3890 [1, 2].map(|n| PublicKeyHex::from(&root(n).verifying_key())),
3891 1,
3892 )
3893 }
3894
3895 pub(super) fn certify(
3897 signers: &[&SigningKey],
3898 statement: KeyCertStatement,
3899 ) -> KeyCertificate {
3900 use ed25519_dalek::Signer;
3901 let message = statement.signed_bytes();
3902 signers.iter().fold(
3903 KeyCertificate::unsigned(statement),
3904 |certificate, signer| {
3905 certificate.with(RootSignature {
3906 root_key: (&signer.verifying_key()).into(),
3907 signature: signer.sign(&message).into(),
3908 })
3909 },
3910 )
3911 }
3912
3913 fn for_new_at(c: &Chain, key: &VerifyingKey) -> KeyCertificate {
3915 certify(
3916 &[&root(1)],
3917 KeyCertStatement::routine(key.into(), c.next_seq(), c.prev_hash()),
3918 )
3919 }
3920
3921 pub(super) fn link(
3922 key: &SigningKey,
3923 n: u32,
3924 prev: Option<&GovernanceChainLink>,
3925 data: &serde_json::Value,
3926 signed_at: DateTime<Utc>,
3927 ) -> GovernanceChainLink {
3928 let created_at = truncate_to_micros(at(n as i64 * 10));
3929 let envelope = Envelope::new(
3930 gov(n),
3931 GovernanceLogEntryType::CouncilDecision,
3932 created_at,
3933 prev.map(|p| p.attestation.entry_hash),
3934 data_hash(data),
3935 );
3936 let attestation = attest(
3937 key,
3938 &envelope,
3939 prev.map_or(1, |p| p.attestation.chain_seq + 1),
3940 signed_at,
3941 );
3942 GovernanceChainLink {
3943 id: gov(n),
3944 entry_type: GovernanceLogEntryType::CouncilDecision,
3945 created_at,
3946 attestation,
3947 data: None,
3948 texts: None,
3949 }
3950 }
3951
3952 pub(super) fn chain(key: &SigningKey, n: u32) -> Vec<GovernanceChainLink> {
3953 let mut out: Vec<GovernanceChainLink> = Vec::new();
3954 for i in 1..=n {
3955 let data = json!({"title": format!("Decision {i}"), "outcome": "approved"});
3956 let l = link(key, i, out.last(), &data, at(i as i64 * 10 + 1));
3957 out.push(l);
3958 }
3959 out
3960 }
3961
3962 pub(super) struct Chain {
3966 pub(super) links: Vec<GovernanceChainLink>,
3967 gov: u32,
3968 amd: u32,
3969 key: u32,
3970 genesis: Option<PublicKeyHex>,
3972 }
3973
3974 impl Chain {
3975 pub(super) fn new() -> Self {
3976 Self {
3977 links: Vec::new(),
3978 gov: 0,
3979 amd: 0,
3980 key: 0,
3981 genesis: None,
3982 }
3983 }
3984
3985 pub(super) fn prev_hash(&self) -> Option<Sha256Hex> {
3986 self.links.last().map(|l| l.attestation.entry_hash)
3987 }
3988
3989 pub(super) fn hash_at(&self, seq: usize) -> Sha256Hex {
3991 self.links[seq - 1].attestation.entry_hash
3992 }
3993
3994 pub(super) fn next_seq(&self) -> u64 {
3996 self.links.len() as u64 + 1
3997 }
3998
3999 pub(super) fn head(&self, seq: usize) -> TrustedHead {
4001 TrustedHead {
4002 id: self.links[seq - 1].id.clone(),
4003 chain_seq: seq as u64,
4004 entry_hash: self.hash_at(seq),
4005 }
4006 }
4007
4008 fn outgoing(&self, rotation: KeyRotation) -> KeyRotation {
4010 match (self.key, self.genesis) {
4011 (0, Some(genesis)) => rotation.with_outgoing(certify(
4012 &[&root(1)],
4013 KeyCertStatement::genesis(genesis),
4014 )),
4015 _ => rotation,
4016 }
4017 }
4018
4019 pub(super) fn routine(
4022 &self,
4023 old: &VerifyingKey,
4024 new: &SigningKey,
4025 ) -> KeyRotation {
4026 let statement = KeyCertStatement::routine(
4027 (&new.verifying_key()).into(),
4028 self.next_seq(),
4029 self.prev_hash(),
4030 );
4031 self.outgoing(KeyRotation::routine(
4032 old.into(),
4033 new,
4034 self.prev_hash(),
4035 at(self.next_seq() as i64 * 10 + 5),
4036 certify(&[&root(1)], statement),
4037 ))
4038 }
4039
4040 pub(super) fn compromise(
4043 &self,
4044 old: &VerifyingKey,
4045 new: &SigningKey,
4046 trusted: usize,
4047 ) -> KeyRotation {
4048 let statement = KeyCertStatement::compromise(
4049 (&new.verifying_key()).into(),
4050 self.next_seq(),
4051 self.prev_hash(),
4052 self.head(trusted),
4053 );
4054 self.outgoing(KeyRotation::compromise(
4055 old.into(),
4056 new,
4057 self.prev_hash(),
4058 at(self.next_seq() as i64 * 10 + 5),
4059 certify(&[&root(1)], statement),
4060 ))
4061 }
4062
4063 pub(super) fn next_amd(&self) -> GovernanceLogId {
4065 amd(self.amd + 1)
4066 }
4067
4068 pub(super) fn push(
4069 &mut self,
4070 signer: &SigningKey,
4071 id: GovernanceLogId,
4072 entry_type: GovernanceLogEntryType,
4073 data: serde_json::Value,
4074 carry: bool,
4075 ) -> GovernanceLogId {
4076 self.genesis
4077 .get_or_insert_with(|| (&signer.verifying_key()).into());
4078 let n = self.links.len() as i64 + 1;
4079 let created_at = truncate_to_micros(at(n * 10));
4080 let envelope = Envelope::new(
4081 id.clone(),
4082 entry_type,
4083 created_at,
4084 self.prev_hash(),
4085 data_hash(&data),
4086 );
4087 let attestation =
4088 attest(signer, &envelope, n as u64, at(n * 10 + 1));
4089 self.links.push(GovernanceChainLink {
4090 id: id.clone(),
4091 entry_type,
4092 created_at,
4093 attestation,
4094 data: carry.then_some(data),
4095 texts: None,
4096 });
4097 id
4098 }
4099
4100 pub(super) fn entry(
4103 &mut self,
4104 signer: &SigningKey,
4105 data: serde_json::Value,
4106 ) -> GovernanceLogId {
4107 self.gov += 1;
4108 let id = gov(self.gov);
4109 self.push(
4110 signer,
4111 id,
4112 GovernanceLogEntryType::CouncilDecision,
4113 data,
4114 false,
4115 )
4116 }
4117
4118 pub(super) fn decision(
4119 &mut self,
4120 signer: &SigningKey,
4121 ) -> GovernanceLogId {
4122 let data = json!({"title": format!("Decision {}", self.gov + 1)});
4123 self.entry(signer, data)
4124 }
4125
4126 pub(super) fn amend(
4128 &mut self,
4129 signer: &SigningKey,
4130 draft: &AmendmentDraft,
4131 ) -> GovernanceLogId {
4132 let draft = resalted(draft, self.next_seq());
4133 let id = self.amend_v1(signer, &draft.amendment);
4134 let link = self.links.last_mut().unwrap();
4135 link.texts = Some(draft.texts);
4136 id
4137 }
4138
4139 pub(super) fn amend_v1(
4142 &mut self,
4143 signer: &SigningKey,
4144 amendment: &Amendment,
4145 ) -> GovernanceLogId {
4146 self.amd += 1;
4147 let id = amd(self.amd);
4148 self.push(
4149 signer,
4150 id,
4151 GovernanceLogEntryType::Amendment,
4152 serde_json::to_value(amendment).unwrap(),
4153 true,
4154 )
4155 }
4156
4157 pub(super) fn rotate(
4158 &mut self,
4159 signer: &SigningKey,
4160 rotation: &KeyRotation,
4161 ) -> GovernanceLogId {
4162 self.key += 1;
4163 let id = key_id(self.key);
4164 self.push(
4165 signer,
4166 id,
4167 GovernanceLogEntryType::KeyRotation,
4168 serde_json::to_value(rotation).unwrap(),
4169 true,
4170 )
4171 }
4172 }
4173
4174 #[test]
4177 fn canonical_json_sorts_keys_at_every_level() {
4178 let v = json!({"b": {"z": 1, "a": [{"y": 2, "x": 3}]}, "a": null});
4179 assert_eq!(
4180 canonical_json(&v),
4181 br#"{"a":null,"b":{"a":[{"x":3,"y":2}],"z":1}}"#
4182 );
4183 }
4184
4185 #[test]
4186 fn canonical_json_is_compact_and_escapes_like_serde() {
4187 let v = json!({"s": "tab\there \"q\" ünïcode \u{1F600}", "n": [1, -2, 3.5, true, false]});
4188 let bytes = canonical_json(&v);
4189 let text = std::str::from_utf8(&bytes).unwrap();
4190 assert_eq!(
4191 text,
4192 r#"{"n":[1,-2,3.5,true,false],"s":"tab\there \"q\" ünïcode 😀"}"#
4193 );
4194 }
4195
4196 #[test]
4197 fn canonical_json_ignores_insertion_order() {
4198 let mut a = serde_json::Map::new();
4199 a.insert("z".into(), json!(1));
4200 a.insert("a".into(), json!(2));
4201 let mut b = serde_json::Map::new();
4202 b.insert("a".into(), json!(2));
4203 b.insert("z".into(), json!(1));
4204 assert_eq!(
4205 canonical_json(&serde_json::Value::Object(a)),
4206 canonical_json(&serde_json::Value::Object(b))
4207 );
4208 }
4209
4210 #[test]
4211 fn canonical_json_empty_containers() {
4212 assert_eq!(canonical_json(&json!({})), b"{}");
4213 assert_eq!(canonical_json(&json!([])), b"[]");
4214 assert_eq!(
4215 canonical_json(&json!({"a": {}, "b": []})),
4216 br#"{"a":{},"b":[]}"#
4217 );
4218 }
4219
4220 #[test]
4223 fn preimage_is_declaration_ordered_json() {
4224 let e = Envelope::new(
4225 gov(1),
4226 GovernanceLogEntryType::AppealsCourtDecision,
4227 at(0),
4228 None,
4229 data_hash(&json!({})),
4230 );
4231 let text = String::from_utf8(e.preimage()).unwrap();
4232 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}");
4233 }
4234
4235 #[test]
4236 fn every_envelope_field_changes_the_hash() {
4237 let base = Envelope::new(
4238 gov(1),
4239 GovernanceLogEntryType::CouncilDecision,
4240 at(0),
4241 None,
4242 data_hash(&json!({"a":1})),
4243 );
4244 let h = base.entry_hash();
4245 let mut e = base.clone();
4246 e.id = gov(2);
4247 assert_ne!(e.entry_hash(), h);
4248 let mut e = base.clone();
4249 e.entry_type = GovernanceLogEntryType::PolicyChange;
4250 assert_ne!(e.entry_hash(), h);
4251 let mut e = base.clone();
4252 e.created_at += 1;
4253 assert_ne!(e.entry_hash(), h);
4254 let mut e = base.clone();
4255 e.prev_hash = Some(h);
4256 assert_ne!(e.entry_hash(), h);
4257 let mut e = base.clone();
4258 e.data_hash = data_hash(&json!({"a":2}));
4259 assert_ne!(e.entry_hash(), h);
4260 assert_eq!(base.entry_hash(), h, "and it is deterministic");
4261 }
4262
4263 #[test]
4264 fn truncation_matches_what_the_envelope_carries() {
4265 let t = at(0);
4266 assert_eq!(truncate_to_micros(t).timestamp_subsec_nanos(), 123_456_000);
4267 assert_eq!(truncate_to_seconds(t).timestamp_subsec_nanos(), 0);
4268 assert_eq!(
4269 Envelope::new(
4270 gov(1),
4271 GovernanceLogEntryType::CouncilDecision,
4272 t,
4273 None,
4274 data_hash(&json!(null))
4275 )
4276 .created_at,
4277 truncate_to_micros(t).timestamp_micros()
4278 );
4279 }
4280
4281 #[test]
4284 fn hex_newtypes_round_trip_and_reject_wrong_lengths() {
4285 let h = data_hash(&json!(1));
4286 let s = serde_json::to_string(&h).unwrap();
4287 assert_eq!(s.len(), 66);
4288 let back: Sha256Hex = serde_json::from_str(&s).unwrap();
4289 assert_eq!(back, h);
4290 assert!(serde_json::from_str::<Sha256Hex>("\"abcd\"").is_err());
4291 assert!("zz".repeat(32).parse::<Sha256Hex>().is_err());
4292 assert!(Sha256Hex::try_from(vec![0u8; 31]).is_err());
4293 assert_eq!(format!("{h:?}"), format!("Sha256Hex({h})"));
4294 }
4295
4296 #[test]
4299 fn attest_then_verify_link() {
4300 let (key, pk) = generate_keypair();
4301 let l = link(&key, 1, None, &json!({"a": 1}), at(5));
4302 assert_eq!(verify_link(&l, &pk), Ok(()));
4303 assert!(verify_data(&l, &json!({"a": 1})));
4304 assert!(!verify_data(&l, &json!({"a": 2})));
4305 assert!(!l.attestation.retroactive);
4306 }
4307
4308 #[test]
4309 fn wrong_key_fails_signature_only() {
4310 let (key, _) = generate_keypair();
4311 let (_, other) = generate_keypair();
4312 let l = link(&key, 1, None, &json!({}), at(5));
4313 assert_eq!(verify_link(&l, &other), Err(LinkError::BadSignature));
4314 }
4315
4316 #[test]
4317 fn tampering_with_any_attested_field_is_detected() {
4318 let (key, pk) = generate_keypair();
4319 let l = link(&key, 1, None, &json!({"a": 1}), at(5));
4320
4321 let mut t = l.clone();
4322 t.created_at += chrono::Duration::microseconds(1);
4323 assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
4324
4325 let mut t = l.clone();
4326 t.entry_type = GovernanceLogEntryType::StewardVeto;
4327 assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
4328
4329 let mut t = l.clone();
4330 t.attestation.data_hash = data_hash(&json!({"a": 2}));
4331 assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
4332
4333 let mut t = l.clone();
4336 t.attestation.signed_at -= chrono::Duration::seconds(1);
4337 assert_eq!(verify_link(&t, &pk), Err(LinkError::BadSignature));
4338
4339 let mut t = l.clone();
4340 t.attestation.envelope_version = 2;
4341 assert_eq!(verify_link(&t, &pk), Err(LinkError::UnsupportedVersion(2)));
4342 }
4343
4344 #[test]
4345 fn a_good_chain_verifies_in_any_input_order() {
4346 let (key, pk) = generate_keypair();
4347 let mut c = chain(&key, 4);
4348 c.reverse();
4349 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
4350 assert!(v.ok, "{v:#?}");
4351 assert_eq!(v.head, Some(gov(4)));
4352 assert_eq!(
4353 v.entries.iter().map(|e| e.chain_seq).collect::<Vec<_>>(),
4354 [1, 2, 3, 4]
4355 );
4356 assert!(v.entries.iter().all(|e| e.content_matches.is_none()
4357 && e.problem.is_none()
4358 && !e.out_of_order));
4359 assert_eq!(v.public_key, PublicKeyHex::from(&pk));
4360 }
4361
4362 #[test]
4363 fn empty_chain_is_ok_with_no_head() {
4364 let (_, pk) = generate_keypair();
4365 let v = verify_chain(&[], &pk, &anchored(&pk), &roots());
4366 assert!(v.ok);
4367 assert!(v.head.is_none());
4368 assert!(v.entries.is_empty());
4369 }
4370
4371 #[test]
4372 fn a_changed_entry_breaks_its_signature_and_the_next_link() {
4373 let (key, pk) = generate_keypair();
4374 let mut c = chain(&key, 3);
4375 let rewritten = link(
4378 &key,
4379 2,
4380 Some(&c[0]),
4381 &json!({"title": "Decision 2", "outcome": "REJECTED"}),
4382 at(21),
4383 );
4384 c[1] = rewritten;
4385 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
4386 assert!(!v.ok);
4387 assert!(
4388 v.entries[1].signature_valid && v.entries[1].link_valid,
4389 "the rewrite itself is well-formed: {:#?}",
4390 v.entries[1]
4391 );
4392 assert!(
4393 !v.entries[2].link_valid,
4394 "but entry 3 no longer points at it: {:#?}",
4395 v.entries[2]
4396 );
4397 assert!(
4398 v.entries[2]
4399 .problem
4400 .as_deref()
4401 .unwrap()
4402 .contains("prev_hash")
4403 );
4404 }
4405
4406 #[test]
4407 fn a_removed_entry_is_a_gap_and_a_broken_link() {
4408 let (key, pk) = generate_keypair();
4409 let mut c = chain(&key, 3);
4410 c.remove(1);
4411 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
4412 assert!(!v.ok);
4413 assert!(v.entries[0].link_valid);
4414 let p = v.entries[1].problem.as_deref().unwrap();
4415 assert!(p.contains("chain_seq 3 where 2 was expected"), "{p}");
4416 assert!(p.contains("prev_hash"), "{p}");
4417 }
4418
4419 #[test]
4422 fn a_summary_counts_and_names_the_failures() {
4423 let (key, pk) = generate_keypair();
4424 let clean =
4425 verify_chain(&chain(&key, 3), &pk, &anchored(&pk), &roots());
4426 let summary = clean.summary();
4427 assert!(summary.ok);
4428 assert_eq!((summary.entries, summary.failed.len()), (3, 0));
4429 assert_eq!(summary.head, clean.head);
4430 assert!(summary.first_failure.is_none());
4431 assert!(summary.how_to_read.contains("full=true"));
4432
4433 let mut c = chain(&key, 3);
4434 c.remove(1);
4435 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
4436 let summary = v.summary();
4437 assert!(!summary.ok);
4438 assert_eq!(summary.link_failures, 1);
4439 assert_eq!(summary.failed, vec![v.entries[1].id.clone()]);
4440 assert_eq!(summary.first_failure.as_ref(), Some(&v.entries[1]));
4441 let json = serde_json::to_value(&summary).unwrap();
4442 assert!(json["entries"].is_u64(), "a count, not the rows: {json}");
4443 }
4444
4445 #[test]
4446 fn a_second_genesis_is_rejected() {
4447 let (key, pk) = generate_keypair();
4448 let mut c = chain(&key, 2);
4449 let rogue = link(&key, 2, None, &json!({}), at(21));
4450 c[1] = rogue;
4451 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
4452 assert!(!v.ok);
4453 assert!(
4454 v.entries[1]
4455 .problem
4456 .as_deref()
4457 .unwrap()
4458 .contains("null mid-chain")
4459 );
4460 }
4461
4462 #[test]
4463 fn retroactive_and_out_of_order_are_recomputed_not_copied() {
4464 let (key, pk) = generate_keypair();
4465 let first = link(&key, 1, None, &json!({}), at(10 + 3600));
4466 let created = truncate_to_micros(at(5));
4469 let envelope = Envelope::new(
4470 gov(2),
4471 GovernanceLogEntryType::CouncilDecision,
4472 created,
4473 Some(first.attestation.entry_hash),
4474 data_hash(&json!({})),
4475 );
4476 let attestation = attest(&key, &envelope, 2, at(6));
4477 let mut second = GovernanceChainLink {
4478 id: gov(2),
4479 entry_type: GovernanceLogEntryType::CouncilDecision,
4480 created_at: created,
4481 attestation,
4482 data: None,
4483 texts: None,
4484 };
4485 second.attestation.retroactive = true; let v = verify_chain(&[first, second], &pk, &anchored(&pk), &roots());
4487 assert!(v.ok, "{v:#?}");
4488 assert!(v.entries[0].retroactive);
4489 assert!(!v.entries[1].retroactive, "recomputed from timestamps");
4490 assert!(v.entries[1].out_of_order);
4491 }
4492
4493 #[test]
4494 fn content_mismatch_settles_to_not_ok() {
4495 let (key, pk) = generate_keypair();
4496 let c = chain(&key, 1);
4497 let mut v = verify_chain(&c, &pk, &anchored(&pk), &roots());
4498 v.entries[0].content_matches = Some(true);
4499 assert!(v.clone().settle().ok);
4500 v.entries[0].content_matches = Some(false);
4501 assert!(!v.settle().ok);
4502 }
4503
4504 #[test]
4507 fn an_amendment_fills_amended_by_on_its_target() {
4508 let (key, pk) = generate_keypair();
4509 let mut c = Chain::new();
4510 let target = c.decision(&key);
4511 c.decision(&key);
4512 let amendment = AmendmentDraft::new(
4513 target,
4514 c.hash_at(1),
4515 AmendmentKind::NonPrecedential,
4516 "§1 (Red Team Cases Recharacterized)",
4517 "diagnostic finding — not citable as moderation precedent",
4518 )
4519 .unwrap()
4520 .with_authority(gov(5))
4521 .with_rationale("§5 leaves the ruling itself standing");
4522 let id = c.amend(&key, &amendment);
4523
4524 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4525 assert!(v.ok, "{v:#?}");
4526 assert_eq!(v.entries[0].amended_by, vec![id]);
4527 assert!(v.entries[1].amended_by.is_empty());
4528 assert!(!v.entries[0].redacted);
4529 assert_eq!(v.head, Some(amd(1)));
4530 assert_eq!(v.entries[2].content_matches, Some(true));
4532 assert_eq!(v.entries[0].content_matches, None);
4533 assert_eq!(
4534 standing([amendment.amendment.kind]),
4535 Standing::NonPrecedential
4536 );
4537 }
4538
4539 #[test]
4540 fn an_amendment_must_name_an_earlier_entry_by_its_exact_hash() {
4541 let (key, pk) = generate_keypair();
4542 let problem = |c: &Chain, at: usize| -> String {
4543 verify_chain(&c.links, &pk, &anchored(&pk), &roots()).entries[at]
4544 .problem
4545 .clone()
4546 .unwrap_or_default()
4547 };
4548
4549 let mut c = Chain::new();
4550 c.decision(&key);
4551 let unknown = AmendmentDraft::new(
4552 gov(99),
4553 c.hash_at(1),
4554 AmendmentKind::Overruled,
4555 "b",
4556 "n",
4557 )
4558 .unwrap();
4559 c.amend(&key, &unknown);
4560 assert!(
4561 problem(&c, 1).contains("is not an entry of this chain"),
4562 "{}",
4563 problem(&c, 1)
4564 );
4565 assert!(!verify_chain(&c.links, &pk, &anchored(&pk), &roots()).ok);
4566
4567 let mut c = Chain::new();
4569 c.decision(&key);
4570 let forward = AmendmentDraft::new(
4571 gov(2),
4572 c.hash_at(1),
4573 AmendmentKind::Overruled,
4574 "b",
4575 "n",
4576 )
4577 .unwrap();
4578 c.amend(&key, &forward);
4579 c.decision(&key);
4580 assert!(
4581 problem(&c, 1).contains("not an earlier entry"),
4582 "{}",
4583 problem(&c, 1)
4584 );
4585
4586 let mut c = Chain::new();
4588 let target = c.decision(&key);
4589 let wrong_hash = AmendmentDraft::new(
4590 target,
4591 data_hash(&json!("some other entry")),
4592 AmendmentKind::Overruled,
4593 "b",
4594 "n",
4595 )
4596 .unwrap();
4597 c.amend(&key, &wrong_hash);
4598 assert!(problem(&c, 1).contains("entry_hash"), "{}", problem(&c, 1));
4599 assert!(
4600 verify_chain(&c.links, &pk, &anchored(&pk), &roots()).entries[0]
4601 .amended_by
4602 .is_empty()
4603 );
4604 }
4605
4606 #[test]
4607 fn redaction_shape_violations_fail_verification() {
4608 let (key, pk) = generate_keypair();
4609 let amend_with = |mutate: &dyn Fn(&mut Amendment)| -> String {
4610 let mut c = Chain::new();
4611 let target = c.decision(&key);
4612 let mut amendment = AmendmentDraft::new(
4613 target,
4614 c.hash_at(1),
4615 AmendmentKind::Correction,
4616 "b",
4617 "n",
4618 )
4619 .unwrap();
4620 mutate(&mut amendment.amendment);
4621 c.amend_v1(&key, &amendment.amendment);
4622 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4623 assert!(!v.ok, "{v:#?}");
4624 v.entries[1].problem.clone().unwrap_or_default()
4625 };
4626
4627 assert_eq!(
4630 AmendmentDraft::new(
4631 gov(1),
4632 data_hash(&json!(null)),
4633 AmendmentKind::Redaction,
4634 "b",
4635 "n"
4636 ),
4637 Err(AmendmentError::MissingRedaction)
4638 );
4639 let p = amend_with(&|a| a.kind = AmendmentKind::Redaction);
4640 assert!(p.contains("requires a `redaction`"), "{p}");
4641
4642 let p = amend_with(&|a| {
4643 a.redaction = Some(Redaction {
4644 fields: vec!["/x".into()],
4645 resulting_data_hash: data_hash(&json!({})),
4646 resulting_latest_hash: None,
4647 })
4648 });
4649 assert!(p.contains("only valid on kind"), "{p}");
4650
4651 let p = amend_with(&|a| a.agora_governance_amendment = 3);
4652 assert!(p.contains("agora_governance_amendment is 3"), "{p}");
4653
4654 let p = amend_with(&|a| a.agora_governance_amendment = 1);
4657 assert!(p.contains("does neither consistently"), "{p}");
4658 let p = amend_with(&|a| a.note = AmendmentText::Plain("n".into()));
4659 assert!(p.contains("does neither consistently"), "{p}");
4660 }
4661
4662 #[test]
4663 fn governance_data_holds_no_number_that_is_not_a_64_bit_integer() {
4664 let fine = json!({"a": [1, -2, u64::MAX, i64::MIN], "b": {"c": "0.5"}});
4665 assert_eq!(non_integer_number(&fine), None);
4666 assert!(blind_data(&fine, Blind::random()).is_ok());
4667
4668 for (text, pointer) in [
4669 (r#"{"a": {"b/c": [1, 0.5]}}"#, "/a/b~1c/1"),
4670 (r#"{"n": 1.0}"#, "/n"),
4671 (r#"{"n": 1e3}"#, "/n"),
4672 (r#"{"n": 18446744073709551616}"#, "/n"),
4673 (r#"{"n": -9223372036854775809}"#, "/n"),
4674 ] {
4675 let data: serde_json::Value = serde_json::from_str(text).unwrap();
4676 assert_eq!(non_integer_number(&data).as_deref(), Some(pointer));
4677 assert_eq!(
4678 blind_data(&data, Blind::random()),
4679 Err(BlindError::NonIntegerNumber(pointer.into())),
4680 "the writer refuses it"
4681 );
4682 assert_eq!(
4683 redact_data(&data, &["/n".into()], &amd(1), Blind::random()),
4684 Err(RedactError::NonIntegerNumber(pointer.into()))
4685 );
4686 }
4687 }
4688
4689 #[test]
4690 fn standing_is_the_last_amendment_that_changes_it() {
4691 use AmendmentKind::*;
4692 assert_eq!(standing([]), Standing::InForce);
4693 assert_eq!(standing([Correction, Redaction]), Standing::InForce);
4694 assert_eq!(
4695 standing([Correction, NonPrecedential, Redaction]),
4696 Standing::NonPrecedential
4697 );
4698 assert_eq!(standing([Overruled, Reinstated]), Standing::InForce);
4699 assert_eq!(standing([Reinstated, Superseded]), Standing::Superseded);
4700 assert_eq!(kind_standing(Reattested), None);
4701 assert_eq!(kind_standing(Reinstated), Some(Standing::InForce));
4702 }
4703
4704 #[test]
4705 fn a_redaction_verifies_against_the_amendment_and_nothing_else() {
4706 let (key, pk) = generate_keypair();
4707 let data = json!({
4708 "finding": "upheld",
4709 "subject": {"handle": "someone", "detail": "personal"},
4710 });
4711 let mut c = Chain::new();
4712 let target = c.entry(&key, data.clone());
4713 let amendment_id = c.next_amd();
4714 let (amendment, redacted) = AmendmentDraft::redaction(
4715 &amendment_id,
4716 target,
4717 c.hash_at(1),
4718 "GDPR Art. 17(1)(a)",
4719 "personal data removed on request",
4720 vec!["/subject/handle".into(), "/subject/detail".into()],
4721 &data,
4722 Blind::from([7; 32]),
4723 &[],
4724 )
4725 .unwrap();
4726 assert_eq!(c.amend(&key, &amendment), amendment_id);
4727 assert_eq!(redacted[BLIND_KEY], json!(Blind::from([7; 32])));
4728
4729 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4730 assert!(v.ok, "{v:#?}");
4731 assert!(v.entries[0].redacted);
4732 assert_eq!(v.entries[0].redacted_data_hash, Some(data_hash(&redacted)));
4733 assert_eq!(
4734 redacted["subject"]["handle"],
4735 json!(format!("[redacted by {amendment_id}]"))
4736 );
4737 assert_eq!(redacted["finding"], json!("upheld"), "and nothing else");
4738
4739 assert!(v.check_content(&c.links[0], &redacted));
4741 assert_eq!(v.entries[0].content_matches, Some(true));
4742 assert!(v.clone().settle().ok);
4743 assert!(v.check_content(&c.links[0], &data));
4745
4746 let mut tampered = redacted.clone();
4748 tampered["finding"] = json!("overturned");
4749 assert!(!v.check_content(&c.links[0], &tampered));
4750 assert_eq!(v.entries[0].content_matches, Some(false));
4751 assert!(!v.settle().ok);
4752 }
4753
4754 fn seats() -> serde_json::Value {
4757 json!({
4758 "title": "A motion",
4759 "rounds": [{
4760 "number": 1,
4761 "responses": [
4762 {"role": "lawyer", "rationale": "Because.", "raw_text": "Because.", "vote": "yes"},
4763 {"role": "artist", "rationale": "Why not.", "raw_text": "Why not?", "vote": "no"},
4764 ],
4765 }],
4766 "subject": {"handle": "someone"},
4767 BLIND_KEY: Blind::from([3; 32]),
4768 })
4769 }
4770
4771 const LAWYER: &str = "/rounds/0/responses/0";
4772
4773 fn patch(ops: serde_json::Value) -> json_patch::Patch {
4775 serde_json::from_value(ops).unwrap()
4776 }
4777
4778 fn dedup(data: &serde_json::Value) -> Edit {
4780 let (raw, rationale) =
4781 (format!("{LAWYER}/raw_text"), format!("{LAWYER}/rationale"));
4782 Revision::remove_duplicates(data, &[(&raw, &rationale)]).unwrap()
4783 }
4784
4785 fn revise(
4786 c: &Chain,
4787 target: &GovernanceLogId,
4788 latest: &serde_json::Value,
4789 patch: impl Into<Edit>,
4790 ) -> (AmendmentDraft, serde_json::Value) {
4791 AmendmentDraft::revision(
4792 target.clone(),
4793 GovernanceLogEntryType::CouncilDecision,
4794 c.hash_at(1),
4795 "Steward's record REC-2026-0001",
4796 "raw_text identical to the rationale removed",
4797 latest,
4798 patch,
4799 )
4800 .unwrap()
4801 }
4802
4803 fn revision_of(draft: &AmendmentDraft) -> &Revision {
4804 draft.amendment.revision.as_ref().unwrap()
4805 }
4806
4807 #[test]
4808 fn a_revision_overwrites_nothing_and_folds_to_the_latest() {
4809 let (key, pk) = generate_keypair();
4810 let data = seats();
4811 let mut c = Chain::new();
4812 let target = c.entry(&key, data.clone());
4813 let (first, v1) = revise(&c, &target, &data, dedup(&data));
4814 c.amend(&key, &first);
4815 let (second, v2) = revise(
4816 &c,
4817 &target,
4818 &v1,
4819 patch(json!([{"op": "move", "from": "/title", "path": "/motion"}])),
4820 );
4821 c.amend(&key, &second);
4822
4823 let seat = &v1["rounds"][0]["responses"];
4824 assert!(seat[0].get("raw_text").is_none(), "removed, no marker");
4825 assert_eq!(seat[1]["raw_text"], json!("Why not?"), "the other stays");
4826 assert_eq!(v2["motion"], json!("A motion"));
4827 assert_eq!(v2[BLIND_KEY], data[BLIND_KEY], "blind kept");
4828 assert_eq!(
4829 latest(&data, [revision_of(&first), revision_of(&second)]).unwrap(),
4830 v2
4831 );
4832
4833 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4834 assert!(v.ok, "{v:#?}");
4835 let entry = &v.entries[0];
4836 assert_eq!(entry.revisions, [amd(1), amd(2)]);
4837 assert_eq!(entry.latest_data_hash, Some(data_hash(&v2)));
4838 assert!(!entry.redacted && entry.redacted_data_hash.is_none());
4839
4840 assert!(v.check_content(&c.links[0], &data));
4842 assert!(v.check_content(&c.links[0], &v2));
4843 assert!(!v.check_content(&c.links[0], &v1), "not the latest");
4844 v.check_content(&c.links[0], &data);
4845 assert!(v.clone().settle().ok);
4846
4847 let wire = serde_json::to_value(&v).unwrap();
4850 assert!(wire["entries"][0].get("history").is_none());
4851 let mut old = wire.clone();
4852 for field in ["revisions", "latest_data_hash", "superseded_revisions"] {
4853 old["entries"][0].as_object_mut().unwrap().remove(field);
4854 }
4855 let old: GovernanceVerification = serde_json::from_value(old).unwrap();
4856 assert!(old.entries[0].revisions.is_empty());
4857 }
4858
4859 #[test]
4861 fn a_revision_that_does_not_produce_its_hash_fails() {
4862 let (key, pk) = generate_keypair();
4863 let data = seats();
4864 let mut c = Chain::new();
4865 let target = c.entry(&key, data.clone());
4866 let (mut draft, _) = revise(&c, &target, &data, dedup(&data));
4867 draft
4868 .amendment
4869 .revision
4870 .as_mut()
4871 .unwrap()
4872 .resulting_data_hash = data_hash(&json!({"something": "else"}));
4873 c.amend(&key, &draft);
4874
4875 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4876 assert!(v.ok, "the chain itself is sound: {v:#?}");
4877 assert!(v.check_content(&c.links[0], &data), "the stored data is");
4878 assert!(
4879 v.entries[0]
4880 .problem
4881 .as_deref()
4882 .is_some_and(|p| p.contains("does not produce")),
4883 "{:?}",
4884 v.entries[0].problem
4885 );
4886 assert!(!v.settle().ok);
4887
4888 c.links[0].data = Some(data);
4890 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4891 assert!(!v.ok);
4892 }
4893
4894 fn redact(
4895 c: &Chain,
4896 target: &GovernanceLogId,
4897 fields: &[&str],
4898 data: &serde_json::Value,
4899 revisions: &[&Revision],
4900 ) -> Result<(AmendmentDraft, serde_json::Value), RedactError> {
4901 AmendmentDraft::redaction(
4902 &c.next_amd(),
4903 target.clone(),
4904 c.hash_at(1),
4905 "GDPR Art. 17(1)(a)",
4906 "removed on request",
4907 fields.iter().map(|f| f.to_string()).collect(),
4908 data,
4909 Blind::from([9; 32]),
4910 revisions,
4911 )
4912 }
4913
4914 #[test]
4917 fn a_revision_then_a_redaction() {
4918 let (key, pk) = generate_keypair();
4919 let data = seats();
4920 let mut c = Chain::new();
4921 let target = c.entry(&key, data.clone());
4922 let (revision, _) = revise(&c, &target, &data, dedup(&data));
4923 c.amend(&key, &revision);
4924 let rationale = format!("{LAWYER}/rationale");
4925 let (redaction, redacted) = redact(
4926 &c,
4927 &target,
4928 &[&rationale],
4929 &data,
4930 &[revision_of(&revision)],
4931 )
4932 .unwrap();
4933 c.amend(&key, &redaction);
4934
4935 let fields = &redaction.amendment.redaction.as_ref().unwrap().fields;
4937 assert_eq!(fields, &[rationale, format!("{LAWYER}/raw_text")]);
4938 let seat = &redacted["rounds"][0]["responses"][0];
4939 assert_eq!(seat["raw_text"], seat["rationale"]);
4940 assert!(seat["raw_text"].as_str().unwrap().starts_with("[redacted"));
4941
4942 let rebased = latest(&redacted, [revision_of(&revision)]).unwrap();
4943 assert!(
4944 rebased["rounds"][0]["responses"][0]
4945 .get("raw_text")
4946 .is_none()
4947 );
4948 assert_eq!(
4949 redaction
4950 .amendment
4951 .redaction
4952 .as_ref()
4953 .unwrap()
4954 .resulting_latest_hash,
4955 Some(data_hash(&rebased))
4956 );
4957
4958 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4959 assert!(v.ok, "{v:#?}");
4960 let entry = &v.entries[0];
4961 assert_eq!(entry.latest_data_hash, Some(data_hash(&rebased)));
4962 assert_eq!(entry.superseded_revisions, [amd(1)]);
4963 assert!(v.check_content(&c.links[0], &redacted));
4964 assert!(v.check_content(&c.links[0], &rebased));
4965 assert!(
4966 v.check_content(&c.links[0], &data),
4967 "for whoever kept a copy"
4968 );
4969 v.check_content(&c.links[0], &redacted);
4970 assert!(v.clone().settle().ok, "{v:#?}");
4971
4972 let mut c2 = Chain::new();
4974 let target = c2.entry(&key, data.clone());
4975 c2.amend(&key, &revision);
4976 let (mut lying, _) = redact(
4977 &c2,
4978 &target,
4979 &["/subject/handle"],
4980 &data,
4981 &[revision_of(&revision)],
4982 )
4983 .unwrap();
4984 let (_, honest) = redact(
4985 &c2,
4986 &target,
4987 &["/subject/handle"],
4988 &data,
4989 &[revision_of(&revision)],
4990 )
4991 .unwrap();
4992 lying
4993 .amendment
4994 .redaction
4995 .as_mut()
4996 .unwrap()
4997 .resulting_latest_hash = Some(data_hash(&json!({})));
4998 c2.amend(&key, &lying);
4999 let mut v = verify_chain(&c2.links, &pk, &anchored(&pk), &roots());
5000 assert!(v.check_content(&c2.links[0], &honest));
5001 assert!(!v.settle().ok);
5002 }
5003
5004 #[test]
5007 fn a_redaction_of_a_revised_entry_must_name_the_latest_hash() {
5008 let (key, pk) = generate_keypair();
5009 let data = seats();
5010 let mut c = Chain::new();
5011 let target = c.entry(&key, data.clone());
5012 let (revision, _) = revise(&c, &target, &data, dedup(&data));
5013 c.amend(&key, &revision);
5014 let (redaction, _) =
5016 redact(&c, &target, &["/subject/handle"], &data, &[]).unwrap();
5017 c.amend(&key, &redaction);
5018 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
5019 assert!(!v.ok);
5020 assert!(
5021 v.entries[2]
5022 .problem
5023 .as_deref()
5024 .is_some_and(|p| p.contains("names no resulting_latest_hash")),
5025 "{v:#?}"
5026 );
5027 }
5028
5029 #[test]
5032 fn a_false_duplicate_fails() {
5033 let (key, pk) = generate_keypair();
5034 let data = seats();
5035 let mut c = Chain::new();
5036 let target = c.entry(&key, data.clone());
5037 let artist = "/rounds/0/responses/1";
5038 let (raw, rationale) =
5039 (format!("{artist}/raw_text"), format!("{artist}/rationale"));
5040 let (mut lying, _) = revise(
5042 &c,
5043 &target,
5044 &data,
5045 patch(json!([{"op": "remove", "path": raw}])),
5046 );
5047 let revised = apply_patch(&data, &revision_of(&lying).patch).unwrap();
5048 let r = lying.amendment.revision.as_mut().unwrap();
5049 r.duplicates = vec![(raw, rationale)];
5050 r.resulting_data_hash = data_hash(&revised);
5051 c.amend(&key, &lying);
5052
5053 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
5054 assert!(v.ok, "unread, the claim is unchecked: {v:#?}");
5055 assert!(v.check_content(&c.links[0], &data));
5056 assert!(
5057 v.entries[0]
5058 .problem
5059 .as_deref()
5060 .is_some_and(|p| p.contains("but they differ")),
5061 "{v:#?}"
5062 );
5063 assert!(!v.settle().ok);
5064 }
5065
5066 #[test]
5068 fn a_redaction_that_breaks_a_revision_is_refused() {
5069 let (key, _) = generate_keypair();
5070 let data = seats();
5071 let mut c = Chain::new();
5072 let target = c.entry(&key, data.clone());
5073 let (moved, _) = revise(
5074 &c,
5075 &target,
5076 &data,
5077 patch(
5078 json!([{"op": "move", "from": "/subject/handle", "path": "/handle"}]),
5079 ),
5080 );
5081 c.amend(&key, &moved);
5082 let err =
5083 redact(&c, &target, &["/subject"], &data, &[revision_of(&moved)])
5084 .unwrap_err();
5085 assert!(matches!(err, RedactError::Rebase(_)), "{err}");
5086 assert!(
5088 redact(
5089 &c,
5090 &target,
5091 &["/subject/handle"],
5092 &data,
5093 &[revision_of(&moved)]
5094 )
5095 .is_ok()
5096 );
5097 }
5098
5099 #[test]
5102 fn a_redaction_follows_every_duplicate_of_what_it_erases() {
5103 let revision = Revision {
5104 patch: patch(json!([{"op": "remove", "path": "/copy"}])),
5105 duplicates: vec![("/copy".into(), "/source/inner".into())],
5106 resulting_data_hash: data_hash(&json!(null)),
5107 };
5108 let extra = |fields: &[&str]| {
5109 let fields: Vec<String> =
5110 fields.iter().map(|f| f.to_string()).collect();
5111 duplicates_of(&fields, &[&revision])
5112 };
5113 assert_eq!(extra(&["/source/inner"]), ["/copy"]);
5114 assert_eq!(extra(&["/source/inner/name"]), ["/copy/name"]);
5115 assert_eq!(extra(&["/source"]), ["/copy"]);
5116 assert!(extra(&["/source/other"]).is_empty());
5117 assert!(extra(&["/source/inn"]).is_empty());
5118 }
5119
5120 #[test]
5123 fn a_redaction_then_a_revision() {
5124 let (key, pk) = generate_keypair();
5125 let data = seats();
5126 let mut c = Chain::new();
5127 let target = c.entry(&key, data.clone());
5128 let (redaction, redacted) =
5129 redact(&c, &target, &["/subject/handle"], &data, &[]).unwrap();
5130 assert_eq!(
5131 redaction
5132 .amendment
5133 .redaction
5134 .as_ref()
5135 .unwrap()
5136 .resulting_latest_hash,
5137 None,
5138 "nothing to rebase"
5139 );
5140 c.amend(&key, &redaction);
5141 let (revision, revised) =
5142 revise(&c, &target, &redacted, dedup(&redacted));
5143 c.amend(&key, &revision);
5144
5145 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
5146 assert!(v.ok, "{v:#?}");
5147 assert_eq!(v.entries[0].latest_data_hash, Some(data_hash(&revised)));
5148 assert!(v.entries[0].superseded_revisions.is_empty());
5149 assert!(v.check_content(&c.links[0], &redacted));
5150 assert!(v.check_content(&c.links[0], &revised));
5151 assert!(v.settle().ok);
5152 }
5153
5154 #[test]
5155 fn a_revision_refuses_what_it_should() {
5156 let data = seats();
5157 let raw = &format!("{LAWYER}/raw_text");
5158 let rationale = &format!("{LAWYER}/rationale");
5159 let artist = "/rounds/0/responses/1";
5160 let dedup =
5161 |pairs: &[(&str, &str)]| Revision::remove_duplicates(&data, pairs);
5162
5163 assert_eq!(
5164 dedup(&[(
5165 &format!("{artist}/raw_text"),
5166 &format!("{artist}/rationale")
5167 )]),
5168 Err(ReviseError::NotIdentical {
5169 path: format!("{artist}/raw_text"),
5170 same_as: format!("{artist}/rationale"),
5171 })
5172 );
5173 assert_eq!(
5174 dedup(&[(raw, rationale), (rationale, raw)]),
5175 Err(ReviseError::SourceRemoved {
5176 path: raw.clone(),
5177 same_as: rationale.clone(),
5178 }),
5179 "a same_as the patch removes"
5180 );
5181 assert_eq!(
5182 dedup(&[("/nope", rationale)]),
5183 Err(ReviseError::Unresolved("/nope".into()))
5184 );
5185
5186 let make = |target_type, p: Edit| {
5187 AmendmentDraft::revision(
5188 gov(1),
5189 target_type,
5190 data_hash(&json!(null)),
5191 "b",
5192 "n",
5193 &data,
5194 p,
5195 )
5196 .map(|(_, v)| v)
5197 };
5198 let council = GovernanceLogEntryType::CouncilDecision;
5199 assert_eq!(
5200 make(council, patch(json!([])).into()),
5201 Err(ReviseError::EmptyPatch)
5202 );
5203 assert!(matches!(
5204 make(
5205 council,
5206 patch(json!([{"op": "remove", "path": "/nope"}])).into()
5207 ),
5208 Err(ReviseError::Patch(_))
5209 ));
5210 assert_eq!(
5211 make(
5212 council,
5213 patch(json!([{"op": "remove", "path": "/_blind"}])).into()
5214 ),
5215 Err(ReviseError::BlindPointer("/_blind".into()))
5216 );
5217 assert_eq!(
5218 make(
5219 council,
5220 patch(json!([{"op": "copy", "from": "/_blind", "path": "/b"}]))
5221 .into()
5222 ),
5223 Err(ReviseError::BlindPointer("/_blind".into()))
5224 );
5225 for entry_type in [
5226 GovernanceLogEntryType::Amendment,
5227 GovernanceLogEntryType::KeyRotation,
5228 GovernanceLogEntryType::StewardRecord,
5229 ] {
5230 assert!(!is_revisable(entry_type));
5231 assert_eq!(
5232 make(entry_type, dedup(&[(raw, rationale)]).unwrap()),
5233 Err(ReviseError::NotRevisable(entry_type))
5234 );
5235 }
5236 assert!(is_revisable(council));
5237 }
5238
5239 #[test]
5241 fn a_revision_that_adds_content_blinds_the_target() {
5242 let unblinded =
5243 json!({"title": "Old", "rationale": "r", "raw_text": "r"});
5244 let revise = |data: &serde_json::Value, p: serde_json::Value| {
5245 AmendmentDraft::revision(
5246 gov(1),
5247 GovernanceLogEntryType::CouncilDecision,
5248 data_hash(&json!(null)),
5249 "b",
5250 "n",
5251 data,
5252 patch(p),
5253 )
5254 .unwrap()
5255 };
5256 let add = json!([{"op": "add", "path": "/attachments", "value": []}]);
5257 let (draft, revised) = revise(&unblinded, add.clone());
5258 assert!(revised.get(BLIND_KEY).is_some());
5259 assert_eq!(revision_of(&draft).patch.len(), 2, "in the same patch");
5260 assert_eq!(
5261 latest(&unblinded, [revision_of(&draft)]).unwrap(),
5262 revised,
5263 "so the fold reproduces it"
5264 );
5265
5266 let (_, revised) =
5267 revise(&unblinded, json!([{"op": "remove", "path": "/raw_text"}]));
5268 assert!(revised.get(BLIND_KEY).is_none(), "a removal adds nothing");
5269
5270 let (draft, revised) = revise(&seats(), add);
5271 assert_eq!(revised[BLIND_KEY], seats()[BLIND_KEY], "never rotated");
5272 assert_eq!(revision_of(&draft).patch.len(), 1);
5273 }
5274
5275 #[test]
5276 fn revision_shape_violations_fail_verification() {
5277 let (key, pk) = generate_keypair();
5278 let amend_with = |mutate: &dyn Fn(&mut Amendment)| -> String {
5279 let mut c = Chain::new();
5280 let target = c.entry(&key, seats());
5281 let (mut draft, _) = revise(&c, &target, &seats(), dedup(&seats()));
5282 mutate(&mut draft.amendment);
5283 c.amend_v1(&key, &draft.amendment);
5284 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
5285 assert!(!v.ok, "{v:#?}");
5286 v.entries[1].problem.clone().unwrap_or_default()
5287 };
5288 assert_eq!(
5289 AmendmentDraft::new(
5290 gov(1),
5291 data_hash(&json!(null)),
5292 AmendmentKind::Revision,
5293 "b",
5294 "n"
5295 ),
5296 Err(AmendmentError::MissingRevision)
5297 );
5298 let p = amend_with(&|a| a.revision = None);
5299 assert!(p.contains("requires a `revision`"), "{p}");
5300 let p = amend_with(&|a| a.kind = AmendmentKind::Correction);
5301 assert!(p.contains("only valid on kind `revision`"), "{p}");
5302 let p = amend_with(&|a| a.revision.as_mut().unwrap().patch.0.clear());
5303 assert!(p.contains("at least one op"), "{p}");
5304 }
5305
5306 #[cfg(feature = "schemars")]
5308 #[test]
5309 fn the_revision_schema_describes_a_patch() {
5310 let schema = crate::responses::inline_schema_for::<Amendment>();
5311 let revision = &schema["properties"]["revision"]["properties"];
5312 assert_eq!(revision["patch"]["type"], json!("array"), "{schema}");
5313 assert_eq!(
5314 revision["patch"]["items"]["properties"]["op"]["enum"],
5315 json!(["add", "remove", "replace", "move", "copy", "test"])
5316 );
5317 assert_eq!(revision["resulting_data_hash"]["type"], json!("string"));
5318 }
5319
5320 #[test]
5321 fn content_that_matches_nothing_fails_without_an_amendment() {
5322 let (key, pk) = generate_keypair();
5323 let mut c = Chain::new();
5324 c.entry(&key, json!({"a": 1}));
5325 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
5326 assert!(!v.check_content(&c.links[0], &json!({"a": 2})));
5327 assert!(!v.clone().settle().ok);
5328 let other = link(&key, 9, None, &json!({}), at(9));
5330 assert!(!v.check_content(&other, &json!({})));
5331 }
5332
5333 #[test]
5334 fn tampering_with_an_amendments_data_is_caught_by_the_data_hash() {
5335 let (key, pk) = generate_keypair();
5336 let mut c = Chain::new();
5337 let target = c.decision(&key);
5338 let amendment = AmendmentDraft::new(
5339 target,
5340 c.hash_at(1),
5341 AmendmentKind::Overruled,
5342 "b",
5343 "overruled by a later decision",
5344 )
5345 .unwrap();
5346 c.amend(&key, &amendment);
5347 c.links[1].data.as_mut().unwrap()["note"] =
5348 json!("reinstated, actually");
5349
5350 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
5351 assert!(!v.ok, "{v:#?}");
5352 let p = v.entries[1].problem.as_deref().unwrap();
5353 assert!(p.contains("does not hash to the attested data_hash"), "{p}");
5354 assert!(
5355 v.entries[0].amended_by.is_empty(),
5356 "an unreadable amendment has no effect"
5357 );
5358 assert!(
5359 v.entries[1].signature_valid && v.entries[1].link_valid,
5360 "the envelope is untouched — only the content is not what it \
5361 committed to"
5362 );
5363 }
5364
5365 #[test]
5366 fn an_amendment_or_rotation_must_carry_its_data() {
5367 let (key, pk) = generate_keypair();
5368 let mut c = Chain::new();
5369 let target = c.decision(&key);
5370 let amendment = AmendmentDraft::new(
5371 target,
5372 c.hash_at(1),
5373 AmendmentKind::Correction,
5374 "b",
5375 "n",
5376 )
5377 .unwrap();
5378 c.amend(&key, &amendment);
5379 let rotation = c.routine(&pk, &generate_keypair().0);
5380 c.rotate(&key, &rotation);
5381 c.links[1].data = None;
5382 c.links[2].data = None;
5383
5384 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
5385 assert!(!v.ok, "{v:#?}");
5386 for (i, entry_type) in [(1, "amendment"), (2, "key_rotation")] {
5387 let p = v.entries[i].problem.as_deref().unwrap();
5388 assert!(p.contains(&format!("a {entry_type} entry")), "{p}");
5389 assert!(p.contains("must carry its `data`"), "{p}");
5390 }
5391 }
5392
5393 #[test]
5394 fn the_id_series_must_match_the_entry_type() {
5395 let (key, pk) = generate_keypair();
5396 let mut c = Chain::new();
5397 let target = c.decision(&key);
5398 let amendment = AmendmentDraft::new(
5399 target,
5400 c.hash_at(1),
5401 AmendmentKind::Correction,
5402 "b",
5403 "n",
5404 )
5405 .unwrap();
5406 c.push(
5407 &key,
5408 gov(7),
5409 GovernanceLogEntryType::Amendment,
5410 serde_json::to_value(&amendment.amendment).unwrap(),
5411 true,
5412 );
5413 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
5414 assert!(!v.ok, "{v:#?}");
5415 let p = v.entries[1].problem.as_deref().unwrap();
5416 assert!(p.contains("must be in the AMD- series"), "{p}");
5417
5418 let mut c = Chain::new();
5419 c.push(
5420 &key,
5421 key_id(1),
5422 GovernanceLogEntryType::CouncilDecision,
5423 json!({}),
5424 false,
5425 );
5426 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
5427 let p = v.entries[0].problem.as_deref().unwrap();
5428 assert!(p.contains("reserved"), "{p}");
5429 }
5430
5431 #[test]
5432 fn redact_data_replaces_whole_values_and_refuses_the_rest() {
5433 let id = amd(3);
5434 let blind = Blind::from([9; 32]);
5435 let data = json!({"a": {"b": [1, {"c": "secret"}]}, "d/e": "slash"});
5436 let out = redact_data(
5437 &data,
5438 &["/a/b/1/c".into(), "/d~1e".into()],
5439 &id,
5440 blind,
5441 )
5442 .unwrap();
5443 assert_eq!(out["a"]["b"][1]["c"], json!(redaction_marker(&id)));
5444 assert_eq!(out["d/e"], json!(redaction_marker(&id)));
5445 assert_eq!(out["a"]["b"][0], json!(1), "untouched");
5446 assert_eq!(out[BLIND_KEY], json!(blind), "a legacy entry gains one");
5447
5448 assert_eq!(
5449 redact_data(&data, &["/a/nope".into()], &id, blind),
5450 Err(RedactError::Unresolved("/a/nope".into()))
5451 );
5452 assert_eq!(
5453 redact_data(&data, &["".into()], &id, blind),
5454 Err(RedactError::WholeEntry)
5455 );
5456 assert_eq!(
5457 redact_data(&data, &[], &id, blind),
5458 Err(RedactError::NoFields)
5459 );
5460 assert_eq!(
5461 redact_data(&data, &["/_blind".into()], &id, blind),
5462 Err(RedactError::BlindPointer("/_blind".into()))
5463 );
5464 }
5465
5466 #[test]
5467 fn blind_data_is_for_objects_and_is_the_writers_to_supply() {
5468 let blind = Blind::from([1; 32]);
5469 let out = blind_data(&json!({"finding": "upheld"}), blind).unwrap();
5470 assert_eq!(out, json!({"finding": "upheld", "_blind": blind}));
5471 assert_eq!(
5472 blind_data(&json!([1]), blind),
5473 Err(BlindError::NotAnObject)
5474 );
5475 assert_eq!(blind_data(&out, blind), Err(BlindError::AlreadyBlinded));
5476 assert_ne!(Blind::random(), Blind::random());
5477
5478 use GovernanceLogEntryType::*;
5479 assert!(!is_redactable(Amendment) && !is_redactable(KeyRotation));
5480 assert!(
5481 is_redactable(CouncilDecision) && is_redactable(EmergencyAction)
5482 );
5483 }
5484
5485 #[test]
5489 fn a_removed_value_cannot_be_confirmed_by_guessing_it() {
5490 fn confirms(
5492 public: &serde_json::Value,
5493 pointer: &str,
5494 guess: &str,
5495 hash: Sha256Hex,
5496 ) -> bool {
5497 let mut attempt = public.clone();
5498 *attempt.pointer_mut(pointer).unwrap() = json!(guess);
5499 data_hash(&attempt) == hash
5500 }
5501 let legacy = json!({"finding": "upheld", "handle": "someone", "city": "Utrecht"});
5502
5503 let written = blind_data(&legacy, Blind::random()).unwrap();
5505 let first = redact_data(
5506 &written,
5507 &["/handle".into()],
5508 &amd(1),
5509 Blind::random(),
5510 )
5511 .unwrap();
5512 assert!(!confirms(&first, "/handle", "someone", data_hash(&written)));
5513
5514 let second =
5517 redact_data(&first, &["/city".into()], &amd(2), Blind::random())
5518 .unwrap();
5519 assert!(!confirms(&second, "/city", "Utrecht", data_hash(&first)));
5520
5521 let mut stripped =
5525 redact_data(&legacy, &["/handle".into()], &amd(3), Blind::random())
5526 .unwrap();
5527 let legacy_first = stripped.clone();
5528 stripped.as_object_mut().unwrap().remove(BLIND_KEY);
5529 assert!(confirms(
5530 &stripped,
5531 "/handle",
5532 "someone",
5533 data_hash(&legacy)
5534 ));
5535 let legacy_second = redact_data(
5537 &legacy_first,
5538 &["/city".into()],
5539 &amd(4),
5540 Blind::random(),
5541 )
5542 .unwrap();
5543 assert!(!confirms(
5544 &legacy_second,
5545 "/city",
5546 "Utrecht",
5547 data_hash(&legacy_first)
5548 ));
5549 }
5550
5551 #[test]
5554 fn a_routine_rotation_moves_the_chain_to_the_new_key() {
5555 let (old, old_pk) = generate_keypair();
5556 let (new, new_pk) = generate_keypair();
5557 let mut c = Chain::new();
5558 c.decision(&old);
5559 let rotation = c.routine(&old_pk, &new);
5560 let rotation_id = c.rotate(&old, &rotation);
5561 c.decision(&new);
5562
5563 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5566 assert!(v.ok, "{v:#?}");
5567 assert_eq!(v.public_key, (&new_pk).into());
5568 assert_eq!(
5569 v.entries[1].signed_by,
5570 Some((&old_pk).into()),
5571 "the rotation itself is signed by the old key"
5572 );
5573 assert_eq!(v.entries[2].signed_by, Some((&new_pk).into()));
5574 assert!(v.unanchored_keys.is_empty());
5575 assert!(v.repudiated.is_empty());
5576 assert_eq!(v.keys.len(), 2);
5577 assert_eq!(v.keys[0].public_key, (&old_pk).into());
5578 assert_eq!(v.keys[0].from_seq, 1);
5579 assert_eq!(v.keys[0].through_seq, Some(2));
5580 assert_eq!(v.keys[0].status, KeyStatus::Retired);
5581 assert_eq!(v.keys[0].introduced_by, None);
5582 assert_eq!(v.keys[0].retired_by.as_ref(), Some(&rotation_id));
5583 assert!(v.keys[0].certified, "retroactively, by the rotation");
5584 assert_eq!(v.keys[1].from_seq, 3);
5585 assert_eq!(v.keys[1].through_seq, None);
5586 assert_eq!(v.keys[1].status, KeyStatus::Active);
5587 assert_eq!(v.keys[1].introduced_by.as_ref(), Some(&rotation_id));
5588 assert!(v.keys[1].certified);
5589 }
5590
5591 #[test]
5592 fn the_old_key_cannot_sign_after_a_routine_rotation() {
5593 let (old, old_pk) = generate_keypair();
5594 let (new, _) = generate_keypair();
5595 let mut c = Chain::new();
5596 c.decision(&old);
5597 let rotation = c.routine(&old_pk, &new);
5598 c.rotate(&old, &rotation);
5599 c.decision(&old);
5600
5601 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5602 assert!(!v.ok, "{v:#?}");
5603 assert!(!v.entries[2].signature_valid);
5604 assert!(
5605 v.entries[2].link_valid,
5606 "the linkage is fine; the key is not"
5607 );
5608 }
5609
5610 #[test]
5613 fn the_online_key_cannot_certify_its_own_successor() {
5614 let (old, old_pk) = generate_keypair();
5615 let (thief, _) = generate_keypair();
5616 let mut c = Chain::new();
5617 c.decision(&old);
5618 let mut rotation = c.routine(&old_pk, &thief);
5619 let statement = rotation.certificate.statement.clone();
5621 rotation.certificate = certify(&[&old], statement);
5622 c.rotate(&old, &rotation);
5623 c.decision(&thief);
5624
5625 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5626 assert!(!v.ok, "{v:#?}");
5627 let p = v.entries[1].problem.as_deref().unwrap();
5628 assert!(p.contains("0 valid root signature"), "{p}");
5629 assert_eq!(v.public_key, (&old_pk).into(), "the chain does not move");
5630 assert!(!v.entries[2].signature_valid);
5631 assert_eq!(v.keys.len(), 1);
5632 }
5633
5634 #[test]
5635 fn a_certificate_counts_distinct_known_roots_only() {
5636 let (old, old_pk) = generate_keypair();
5637 let (new, _) = generate_keypair();
5638 let (stranger, _) = generate_keypair();
5639 let two_of_two = RootSet::new(
5640 [
5641 (&root(1).verifying_key()).into(),
5642 (&root(2).verifying_key()).into(),
5643 ],
5644 2,
5645 );
5646 let verdict = |signers: &[&SigningKey], roots: &RootSet| {
5647 let mut c = Chain::new();
5648 c.decision(&old);
5649 let mut rotation = c.routine(&old_pk, &new);
5650 let statement = rotation.certificate.statement.clone();
5651 rotation.certificate = certify(signers, statement);
5652 let genesis = KeyCertStatement::genesis((&old_pk).into());
5654 rotation.outgoing_certificate =
5655 Some(certify(&[&root(1), &root(2)], genesis));
5656 c.rotate(&old, &rotation);
5657 verify_chain(&c.links, &old_pk, &anchored(&old_pk), roots)
5658 };
5659
5660 assert!(verdict(&[&root(1), &root(2)], &two_of_two).ok);
5661 assert!(verdict(&[&root(2)], &roots()).ok, "either root, 1-of-2");
5662
5663 for (signers, why) in [
5664 (vec![&root(1)], "below the threshold"),
5665 (vec![&root(1), &root(1)], "one root twice is one root"),
5666 (vec![&root(1), &stranger], "an unknown root is nobody"),
5667 (vec![], "unsigned"),
5668 ] {
5669 let v = verdict(&signers, &two_of_two);
5670 assert!(!v.ok, "{why}: {v:#?}");
5671 let p = v.entries[1].problem.as_deref().unwrap();
5672 assert!(p.contains("where 2 are needed"), "{why}: {p}");
5673 }
5674
5675 assert!(verdict(&[&stranger, &root(1)], &roots()).ok);
5677 }
5678
5679 #[test]
5680 fn a_certificate_is_good_for_one_statement_at_one_position() {
5681 let (old, old_pk) = generate_keypair();
5682 let (new, new_pk) = generate_keypair();
5683 let (other, other_pk) = generate_keypair();
5684 let anchor = anchored(&old_pk);
5685
5686 let mut c = Chain::new();
5690 c.decision(&old);
5691 let early = c.routine(&old_pk, &new);
5692 c.decision(&old);
5693 let mut rotation = c.routine(&old_pk, &new);
5694 rotation.certificate = early.certificate;
5695 c.rotate(&old, &rotation);
5696 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5697 assert!(!v.ok, "{v:#?}");
5698 let p = v.entries[2].problem.as_deref().unwrap();
5699 assert!(
5700 p.contains("different key, purpose or chain position"),
5701 "{p}"
5702 );
5703 assert_eq!(v.public_key, (&old_pk).into());
5704
5705 let mut c = Chain::new();
5707 c.decision(&old);
5708 let for_new = c.routine(&old_pk, &new);
5709 let mut rotation = c.routine(&old_pk, &other);
5710 rotation.certificate = for_new.certificate;
5711 c.rotate(&old, &rotation);
5712 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5713 let p = v.entries[1].problem.as_deref().unwrap();
5714 assert!(
5715 p.contains("different key, purpose or chain position"),
5716 "{p}"
5717 );
5718
5719 let mut c = Chain::new();
5721 c.decision(&old);
5722 let mut rotation = c.routine(&old_pk, &other);
5723 rotation.certificate = for_new_at(&c, &new_pk);
5724 rotation.certificate.statement.key = (&other_pk).into();
5725 c.rotate(&old, &rotation);
5726 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5727 let p = v.entries[1].problem.as_deref().unwrap();
5728 assert!(p.contains("0 valid root signature"), "{p}");
5729
5730 let mut c = Chain::new();
5732 c.decision(&old);
5733 c.decision(&old);
5734 let mut rotation = c.compromise(&old_pk, &new, 1);
5735 rotation.certificate = for_new_at(&c, &new_pk);
5736 c.rotate(&new, &rotation);
5737 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
5738 assert!(!v.ok);
5739 assert!(v.repudiated.is_empty(), "{v:#?}");
5740 assert_eq!(v.public_key, (&old_pk).into());
5741 }
5742
5743 #[test]
5746 fn a_root_signature_without_the_domain_prefix_certifies_nothing() {
5747 use ed25519_dalek::Signer;
5748 let (old, old_pk) = generate_keypair();
5749 let (new, _) = generate_keypair();
5750 let mut c = Chain::new();
5751 c.decision(&old);
5752 let mut rotation = c.routine(&old_pk, &new);
5753 let statement = rotation.certificate.statement.clone();
5754 let bare = canonical_json(&serde_json::to_value(&statement).unwrap());
5755 assert_eq!(
5756 statement.signed_bytes(),
5757 [ROOT_DOMAIN, bare.as_slice()].concat()
5758 );
5759 rotation.certificate =
5760 KeyCertificate::unsigned(statement).with(RootSignature {
5761 root_key: (&root(1).verifying_key()).into(),
5762 signature: root(1).sign(&bare).into(),
5763 });
5764 c.rotate(&old, &rotation);
5765 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5766 assert!(!v.ok, "{v:#?}");
5767 assert_eq!(v.public_key, (&old_pk).into());
5768 }
5769
5770 #[test]
5771 fn the_first_rotation_carries_the_genesis_certificate_and_only_it_does() {
5772 let (k1, k1_pk) = generate_keypair();
5773 let (k2, k2_pk) = generate_keypair();
5774 let (k3, _) = generate_keypair();
5775 let genesis =
5776 || certify(&[&root(1)], KeyCertStatement::genesis((&k1_pk).into()));
5777
5778 let mut c = Chain::new();
5780 c.decision(&k1);
5781 let mut rotation = c.routine(&k1_pk, &k2);
5782 rotation.outgoing_certificate = None;
5783 c.rotate(&k1, &rotation);
5784 let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5785 assert!(!v.ok);
5786 let p = v.entries[1].problem.as_deref().unwrap();
5787 assert!(p.contains("genesis key's outgoing_certificate"), "{p}");
5788 assert_eq!(v.public_key, (&k1_pk).into());
5789
5790 let mut c = Chain::new();
5792 c.decision(&k1);
5793 let rotation = c.routine(&k1_pk, &k2).with_outgoing(certify(
5794 &[&root(1)],
5795 KeyCertStatement::genesis((&k2_pk).into()),
5796 ));
5797 c.rotate(&k1, &rotation);
5798 let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5799 let p = v.entries[1].problem.as_deref().unwrap();
5800 assert!(p.starts_with("outgoing_certificate:"), "{p}");
5801
5802 let mut c = Chain::new();
5805 c.decision(&k1);
5806 let rotation = c.routine(&k1_pk, &k2);
5807 assert_eq!(rotation.outgoing_certificate, Some(genesis()));
5808 c.rotate(&k1, &rotation);
5809 let v = verify_chain(&c.links, &k1_pk, &KeyAnchor::default(), &roots());
5810 assert!(v.ok, "{v:#?}");
5811 assert!(v.unanchored_keys.is_empty(), "{v:#?}");
5812
5813 let again = c.routine(&k2_pk, &k3).with_outgoing(genesis());
5815 c.rotate(&k2, &again);
5816 let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5817 assert!(!v.ok);
5818 let p = v.entries[2].problem.as_deref().unwrap();
5819 assert!(p.contains("nowhere else"), "{p}");
5820 }
5821
5822 #[test]
5823 fn a_forged_proof_of_possession_is_rejected() {
5824 let (old, old_pk) = generate_keypair();
5825 let (_, new_pk) = generate_keypair();
5826 let (thief, _) = generate_keypair();
5827 let mut c = Chain::new();
5828 c.decision(&old);
5829 let mut rotation = c.routine(&old_pk, &thief);
5832 rotation.new_key = (&new_pk).into();
5833 rotation.certificate = for_new_at(&c, &new_pk);
5834 let statement = rotation.statement(c.prev_hash());
5835 rotation.proof = crypto::sign(
5836 &old,
5837 statement.hash().as_bytes(),
5838 rotation.proof_signed_at,
5839 )
5840 .into();
5841 c.rotate(&old, &rotation);
5842
5843 assert_eq!(
5844 rotation.verify_proof(c.links[1].attestation.prev_hash),
5845 Err(RotationError::BadProof)
5846 );
5847 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5848 assert!(!v.ok, "{v:#?}");
5849 assert!(
5850 v.entries[1]
5851 .problem
5852 .as_deref()
5853 .unwrap()
5854 .contains("proof of possession")
5855 );
5856 assert_eq!(v.public_key, (&old_pk).into(), "the chain does not move");
5857 }
5858
5859 #[test]
5860 fn a_compromise_repudiates_the_window_and_a_reattestation_restores_one() {
5861 let (old, old_pk) = generate_keypair();
5862 let (new, new_pk) = generate_keypair();
5863 let mut c = Chain::new();
5864 c.decision(&old); c.decision(&old); let reattested = c.decision(&old); let rotation = c.compromise(&old_pk, &new, 1);
5868 let rotation_id = c.rotate(&new, &rotation); let vouch = AmendmentDraft::new(
5870 reattested,
5871 c.hash_at(3),
5872 AmendmentKind::Reattested,
5873 "Art. VII",
5874 "independently verified; the Steward vouches for it",
5875 )
5876 .unwrap();
5877 let vouch_id = c.amend(&new, &vouch); let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5880 assert!(
5881 v.ok,
5882 "repudiation is a declared state, not a defect: {v:#?}"
5883 );
5884 assert_eq!(v.repudiated, vec![gov(2)]);
5885 assert!(v.entries[1].repudiated);
5886 assert!(!v.entries[2].repudiated);
5887 assert_eq!(v.entries[2].reattested_by, vec![vouch_id.clone()]);
5888 assert_eq!(v.entries[2].amended_by, vec![vouch_id]);
5889 assert_eq!(v.entries[3].signed_by, Some((&new_pk).into()));
5890 assert_eq!(v.public_key, (&new_pk).into());
5891 assert_eq!(v.keys.len(), 2);
5892 assert_eq!(v.keys[0].status, KeyStatus::Compromised);
5893 assert_eq!(
5894 v.keys[0].through_seq,
5895 Some(1),
5896 "trusted through the last trusted entry, not through the \
5897 declaration"
5898 );
5899 assert_eq!(v.keys[0].retired_by.as_ref(), Some(&rotation_id));
5900 assert_eq!(v.keys[1].from_seq, 4, "the declaration is its own first");
5901 }
5902
5903 #[test]
5906 fn last_trusted_is_the_roots_to_say() {
5907 let (old, old_pk) = generate_keypair();
5908 let (new, _) = generate_keypair();
5909 let mut c = Chain::new();
5910 c.decision(&old); c.decision(&old); let mut rotation = c.compromise(&old_pk, &new, 1);
5913 rotation.certificate.statement.last_trusted = Some(c.head(2));
5914 c.rotate(&new, &rotation);
5915 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
5916 assert!(!v.ok, "{v:#?}");
5917 assert!(v.repudiated.is_empty());
5918 assert_eq!(v.public_key, (&old_pk).into());
5919 }
5920
5921 #[test]
5922 fn a_stolen_key_cannot_be_rotated_back_in() {
5923 let (k1, k1_pk) = generate_keypair();
5927 let (k2, k2_pk) = generate_keypair();
5928 let mut c = Chain::new();
5929 c.decision(&k1); let real = c.compromise(&k1_pk, &k2, 1);
5931 c.rotate(&k2, &real); c.decision(&k2); let honest =
5934 verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5935 assert!(honest.ok, "{honest:#?}");
5936
5937 let hijack = c.compromise(&k2_pk, &k1, 3);
5938 c.rotate(&k1, &hijack); c.decision(&k1); let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5942 assert!(!v.ok);
5943 assert_eq!(v.public_key, (&k2_pk).into(), "the chain stays with K2");
5944 let p = v.entries[3].problem.as_deref().unwrap();
5945 assert!(p.contains("never brought back"), "{p}");
5946 assert!(!v.entries[3].signature_valid, "{:#?}", v.entries[3]);
5947 assert!(!v.entries[4].signature_valid, "K1 signs nothing again");
5948 assert_eq!(v.keys.len(), 2);
5949 assert_eq!(v.keys[1].status, KeyStatus::Active);
5950 }
5951
5952 #[test]
5953 fn a_routine_rotation_cannot_reuse_a_key_either() {
5954 let (k1, k1_pk) = generate_keypair();
5955 let (k2, k2_pk) = generate_keypair();
5956 let mut c = Chain::new();
5957 c.decision(&k1);
5958 let out = c.routine(&k1_pk, &k2);
5959 c.rotate(&k1, &out);
5960 let back = c.routine(&k2_pk, &k1);
5961 c.rotate(&k2, &back);
5962 let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
5963 assert!(!v.ok);
5964 assert!(
5965 v.entries[2]
5966 .problem
5967 .as_deref()
5968 .unwrap()
5969 .contains("never brought back"),
5970 "{:#?}",
5971 v.entries[2]
5972 );
5973 assert_eq!(v.public_key, (&k2_pk).into());
5974 }
5975
5976 #[test]
5977 fn a_forged_entry_has_no_effects() {
5978 let (steward, steward_pk) = generate_keypair();
5979 let (forger, _) = generate_keypair();
5980 let anchor = anchored(&steward_pk);
5981 let mut c = Chain::new();
5982 let target = c.decision(&steward); let fake = AmendmentDraft::new(
5984 target,
5985 c.hash_at(1),
5986 AmendmentKind::Overruled,
5987 "none",
5988 "overruled, says nobody with the key",
5989 )
5990 .unwrap();
5991 c.amend(&forger, &fake); let grab = c.routine(&steward_pk, &forger);
5995 c.rotate(&forger, &grab); let v = verify_chain(&c.links, &steward_pk, &anchor, &roots());
5998 assert!(!v.ok);
5999 assert!(v.entries[0].amended_by.is_empty(), "{:#?}", v.entries[0]);
6000 assert_eq!(v.public_key, (&steward_pk).into());
6001 assert_eq!(v.keys.len(), 1);
6002 assert!(v.unanchored_keys.is_empty());
6003 }
6004
6005 #[test]
6006 fn a_repeated_id_is_a_problem() {
6007 let (key, pk) = generate_keypair();
6008 let mut c = Chain::new();
6009 c.decision(&key);
6010 c.gov = 0;
6011 c.decision(&key); let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
6013 assert!(!v.ok);
6014 assert!(v.entries.iter().all(|e| {
6015 e.problem
6016 .as_deref()
6017 .is_some_and(|p| p.contains("more than once"))
6018 }));
6019 }
6020
6021 #[test]
6022 fn a_second_compromise_cannot_anchor_inside_the_first_window() {
6023 let (k1, k1_pk) = generate_keypair();
6024 let (k2, _) = generate_keypair();
6025 let (k3, _) = generate_keypair();
6026 let mut c = Chain::new();
6027 c.decision(&k1); c.decision(&k1); let first = c.compromise(&k1_pk, &k2, 1);
6030 c.rotate(&k2, &first); let second = c.compromise(&k1_pk, &k3, 2);
6032 c.rotate(&k3, &second); let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
6035 assert!(!v.ok);
6036 let p = v.entries[3].problem.as_deref().unwrap();
6037 assert!(p.contains("repudiated"), "{p}");
6038 assert_eq!(v.keys.len(), 2, "K1 and K2; K3 never took the chain");
6039 assert_eq!(v.keys[0].through_seq, Some(1));
6040 }
6041
6042 #[test]
6043 fn an_uncertified_compromise_fails_closed() {
6044 let (old, old_pk) = generate_keypair();
6045 let (new, new_pk) = generate_keypair();
6046 let mut c = Chain::new();
6047 c.decision(&old);
6048 c.decision(&old);
6049 let mut rotation = c.compromise(&old_pk, &new, 1);
6050 let statement = rotation.certificate.statement.clone();
6051 rotation.certificate = certify(&[&new], statement);
6052 c.rotate(&new, &rotation);
6053
6054 let anchor = anchored(&old_pk).with((&new_pk).into());
6056 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
6057 assert!(!v.ok, "{v:#?}");
6058 let p = v.entries[2].problem.as_deref().unwrap();
6059 assert!(p.contains("0 valid root signature"), "{p}");
6060 assert!(!v.entries[2].signature_valid, "checked under the old key");
6061 assert!(v.repudiated.is_empty(), "and nothing is repudiated");
6062 assert_eq!(v.public_key, (&old_pk).into());
6063 }
6064
6065 #[test]
6070 fn a_rotation_inside_the_window_is_void_with_it() {
6071 let (steward, steward_pk) = generate_keypair();
6072 let (successor, _) = generate_keypair();
6073 let (recovery, recovery_pk) = generate_keypair();
6074
6075 let mut c = Chain::new();
6076 c.decision(&steward); c.decision(&steward); let routine = c.routine(&steward_pk, &successor);
6079 c.rotate(&steward, &routine); c.decision(&successor); let declaration = c.compromise(&steward_pk, &recovery, 1);
6083 c.rotate(&recovery, &declaration); let v = verify_chain(
6086 &c.links,
6087 &steward_pk,
6088 &anchored(&steward_pk),
6089 &roots(),
6090 );
6091 assert!(v.ok, "{v:#?}");
6092 assert_eq!(v.repudiated, vec![gov(2), key_id(1), gov(3)]);
6093 assert_eq!(v.public_key, (&recovery_pk).into());
6094 assert_eq!(v.keys.len(), 2, "the successor is not part of history");
6095 assert_eq!(v.keys[0].public_key, (&steward_pk).into());
6096 assert_eq!(v.keys[0].status, KeyStatus::Compromised);
6097 assert!(
6098 v.keys[0].certified,
6099 "the genesis certificate rode in on the voided rotation and \
6100 is the root's word all the same"
6101 );
6102 assert_eq!(v.keys[1].public_key, (&recovery_pk).into());
6103 }
6104
6105 #[test]
6106 fn a_compromise_must_name_a_real_head_and_the_key_that_held_it() {
6107 let (old, old_pk) = generate_keypair();
6108 let (new, new_pk) = generate_keypair();
6109 let anchor = anchored(&old_pk);
6110
6111 let mut c = Chain::new();
6114 c.decision(&old);
6115 let mut trusted = c.head(1);
6116 trusted.entry_hash = data_hash(&json!("nope"));
6117 let statement = KeyCertStatement::compromise(
6118 (&new_pk).into(),
6119 2,
6120 c.prev_hash(),
6121 trusted,
6122 );
6123 let mut rotation = c.compromise(&old_pk, &new, 1);
6124 rotation.certificate = certify(&[&root(1)], statement);
6125 c.rotate(&new, &rotation);
6126 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
6127 let p = v.entries[1].problem.as_deref().unwrap();
6128 assert!(p.contains("last_trusted"), "{p}");
6129
6130 let (_, other_pk) = generate_keypair();
6132 let mut c = Chain::new();
6133 c.decision(&old);
6134 let rotation = c.compromise(&other_pk, &new, 1);
6135 c.rotate(&new, &rotation);
6136 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
6137 let p = v.entries[1].problem.as_deref().unwrap();
6138 assert!(p.contains("old_key is not the key"), "{p}");
6139
6140 let mut c = Chain::new();
6142 c.decision(&old);
6143 let mut rotation = c.compromise(&old_pk, &new, 1);
6144 rotation.certificate.statement.last_trusted = None;
6145 c.rotate(&new, &rotation);
6146 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
6147 let p = v.entries[1].problem.as_deref().unwrap();
6148 assert!(p.contains("must name last_trusted"), "{p}");
6149 }
6150
6151 #[test]
6152 fn a_rotation_carries_no_free_text() {
6153 let (old, old_pk) = generate_keypair();
6154 let (new, _) = generate_keypair();
6155 let mut c = Chain::new();
6156 c.decision(&old);
6157 let rotation = c.routine(&old_pk, &new);
6158 let mut value = serde_json::to_value(&rotation).unwrap();
6159 assert!(serde_json::from_value::<KeyRotation>(value.clone()).is_ok());
6160 value["note"] = json!("at the request of …");
6161 assert!(serde_json::from_value::<KeyRotation>(value).is_err());
6162
6163 let mut head = serde_json::to_value(c.head(1)).unwrap();
6164 head["comment"] = json!("the last one I remember signing");
6165 assert!(serde_json::from_value::<TrustedHead>(head).is_err());
6166
6167 let mut statement =
6168 serde_json::to_value(&rotation.certificate.statement).unwrap();
6169 statement["comment"] = json!("signed in the kitchen");
6170 assert!(serde_json::from_value::<KeyCertStatement>(statement).is_err());
6171 }
6172
6173 #[test]
6174 fn the_root_statement_bytes_are_pinned() {
6175 let key: PublicKeyHex = PUBLISHED_KEYS[0].parse().unwrap();
6176 assert_eq!(
6177 String::from_utf8(KeyCertStatement::genesis(key).signed_bytes())
6178 .unwrap(),
6179 "agora-governance-root-v1\n\
6180 {\"agora_governance_key_cert\":1,\"from_seq\":1,\
6181 \"key\":\"ebb3091dd328f1463362c171121921b2fe14628e3fc4c145deaccefb85c0e78a\",\
6182 \"last_trusted\":null,\"prev_hash\":null,\"purpose\":\"genesis\"}"
6183 );
6184
6185 let statement = KeyCertStatement::compromise(
6189 Sha256Hex::from([0xab; 32]).to_string().parse().unwrap(),
6190 15,
6191 Some([0xcd; 32].into()),
6192 TrustedHead {
6193 id: gov(10),
6194 chain_seq: 11,
6195 entry_hash: [0xef; 32].into(),
6196 },
6197 );
6198 assert_eq!(
6199 Sha256Hex::from(<[u8; 32]>::from(Sha256::digest(
6200 statement.signed_bytes()
6201 )))
6202 .to_string(),
6203 "633685771e08be126fd12ca4eb98c77120e5868236ff541ecba85ce6a21e6b68"
6204 );
6205 }
6206
6207 #[test]
6208 fn root_keys_are_curve_points_and_make_a_root_set() {
6209 let roots = RootSet::published();
6210 assert_eq!(roots.keys().count(), ROOT_KEYS.len());
6211 assert_eq!(roots.threshold(), ROOT_THRESHOLD);
6212 assert!(ROOT_THRESHOLD >= 1 && ROOT_THRESHOLD <= ROOT_KEYS.len());
6213 for root in ROOT_KEYS {
6214 let key: PublicKeyHex = root.parse().unwrap();
6215 assert!(roots.contains(&key));
6216 assert!(
6217 key.to_verifying_key().is_ok(),
6218 "{root} is not a valid Ed25519 public key"
6219 );
6220 assert!(
6221 !PUBLISHED_KEYS.contains(root),
6222 "a root key never signs entries"
6223 );
6224 }
6225 assert_eq!(RootSet::new([], 0).threshold(), 1);
6226 }
6227
6228 #[test]
6229 fn a_genesis_key_outside_the_anchor_is_reported_not_rejected() {
6230 let (key, pk) = generate_keypair();
6231 let c = chain(&key, 2);
6232 let v = verify_chain(&c, &pk, &KeyAnchor::default(), &roots());
6233 assert!(v.ok, "{v:#?}");
6234 assert_eq!(v.unanchored_keys, vec![PublicKeyHex::from(&pk)]);
6235 assert_eq!(v.keys.len(), 1);
6236 assert_eq!(v.keys[0].status, KeyStatus::Active);
6237 assert_eq!(v.keys[0].from_seq, 1);
6238 assert_eq!(v.keys[0].through_seq, None);
6239 }
6240
6241 #[test]
6242 fn published_keys_are_curve_points_and_make_an_anchor() {
6243 let anchor = KeyAnchor::published();
6244 assert!(!anchor.is_empty());
6245 assert_eq!(anchor.keys().count(), PUBLISHED_KEYS.len());
6246 for published in PUBLISHED_KEYS {
6247 let key: PublicKeyHex = published.parse().unwrap();
6248 assert!(anchor.contains(&key));
6249 assert!(
6250 key.to_verifying_key().is_ok(),
6251 "{published} is not a valid Ed25519 public key"
6252 );
6253 }
6254 let (_, other) = generate_keypair();
6255 assert!(!anchor.contains(&(&other).into()));
6256 assert!(
6257 anchor
6258 .clone()
6259 .with((&other).into())
6260 .contains(&(&other).into())
6261 );
6262 }
6263
6264 #[test]
6267 fn amendments_and_rotations_round_trip_as_entry_data() {
6268 let (key, pk) = generate_keypair();
6269 let amendment = AmendmentDraft::new(
6270 gov(1),
6271 data_hash(&json!("x")),
6272 AmendmentKind::Superseded,
6273 "Art. VI § 2",
6274 "superseded by GOV-2026-0009",
6275 )
6276 .unwrap()
6277 .with_authority(gov(9))
6278 .with_rationale("the later decision covers the same subject");
6279 let AmendmentDraft { amendment, texts } = amendment;
6280 let value = serde_json::to_value(&amendment).unwrap();
6281 assert_eq!(value["kind"], "superseded");
6282 assert_eq!(value["agora_governance_amendment"], 2);
6283 assert!(value.get("redaction").is_none(), "{value}");
6284 let text = value.to_string();
6285 assert!(!text.contains("Art. VI"), "no text in signed data: {text}");
6286 assert!(!text.contains("salt"), "and no salt: {text}");
6287 assert_eq!(
6288 value["note"]["commitment"],
6289 texts
6290 .note
6291 .as_ref()
6292 .unwrap()
6293 .commitment()
6294 .commitment
6295 .to_string()
6296 );
6297 assert_eq!(
6298 serde_json::from_value::<Amendment>(value).unwrap(),
6299 amendment
6300 );
6301 let beside = serde_json::to_value(&texts).unwrap();
6302 assert_eq!(beside["basis"]["text"], "Art. VI § 2");
6303 assert_eq!(
6304 serde_json::from_value::<AmendmentTexts>(beside).unwrap(),
6305 texts
6306 );
6307
6308 let legacy = json!({
6310 "agora_governance_amendment": 1,
6311 "target": "GOV-2026-0001",
6312 "target_entry_hash": data_hash(&json!("x")),
6313 "kind": "non_precedential",
6314 "authority": "GOV-2026-0005",
6315 "basis": "§1",
6316 "note": "diagnostic finding",
6317 });
6318 let legacy: Amendment = serde_json::from_value(legacy).unwrap();
6319 assert_eq!(legacy.validate(), Ok(()));
6320 assert_eq!(legacy.basis, AmendmentText::Plain("§1".into()));
6321
6322 let mut c = Chain::new();
6323 c.decision(&key);
6324 let rotation = c.compromise(&pk, &generate_keypair().0, 1);
6325 let value = serde_json::to_value(&rotation).unwrap();
6326 assert_eq!(value["agora_governance_key_rotation"], 2);
6327 assert_eq!(value["reason"], "compromise");
6328 let statement = &value["certificate"]["statement"];
6329 assert_eq!(statement["purpose"], "compromise");
6330 assert_eq!(statement["last_trusted"]["chain_seq"], 1);
6331 assert_eq!(
6332 value["outgoing_certificate"]["statement"]["purpose"],
6333 "genesis"
6334 );
6335 assert!(value.get("note").is_none(), "{value}");
6336 assert_eq!(
6337 serde_json::from_value::<KeyRotation>(value).unwrap(),
6338 rotation
6339 );
6340
6341 let notice =
6342 AmendmentNotice::new(amd(1), at(0), &amendment, Some(&texts));
6343 let value = serde_json::to_value(¬ice).unwrap();
6344 assert_eq!(value["id"], "AMD-2026-0001");
6345 assert_eq!(value["note"], "superseded by GOV-2026-0009");
6346 assert_eq!(
6347 serde_json::from_value::<AmendmentNotice>(value).unwrap(),
6348 notice
6349 );
6350
6351 let erased = AmendmentTexts {
6353 rationale: None,
6354 ..texts.clone()
6355 };
6356 let notice =
6357 AmendmentNotice::new(amd(1), at(0), &amendment, Some(&erased));
6358 assert_eq!(notice.note, "superseded by GOV-2026-0009");
6359 assert_eq!(notice.rationale.as_deref(), Some(WITHHELD_TEXT));
6360 let notice = AmendmentNotice::new(amd(1), at(0), &amendment, None);
6361 assert_eq!(notice.basis, WITHHELD_TEXT);
6362 let notice = AmendmentNotice::new(amd(1), at(0), &legacy, None);
6363 assert_eq!(notice.note, "diagnostic finding");
6364 }
6365
6366 #[test]
6370 fn an_amendment_verifies_with_or_without_its_optional_fields() {
6371 let (key, pk) = generate_keypair();
6372 let mut c = Chain::new();
6373 let target = c.decision(&key);
6374 let bare = AmendmentDraft::new(
6375 target.clone(),
6376 c.hash_at(1),
6377 AmendmentKind::Correction,
6378 "clerical",
6379 "typo in the citation",
6380 )
6381 .unwrap();
6382 assert!(
6383 serde_json::to_value(&bare.amendment)
6384 .unwrap()
6385 .get("rationale")
6386 .is_none()
6387 );
6388 c.amend(&key, &bare);
6389 let full = bare.clone().with_rationale("at length: …");
6390 c.amend(&key, &full);
6391 let mut future = serde_json::to_value(&full.amendment).unwrap();
6393 future["superseded_by_something_new"] = json!(["later"]);
6394 c.push(
6395 &key,
6396 amd(3),
6397 GovernanceLogEntryType::Amendment,
6398 future,
6399 true,
6400 );
6401
6402 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
6403 assert!(v.ok, "{v:#?}");
6404 assert_eq!(
6405 v.entries[0].amended_by,
6406 vec![amd(1), amd(2), amd(3)],
6407 "all three name the target"
6408 );
6409 }
6410
6411 #[test]
6414 fn pre_0_26_wire_still_deserializes() {
6415 let link: GovernanceChainLink = serde_json::from_value(json!({
6416 "id": "GOV-2026-0001",
6417 "entry_type": "council_decision",
6418 "created_at": "2023-11-14T22:13:20.123456Z",
6419 "attestation": {
6420 "envelope_version": 1,
6421 "chain_seq": 1,
6422 "prev_hash": null,
6423 "data_hash": "00".repeat(32),
6424 "entry_hash": "11".repeat(32),
6425 "signature": "22".repeat(64),
6426 "signed_at": "2023-11-14T22:13:21Z",
6427 "retroactive": false,
6428 },
6429 }))
6430 .unwrap();
6431 assert!(link.data.is_none());
6432 assert!(
6433 serde_json::to_value(&link).unwrap().get("data").is_none(),
6434 "and a link without data does not grow a null field"
6435 );
6436
6437 let verdict: EntryVerdict = serde_json::from_value(json!({
6438 "id": "GOV-2026-0001",
6439 "chain_seq": 1,
6440 "signature_valid": true,
6441 "link_valid": true,
6442 "content_matches": null,
6443 "retroactive": false,
6444 "out_of_order": false,
6445 "amended_by": [],
6446 }))
6447 .unwrap();
6448 assert!(!verdict.repudiated && !verdict.redacted);
6449 assert!(verdict.signed_by.is_none());
6450 assert!(verdict.reattested_by.is_empty());
6451
6452 let verification: GovernanceVerification =
6453 serde_json::from_value(json!({
6454 "public_key": "33".repeat(32),
6455 "ok": true,
6456 "head": "GOV-2026-0001",
6457 "entries": [],
6458 }))
6459 .unwrap();
6460 assert!(verification.keys.is_empty());
6461 assert!(verification.unanchored_keys.is_empty());
6462 assert!(verification.repudiated.is_empty());
6463 }
6464
6465 #[test]
6466 fn the_report_round_trips() {
6467 let (key, pk) = generate_keypair();
6468 let mut c = Chain::new();
6469 let target = c.decision(&key);
6470 let amendment = AmendmentDraft::new(
6471 target,
6472 c.hash_at(1),
6473 AmendmentKind::Overruled,
6474 "b",
6475 "n",
6476 )
6477 .unwrap();
6478 c.amend(&key, &amendment);
6479 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
6480 let text = serde_json::to_string(&v).unwrap();
6481 assert_eq!(
6482 serde_json::from_str::<GovernanceVerification>(&text).unwrap(),
6483 v
6484 );
6485
6486 let keys = GovernanceSigningKeys { keys: v.keys };
6487 let value = serde_json::to_value(&keys).unwrap();
6488 assert!(value["keys"].is_array(), "an object, not a bare array");
6489 assert_eq!(value["keys"][0]["status"], "active");
6490 assert_eq!(
6491 serde_json::from_value::<GovernanceSigningKeys>(value).unwrap(),
6492 keys
6493 );
6494 }
6495
6496 #[test]
6500 fn envelope_v1_preimage_and_hash_are_pinned() {
6501 let envelope = Envelope::new(
6502 gov(6),
6503 GovernanceLogEntryType::CouncilDecision,
6504 at(0),
6505 Some(Sha256Hex::from([0x11; 32])),
6506 data_hash(&json!({"outcome": "approved", "title": "Ratification"})),
6507 );
6508 assert_eq!(
6509 String::from_utf8(envelope.preimage()).unwrap(),
6510 "{\"agora_governance_log\":1,\"id\":\"GOV-2026-0006\",\
6511 \"entry_type\":\"council_decision\",\
6512 \"created_at\":1700000000123456,\
6513 \"prev_hash\":\"1111111111111111111111111111111111111111111111111111111111111111\",\
6514 \"data_hash\":\"a4adf645ae3f60c56484d01aea87d6d490321d7fc66b1607df14b023fe567c7b\"}"
6515 );
6516 assert_eq!(
6517 envelope.entry_hash().to_hex(),
6518 "ba27577432f81e415f1c01cc4cfabab6070e3ac50fd468fffe195ef19c0e9464"
6519 );
6520 }
6521
6522 #[cfg(feature = "agora-client")]
6536 #[tokio::test]
6537 #[ignore = "networked: hits the live platform"]
6538 async fn the_published_key_is_the_one_the_platform_serves() {
6539 let client = crate::client::Client::new(
6540 url::Url::parse("https://subliminal.technology").unwrap(),
6541 )
6542 .unwrap();
6543 let served = client.get_governance_signing_key().await.unwrap();
6544 assert_eq!(served.algorithm, "ed25519");
6545
6546 let genesis: PublicKeyHex = PUBLISHED_KEYS
6547 .first()
6548 .expect("PUBLISHED_KEYS is never empty")
6549 .parse()
6550 .unwrap();
6551 let links = client.get_governance_chain().await.unwrap();
6552 let report = verify_chain(
6553 &links,
6554 &genesis.to_verifying_key().unwrap(),
6555 &KeyAnchor::published(),
6556 &RootSet::published(),
6557 );
6558 let problems: Vec<_> = report
6559 .entries
6560 .iter()
6561 .filter_map(|e| {
6562 e.problem.as_ref().map(|p| (e.id.clone(), p.clone()))
6563 })
6564 .collect();
6565 assert!(
6566 report.ok,
6567 "the live chain does not verify under this build's genesis key \
6568 and roots: {problems:#?}"
6569 );
6570 assert!(report.unanchored_keys.is_empty(), "{report:#?}");
6571 assert_eq!(
6572 served.public_key, report.public_key,
6573 "the platform serves {} but the chain, followed under the \
6574 published roots, is held by {}",
6575 served.public_key, report.public_key
6576 );
6577 }
6578
6579 #[cfg(feature = "schemars")]
6580 #[test]
6581 fn wire_schemas_are_ref_free() {
6582 use crate::responses::inline_schema_for;
6583 for (name, schema) in [
6584 (
6585 "GovernanceAttestation",
6586 inline_schema_for::<GovernanceAttestation>(),
6587 ),
6588 (
6589 "GovernanceChainLink",
6590 inline_schema_for::<GovernanceChainLink>(),
6591 ),
6592 (
6593 "GovernanceSigningKey",
6594 inline_schema_for::<GovernanceSigningKey>(),
6595 ),
6596 (
6597 "GovernanceVerification",
6598 inline_schema_for::<GovernanceVerification>(),
6599 ),
6600 (
6601 "Vec<GovernanceChainLink>",
6602 inline_schema_for::<Vec<GovernanceChainLink>>(),
6603 ),
6604 ("Amendment", inline_schema_for::<Amendment>()),
6605 ("Redaction", inline_schema_for::<Redaction>()),
6606 ("Revision", inline_schema_for::<Revision>()),
6607 ("AmendmentNotice", inline_schema_for::<AmendmentNotice>()),
6608 ("KeyRotation", inline_schema_for::<KeyRotation>()),
6609 ("TrustedHead", inline_schema_for::<TrustedHead>()),
6610 (
6611 "GovernanceKeyRecord",
6612 inline_schema_for::<GovernanceKeyRecord>(),
6613 ),
6614 (
6615 "GovernanceSigningKeys",
6616 inline_schema_for::<GovernanceSigningKeys>(),
6617 ),
6618 ("AmendmentKind", inline_schema_for::<AmendmentKind>()),
6619 ("Standing", inline_schema_for::<Standing>()),
6620 ("KeyStatus", inline_schema_for::<KeyStatus>()),
6621 ("RotationReason", inline_schema_for::<RotationReason>()),
6622 ] {
6623 let text = serde_json::to_string(&schema).unwrap();
6624 assert!(!text.contains("$ref"), "{name} must be $ref-free: {text}");
6625 assert!(
6626 !text.contains("$defs"),
6627 "{name} must be $defs-free: {text}"
6628 );
6629 }
6630 let text =
6631 serde_json::to_string(&inline_schema_for::<Sha256Hex>()).unwrap();
6632 assert!(text.contains("^[0-9a-f]{64}$"), "{text}");
6633 let text = serde_json::to_string(&inline_schema_for::<SignatureHex>())
6634 .unwrap();
6635 assert!(text.contains("^[0-9a-f]{128}$"), "{text}");
6636 }
6637}