#[cfg(test)]
use alloc::borrow::Cow;
#[cfg(feature = "alloc")]
#[allow(
unused_imports,
reason = "alloc prelude items; subset used per cfg/feature combination"
)]
use alloc::{borrow::ToOwned, format, string::ToString, vec, vec::Vec};
use core::marker::PhantomData;
#[cfg(test)]
use crate::core::matter::builder::MatterBuilder;
use crate::core::matter::code::DigestCode;
#[cfg(test)]
use crate::core::matter::code::VerKeyCode;
use crate::core::primitives::{Diger, Prefixer, Saider, Seqner, Tholder, Verfer};
use crate::keri::{ConfigTrait, Identifier, InceptionEvent, Seal};
use super::witness::{validate_distinct, validate_toad};
use crate::serder::ample::ample;
use crate::serder::error::SerderError;
use crate::serder::said::compute_digest;
use crate::serder::serialize::SerializedEvent;
pub struct NeedsKeys;
pub struct Ready;
#[must_use]
pub struct InceptionBuilder<State = NeedsKeys> {
keys: Vec<Verfer<'static>>,
threshold: Option<Tholder>,
next_keys: Vec<Diger<'static>>,
next_threshold: Option<Tholder>,
witnesses: Vec<Prefixer<'static>>,
witness_threshold: Option<u32>,
config: Vec<ConfigTrait>,
anchors: Vec<Seal>,
said_code: DigestCode,
_state: PhantomData<State>,
}
pub(crate) fn dummy_saider(code: DigestCode) -> Result<Saider<'static>, SerderError> {
compute_digest(&[], code)
}
#[cfg(test)]
pub(crate) fn dummy_prefixer() -> Result<Prefixer<'static>, SerderError> {
MatterBuilder::new()
.with_code(VerKeyCode::Ed25519)
.with_raw(Cow::<[u8]>::Owned(vec![0u8; 32]))
.map_err(|e| SerderError::Validation(e.to_string()))?
.build()
.map_err(|e| SerderError::Validation(e.to_string()))
}
pub(crate) fn majority(n: usize) -> Result<u64, SerderError> {
let m = 1.max(n.div_ceil(2));
u64::try_from(m).map_err(|_| {
SerderError::Validation(format!(
"signing threshold majority for {n} keys exceeds the supported u64 range"
))
})
}
impl InceptionBuilder<NeedsKeys> {
pub const fn new() -> Self {
Self {
keys: Vec::new(),
threshold: None,
next_keys: Vec::new(),
next_threshold: None,
witnesses: Vec::new(),
witness_threshold: None,
config: Vec::new(),
anchors: Vec::new(),
said_code: DigestCode::Blake3_256,
_state: PhantomData,
}
}
pub fn keys(self, keys: Vec<Verfer<'static>>) -> InceptionBuilder<Ready> {
InceptionBuilder {
keys,
threshold: self.threshold,
next_keys: self.next_keys,
next_threshold: self.next_threshold,
witnesses: self.witnesses,
witness_threshold: self.witness_threshold,
config: self.config,
anchors: self.anchors,
said_code: self.said_code,
_state: PhantomData,
}
}
}
impl Default for InceptionBuilder<NeedsKeys> {
fn default() -> Self {
Self::new()
}
}
impl InceptionBuilder<Ready> {
pub fn threshold(mut self, threshold: Tholder) -> Self {
self.threshold = Some(threshold);
self
}
pub fn next_keys(mut self, next_keys: Vec<Diger<'static>>) -> Self {
self.next_keys = next_keys;
self
}
pub fn next_threshold(mut self, next_threshold: Tholder) -> Self {
self.next_threshold = Some(next_threshold);
self
}
pub fn witnesses(mut self, witnesses: Vec<Prefixer<'static>>) -> Self {
self.witnesses = witnesses;
self
}
pub const fn witness_threshold(mut self, witness_threshold: u32) -> Self {
self.witness_threshold = Some(witness_threshold);
self
}
pub fn config(mut self, config: Vec<ConfigTrait>) -> Self {
self.config = config;
self
}
pub fn anchors(mut self, anchors: Vec<Seal>) -> Self {
self.anchors = anchors;
self
}
pub const fn said_code(mut self, code: DigestCode) -> Self {
self.said_code = code;
self
}
pub fn build(self) -> Result<SerializedEvent, SerderError> {
if self.keys.is_empty() {
return Err(SerderError::Validation("keys must not be empty".to_owned()));
}
let threshold = match self.threshold {
Some(explicit) => explicit,
None => Tholder::Simple(majority(self.keys.len())?),
};
validate_threshold(&threshold, self.keys.len(), "signing")?;
let next_threshold = match self.next_threshold {
Some(explicit) => explicit,
None if self.next_keys.is_empty() => Tholder::Simple(0),
None => Tholder::Simple(majority(self.next_keys.len())?),
};
if !self.next_keys.is_empty() {
validate_threshold(&next_threshold, self.next_keys.len(), "next signing")?;
}
validate_distinct(&self.witnesses, "witnesses")?;
let witness_threshold = match self.witness_threshold {
Some(explicit) => explicit,
None => ample(self.witnesses.len())?,
};
validate_toad(witness_threshold, self.witnesses.len())?;
let event = InceptionEvent::new(
Identifier::SelfAddressing(dummy_saider(self.said_code)?),
Seqner::new(0),
dummy_saider(self.said_code)?,
self.keys,
threshold,
self.next_keys,
next_threshold,
self.witnesses,
witness_threshold,
self.config,
self.anchors,
);
crate::serder::serialize::icp::serialize_inception(&event)
}
}
pub(crate) fn validate_threshold(
threshold: &Tholder,
key_count: usize,
label: &str,
) -> Result<(), SerderError> {
threshold
.check_well_formed(key_count)
.map_err(|e| SerderError::Validation(format!("{label} threshold: {e}")))
}
#[cfg(test)]
#[allow(clippy::panic, reason = "panics are expected in test assertions")]
mod tests {
use alloc::borrow::Cow;
use crate::core::matter::builder::MatterBuilder;
use crate::core::matter::code::{DigestCode, VerKeyCode};
use crate::core::primitives::{Diger, Verfer};
use super::*;
fn make_verfer() -> Verfer<'static> {
MatterBuilder::new()
.with_code(VerKeyCode::Ed25519)
.with_raw(Cow::<[u8]>::Owned(vec![1u8; 32]))
.unwrap()
.build()
.unwrap()
}
fn make_diger() -> Diger<'static> {
MatterBuilder::new()
.with_code(DigestCode::Blake3_256)
.with_raw(Cow::<[u8]>::Owned(vec![2u8; 32]))
.unwrap()
.build()
.unwrap()
}
fn make_prefixer() -> Prefixer<'static> {
MatterBuilder::new()
.with_code(VerKeyCode::Ed25519)
.with_raw(Cow::<[u8]>::Owned(vec![3u8; 32]))
.unwrap()
.build()
.unwrap()
}
fn make_prefixer_tag(tag: u8) -> Prefixer<'static> {
MatterBuilder::new()
.with_code(VerKeyCode::Ed25519)
.with_raw(Cow::<[u8]>::Owned(vec![tag; 32]))
.unwrap()
.build()
.unwrap()
}
#[test]
fn majority_matches_keripy_default_threshold_table() {
let expected: [(usize, u64); 14] = [
(0, 1),
(1, 1),
(2, 1),
(3, 2),
(4, 2),
(5, 3),
(6, 3),
(7, 4),
(8, 4),
(9, 5),
(10, 5),
(11, 6),
(12, 6),
(13, 7),
];
for (n, want) in expected {
assert_eq!(majority(n).unwrap(), want, "majority({n})");
}
}
#[cfg(target_pointer_width = "64")]
#[test]
fn majority_succeeds_at_usize_boundary() {
assert_eq!(majority(usize::MAX).unwrap(), u64::MAX / 2 + 1);
assert_eq!(majority(usize::MAX - 1).unwrap(), u64::MAX / 2);
}
#[test]
fn build_minimal_inception() {
let result = InceptionBuilder::new()
.keys(vec![make_verfer()])
.build()
.unwrap();
assert_eq!(result.ilk(), crate::keri::Ilk::Icp);
let parsed: serde_json::Value = serde_json::from_slice(result.as_bytes()).unwrap();
assert_eq!(parsed["t"].as_str().unwrap(), "icp");
assert_eq!(parsed["s"].as_str().unwrap(), "0");
}
#[test]
fn build_with_all_options() {
let result = InceptionBuilder::new()
.keys(vec![make_verfer(), make_verfer()])
.threshold(Tholder::Simple(1))
.next_keys(vec![make_diger()])
.next_threshold(Tholder::Simple(1))
.witnesses(vec![make_prefixer()])
.witness_threshold(1)
.config(vec![ConfigTrait::EstOnly])
.anchors(vec![])
.build()
.unwrap();
let parsed: serde_json::Value = serde_json::from_slice(result.as_bytes()).unwrap();
assert_eq!(parsed["t"].as_str().unwrap(), "icp");
assert_eq!(parsed["kt"].as_str().unwrap(), "1");
let k = parsed["k"].as_array().unwrap();
assert_eq!(k.len(), 2);
let n = parsed["n"].as_array().unwrap();
assert_eq!(n.len(), 1);
let b = parsed["b"].as_array().unwrap();
assert_eq!(b.len(), 1);
let c = parsed["c"].as_array().unwrap();
assert_eq!(c.len(), 1);
assert_eq!(c[0].as_str().unwrap(), "EO");
}
#[test]
fn threshold_default_majority() {
let result = InceptionBuilder::new()
.keys(vec![make_verfer(), make_verfer(), make_verfer()])
.build()
.unwrap();
let parsed: serde_json::Value = serde_json::from_slice(result.as_bytes()).unwrap();
assert_eq!(parsed["kt"].as_str().unwrap(), "2");
}
#[test]
fn next_threshold_default_majority() {
let result = InceptionBuilder::new()
.keys(vec![make_verfer()])
.next_keys(vec![make_diger(), make_diger(), make_diger()])
.build()
.unwrap();
let parsed: serde_json::Value = serde_json::from_slice(result.as_bytes()).unwrap();
assert_eq!(parsed["nt"].as_str().unwrap(), "2");
}
#[test]
fn witness_threshold_default_ample() {
let result = InceptionBuilder::new()
.keys(vec![make_verfer()])
.witnesses(vec![
make_prefixer_tag(3),
make_prefixer_tag(4),
make_prefixer_tag(5),
])
.build()
.unwrap();
let parsed: serde_json::Value = serde_json::from_slice(result.as_bytes()).unwrap();
assert_eq!(parsed["bt"].as_str().unwrap(), "3");
}
#[test]
fn empty_next_keys_zero_threshold() {
let result = InceptionBuilder::new()
.keys(vec![make_verfer()])
.build()
.unwrap();
let parsed: serde_json::Value = serde_json::from_slice(result.as_bytes()).unwrap();
assert_eq!(parsed["nt"].as_str().unwrap(), "0");
let n = parsed["n"].as_array().unwrap();
assert!(n.is_empty());
}
#[test]
fn roundtrip() {
let serialized = InceptionBuilder::new()
.keys(vec![make_verfer()])
.next_keys(vec![make_diger()])
.build()
.unwrap();
let recovered =
crate::serder::deserialize::deserialize_inception(serialized.as_bytes()).unwrap();
assert_eq!(recovered.sn().value(), 0);
assert_eq!(recovered.keys().len(), 1);
assert_eq!(recovered.next_keys().len(), 1);
}
#[test]
fn said_is_valid() {
let result = InceptionBuilder::new()
.keys(vec![make_verfer()])
.build()
.unwrap();
let parsed: serde_json::Value = serde_json::from_slice(result.as_bytes()).unwrap();
let d = parsed["d"].as_str().unwrap();
let i = parsed["i"].as_str().unwrap();
assert_eq!(d, i, "d and i must be equal for inception events");
assert!(d.starts_with('E'));
assert_eq!(d.len(), 44);
}
#[test]
fn said_code_selects_digest_for_said_and_prefix() {
for code in [DigestCode::SHA3_256, DigestCode::Blake2b_256] {
let result = InceptionBuilder::new()
.keys(vec![make_verfer()])
.said_code(code)
.build()
.unwrap();
assert_eq!(*result.said().code(), code);
crate::serder::said::verify_said(result.as_bytes(), code)
.expect("SAID must verify under the selected code");
let parsed: serde_json::Value = serde_json::from_slice(result.as_bytes()).unwrap();
assert_eq!(
parsed["d"], parsed["i"],
"double-SAID must hold under the selected code"
);
let recovered =
crate::serder::deserialize::deserialize_inception(result.as_bytes()).unwrap();
assert_eq!(
*recovered.said().code(),
code,
"read path must infer the selected code"
);
}
}
#[test]
fn empty_keys_rejected() {
let result = InceptionBuilder::new().keys(vec![]).build();
let Err(err) = result else {
panic!("expected error");
};
assert!(err.to_string().contains("keys must not be empty"));
}
#[test]
fn threshold_exceeds_keys_rejected() {
let result = InceptionBuilder::new()
.keys(vec![make_verfer()])
.threshold(Tholder::Simple(5))
.build();
let Err(err) = result else {
panic!("expected error");
};
assert!(
err.to_string()
.contains("requires 5 keys but only 1 available")
);
}
#[test]
fn empty_weighted_clause_list_rejected() {
let result = InceptionBuilder::new()
.keys(vec![make_verfer()])
.threshold(Tholder::Weighted(vec![]))
.build();
let Err(err) = result else {
panic!("expected error");
};
assert!(err.to_string().contains("no clauses"));
}
#[test]
fn empty_weighted_clause_rejected() {
let result = InceptionBuilder::new()
.keys(vec![make_verfer()])
.threshold(Tholder::Weighted(vec![vec![]]))
.build();
let Err(err) = result else {
panic!("expected error");
};
assert!(err.to_string().contains("empty clause"));
}
#[test]
fn weighted_threshold_builds_end_to_end() {
let serialized = InceptionBuilder::new()
.keys(vec![make_verfer(), make_verfer(), make_verfer()])
.threshold(Tholder::Weighted(vec![vec![(1, 2), (1, 2), (1, 2)]]))
.build()
.unwrap();
let parsed: serde_json::Value = serde_json::from_slice(serialized.as_bytes()).unwrap();
assert_eq!(parsed["kt"], serde_json::json!(["1/2", "1/2", "1/2"]));
let recovered =
crate::serder::deserialize::deserialize_inception(serialized.as_bytes()).unwrap();
assert_eq!(
*recovered.threshold(),
Tholder::Weighted(vec![vec![(1, 2), (1, 2), (1, 2)]])
);
}
#[test]
fn sn_always_zero() {
let result = InceptionBuilder::new()
.keys(vec![make_verfer()])
.build()
.unwrap();
let parsed: serde_json::Value = serde_json::from_slice(result.as_bytes()).unwrap();
assert_eq!(parsed["s"].as_str().unwrap(), "0");
}
#[test]
fn default_impl() {
let builder = InceptionBuilder::default();
let result = builder.keys(vec![make_verfer()]).build().unwrap();
assert_eq!(result.ilk(), crate::keri::Ilk::Icp);
}
#[test]
fn duplicate_witnesses_rejected() {
let result = InceptionBuilder::new()
.keys(vec![make_verfer()])
.witnesses(vec![make_prefixer(), make_prefixer()])
.build();
let Err(SerderError::Validation(msg)) = result else {
panic!("duplicate witnesses must be rejected");
};
assert!(msg.contains("duplicates"), "unexpected message: {msg}");
}
#[test]
fn toad_exceeding_witness_count_rejected() {
let result = InceptionBuilder::new()
.keys(vec![make_verfer()])
.witnesses(vec![make_prefixer()])
.witness_threshold(2)
.build();
let Err(SerderError::Validation(msg)) = result else {
panic!("toad above the witness count must be rejected");
};
assert!(msg.contains("out of bounds"), "unexpected message: {msg}");
}
#[test]
fn toad_zero_with_witnesses_rejected() {
let result = InceptionBuilder::new()
.keys(vec![make_verfer()])
.witnesses(vec![make_prefixer()])
.witness_threshold(0)
.build();
let Err(SerderError::Validation(msg)) = result else {
panic!("zero toad alongside witnesses must be rejected");
};
assert!(msg.contains("out of bounds"), "unexpected message: {msg}");
}
#[test]
fn toad_nonzero_without_witnesses_rejected() {
let result = InceptionBuilder::new()
.keys(vec![make_verfer()])
.witness_threshold(1)
.build();
let Err(SerderError::Validation(msg)) = result else {
panic!("nonzero toad with no witnesses must be rejected");
};
assert!(msg.contains("out of bounds"), "unexpected message: {msg}");
}
}