use std::collections::HashSet;
use std::error::Error;
use std::fmt;
use std::fs::File;
use std::io::{self, BufRead, BufReader, BufWriter, Write};
use std::path::Path;
use std::str::FromStr;
use sha2::{Digest, Sha256};
use crate::{AgentId, ID_SPACE, ParseAgentIdError};
const DOMAIN_TAG: &[u8] = b"agenticon/v1";
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Chain {
Bitcoin,
Ethereum,
}
impl fmt::Display for Chain {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Chain::Bitcoin => "bitcoin",
Chain::Ethereum => "ethereum",
})
}
}
impl FromStr for Chain {
type Err = ParseChainError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"bitcoin" | "btc" => Ok(Chain::Bitcoin),
"ethereum" | "eth" => Ok(Chain::Ethereum),
_ => Err(ParseChainError { input: s.to_string() }),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ParseChainError {
input: String,
}
impl fmt::Display for ParseChainError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "unknown chain {:?}", self.input)
}
}
impl Error for ParseChainError {}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ChainAnchor {
pub chain: Chain,
pub height: u64,
pub block_hash: [u8; 32],
}
impl ChainAnchor {
pub fn from_hex(
chain: Chain,
height: u64,
hash_hex: &str,
) -> Result<Self, ParseBlockHashError> {
let err = || ParseBlockHashError { input: hash_hex.to_string() };
let hex = hash_hex.trim().trim_start_matches("0x");
if hex.len() != 64 || !hex.is_ascii() {
return Err(err());
}
let mut block_hash = [0u8; 32];
for (i, byte) in block_hash.iter_mut().enumerate() {
*byte = u8::from_str_radix(&hex[2 * i..2 * i + 2], 16)
.map_err(|_| err())?;
}
Ok(ChainAnchor { chain, height, block_hash })
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ParseBlockHashError {
input: String,
}
impl fmt::Display for ParseBlockHashError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"invalid block hash {:?}: expected 64 hex digits",
self.input
)
}
}
impl Error for ParseBlockHashError {}
#[must_use]
pub fn derive_id(anchor: &ChainAnchor, minter: &[u8], nonce: u32) -> AgentId {
let mut h = Sha256::new();
h.update(DOMAIN_TAG);
h.update(anchor.chain.to_string().as_bytes());
h.update(anchor.height.to_be_bytes());
h.update(anchor.block_hash);
h.update((minter.len() as u64).to_be_bytes());
h.update(minter);
h.update(nonce.to_be_bytes());
let digest = h.finalize();
let mut first = [0u8; 8];
first.copy_from_slice(&digest[..8]);
AgentId::try_new(u64::from_be_bytes(first) % ID_SPACE)
.expect("a value reduced modulo ID_SPACE is a valid id")
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct Minted {
pub id: AgentId,
pub nonce: u32,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Registry {
taken: HashSet<AgentId>,
}
impl Registry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn len(&self) -> usize {
self.taken.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.taken.is_empty()
}
#[must_use]
pub fn contains(&self, id: AgentId) -> bool {
self.taken.contains(&id)
}
pub fn insert(&mut self, id: AgentId) -> bool {
self.taken.insert(id)
}
pub fn mint(&mut self, anchor: &ChainAnchor, minter: &[u8]) -> Minted {
let mut nonce = 0u32;
loop {
let id = derive_id(anchor, minter, nonce);
if self.taken.insert(id) {
return Minted { id, nonce };
}
nonce = nonce
.checked_add(1)
.expect("every nonce for this anchor and minter is taken");
}
}
pub fn load(path: impl AsRef<Path>) -> Result<Self, LoadRegistryError> {
let file = File::open(path)?;
let mut reg = Registry::new();
for (i, line) in BufReader::new(file).lines().enumerate() {
let line = line?;
let line = line.trim();
if !line.is_empty() {
let id = line.parse().map_err(|source| {
LoadRegistryError::Parse { line: i + 1, source }
})?;
reg.taken.insert(id);
}
}
Ok(reg)
}
pub fn save(&self, path: impl AsRef<Path>) -> io::Result<()> {
let mut ids: Vec<AgentId> = self.taken.iter().copied().collect();
ids.sort_unstable();
let mut f = BufWriter::new(File::create(path)?);
for id in ids {
writeln!(f, "{id}")?;
}
f.flush()
}
}
impl FromIterator<AgentId> for Registry {
fn from_iter<I: IntoIterator<Item = AgentId>>(iter: I) -> Self {
Registry { taken: iter.into_iter().collect() }
}
}
impl Extend<AgentId> for Registry {
fn extend<I: IntoIterator<Item = AgentId>>(&mut self, iter: I) {
self.taken.extend(iter);
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum LoadRegistryError {
Io(io::Error),
Parse {
line: usize,
source: ParseAgentIdError,
},
}
impl fmt::Display for LoadRegistryError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LoadRegistryError::Io(_) => f.write_str("cannot read registry"),
LoadRegistryError::Parse { line, .. } => {
write!(f, "invalid agent id on registry line {line}")
}
}
}
}
impl Error for LoadRegistryError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
LoadRegistryError::Io(e) => Some(e),
LoadRegistryError::Parse { source, .. } => Some(source),
}
}
}
impl From<io::Error> for LoadRegistryError {
fn from(e: io::Error) -> Self {
LoadRegistryError::Io(e)
}
}