open-standards/smart-byte/smart-byte-rs/crates/smart-byte-acdc/src/edge.rs
Transaction Science 68b8a2dbee ingest wave F: 3 crates · op-crypto-extra + smart-byte-acdc + eoc-memory
OpenPay (1 crate, 3751 LOC, 83 tests)
  op-crypto-extra      Extended crypto rails. Stablecoin catalog (USDC, USDT,
                       PYUSD, DAI, RLUSD, EURC, FDUSD — 26 deployments). L2
                       catalog (12 chains incl. Optimism, Arbitrum, Base,
                       zkSync Era, Polygon zkEVM, Linea, Scroll, Starknet).
                       CCTP v2 BurnMessage encode/decode + domain ids +
                       attestation. ERC-4337 UserOperation v0.7 +
                       PackedUserOperation. EIP-7702 set-EOA-code with RLP
                       signing preimage. Uniswap Permit2 single + batch
                       transfer EIP-712 hashes. EIP-2612 ERC-20 permit.
                       BOLT-11 bech32 invoices + LNURL decode. Bitcoin
                       PSBT v2 (BIP-370). Atomic-swap HTLC. Solana SPL /
                       Aleo / Aztec confidential-transfer sketches.

Smart Byte (1 crate, 2304 LOC, 30 tests)
  smart-byte-acdc      Authentic Chained Data Containers (KERI ecosystem).
                       Acdc + AcdcBuilder (targeted + untargeted) with SAID
                       derivation. JSON Schema 2020-12 validator with
                       oneOf partial-disclosure. TEL (RIP/VRT/ISS/REV)
                       with hash-chained verification. IPEX (apply/offer/
                       agree/grant/admit/spurn) chained by SAID. Edge
                       records (I2I/NI2I/DI2I) + EdgeGraph BFS traversal
                       with cycle detection. Selective disclosure with
                       full/none/subset plans + compact packing.
                       CredentialRegistry trait + InMemoryRegistry over
                       TEL. Composes with smart-byte-core + smart-byte-keri-witness.

EOC (1 crate, 1764 LOC, 16 tests)
  eoc-memory           Long-term memory primitives. Memory trait + MemoryKind +
                       EpisodeId. Episodic event log with TimeIndex range
                       queries. RDF-style Triple store with S/P/O indices.
                       Bounded attention-budget Scratchpad with
                       salience-weighted eviction. Rolling SummaryBuffer +
                       FIFO SlidingWindow. Dream-cycle consolidate()
                       (episodic → semantic). Ebbinghaus forgetting curve
                       with reinforcement. Retriever blending cosine
                       similarity + recency + saturating frequency.
                       MemGpt three-tier shape (Main/Recall/Archival).

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-24 17:28:24 -04:00

289 lines
8.9 KiB
Rust

