use serde::{Deserialize, Serialize};
use crate::events::Event;
use crate::types::{ConfigTrait, Prefix, Said, Threshold};
use crate::witness::StoredReceipt;
use crate::witness::agreement::{AgreementStatus, WitnessAgreement};
use crate::{CesrKey, KeyState};
pub const KSN_VERSION: u32 = 1;
pub const KSN_TYPE: &str = "ksn";
pub const KERI_KEY_STATE_VERSION: [u32; 2] = [1, 0];
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LatestEstablishmentEvent {
pub s: String,
pub d: Said,
#[serde(default)]
pub br: Vec<Prefix>,
#[serde(default)]
pub ba: Vec<Prefix>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct KeyStateRecord {
pub vn: [u32; 2],
pub i: Prefix,
pub s: String,
pub p: String,
pub d: Said,
pub f: String,
pub dt: String,
pub et: String,
pub kt: Threshold,
pub k: Vec<CesrKey>,
pub nt: Threshold,
pub n: Vec<Said>,
pub bt: Threshold,
pub b: Vec<Prefix>,
pub c: Vec<ConfigTrait>,
pub ee: LatestEstablishmentEvent,
pub di: String,
}
impl KeyStateRecord {
pub fn from_kel(events: &[Event], state: &KeyState, dt: impl Into<String>) -> Option<Self> {
let last = events.last()?;
let latest_est = events.iter().rev().find(|e| !e.is_interaction())?;
Some(Self {
vn: KERI_KEY_STATE_VERSION,
i: state.prefix.clone(),
s: format!("{:x}", state.sequence),
p: last
.previous()
.map(|s| s.as_str().to_string())
.unwrap_or_default(),
d: state.last_event_said.clone(),
f: format!("{:x}", state.sequence),
dt: dt.into(),
et: establishment_type(latest_est).to_string(),
kt: state.threshold.clone(),
k: state.current_keys.clone(),
nt: state.next_threshold.clone(),
n: state.next_commitment.clone(),
bt: state.backer_threshold.clone(),
b: state.backers.clone(),
c: state.config_traits.clone(),
ee: LatestEstablishmentEvent {
s: format!("{:x}", latest_est.sequence().value()),
d: latest_est.said().clone(),
br: Vec::new(),
ba: Vec::new(),
},
di: state
.delegator
.as_ref()
.map(|p| p.as_str().to_string())
.unwrap_or_default(),
})
}
pub fn sequence(&self) -> u128 {
u128::from_str_radix(self.s.trim_start_matches("0x"), 16).unwrap_or(0)
}
pub fn check_not_stale(&self, last_seen_seq: u128) -> Result<(), KsnError> {
let got = self.sequence();
if got < last_seen_seq {
return Err(KsnError::Stale {
got,
seen: last_seen_seq,
});
}
Ok(())
}
pub fn into_key_state(self) -> KeyState {
let sequence = u128::from_str_radix(self.s.trim_start_matches("0x"), 16).unwrap_or(0);
let last_est_seq =
u128::from_str_radix(self.ee.s.trim_start_matches("0x"), 16).unwrap_or(0);
let delegator = if self.di.is_empty() {
None
} else {
Some(Prefix::new_unchecked(self.di))
};
KeyState {
prefix: self.i,
current_keys: self.k,
next_commitment: self.n.clone(),
sequence,
last_event_said: self.d,
is_abandoned: self.n.is_empty() && self.et != "icp" && self.et != "dip",
threshold: self.kt,
next_threshold: self.nt,
backers: self.b,
backer_threshold: self.bt,
config_traits: self.c,
is_non_transferable: self.n.is_empty(),
delegator,
last_establishment_sequence: last_est_seq,
}
}
}
fn establishment_type(event: &Event) -> &'static str {
match event {
Event::Icp(_) => "icp",
Event::Rot(_) => "rot",
Event::Dip(_) => "dip",
Event::Drt(_) => "drt",
Event::Ixn(_) => "ixn",
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum KsnError {
#[error("KSN serialization failed: {0}")]
Serialize(String),
#[error("KSN signing failed: {0}")]
Signer(String),
#[error("KSN names no current key")]
NoCurrentKey,
#[error("not a KSN (t = {0:?})")]
WrongType(String),
#[error("KSN signature is invalid")]
BadSignature,
#[error("KSN current key is undecodable: {0}")]
UndecodableKey(String),
#[error("KSN is stale: seq {got} < last-seen {seen}")]
Stale {
got: u128,
seen: u128,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct KeyStateNotice {
pub version: u32,
pub t: String,
pub state: KeyState,
pub dt: String,
}
impl KeyStateNotice {
pub fn new(state: KeyState, dt: impl Into<String>) -> Self {
Self {
version: KSN_VERSION,
t: KSN_TYPE.to_string(),
state,
dt: dt.into(),
}
}
pub fn canonical_bytes(&self) -> Result<Vec<u8>, KsnError> {
serde_json::to_vec(self).map_err(|e| KsnError::Serialize(e.to_string()))
}
pub fn signing_key(&self) -> Option<&CesrKey> {
self.state.current_keys.first()
}
pub fn sequence(&self) -> u128 {
self.state.sequence
}
pub fn names_delegated_device(&self) -> bool {
self.state.delegator.is_some()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SignedKsn {
pub notice: KeyStateNotice,
#[serde(with = "hex::serde")]
pub signature: Vec<u8>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub receipts: Vec<StoredReceipt>,
}
impl SignedKsn {
pub fn sign_with(
notice: KeyStateNotice,
signer: impl FnOnce(&[u8]) -> Result<Vec<u8>, String>,
) -> Result<Self, KsnError> {
if notice.signing_key().is_none() {
return Err(KsnError::NoCurrentKey);
}
let bytes = notice.canonical_bytes()?;
let signature = signer(&bytes).map_err(KsnError::Signer)?;
Ok(Self {
notice,
signature,
receipts: Vec::new(),
})
}
pub fn with_receipts(mut self, candidates: Vec<StoredReceipt>) -> Self {
let state = &self.notice.state;
let said = state.last_event_said.clone();
let valid: Vec<StoredReceipt> = candidates
.into_iter()
.filter(|r| {
r.signed.receipt.d == said
&& state.backers.iter().any(|b| b == &r.witness)
&& receipt_signature_valid(r)
})
.collect();
self.receipts = valid;
self
}
pub fn verify(&self) -> Result<VerifiedKsn, KsnError> {
if self.notice.t != KSN_TYPE {
return Err(KsnError::WrongType(self.notice.t.clone()));
}
let key_cesr = self.notice.signing_key().ok_or(KsnError::NoCurrentKey)?;
let key = crate::KeriPublicKey::parse(key_cesr.as_str())
.map_err(|e| KsnError::UndecodableKey(e.to_string()))?;
let bytes = self.notice.canonical_bytes()?;
key.verify_signature(&bytes, &self.signature)
.map_err(|_| KsnError::BadSignature)?;
Ok(VerifiedKsn {
state: self.notice.state.clone(),
trust: self.witness_trust(),
})
}
fn witness_trust(&self) -> KsnTrust {
let state = &self.notice.state;
let required = state.backer_threshold.simple_value().unwrap_or(0) as usize;
if state.backers.is_empty() || required == 0 {
return KsnTrust::TrustOnFirstSight;
}
let said = &state.last_event_said;
let sn = state.sequence as u64;
let agreement = WitnessAgreement::new(1);
agreement.submit_event(
&state.prefix,
sn,
said,
&state.backer_threshold,
&state.backers,
);
let mut distinct = std::collections::HashSet::new();
for r in &self.receipts {
if &r.signed.receipt.d == said && state.backers.iter().any(|b| b == &r.witness) {
agreement.add_receipt(&state.prefix, sn, said, r.witness.as_str());
distinct.insert(r.witness.as_str());
}
}
match agreement.status(&state.prefix, sn, said) {
AgreementStatus::Accepted => KsnTrust::Witnessed {
receipts: distinct.len(),
threshold: required,
},
AgreementStatus::Pending { .. } => KsnTrust::TrustOnFirstSight,
}
}
pub fn check_not_stale(&self, last_seen_seq: u128) -> Result<(), KsnError> {
let got = self.notice.sequence();
if got < last_seen_seq {
return Err(KsnError::Stale {
got,
seen: last_seen_seq,
});
}
Ok(())
}
}
fn receipt_signature_valid(stored: &StoredReceipt) -> bool {
let Ok(key) = crate::KeriPublicKey::parse(stored.witness.as_str()) else {
return false;
};
let Ok(payload) = serde_json::to_vec(&stored.signed.receipt) else {
return false;
};
key.verify_signature(&payload, &stored.signed.signature)
.is_ok()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum KsnTrust {
TrustOnFirstSight,
Witnessed {
receipts: usize,
threshold: usize,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerifiedKsn {
pub state: KeyState,
pub trust: KsnTrust,
}
impl VerifiedKsn {
pub fn is_authoritative_over_kel(&self) -> bool {
false
}
pub fn satisfies_revocation_check(&self) -> bool {
false
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::{KeriPublicKey, Prefix, Said, Threshold};
use ring::rand::SystemRandom;
use ring::signature::{Ed25519KeyPair, KeyPair};
fn real_keypair() -> Ed25519KeyPair {
let rng = SystemRandom::new();
let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap()
}
fn state_for_key(kp: &Ed25519KeyPair, seq: u128) -> KeyState {
let cesr = KeriPublicKey::ed25519(kp.public_key().as_ref())
.unwrap()
.to_qb64()
.unwrap();
let mut state = key_state(seq, false);
state.current_keys = vec![CesrKey::new_unchecked(cesr)];
state
}
fn sign_ksn(kp: &Ed25519KeyPair, notice: KeyStateNotice) -> SignedKsn {
SignedKsn::sign_with(notice, |bytes| Ok(kp.sign(bytes).as_ref().to_vec())).unwrap()
}
fn key_state(seq: u128, delegated: bool) -> KeyState {
let key = KeriPublicKey::ed25519(&[3u8; 32]).unwrap();
let mut state = KeyState::from_inception(
Prefix::new_unchecked("EksnTestPrefix000000000000000000000000000000".to_string()),
vec![CesrKey::new_unchecked(key.to_qb64().unwrap())],
vec![Said::new_unchecked(
"ENextCommitment0000000000000000000000000000".to_string(),
)],
Threshold::Simple(1),
Threshold::Simple(1),
Said::new_unchecked("ELastEvent00000000000000000000000000000000000".to_string()),
vec![],
Threshold::Simple(0),
vec![],
);
state.sequence = seq;
if delegated {
state.delegator = Some(Prefix::new_unchecked(
"ERootDelegator00000000000000000000000000000".to_string(),
));
}
state
}
#[test]
fn canonical_bytes_is_deterministic() {
let notice = KeyStateNotice::new(key_state(2, false), "2026-06-03T00:00:00Z");
assert_eq!(
notice.canonical_bytes().unwrap(),
notice.canonical_bytes().unwrap()
);
}
#[test]
fn sign_with_and_round_trips() {
let notice = KeyStateNotice::new(key_state(0, false), "2026-06-03T00:00:00Z");
let signed = SignedKsn::sign_with(notice, |_| Ok(vec![7u8; 64])).unwrap();
assert_eq!(signed.signature, vec![7u8; 64]);
assert!(signed.receipts.is_empty());
let json = serde_json::to_string(&signed).unwrap();
assert!(!json.contains("receipts"));
let parsed: SignedKsn = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, signed);
}
#[test]
fn sign_with_rejects_no_current_key() {
let mut state = key_state(0, false);
state.current_keys.clear();
let notice = KeyStateNotice::new(state, "2026-06-03T00:00:00Z");
let err = SignedKsn::sign_with(notice, |_| Ok(vec![0u8; 64])).unwrap_err();
assert!(matches!(err, KsnError::NoCurrentKey));
}
#[test]
fn signer_error_propagates() {
let notice = KeyStateNotice::new(key_state(0, false), "2026-06-03T00:00:00Z");
let err = SignedKsn::sign_with(notice, |_| Err("keychain locked".to_string())).unwrap_err();
assert!(matches!(err, KsnError::Signer(_)));
}
#[test]
fn delegated_device_is_flagged() {
assert!(KeyStateNotice::new(key_state(1, true), "t").names_delegated_device());
assert!(!KeyStateNotice::new(key_state(1, false), "t").names_delegated_device());
}
#[test]
fn verify_accepts_valid_ksn() {
let kp = real_keypair();
let notice = KeyStateNotice::new(state_for_key(&kp, 1), "2026-06-03T00:00:00Z");
let signed = sign_ksn(&kp, notice);
let verified = signed.verify().unwrap();
assert_eq!(verified.trust, KsnTrust::TrustOnFirstSight);
assert!(!verified.is_authoritative_over_kel());
assert!(!verified.satisfies_revocation_check());
}
#[test]
fn verify_rejects_tampered_notice() {
let kp = real_keypair();
let notice = KeyStateNotice::new(state_for_key(&kp, 1), "2026-06-03T00:00:00Z");
let mut signed = sign_ksn(&kp, notice);
signed.notice.dt = "2099-01-01T00:00:00Z".to_string(); assert!(matches!(signed.verify(), Err(KsnError::BadSignature)));
}
#[test]
fn verify_rejects_signature_by_non_current_key() {
let signer = real_keypair();
let other = real_keypair();
let notice = KeyStateNotice::new(state_for_key(&other, 1), "2026-06-03T00:00:00Z");
let signed = sign_ksn(&signer, notice);
assert!(matches!(signed.verify(), Err(KsnError::BadSignature)));
}
#[test]
fn verify_rejects_unsigned_or_garbage_signature() {
let kp = real_keypair();
let notice = KeyStateNotice::new(state_for_key(&kp, 1), "2026-06-03T00:00:00Z");
let mut signed = sign_ksn(&kp, notice);
signed.signature = vec![0u8; 64]; assert!(matches!(signed.verify(), Err(KsnError::BadSignature)));
}
#[test]
fn verify_rejects_wrong_type() {
let kp = real_keypair();
let mut notice = KeyStateNotice::new(state_for_key(&kp, 1), "t");
notice.t = "rpy".to_string();
let signed = sign_ksn(&kp, notice);
assert!(matches!(signed.verify(), Err(KsnError::WrongType(_))));
}
#[test]
fn check_not_stale_rejects_rollback() {
let kp = real_keypair();
let signed = sign_ksn(&kp, KeyStateNotice::new(state_for_key(&kp, 2), "t"));
assert!(matches!(
signed.check_not_stale(3),
Err(KsnError::Stale { .. })
));
assert!(signed.check_not_stale(2).is_ok());
assert!(signed.check_not_stale(1).is_ok());
}
use crate::witness::{Receipt, ReceiptTag, SignedReceipt};
use crate::{KeriSequence, VersionString};
fn witness_kp_and_aid() -> (Ed25519KeyPair, String) {
let kp = real_keypair();
let aid = KeriPublicKey::ed25519(kp.public_key().as_ref())
.unwrap()
.to_qb64()
.unwrap();
(kp, aid)
}
fn witness_receipt(
witness_kp: &Ed25519KeyPair,
witness_aid: &str,
controller: &str,
seq: u128,
event_said: &str,
) -> StoredReceipt {
let receipt = Receipt {
v: VersionString::placeholder(),
t: ReceiptTag,
d: Said::new_unchecked(event_said.to_string()),
i: Prefix::new_unchecked(controller.to_string()),
s: KeriSequence::new(seq),
};
let payload = serde_json::to_vec(&receipt).unwrap();
let signature = witness_kp.sign(&payload).as_ref().to_vec();
StoredReceipt {
signed: SignedReceipt { receipt, signature },
witness: Prefix::new_unchecked(witness_aid.to_string()),
}
}
fn witnessed_state(
controller_kp: &Ed25519KeyPair,
seq: u128,
backers: &[&str],
bt: u64,
delegated: bool,
) -> KeyState {
let mut state = state_for_key(controller_kp, seq);
state.backers = backers
.iter()
.map(|a| Prefix::new_unchecked(a.to_string()))
.collect();
state.backer_threshold = Threshold::Simple(bt);
if delegated {
state.delegator = Some(Prefix::new_unchecked(
"ERootDelegator00000000000000000000000000000".to_string(),
));
}
state
}
#[test]
fn ksn_witnessed_when_quorum_met() {
let ckp = real_keypair();
let (w1kp, w1) = witness_kp_and_aid();
let (w2kp, w2) = witness_kp_and_aid();
let state = witnessed_state(&ckp, 1, &[&w1, &w2], 2, false);
let controller = state.prefix.as_str().to_string();
let said = state.last_event_said.as_str().to_string();
let signed = sign_ksn(&ckp, KeyStateNotice::new(state, "t")).with_receipts(vec![
witness_receipt(&w1kp, &w1, &controller, 1, &said),
witness_receipt(&w2kp, &w2, &controller, 1, &said),
]);
assert_eq!(
signed.verify().unwrap().trust,
KsnTrust::Witnessed {
receipts: 2,
threshold: 2
}
);
}
#[test]
fn ksn_stays_tofu_under_quorum() {
let ckp = real_keypair();
let (w1kp, w1) = witness_kp_and_aid();
let (_w2kp, w2) = witness_kp_and_aid();
let state = witnessed_state(&ckp, 1, &[&w1, &w2], 2, false);
let controller = state.prefix.as_str().to_string();
let said = state.last_event_said.as_str().to_string();
let signed = sign_ksn(&ckp, KeyStateNotice::new(state, "t"))
.with_receipts(vec![witness_receipt(&w1kp, &w1, &controller, 1, &said)]);
assert_eq!(signed.verify().unwrap().trust, KsnTrust::TrustOnFirstSight);
}
#[test]
fn ksn_ignores_duplicate_witness_receipts() {
let ckp = real_keypair();
let (w1kp, w1) = witness_kp_and_aid();
let (_w2kp, w2) = witness_kp_and_aid();
let (_w3kp, w3) = witness_kp_and_aid();
let state = witnessed_state(&ckp, 1, &[&w1, &w2, &w3], 2, false);
let controller = state.prefix.as_str().to_string();
let said = state.last_event_said.as_str().to_string();
let signed = sign_ksn(&ckp, KeyStateNotice::new(state, "t")).with_receipts(vec![
witness_receipt(&w1kp, &w1, &controller, 1, &said),
witness_receipt(&w1kp, &w1, &controller, 1, &said),
]);
assert_eq!(signed.verify().unwrap().trust, KsnTrust::TrustOnFirstSight);
}
#[test]
fn ksn_ignores_receipt_for_wrong_said() {
let ckp = real_keypair();
let (w1kp, w1) = witness_kp_and_aid();
let (w2kp, w2) = witness_kp_and_aid();
let state = witnessed_state(&ckp, 1, &[&w1, &w2], 2, false);
let controller = state.prefix.as_str().to_string();
let mut signed = sign_ksn(&ckp, KeyStateNotice::new(state, "t"));
let wrong = "EWrongEventSaid0000000000000000000000000000";
signed.receipts = vec![
witness_receipt(&w1kp, &w1, &controller, 1, wrong),
witness_receipt(&w2kp, &w2, &controller, 1, wrong),
];
assert_eq!(signed.verify().unwrap().trust, KsnTrust::TrustOnFirstSight);
}
#[test]
fn ksn_bt_zero_stays_tofu() {
let ckp = real_keypair();
let signed = sign_ksn(&ckp, KeyStateNotice::new(state_for_key(&ckp, 1), "t"));
assert_eq!(signed.verify().unwrap().trust, KsnTrust::TrustOnFirstSight);
}
#[test]
fn witnessed_device_ksn_still_refuses_revocation() {
let ckp = real_keypair();
let (w1kp, w1) = witness_kp_and_aid();
let (w2kp, w2) = witness_kp_and_aid();
let state = witnessed_state(&ckp, 1, &[&w1, &w2], 2, true); let controller = state.prefix.as_str().to_string();
let said = state.last_event_said.as_str().to_string();
let signed = sign_ksn(&ckp, KeyStateNotice::new(state, "t")).with_receipts(vec![
witness_receipt(&w1kp, &w1, &controller, 1, &said),
witness_receipt(&w2kp, &w2, &controller, 1, &said),
]);
let v = signed.verify().unwrap();
assert!(matches!(v.trust, KsnTrust::Witnessed { .. }));
assert!(!v.satisfies_revocation_check());
assert!(!v.is_authoritative_over_kel());
}
#[test]
fn populating_receipts_preserves_controller_signature() {
let ckp = real_keypair();
let (w1kp, w1) = witness_kp_and_aid();
let (w2kp, w2) = witness_kp_and_aid();
let state = witnessed_state(&ckp, 1, &[&w1, &w2], 2, false);
let controller = state.prefix.as_str().to_string();
let said = state.last_event_said.as_str().to_string();
let signed = sign_ksn(&ckp, KeyStateNotice::new(state, "t"));
assert!(signed.verify().is_ok());
let published = signed.with_receipts(vec![
witness_receipt(&w1kp, &w1, &controller, 1, &said),
witness_receipt(&w2kp, &w2, &controller, 1, &said),
]);
let v = published
.verify()
.expect("controller signature must still verify after attaching receipts");
assert!(matches!(v.trust, KsnTrust::Witnessed { .. }));
}
const ICP_KEL_JSON: &str = r#"[{
"v":"KERI10JSON0000fd_","t":"icp",
"d":"EOoC9AuwxiwcyUDsa2yNAaZOVWqfiAt4o3R31_8K2Z1J",
"i":"EOoC9AuwxiwcyUDsa2yNAaZOVWqfiAt4o3R31_8K2Z1J",
"s":"0","kt":"1",
"k":["DAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4f"],
"nt":"0","n":[],"bt":"0","b":[],"c":[],"a":[]
}]"#;
#[test]
fn key_state_record_emits_keri_wire_shape() {
let events = crate::validate::parse_kel_json(ICP_KEL_JSON).unwrap();
let state = crate::validate::TrustedKel::from_trusted_source(&events)
.replay()
.unwrap();
let record =
KeyStateRecord::from_kel(&events, &state, "2026-06-12T02:49:41.677319+00:00").unwrap();
let json = serde_json::to_value(&record).unwrap();
let obj = json.as_object().unwrap();
let keys: Vec<&str> = obj.keys().map(String::as_str).collect();
assert_eq!(
keys,
vec![
"vn", "i", "s", "p", "d", "f", "dt", "et", "kt", "k", "nt", "n", "bt", "b", "c",
"ee", "di"
]
);
assert_eq!(obj["vn"], serde_json::json!([1, 0]));
assert_eq!(obj["s"], "0");
assert_eq!(obj["p"], "");
assert_eq!(obj["et"], "icp");
assert_eq!(obj["kt"], "1");
assert_eq!(obj["di"], "");
let ee = obj["ee"].as_object().unwrap();
assert_eq!(
ee.keys().map(String::as_str).collect::<Vec<_>>(),
vec!["s", "d", "br", "ba"]
);
}
#[test]
fn key_state_record_round_trips_through_key_state() {
let events = crate::validate::parse_kel_json(ICP_KEL_JSON).unwrap();
let state = crate::validate::TrustedKel::from_trusted_source(&events)
.replay()
.unwrap();
let record = KeyStateRecord::from_kel(&events, &state, "t").unwrap();
let wire = serde_json::to_string(&record).unwrap();
let parsed: KeyStateRecord = serde_json::from_str(&wire).unwrap();
assert_eq!(parsed, record);
let projected = parsed.into_key_state();
assert_eq!(projected.prefix, state.prefix);
assert_eq!(projected.current_keys, state.current_keys);
assert_eq!(projected.sequence, state.sequence);
assert_eq!(projected.last_event_said, state.last_event_said);
assert_eq!(projected.threshold, state.threshold);
assert!(projected.is_non_transferable);
}
#[test]
fn key_state_record_check_not_stale() {
let events = crate::validate::parse_kel_json(ICP_KEL_JSON).unwrap();
let state = crate::validate::TrustedKel::from_trusted_source(&events)
.replay()
.unwrap();
let record = KeyStateRecord::from_kel(&events, &state, "t").unwrap();
assert_eq!(record.sequence(), 0);
assert!(record.check_not_stale(0).is_ok());
assert!(matches!(
record.check_not_stale(1),
Err(KsnError::Stale { got: 0, seen: 1 })
));
}
#[test]
fn key_state_record_ingests_peer_published_record() {
let wire = r#"{
"vn":[1,0],
"i":"EOoC9AuwxiwcyUDsa2yNAaZOVWqfiAt4o3R31_8K2Z1J",
"s":"0","p":"",
"d":"EOoC9AuwxiwcyUDsa2yNAaZOVWqfiAt4o3R31_8K2Z1J",
"f":"0","dt":"2026-06-12T02:49:41.677319+00:00","et":"icp",
"kt":"1","k":["DAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4f"],
"nt":"0","n":[],"bt":"0","b":[],"c":[],
"ee":{"s":"0","d":"EOoC9AuwxiwcyUDsa2yNAaZOVWqfiAt4o3R31_8K2Z1J","br":[],"ba":[]},
"di":""
}"#;
let record: KeyStateRecord = serde_json::from_str(wire).unwrap();
let state = record.into_key_state();
assert_eq!(
state.prefix.as_str(),
"EOoC9AuwxiwcyUDsa2yNAaZOVWqfiAt4o3R31_8K2Z1J"
);
assert_eq!(state.sequence, 0);
assert!(state.is_non_transferable);
assert!(state.delegator.is_none());
}
}