1use crate::crypto::{self, Signature, SigningKey, VerifyingKey};
29use crate::enums::GovernanceLogEntryType;
30use crate::ids::GovernanceLogId;
31use chrono::{DateTime, Utc};
32use serde::{Deserialize, Serialize};
33use sha2::{Digest, Sha256};
34
35pub const ENVELOPE_VERSION: u32 = 1;
37
38pub const RETROACTIVE_AFTER: chrono::Duration = chrono::Duration::seconds(60);
41
42#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
49#[error("{type_name}: expected {expected} bytes of hex, got {got:?}")]
50pub struct HexLengthError {
51 pub type_name: &'static str,
52 pub expected: usize,
53 pub got: String,
54}
55
56macro_rules! hex_bytes {
57 ($(#[$meta:meta])* $name:ident, $len:expr) => {
58 $(#[$meta])*
59 #[derive(Clone, Copy, PartialEq, Eq, Hash)]
60 #[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
61 #[cfg_attr(feature = "sqlx", sqlx(transparent))]
62 pub struct $name([u8; $len]);
63
64 impl $name {
65 pub fn as_bytes(&self) -> &[u8; $len] {
67 &self.0
68 }
69
70 pub fn to_hex(&self) -> String {
72 hex::encode(self.0)
73 }
74 }
75
76 impl From<[u8; $len]> for $name {
77 fn from(bytes: [u8; $len]) -> Self {
78 Self(bytes)
79 }
80 }
81
82 impl TryFrom<&[u8]> for $name {
83 type Error = HexLengthError;
84
85 fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
86 <[u8; $len]>::try_from(bytes).map(Self).map_err(|_| {
87 HexLengthError {
88 type_name: stringify!($name),
89 expected: $len,
90 got: hex::encode(bytes),
91 }
92 })
93 }
94 }
95
96 impl TryFrom<Vec<u8>> for $name {
97 type Error = HexLengthError;
98
99 fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
100 Self::try_from(bytes.as_slice())
101 }
102 }
103
104 impl std::str::FromStr for $name {
105 type Err = HexLengthError;
106
107 fn from_str(s: &str) -> Result<Self, Self::Err> {
108 let bytes = hex::decode(s.trim()).map_err(|_| HexLengthError {
109 type_name: stringify!($name),
110 expected: $len,
111 got: s.to_string(),
112 })?;
113 Self::try_from(bytes.as_slice())
114 }
115 }
116
117 impl std::fmt::Display for $name {
118 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 f.write_str(&self.to_hex())
120 }
121 }
122
123 impl std::fmt::Debug for $name {
124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125 write!(f, "{}({})", stringify!($name), self.to_hex())
126 }
127 }
128
129 impl Serialize for $name {
130 fn serialize<S: serde::Serializer>(
131 &self,
132 s: S,
133 ) -> Result<S::Ok, S::Error> {
134 s.serialize_str(&self.to_hex())
135 }
136 }
137
138 impl<'de> Deserialize<'de> for $name {
139 fn deserialize<D: serde::Deserializer<'de>>(
140 d: D,
141 ) -> Result<Self, D::Error> {
142 let s = String::deserialize(d)?;
143 s.parse().map_err(serde::de::Error::custom)
144 }
145 }
146
147 #[cfg(feature = "schemars")]
151 impl schemars::JsonSchema for $name {
152 fn inline_schema() -> bool {
153 true
154 }
155
156 fn schema_name() -> std::borrow::Cow<'static, str> {
157 std::borrow::Cow::Borrowed(stringify!($name))
158 }
159
160 fn schema_id() -> std::borrow::Cow<'static, str> {
161 std::borrow::Cow::Borrowed(concat!(
162 module_path!(),
163 "::",
164 stringify!($name)
165 ))
166 }
167
168 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
169 schemars::json_schema!({
170 "type": "string",
171 "pattern": format!("^[0-9a-f]{{{}}}$", $len * 2),
172 "description": format!("{} bytes, lowercase hex", $len),
173 })
174 }
175 }
176 };
177}
178
179hex_bytes!(
180 Sha256Hex,
182 32
183);
184
185hex_bytes!(
186 SignatureHex,
188 64
189);
190
191hex_bytes!(
192 PublicKeyHex,
194 32
195);
196
197impl From<Signature> for SignatureHex {
198 fn from(sig: Signature) -> Self {
199 Self(sig.to_bytes())
200 }
201}
202
203impl From<&SignatureHex> for Signature {
204 fn from(sig: &SignatureHex) -> Self {
205 Signature::from_bytes(&sig.0)
206 }
207}
208
209impl From<&VerifyingKey> for PublicKeyHex {
210 fn from(key: &VerifyingKey) -> Self {
211 Self(key.to_bytes())
212 }
213}
214
215impl PublicKeyHex {
216 pub fn to_verifying_key(
218 &self,
219 ) -> Result<VerifyingKey, ed25519_dalek::SignatureError> {
220 VerifyingKey::from_bytes(&self.0)
221 }
222}
223
224pub fn canonical_json(value: &serde_json::Value) -> Vec<u8> {
237 let mut out = Vec::new();
238 write_canonical(value, &mut out);
239 out
240}
241
242fn write_canonical(value: &serde_json::Value, out: &mut Vec<u8>) {
243 use serde_json::Value;
244 match value {
245 Value::Null => out.extend_from_slice(b"null"),
246 Value::Bool(b) => {
247 out.extend_from_slice(if *b { b"true" } else { b"false" })
248 }
249 Value::Number(n) => serde_json::to_writer(&mut *out, n)
250 .expect("a number always serializes"),
251 Value::String(s) => serde_json::to_writer(&mut *out, s)
252 .expect("a string always serializes"),
253 Value::Array(items) => {
254 out.push(b'[');
255 for (i, item) in items.iter().enumerate() {
256 if i > 0 {
257 out.push(b',');
258 }
259 write_canonical(item, out);
260 }
261 out.push(b']');
262 }
263 Value::Object(map) => {
264 let mut keys: Vec<&String> = map.keys().collect();
265 keys.sort_unstable();
266 out.push(b'{');
267 for (i, key) in keys.into_iter().enumerate() {
268 if i > 0 {
269 out.push(b',');
270 }
271 serde_json::to_writer(&mut *out, key)
272 .expect("a string always serializes");
273 out.push(b':');
274 write_canonical(&map[key], out);
275 }
276 out.push(b'}');
277 }
278 }
279}
280
281pub fn data_hash(data: &serde_json::Value) -> Sha256Hex {
283 Sha256Hex(Sha256::digest(canonical_json(data)).into())
284}
285
286#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
291pub struct Envelope {
292 pub agora_governance_log: u32,
294 pub id: GovernanceLogId,
295 pub entry_type: GovernanceLogEntryType,
296 pub created_at: i64,
298 pub prev_hash: Option<Sha256Hex>,
299 pub data_hash: Sha256Hex,
300}
301
302impl Envelope {
303 pub fn new(
305 id: GovernanceLogId,
306 entry_type: GovernanceLogEntryType,
307 created_at: DateTime<Utc>,
308 prev_hash: Option<Sha256Hex>,
309 data_hash: Sha256Hex,
310 ) -> Self {
311 Self {
312 agora_governance_log: ENVELOPE_VERSION,
313 id,
314 entry_type,
315 created_at: created_at.timestamp_micros(),
316 prev_hash,
317 data_hash,
318 }
319 }
320
321 pub fn created_at(&self) -> DateTime<Utc> {
323 DateTime::from_timestamp_micros(self.created_at)
324 .expect("an Envelope only ever holds an in-range timestamp")
325 }
326
327 pub fn preimage(&self) -> Vec<u8> {
329 serde_json::to_vec(self).expect("an Envelope always serializes")
330 }
331
332 pub fn entry_hash(&self) -> Sha256Hex {
334 Sha256Hex(Sha256::digest(self.preimage()).into())
335 }
336}
337
338pub fn truncate_to_micros(t: DateTime<Utc>) -> DateTime<Utc> {
341 DateTime::from_timestamp_micros(t.timestamp_micros())
342 .expect("a timestamp that came from a DateTime is in range")
343}
344
345pub fn truncate_to_seconds(t: DateTime<Utc>) -> DateTime<Utc> {
348 DateTime::from_timestamp(t.timestamp(), 0)
349 .expect("a timestamp that came from a DateTime is in range")
350}
351
352pub fn is_retroactive(
356 created_at: DateTime<Utc>,
357 signed_at: DateTime<Utc>,
358) -> bool {
359 signed_at - created_at > RETROACTIVE_AFTER
360}
361
362#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
368#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
369#[cfg_attr(feature = "schemars", schemars(inline))]
370pub struct GovernanceAttestation {
371 pub envelope_version: u32,
373 pub chain_seq: u64,
376 pub prev_hash: Option<Sha256Hex>,
378 pub data_hash: Sha256Hex,
380 pub entry_hash: Sha256Hex,
382 pub signature: SignatureHex,
385 pub signed_at: DateTime<Utc>,
388 pub retroactive: bool,
392}
393
394#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
397#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
398#[cfg_attr(feature = "schemars", schemars(inline))]
399pub struct GovernanceChainLink {
400 pub id: GovernanceLogId,
401 pub entry_type: GovernanceLogEntryType,
402 pub created_at: DateTime<Utc>,
403 pub attestation: GovernanceAttestation,
404}
405
406#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
409#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
410#[cfg_attr(feature = "schemars", schemars(inline))]
411pub struct GovernanceSigningKey {
412 pub algorithm: String,
414 pub public_key: PublicKeyHex,
415 pub envelope_version: u32,
417}
418
419impl GovernanceSigningKey {
420 pub fn new(key: &VerifyingKey) -> Self {
422 Self {
423 algorithm: "ed25519".to_string(),
424 public_key: key.into(),
425 envelope_version: ENVELOPE_VERSION,
426 }
427 }
428}
429
430#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
432#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
433#[cfg_attr(feature = "schemars", schemars(inline))]
434pub struct EntryVerdict {
435 pub id: GovernanceLogId,
436 pub chain_seq: u64,
437 pub signature_valid: bool,
440 pub link_valid: bool,
443 #[serde(default)]
448 pub content_matches: Option<bool>,
449 pub retroactive: bool,
451 pub out_of_order: bool,
454 #[serde(default)]
456 pub amended_by: Vec<GovernanceLogId>,
457 #[serde(default, skip_serializing_if = "Option::is_none")]
459 pub problem: Option<String>,
460}
461
462#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
464#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
465#[cfg_attr(feature = "schemars", schemars(inline))]
466pub struct GovernanceVerification {
467 pub public_key: PublicKeyHex,
468 pub ok: bool,
471 #[serde(default)]
473 pub head: Option<GovernanceLogId>,
474 pub entries: Vec<EntryVerdict>,
476}
477
478impl GovernanceVerification {
479 pub fn settle(mut self) -> Self {
481 self.ok = self.entries.iter().all(|e| {
482 e.signature_valid
483 && e.link_valid
484 && e.content_matches != Some(false)
485 });
486 self
487 }
488}
489
490pub fn attest(
501 key: &SigningKey,
502 envelope: &Envelope,
503 chain_seq: u64,
504 signed_at: DateTime<Utc>,
505) -> GovernanceAttestation {
506 let signed_at = truncate_to_seconds(signed_at);
507 let entry_hash = envelope.entry_hash();
508 let signature =
509 crypto::sign(key, entry_hash.as_bytes(), signed_at.timestamp());
510 GovernanceAttestation {
511 envelope_version: envelope.agora_governance_log,
512 chain_seq,
513 prev_hash: envelope.prev_hash,
514 data_hash: envelope.data_hash,
515 entry_hash,
516 signature: signature.into(),
517 signed_at,
518 retroactive: is_retroactive(envelope.created_at(), signed_at),
519 }
520}
521
522#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
528pub enum LinkError {
529 #[error(
530 "envelope version {0} is not supported (this verifier knows {ENVELOPE_VERSION})"
531 )]
532 UnsupportedVersion(u32),
533 #[error("entry_hash does not recompute from the envelope fields")]
534 HashMismatch,
535 #[error("signature does not verify under the published key")]
536 BadSignature,
537}
538
539pub fn recompute_entry_hash(link: &GovernanceChainLink) -> Sha256Hex {
541 Envelope::new(
542 link.id.clone(),
543 link.entry_type,
544 link.created_at,
545 link.attestation.prev_hash,
546 link.attestation.data_hash,
547 )
548 .entry_hash()
549}
550
551pub fn verify_link(
553 link: &GovernanceChainLink,
554 key: &VerifyingKey,
555) -> Result<(), LinkError> {
556 let a = &link.attestation;
557 if a.envelope_version != ENVELOPE_VERSION {
558 return Err(LinkError::UnsupportedVersion(a.envelope_version));
559 }
560 if recompute_entry_hash(link) != a.entry_hash {
561 return Err(LinkError::HashMismatch);
562 }
563 if !crypto::verify(
564 key,
565 a.entry_hash.as_bytes(),
566 a.signed_at.timestamp(),
567 &Signature::from(&a.signature),
568 ) {
569 return Err(LinkError::BadSignature);
570 }
571 Ok(())
572}
573
574pub fn verify_data(
576 link: &GovernanceChainLink,
577 data: &serde_json::Value,
578) -> bool {
579 data_hash(data) == link.attestation.data_hash
580}
581
582pub fn verify_chain(
589 links: &[GovernanceChainLink],
590 key: &VerifyingKey,
591) -> GovernanceVerification {
592 let mut links: Vec<&GovernanceChainLink> = links.iter().collect();
593 links.sort_by_key(|l| l.attestation.chain_seq);
594
595 let mut entries = Vec::with_capacity(links.len());
596 let mut prev: Option<&GovernanceChainLink> = None;
597 for (i, link) in links.iter().enumerate() {
598 let a = &link.attestation;
599 let expected_seq = i as u64 + 1;
600 let mut problems: Vec<String> = Vec::new();
601
602 let (hash_ok, signature_valid) = match verify_link(link, key) {
603 Ok(()) => (true, true),
604 Err(LinkError::BadSignature) => {
605 problems.push(LinkError::BadSignature.to_string());
606 (true, false)
607 }
608 Err(e) => {
609 problems.push(e.to_string());
610 (false, false)
611 }
612 };
613
614 let mut link_valid = hash_ok;
615 if a.chain_seq != expected_seq {
616 link_valid = false;
617 problems.push(format!(
618 "chain_seq {} where {expected_seq} was expected",
619 a.chain_seq
620 ));
621 }
622 let expected_prev = prev.map(|p| p.attestation.entry_hash);
623 if a.prev_hash != expected_prev {
624 link_valid = false;
625 problems.push(match (a.prev_hash, expected_prev) {
626 (Some(_), None) => "first entry names a predecessor".into(),
627 (None, Some(_)) => "prev_hash is null mid-chain".into(),
628 _ => "prev_hash is not the previous entry's entry_hash".into(),
629 });
630 }
631 let out_of_order = prev.is_some_and(|p| link.created_at < p.created_at);
632
633 entries.push(EntryVerdict {
634 id: link.id.clone(),
635 chain_seq: a.chain_seq,
636 signature_valid,
637 link_valid,
638 content_matches: None,
639 retroactive: is_retroactive(link.created_at, a.signed_at),
640 out_of_order,
641 amended_by: Vec::new(),
642 problem: (!problems.is_empty()).then(|| problems.join("; ")),
643 });
644 prev = Some(link);
645 }
646
647 GovernanceVerification {
648 public_key: key.into(),
649 ok: false,
650 head: prev.map(|p| p.id.clone()),
651 entries,
652 }
653 .settle()
654}
655
656#[cfg(test)]
657mod tests {
658 use super::*;
659 use crate::crypto::generate_keypair;
660 use serde_json::json;
661
662 fn gov(n: u32) -> GovernanceLogId {
663 format!("GOV-2026-{n:04}").parse().unwrap()
664 }
665
666 fn at(secs: i64) -> DateTime<Utc> {
667 DateTime::from_timestamp(1_700_000_000 + secs, 123_456_789).unwrap()
668 }
669
670 fn link(
671 key: &SigningKey,
672 n: u32,
673 prev: Option<&GovernanceChainLink>,
674 data: &serde_json::Value,
675 signed_at: DateTime<Utc>,
676 ) -> GovernanceChainLink {
677 let created_at = truncate_to_micros(at(n as i64 * 10));
678 let envelope = Envelope::new(
679 gov(n),
680 GovernanceLogEntryType::CouncilDecision,
681 created_at,
682 prev.map(|p| p.attestation.entry_hash),
683 data_hash(data),
684 );
685 let attestation = attest(
686 key,
687 &envelope,
688 prev.map_or(1, |p| p.attestation.chain_seq + 1),
689 signed_at,
690 );
691 GovernanceChainLink {
692 id: gov(n),
693 entry_type: GovernanceLogEntryType::CouncilDecision,
694 created_at,
695 attestation,
696 }
697 }
698
699 fn chain(key: &SigningKey, n: u32) -> Vec<GovernanceChainLink> {
700 let mut out: Vec<GovernanceChainLink> = Vec::new();
701 for i in 1..=n {
702 let data = json!({"title": format!("Decision {i}"), "outcome": "approved"});
703 let l = link(key, i, out.last(), &data, at(i as i64 * 10 + 1));
704 out.push(l);
705 }
706 out
707 }
708
709 #[test]
712 fn canonical_json_sorts_keys_at_every_level() {
713 let v = json!({"b": {"z": 1, "a": [{"y": 2, "x": 3}]}, "a": null});
714 assert_eq!(
715 canonical_json(&v),
716 br#"{"a":null,"b":{"a":[{"x":3,"y":2}],"z":1}}"#
717 );
718 }
719
720 #[test]
721 fn canonical_json_is_compact_and_escapes_like_serde() {
722 let v = json!({"s": "tab\there \"q\" ünïcode \u{1F600}", "n": [1, -2, 3.5, true, false]});
723 let bytes = canonical_json(&v);
724 let text = std::str::from_utf8(&bytes).unwrap();
725 assert_eq!(
726 text,
727 r#"{"n":[1,-2,3.5,true,false],"s":"tab\there \"q\" ünïcode 😀"}"#
728 );
729 }
730
731 #[test]
732 fn canonical_json_ignores_insertion_order() {
733 let mut a = serde_json::Map::new();
734 a.insert("z".into(), json!(1));
735 a.insert("a".into(), json!(2));
736 let mut b = serde_json::Map::new();
737 b.insert("a".into(), json!(2));
738 b.insert("z".into(), json!(1));
739 assert_eq!(
740 canonical_json(&serde_json::Value::Object(a)),
741 canonical_json(&serde_json::Value::Object(b))
742 );
743 }
744
745 #[test]
746 fn canonical_json_empty_containers() {
747 assert_eq!(canonical_json(&json!({})), b"{}");
748 assert_eq!(canonical_json(&json!([])), b"[]");
749 assert_eq!(
750 canonical_json(&json!({"a": {}, "b": []})),
751 br#"{"a":{},"b":[]}"#
752 );
753 }
754
755 #[test]
758 fn preimage_is_declaration_ordered_json() {
759 let e = Envelope::new(
760 gov(1),
761 GovernanceLogEntryType::AppealsCourtDecision,
762 at(0),
763 None,
764 data_hash(&json!({})),
765 );
766 let text = String::from_utf8(e.preimage()).unwrap();
767 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}");
768 }
769
770 #[test]
771 fn every_envelope_field_changes_the_hash() {
772 let base = Envelope::new(
773 gov(1),
774 GovernanceLogEntryType::CouncilDecision,
775 at(0),
776 None,
777 data_hash(&json!({"a":1})),
778 );
779 let h = base.entry_hash();
780 let mut e = base.clone();
781 e.id = gov(2);
782 assert_ne!(e.entry_hash(), h);
783 let mut e = base.clone();
784 e.entry_type = GovernanceLogEntryType::PolicyChange;
785 assert_ne!(e.entry_hash(), h);
786 let mut e = base.clone();
787 e.created_at += 1;
788 assert_ne!(e.entry_hash(), h);
789 let mut e = base.clone();
790 e.prev_hash = Some(h);
791 assert_ne!(e.entry_hash(), h);
792 let mut e = base.clone();
793 e.data_hash = data_hash(&json!({"a":2}));
794 assert_ne!(e.entry_hash(), h);
795 assert_eq!(base.entry_hash(), h, "and it is deterministic");
796 }
797
798 #[test]
799 fn truncation_matches_what_the_envelope_carries() {
800 let t = at(0);
801 assert_eq!(truncate_to_micros(t).timestamp_subsec_nanos(), 123_456_000);
802 assert_eq!(truncate_to_seconds(t).timestamp_subsec_nanos(), 0);
803 assert_eq!(
804 Envelope::new(
805 gov(1),
806 GovernanceLogEntryType::CouncilDecision,
807 t,
808 None,
809 data_hash(&json!(null))
810 )
811 .created_at,
812 truncate_to_micros(t).timestamp_micros()
813 );
814 }
815
816 #[test]
819 fn hex_newtypes_round_trip_and_reject_wrong_lengths() {
820 let h = data_hash(&json!(1));
821 let s = serde_json::to_string(&h).unwrap();
822 assert_eq!(s.len(), 66);
823 let back: Sha256Hex = serde_json::from_str(&s).unwrap();
824 assert_eq!(back, h);
825 assert!(serde_json::from_str::<Sha256Hex>("\"abcd\"").is_err());
826 assert!("zz".repeat(32).parse::<Sha256Hex>().is_err());
827 assert!(Sha256Hex::try_from(vec![0u8; 31]).is_err());
828 assert_eq!(format!("{h:?}"), format!("Sha256Hex({h})"));
829 }
830
831 #[test]
834 fn attest_then_verify_link() {
835 let (key, pk) = generate_keypair();
836 let l = link(&key, 1, None, &json!({"a": 1}), at(5));
837 assert_eq!(verify_link(&l, &pk), Ok(()));
838 assert!(verify_data(&l, &json!({"a": 1})));
839 assert!(!verify_data(&l, &json!({"a": 2})));
840 assert!(!l.attestation.retroactive);
841 }
842
843 #[test]
844 fn wrong_key_fails_signature_only() {
845 let (key, _) = generate_keypair();
846 let (_, other) = generate_keypair();
847 let l = link(&key, 1, None, &json!({}), at(5));
848 assert_eq!(verify_link(&l, &other), Err(LinkError::BadSignature));
849 }
850
851 #[test]
852 fn tampering_with_any_attested_field_is_detected() {
853 let (key, pk) = generate_keypair();
854 let l = link(&key, 1, None, &json!({"a": 1}), at(5));
855
856 let mut t = l.clone();
857 t.created_at += chrono::Duration::microseconds(1);
858 assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
859
860 let mut t = l.clone();
861 t.entry_type = GovernanceLogEntryType::StewardVeto;
862 assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
863
864 let mut t = l.clone();
865 t.attestation.data_hash = data_hash(&json!({"a": 2}));
866 assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
867
868 let mut t = l.clone();
871 t.attestation.signed_at -= chrono::Duration::seconds(1);
872 assert_eq!(verify_link(&t, &pk), Err(LinkError::BadSignature));
873
874 let mut t = l.clone();
875 t.attestation.envelope_version = 2;
876 assert_eq!(verify_link(&t, &pk), Err(LinkError::UnsupportedVersion(2)));
877 }
878
879 #[test]
880 fn a_good_chain_verifies_in_any_input_order() {
881 let (key, pk) = generate_keypair();
882 let mut c = chain(&key, 4);
883 c.reverse();
884 let v = verify_chain(&c, &pk);
885 assert!(v.ok, "{v:#?}");
886 assert_eq!(v.head, Some(gov(4)));
887 assert_eq!(
888 v.entries.iter().map(|e| e.chain_seq).collect::<Vec<_>>(),
889 [1, 2, 3, 4]
890 );
891 assert!(v.entries.iter().all(|e| e.content_matches.is_none()
892 && e.problem.is_none()
893 && !e.out_of_order));
894 assert_eq!(v.public_key, PublicKeyHex::from(&pk));
895 }
896
897 #[test]
898 fn empty_chain_is_ok_with_no_head() {
899 let (_, pk) = generate_keypair();
900 let v = verify_chain(&[], &pk);
901 assert!(v.ok);
902 assert!(v.head.is_none());
903 assert!(v.entries.is_empty());
904 }
905
906 #[test]
907 fn a_changed_entry_breaks_its_signature_and_the_next_link() {
908 let (key, pk) = generate_keypair();
909 let mut c = chain(&key, 3);
910 let rewritten = link(
913 &key,
914 2,
915 Some(&c[0]),
916 &json!({"title": "Decision 2", "outcome": "REJECTED"}),
917 at(21),
918 );
919 c[1] = rewritten;
920 let v = verify_chain(&c, &pk);
921 assert!(!v.ok);
922 assert!(
923 v.entries[1].signature_valid && v.entries[1].link_valid,
924 "the rewrite itself is well-formed: {:#?}",
925 v.entries[1]
926 );
927 assert!(
928 !v.entries[2].link_valid,
929 "but entry 3 no longer points at it: {:#?}",
930 v.entries[2]
931 );
932 assert!(
933 v.entries[2]
934 .problem
935 .as_deref()
936 .unwrap()
937 .contains("prev_hash")
938 );
939 }
940
941 #[test]
942 fn a_removed_entry_is_a_gap_and_a_broken_link() {
943 let (key, pk) = generate_keypair();
944 let mut c = chain(&key, 3);
945 c.remove(1);
946 let v = verify_chain(&c, &pk);
947 assert!(!v.ok);
948 assert!(v.entries[0].link_valid);
949 let p = v.entries[1].problem.as_deref().unwrap();
950 assert!(p.contains("chain_seq 3 where 2 was expected"), "{p}");
951 assert!(p.contains("prev_hash"), "{p}");
952 }
953
954 #[test]
955 fn a_second_genesis_is_rejected() {
956 let (key, pk) = generate_keypair();
957 let mut c = chain(&key, 2);
958 let rogue = link(&key, 2, None, &json!({}), at(21));
959 c[1] = rogue;
960 let v = verify_chain(&c, &pk);
961 assert!(!v.ok);
962 assert!(
963 v.entries[1]
964 .problem
965 .as_deref()
966 .unwrap()
967 .contains("null mid-chain")
968 );
969 }
970
971 #[test]
972 fn retroactive_and_out_of_order_are_recomputed_not_copied() {
973 let (key, pk) = generate_keypair();
974 let first = link(&key, 1, None, &json!({}), at(10 + 3600));
975 let created = truncate_to_micros(at(5));
978 let envelope = Envelope::new(
979 gov(2),
980 GovernanceLogEntryType::CouncilDecision,
981 created,
982 Some(first.attestation.entry_hash),
983 data_hash(&json!({})),
984 );
985 let attestation = attest(&key, &envelope, 2, at(6));
986 let mut second = GovernanceChainLink {
987 id: gov(2),
988 entry_type: GovernanceLogEntryType::CouncilDecision,
989 created_at: created,
990 attestation,
991 };
992 second.attestation.retroactive = true; let v = verify_chain(&[first, second], &pk);
994 assert!(v.ok, "{v:#?}");
995 assert!(v.entries[0].retroactive);
996 assert!(!v.entries[1].retroactive, "recomputed from timestamps");
997 assert!(v.entries[1].out_of_order);
998 }
999
1000 #[test]
1001 fn content_mismatch_settles_to_not_ok() {
1002 let (key, pk) = generate_keypair();
1003 let c = chain(&key, 1);
1004 let mut v = verify_chain(&c, &pk);
1005 v.entries[0].content_matches = Some(true);
1006 assert!(v.clone().settle().ok);
1007 v.entries[0].content_matches = Some(false);
1008 assert!(!v.settle().ok);
1009 }
1010
1011 #[cfg(feature = "schemars")]
1012 #[test]
1013 fn wire_schemas_are_ref_free() {
1014 use crate::responses::inline_schema_for;
1015 for (name, schema) in [
1016 (
1017 "GovernanceAttestation",
1018 inline_schema_for::<GovernanceAttestation>(),
1019 ),
1020 (
1021 "GovernanceChainLink",
1022 inline_schema_for::<GovernanceChainLink>(),
1023 ),
1024 (
1025 "GovernanceSigningKey",
1026 inline_schema_for::<GovernanceSigningKey>(),
1027 ),
1028 (
1029 "GovernanceVerification",
1030 inline_schema_for::<GovernanceVerification>(),
1031 ),
1032 (
1033 "Vec<GovernanceChainLink>",
1034 inline_schema_for::<Vec<GovernanceChainLink>>(),
1035 ),
1036 ] {
1037 let text = serde_json::to_string(&schema).unwrap();
1038 assert!(!text.contains("$ref"), "{name} must be $ref-free: {text}");
1039 assert!(
1040 !text.contains("$defs"),
1041 "{name} must be $defs-free: {text}"
1042 );
1043 }
1044 let text =
1045 serde_json::to_string(&inline_schema_for::<Sha256Hex>()).unwrap();
1046 assert!(text.contains("^[0-9a-f]{64}$"), "{text}");
1047 let text = serde_json::to_string(&inline_schema_for::<SignatureHex>())
1048 .unwrap();
1049 assert!(text.contains("^[0-9a-f]{128}$"), "{text}");
1050 }
1051}