//! Edge section + graph traversal.
//!
//! An ACDC's `e` field is a JSON object mapping local labels to *edge
//! records*. Per spec §4 an edge record has the shape:
//!
//! ```json
//! { "n": "<target ACDC SAID>", "s": "<schema SAID>", "o": "I2I" }
//! ```
//!
//! where:
//!
//! * `n` — node SAID (the target credential the edge points to);
//! * `s` — schema SAID expected at the target;
//! * `o` — operator (e.g. `I2I` issuer-to-issuer, `NI2I` not-issuer,
//! `DI2I` delegated). Defaults to `I2I`.
//!
//! Multiple edges form a directed graph among ACDCs. [`EdgeGraph`]
//! stores credentials by SAID and supports breadth-first traversal +
//! cycle detection.
use std::collections::{HashMap, HashSet, VecDeque};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use smart_byte_core::Said;
use crate::acdc::{Acdc, EdgeSection};
use crate::error::{AcdcError, Result};
/// Edge operator vocabulary.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum EdgeOp {
/// Issuer-to-Issuer. Default operator.
I2I,
/// Not Issuer-to-Issuer (anyone may issue the target).
Ni2i,
/// Delegated Issuer-to-Issuer.
Di2i,
}
impl EdgeOp {
/// Parse from the canonical text encoding (`"I2I"`, `"NI2I"`,
/// `"DI2I"`).
pub fn from_str(s: &str) -> Result<Self> {
match s {
"I2I" => Ok(Self::I2I),
"NI2I" => Ok(Self::Ni2i),
"DI2I" => Ok(Self::Di2i),
other => Err(AcdcError::MalformedField {
field: "o",
detail: format!("unknown edge operator {other}"),
}),
}
}
}
/// A parsed edge record.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Edge {
/// Local edge label (key in the `e` object).
pub label: String,
/// Target ACDC SAID (`n`).
pub target: Said,
/// Expected schema SAID at the target (`s`). `None` if the edge
/// does not constrain the target schema.
pub schema: Option<Said>,
/// Operator. Defaults to [`EdgeOp::I2I`].
pub operator: EdgeOp,
}
impl Edge {
/// Parse all edges out of an [`EdgeSection`]. Errors if any edge
/// record is malformed.
pub fn parse_section(section: &EdgeSection) -> Result<Vec<Self>> {
let mut out = Vec::with_capacity(section.0.len());
for (label, v) in &section.0 {
let obj = v.as_object().ok_or_else(|| AcdcError::MalformedField {
field: "e",
detail: format!("edge `{label}` is not an object"),
})?;
let target_s = obj
.get("n")
.and_then(|v| v.as_str())
.ok_or(AcdcError::MissingField("e.*.n"))?;
let target = Said::from_base32(target_s).map_err(|e| AcdcError::MalformedField {
field: "e.*.n",
detail: e.to_string(),
})?;
let schema = match obj.get("s").and_then(|v| v.as_str()) {
Some(s) => Some(Said::from_base32(s).map_err(|e| AcdcError::MalformedField {
field: "e.*.s",
detail: e.to_string(),
})?),
None => None,
};
let operator = match obj.get("o").and_then(|v| v.as_str()) {
Some(s) => EdgeOp::from_str(s)?,
None => EdgeOp::I2I,
};
out.push(Self {
label: label.clone(),
target,
schema,
operator,
});
}
Ok(out)
}
/// Build a JSON record suitable for embedding into an
/// [`EdgeSection`].
pub fn to_json(&self) -> Value {
let mut m = serde_json::Map::new();
m.insert("n".into(), Value::String(self.target.to_base32()));
if let Some(s) = self.schema {
m.insert("s".into(), Value::String(s.to_base32()));
}
let op = match self.operator {
EdgeOp::I2I => "I2I",
EdgeOp::Ni2i => "NI2I",
EdgeOp::Di2i => "DI2I",
};
m.insert("o".into(), Value::String(op.into()));
Value::Object(m)
}
}
/// In-memory directed graph of ACDCs keyed by SAID.
#[derive(Clone, Debug, Default)]
pub struct EdgeGraph {
nodes: HashMap<Said, Acdc>,
}
impl EdgeGraph {
/// Empty graph.
pub fn new() -> Self {
Self::default()
}
/// Insert (or replace) an ACDC node.
pub fn insert(&mut self, acdc: Acdc) -> Result<()> {
acdc.verify_said()?;
self.nodes.insert(acdc.d, acdc);
Ok(())
}
/// Number of nodes.
pub fn len(&self) -> usize {
self.nodes.len()
}
/// Is empty.
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
/// Look up an ACDC by SAID.
pub fn get(&self, said: &Said) -> Option<&Acdc> {
self.nodes.get(said)
}
/// Parsed outgoing edges of an ACDC.
pub fn edges_of(&self, said: &Said) -> Result<Vec<Edge>> {
let node = self
.nodes
.get(said)
.ok_or_else(|| AcdcError::EdgeMissing(*said))?;
Edge::parse_section(&node.e)
}
/// Breadth-first traversal beginning at `start`. Visits each
/// reachable node at most once; errors if an edge points to a SAID
/// not present in the graph or if a cycle is detected.
pub fn traverse(&self, start: &Said) -> Result<Vec<Said>> {
if !self.nodes.contains_key(start) {
return Err(AcdcError::EdgeMissing(*start));
}
let mut order = Vec::new();
let mut seen: HashSet<Said> = HashSet::new();
let mut on_path: HashSet<Said> = HashSet::new();
let mut queue: VecDeque<Said> = VecDeque::new();
queue.push_back(*start);
seen.insert(*start);
while let Some(node_said) = queue.pop_front() {
if on_path.contains(&node_said) {
return Err(AcdcError::EdgeCycle(node_said));
}
on_path.insert(node_said);
order.push(node_said);
let edges = self.edges_of(&node_said)?;
for edge in edges {
if edge.target == node_said {
return Err(AcdcError::EdgeCycle(node_said));
}
if !self.nodes.contains_key(&edge.target) {
return Err(AcdcError::EdgeMissing(edge.target));
}
if seen.contains(&edge.target) {
// Already visited or queued; if it's currently on
// the active path that's a cycle.
if on_path.contains(&edge.target) {
return Err(AcdcError::EdgeCycle(edge.target));
}
continue;
}
seen.insert(edge.target);
queue.push_back(edge.target);
}
}
Ok(order)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::acdc::{AcdcBuilder, AttributeSection, SchemaSection};
use serde_json::json;
fn mk_acdc(name: &str, edges: Vec<Edge>) -> Acdc {
let mut s = serde_json::Map::new();
s.insert("$id".into(), json!(name));
let mut a = serde_json::Map::new();
a.insert("name".into(), json!(name));
let mut edge_section = EdgeSection::default();
for e in edges {
edge_section.0.insert(e.label.clone(), e.to_json());
}
AcdcBuilder::new()
.issuer("Bissuer")
.schema(SchemaSection::Inline(s))
.attributes(AttributeSection::Inline(a))
.edges(edge_section)
.build()
.expect("build")
}
#[test]
fn parses_and_traverses_chain() {
let leaf = mk_acdc("leaf", vec![]);
let mid = mk_acdc(
"mid",
vec![Edge {
label: "down".into(),
target: leaf.d,
schema: None,
operator: EdgeOp::I2I,
}],
);
let root = mk_acdc(
"root",
vec![Edge {
label: "child".into(),
target: mid.d,
schema: None,
operator: EdgeOp::Ni2i,
}],
);
let mut g = EdgeGraph::new();
let root_id = root.d;
g.insert(leaf).expect("ins");
g.insert(mid).expect("ins");
g.insert(root).expect("ins");
let order = g.traverse(&root_id).expect("traverse");
assert_eq!(order.len(), 3);
}
#[test]
fn missing_edge_target_detected() {
let leaf = mk_acdc(
"orphan",
vec![Edge {
label: "ghost".into(),
target: Said([0xAB; 32]),
schema: None,
operator: EdgeOp::I2I,
}],
);
let leaf_id = leaf.d;
let mut g = EdgeGraph::new();
g.insert(leaf).expect("ins");
assert!(matches!(
g.traverse(&leaf_id),
Err(AcdcError::EdgeMissing(_))
));
}
}