use std::fmt;
use prikk_error::PrikkError;
use prikk_object::NodeId;
use crate::node_lifecycle::NodeLifecycleState;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum NodeIdMintError {
EntropyUnavailable(String),
ZeroNodeIdDraw,
NodeIdCollision(NodeId),
}
impl fmt::Display for NodeIdMintError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EntropyUnavailable(detail) => {
write!(
f,
"node-id minting failed: OS entropy unavailable: {detail}"
)
}
Self::ZeroNodeIdDraw => {
write!(f, "node-id minting failed: repeated all-zero CSPRNG draw")
}
Self::NodeIdCollision(id) => {
let hex: String = id.as_bytes().iter().map(|b| format!("{b:02x}")).collect();
write!(
f,
"node-id minting failed: repeated draw collided with a known node id {hex}"
)
}
}
}
}
impl std::error::Error for NodeIdMintError {}
impl From<NodeIdMintError> for PrikkError {
fn from(e: NodeIdMintError) -> Self {
PrikkError::Integrity(e.to_string())
}
}
pub(crate) trait NodeIdEntropySource {
fn fill_node_id_bytes(&mut self, out: &mut [u8; 32]) -> Result<(), NodeIdMintError>;
}
pub(crate) struct OsEntropySource;
impl NodeIdEntropySource for OsEntropySource {
fn fill_node_id_bytes(&mut self, out: &mut [u8; 32]) -> Result<(), NodeIdMintError> {
getrandom::getrandom(out).map_err(|e| NodeIdMintError::EntropyUnavailable(e.to_string()))
}
}
pub(crate) struct NodeIdGenerator<S> {
source: S,
}
enum Rejection {
Zero,
Collision(NodeId),
}
impl NodeIdGenerator<OsEntropySource> {
pub(crate) fn production() -> Self {
Self {
source: OsEntropySource,
}
}
}
impl<S: NodeIdEntropySource> NodeIdGenerator<S> {
#[cfg(test)]
pub(crate) fn with_source(source: S) -> Self {
Self { source }
}
fn draw_candidate(
&mut self,
baseline: &NodeLifecycleState,
) -> Result<Result<NodeId, Rejection>, NodeIdMintError> {
let mut bytes = [0_u8; 32];
self.source.fill_node_id_bytes(&mut bytes)?;
let candidate = match NodeId::try_from_bytes(bytes) {
Ok(id) => id,
Err(_) => return Ok(Err(Rejection::Zero)),
};
if baseline.contains_seen_node_id(&candidate) {
return Ok(Err(Rejection::Collision(candidate)));
}
Ok(Ok(candidate))
}
pub(crate) fn mint_fresh(
&mut self,
baseline: &NodeLifecycleState,
) -> Result<NodeId, NodeIdMintError> {
if let Ok(id) = self.draw_candidate(baseline)? {
return Ok(id);
}
match self.draw_candidate(baseline)? {
Ok(id) => Ok(id),
Err(Rejection::Zero) => Err(NodeIdMintError::ZeroNodeIdDraw),
Err(Rejection::Collision(id)) => Err(NodeIdMintError::NodeIdCollision(id)),
}
}
}
#[cfg(test)]
mod tests;
#[cfg(test)]
pub(crate) struct SequenceEntropySource {
queue: std::collections::VecDeque<[u8; 32]>,
}
#[cfg(test)]
impl SequenceEntropySource {
pub(crate) fn new(candidates: &[[u8; 32]]) -> Self {
Self {
queue: candidates.iter().copied().collect(),
}
}
}
#[cfg(test)]
impl NodeIdEntropySource for SequenceEntropySource {
fn fill_node_id_bytes(&mut self, out: &mut [u8; 32]) -> Result<(), NodeIdMintError> {
match self.queue.pop_front() {
Some(bytes) => {
*out = bytes;
Ok(())
}
None => Err(NodeIdMintError::EntropyUnavailable(
"test sequence exhausted".to_string(),
)),
}
}
}