use std::borrow::Borrow;
use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Deserializer, Serialize};
use crate::keys::{KeriDecodeError, KeriPublicKey};
#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
#[error("Invalid KERI {type_name}: {reason}")]
pub struct KeriTypeError {
pub type_name: &'static str,
pub reason: String,
}
fn validate_prefix_derivation_code(s: &str) -> Result<(), KeriTypeError> {
if s.is_empty() {
return Err(KeriTypeError {
type_name: "Prefix",
reason: "must not be empty".into(),
});
}
let first = s.as_bytes()[0];
if !first.is_ascii_uppercase() && !first.is_ascii_digit() {
return Err(KeriTypeError {
type_name: "Prefix",
reason: format!(
"must start with a CESR derivation code (uppercase letter or digit), got '{}'",
&s[..s.len().min(10)]
),
});
}
Ok(())
}
fn validate_said_derivation_code(s: &str) -> Result<(), KeriTypeError> {
if s.is_empty() {
return Err(KeriTypeError {
type_name: "Said",
reason: "must not be empty".into(),
});
}
if !s.starts_with('E') {
return Err(KeriTypeError {
type_name: "Said",
reason: format!(
"must start with 'E' (Blake3 derivation code), got '{}'",
&s[..s.len().min(10)]
),
});
}
Ok(())
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[repr(transparent)]
pub struct Prefix(String);
impl Prefix {
pub fn new(s: String) -> Result<Self, KeriTypeError> {
validate_prefix_derivation_code(&s)?;
Ok(Self(s))
}
pub fn new_unchecked(s: String) -> Self {
Self(s)
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl<'de> Deserialize<'de> for Prefix {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
if s.is_empty() {
return Err(serde::de::Error::custom("Prefix must not be empty"));
}
Ok(Self(s))
}
}
impl fmt::Display for Prefix {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for Prefix {
fn as_ref(&self) -> &str {
&self.0
}
}
impl Borrow<str> for Prefix {
fn borrow(&self) -> &str {
&self.0
}
}
impl From<Prefix> for String {
fn from(p: Prefix) -> String {
p.0
}
}
impl PartialEq<str> for Prefix {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl PartialEq<&str> for Prefix {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
impl PartialEq<Prefix> for str {
fn eq(&self, other: &Prefix) -> bool {
self == other.0
}
}
impl PartialEq<Prefix> for &str {
fn eq(&self, other: &Prefix) -> bool {
*self == other.0
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[repr(transparent)]
pub struct Said(String);
impl Said {
pub fn new(s: String) -> Result<Self, KeriTypeError> {
validate_said_derivation_code(&s)?;
Ok(Self(s))
}
pub fn new_unchecked(s: String) -> Self {
Self(s)
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl<'de> Deserialize<'de> for Said {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
if s.is_empty() {
return Err(serde::de::Error::custom("Said must not be empty"));
}
Ok(Self(s))
}
}
impl fmt::Display for Said {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for Said {
fn as_ref(&self) -> &str {
&self.0
}
}
impl Borrow<str> for Said {
fn borrow(&self) -> &str {
&self.0
}
}
impl From<Said> for String {
fn from(s: Said) -> String {
s.0
}
}
impl PartialEq<str> for Said {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl PartialEq<&str> for Said {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
impl PartialEq<Said> for str {
fn eq(&self, other: &Said) -> bool {
self == other.0
}
}
impl PartialEq<Said> for &str {
fn eq(&self, other: &Said) -> bool {
*self == other.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Fraction {
pub numerator: u64,
pub denominator: u64,
}
#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
pub enum FractionError {
#[error("missing '/' separator in fraction: {0:?}")]
MissingSeparator(String),
#[error("invalid integer in fraction: {0}")]
InvalidInt(String),
#[error("fraction denominator must not be zero")]
ZeroDenominator,
}
impl Fraction {
pub fn sum_meets_one(fractions: &[&Fraction]) -> bool {
if fractions.is_empty() {
return false;
}
let mut num: u128 = 0;
let mut den: u128 = 1;
for f in fractions {
num = num * (f.denominator as u128) + (f.numerator as u128) * den;
den *= f.denominator as u128;
}
num >= den
}
}
impl FromStr for Fraction {
type Err = FractionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (num_str, den_str) = s
.split_once('/')
.ok_or_else(|| FractionError::MissingSeparator(s.to_string()))?;
let numerator: u64 = num_str
.parse()
.map_err(|_| FractionError::InvalidInt(num_str.to_string()))?;
let denominator: u64 = den_str
.parse()
.map_err(|_| FractionError::InvalidInt(den_str.to_string()))?;
if denominator == 0 {
return Err(FractionError::ZeroDenominator);
}
Ok(Self {
numerator,
denominator,
})
}
}
impl fmt::Display for Fraction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}", self.numerator, self.denominator)
}
}
impl Serialize for Fraction {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for Fraction {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Threshold {
Simple(u64),
Weighted(Vec<Vec<Fraction>>),
}
impl Threshold {
pub fn simple_value(&self) -> Option<u64> {
match self {
Threshold::Simple(v) => Some(*v),
Threshold::Weighted(_) => None,
}
}
pub fn validate_satisfiable(&self, count: usize) -> Result<(), KeriTypeError> {
match self {
Threshold::Simple(0) => Ok(()),
Threshold::Simple(n) if *n as usize > count => Err(KeriTypeError {
type_name: "Threshold",
reason: format!("simple threshold {n} exceeds list length {count}"),
}),
Threshold::Simple(_) => Ok(()),
Threshold::Weighted(clauses) => {
for clause in clauses {
if clause.len() != count {
return Err(KeriTypeError {
type_name: "Threshold",
reason: format!(
"weighted clause length {} != list length {count}",
clause.len()
),
});
}
}
Ok(())
}
}
}
pub fn is_satisfied(&self, verified_indices: &[u32], key_count: usize) -> bool {
let mut unique: std::collections::HashSet<u32> = std::collections::HashSet::new();
for &idx in verified_indices {
if (idx as usize) < key_count {
unique.insert(idx);
}
}
match self {
Threshold::Simple(required) => unique.len() as u64 >= *required,
Threshold::Weighted(clauses) => {
for clause in clauses {
let verified_fractions: Vec<&Fraction> = clause
.iter()
.enumerate()
.filter(|(i, _)| unique.contains(&(*i as u32)))
.map(|(_, f)| f)
.collect();
if !Fraction::sum_meets_one(&verified_fractions) {
return false;
}
}
true
}
}
}
}
impl Serialize for Threshold {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
Threshold::Simple(v) => serializer.serialize_str(&format!("{v:x}")),
Threshold::Weighted(clauses) => clauses.serialize(serializer),
}
}
}
impl<'de> Deserialize<'de> for Threshold {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = serde_json::Value::deserialize(deserializer)?;
match value {
serde_json::Value::String(s) => {
let v = u64::from_str_radix(&s, 16).map_err(|_| {
serde::de::Error::custom(format!("invalid hex threshold: {s:?}"))
})?;
Ok(Threshold::Simple(v))
}
serde_json::Value::Array(arr) => {
let clauses: Vec<Vec<Fraction>> = arr
.into_iter()
.map(|clause| match clause {
serde_json::Value::Array(weights) => weights
.into_iter()
.map(|w| match w {
serde_json::Value::String(s) => {
s.parse().map_err(serde::de::Error::custom)
}
_ => Err(serde::de::Error::custom("weight must be a string")),
})
.collect::<Result<Vec<_>, _>>(),
_ => Err(serde::de::Error::custom("clause must be an array")),
})
.collect::<Result<Vec<_>, _>>()?;
Ok(Threshold::Weighted(clauses))
}
_ => Err(serde::de::Error::custom(
"threshold must be a hex string or array of clause arrays",
)),
}
}
}
impl Default for Threshold {
fn default() -> Self {
Threshold::Simple(0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[repr(transparent)]
pub struct CesrKey(String);
impl CesrKey {
pub fn new_unchecked(s: String) -> Self {
Self(s)
}
pub fn parse(&self) -> Result<KeriPublicKey, KeriDecodeError> {
KeriPublicKey::parse(&self.0)
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
}
impl fmt::Display for CesrKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for CesrKey {
fn as_ref(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum ConfigTrait {
#[serde(rename = "EO")]
EstablishmentOnly,
#[serde(rename = "DND")]
DoNotDelegate,
#[serde(rename = "RB")]
RegistrarBackers,
#[serde(rename = "NRB")]
NoRegistrarBackers,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VersionString {
pub kind: String,
pub size: u32,
}
impl VersionString {
pub fn json(size: u32) -> Self {
Self {
kind: "JSON".to_string(),
size,
}
}
pub fn placeholder() -> Self {
Self {
kind: "JSON".to_string(),
size: 0,
}
}
}
impl Default for VersionString {
fn default() -> Self {
Self::placeholder()
}
}
impl fmt::Display for VersionString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "KERI10{}{:06x}_", self.kind, self.size)
}
}
impl Serialize for VersionString {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for VersionString {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
if s.len() >= 17 && s.ends_with('_') {
let size_hex = &s[10..16];
let size = u32::from_str_radix(size_hex, 16).map_err(|_| {
serde::de::Error::custom(format!("invalid version string size: {size_hex:?}"))
})?;
let kind = s[6..10].to_string();
Ok(Self { kind, size })
} else {
Err(serde::de::Error::custom(format!(
"invalid KERI version string: {s:?}"
)))
}
}
}
#[cfg(feature = "schema")]
mod schema_impls {
use super::*;
impl schemars::JsonSchema for Fraction {
fn schema_name() -> String {
"Fraction".to_string()
}
fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
schemars::schema::SchemaObject {
instance_type: Some(schemars::schema::InstanceType::String.into()),
..Default::default()
}
.into()
}
}
impl schemars::JsonSchema for Threshold {
fn schema_name() -> String {
"Threshold".to_string()
}
fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
schemars::schema::Schema::Bool(true)
}
}
impl schemars::JsonSchema for crate::events::Seal {
fn schema_name() -> String {
"Seal".to_string()
}
fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
schemars::schema::Schema::Bool(true)
}
}
impl schemars::JsonSchema for VersionString {
fn schema_name() -> String {
"VersionString".to_string()
}
fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
schemars::schema::SchemaObject {
instance_type: Some(schemars::schema::InstanceType::String.into()),
..Default::default()
}
.into()
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn fraction_parse_valid() {
let f: Fraction = "1/3".parse().unwrap();
assert_eq!(f.numerator, 1);
assert_eq!(f.denominator, 3);
}
#[test]
fn fraction_parse_rejects_zero_denominator() {
let err = "1/0".parse::<Fraction>().unwrap_err();
assert_eq!(err, FractionError::ZeroDenominator);
}
#[test]
fn fraction_parse_rejects_missing_separator() {
assert!("42".parse::<Fraction>().is_err());
}
#[test]
fn fraction_serde_roundtrip() {
let f = Fraction {
numerator: 1,
denominator: 2,
};
let json = serde_json::to_string(&f).unwrap();
assert_eq!(json, "\"1/2\"");
let parsed: Fraction = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, f);
}
#[test]
fn fraction_display() {
let f = Fraction {
numerator: 3,
denominator: 4,
};
assert_eq!(f.to_string(), "3/4");
}
#[test]
fn threshold_simple_from_hex() {
let t: Threshold = serde_json::from_str("\"a\"").unwrap();
assert_eq!(t, Threshold::Simple(10));
assert_eq!(t.simple_value(), Some(10));
}
#[test]
fn threshold_simple_serialize_as_hex() {
let t = Threshold::Simple(16);
let json = serde_json::to_string(&t).unwrap();
assert_eq!(json, "\"10\""); }
#[test]
fn threshold_simple_roundtrip() {
let t = Threshold::Simple(2);
let json = serde_json::to_string(&t).unwrap();
let parsed: Threshold = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, t);
}
#[test]
fn threshold_weighted_roundtrip() {
let json = r#"[["1/2","1/2"],["1/3","1/3","1/3"]]"#;
let t: Threshold = serde_json::from_str(json).unwrap();
assert!(t.simple_value().is_none());
if let Threshold::Weighted(clauses) = &t {
assert_eq!(clauses.len(), 2);
assert_eq!(clauses[0].len(), 2);
assert_eq!(clauses[1].len(), 3);
assert_eq!(clauses[0][0].numerator, 1);
assert_eq!(clauses[0][0].denominator, 2);
} else {
panic!("expected Weighted");
}
let reserialized = serde_json::to_string(&t).unwrap();
let reparsed: Threshold = serde_json::from_str(&reserialized).unwrap();
assert_eq!(reparsed, t);
}
#[test]
fn threshold_rejects_invalid_hex() {
let result = serde_json::from_str::<Threshold>("\"xyz\"");
assert!(result.is_err());
}
#[test]
fn fraction_sum_one_third_times_three() {
let f: Fraction = "1/3".parse().unwrap();
assert!(Fraction::sum_meets_one(&[&f, &f, &f]));
}
#[test]
fn fraction_sum_two_thirds_not_enough() {
let f: Fraction = "1/3".parse().unwrap();
assert!(!Fraction::sum_meets_one(&[&f, &f]));
}
#[test]
fn fraction_sum_halves() {
let f: Fraction = "1/2".parse().unwrap();
assert!(Fraction::sum_meets_one(&[&f, &f]));
assert!(!Fraction::sum_meets_one(&[&f]));
}
#[test]
fn fraction_sum_empty_is_false() {
assert!(!Fraction::sum_meets_one(&[]));
}
#[test]
fn threshold_simple_satisfied() {
let t = Threshold::Simple(2);
assert!(t.is_satisfied(&[0, 1], 3));
assert!(t.is_satisfied(&[0, 1, 2], 3));
assert!(!t.is_satisfied(&[0], 3));
}
#[test]
fn threshold_simple_zero_always_satisfied() {
let t = Threshold::Simple(0);
assert!(t.is_satisfied(&[], 3));
}
#[test]
fn threshold_simple_deduplicates_indices() {
let t = Threshold::Simple(2);
assert!(!t.is_satisfied(&[0, 0], 3));
}
#[test]
fn threshold_simple_rejects_out_of_range() {
let t = Threshold::Simple(1);
assert!(!t.is_satisfied(&[5], 3));
}
#[test]
fn threshold_weighted_two_of_three() {
let t: Threshold = serde_json::from_str(r#"[["1/2","1/2","1/2"]]"#).unwrap();
assert!(t.is_satisfied(&[0, 1], 3));
assert!(t.is_satisfied(&[1, 2], 3));
assert!(!t.is_satisfied(&[0], 3));
}
#[test]
fn threshold_weighted_with_reserves() {
let t: Threshold = serde_json::from_str(r#"[["1/2","1/2","1/2","1/4","1/4"]]"#).unwrap();
assert!(t.is_satisfied(&[0, 1], 5)); assert!(t.is_satisfied(&[0, 3, 4], 5)); assert!(!t.is_satisfied(&[3, 4], 5)); }
#[test]
fn threshold_weighted_multi_clause_and() {
let t: Threshold = serde_json::from_str(r#"[["1/2","1/2"],["1/3","1/3","1/3"]]"#).unwrap();
assert!(!t.is_satisfied(&[0, 1], 3));
assert!(t.is_satisfied(&[0, 1, 2], 3));
}
#[test]
fn cesr_key_roundtrip() {
let key_str = "DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
let key = CesrKey::new_unchecked(key_str.to_string());
let json = serde_json::to_string(&key).unwrap();
let parsed: CesrKey = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.as_str(), key_str);
}
#[test]
fn cesr_key_parse_valid() {
let key =
CesrKey::new_unchecked("DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string());
assert!(key.parse().is_ok());
}
#[test]
fn cesr_key_parse_invalid() {
let key = CesrKey::new_unchecked("not-a-valid-key".to_string());
assert!(key.parse().is_err());
}
#[test]
fn config_trait_serde_roundtrip() {
let traits = vec![ConfigTrait::EstablishmentOnly, ConfigTrait::DoNotDelegate];
let json = serde_json::to_string(&traits).unwrap();
assert_eq!(json, r#"["EO","DND"]"#);
let parsed: Vec<ConfigTrait> = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, traits);
}
#[test]
fn config_trait_all_variants_roundtrip() {
let all = vec![
ConfigTrait::EstablishmentOnly,
ConfigTrait::DoNotDelegate,
ConfigTrait::RegistrarBackers,
ConfigTrait::NoRegistrarBackers,
];
let json = serde_json::to_string(&all).unwrap();
assert_eq!(json, r#"["EO","DND","RB","NRB"]"#);
let parsed: Vec<ConfigTrait> = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, all);
}
#[test]
fn version_string_display() {
let vs = VersionString::json(256);
assert_eq!(vs.to_string(), "KERI10JSON000100_");
}
#[test]
fn version_string_placeholder() {
let vs = VersionString::placeholder();
assert_eq!(vs.to_string(), "KERI10JSON000000_");
assert_eq!(vs.size, 0);
}
#[test]
fn version_string_parse_full() {
let vs: VersionString = serde_json::from_str("\"KERI10JSON000100_\"").unwrap();
assert_eq!(vs.kind, "JSON");
assert_eq!(vs.size, 256);
}
#[test]
fn version_string_rejects_legacy_short() {
assert!(serde_json::from_str::<VersionString>("\"KERI10JSON\"").is_err());
assert!(serde_json::from_str::<VersionString>("\"KERI10JSON0001\"").is_err());
}
#[test]
fn version_string_roundtrip() {
let vs = VersionString::json(1024);
let json = serde_json::to_string(&vs).unwrap();
let parsed: VersionString = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, vs);
}
#[test]
fn version_string_rejects_invalid() {
assert!(serde_json::from_str::<VersionString>("\"INVALID\"").is_err());
}
}