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]>
311 lines
10 KiB
Rust
311 lines
10 KiB
Rust
//! The Authentic Chained Data Container body.
|
|
//!
|
|
//! An ACDC is a labeled bundle of SAIDed sections:
|
|
//!
|
|
//! | Field | Meaning |
|
|
//! |-------|-----------------------------------------------------------|
|
|
//! | `v` | Version string (e.g. [`crate::VERSION_STRING`]). |
|
|
//! | `d` | SAID of the whole credential. |
|
|
//! | `i` | Issuer AID (KERI controller). |
|
|
//! | `ri` | Registry SAID (TEL anchor) — optional for untargeted. |
|
|
//! | `s` | Schema section SAID. |
|
|
//! | `a` | Attribute section: either an inline object or a SAID. |
|
|
//! | `e` | Edge section: links to other ACDCs by SAID. |
|
|
//! | `r` | Rule section: machine-readable terms of use. |
|
|
//!
|
|
//! Two flavours are supported per spec §3.4:
|
|
//!
|
|
//! * **Targeted**: `a` contains an `i` (subject AID). The credential
|
|
//! binds a claim about a specific party.
|
|
//! * **Untargeted**: `a` has no `i`; the credential is a bearer-style
|
|
//! claim (e.g. a schema attestation).
|
|
//!
|
|
//! The SAID of an ACDC is derived as in KERI: place
|
|
//! [`crate::SAID_PLACEHOLDER`] into `d`, JCS-encode, BLAKE3-256 the
|
|
//! bytes, store the digest back. Every section that carries its own
|
|
//! `d` field uses the same procedure.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::Value;
|
|
use smart_byte_core::Said;
|
|
|
|
use crate::error::{AcdcError, Result};
|
|
|
|
/// The schema section of an ACDC.
|
|
///
|
|
/// In the wire form `s` may be either an inline schema object or a
|
|
/// pointer (just the schema SAID). We store both arms explicitly so
|
|
/// callers can build either shape.
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
#[serde(untagged)]
|
|
pub enum SchemaSection {
|
|
/// Inline schema. The map's `$id` is the schema SAID.
|
|
Inline(serde_json::Map<String, Value>),
|
|
/// Just a SAID reference to a schema known out-of-band.
|
|
Reference(Said),
|
|
}
|
|
|
|
/// The registry section. An ACDC anchored to a TEL carries the
|
|
/// registry's SAID here (`ri`). Untargeted/uncontrolled ACDCs omit it.
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
#[serde(transparent)]
|
|
pub struct RegistrySection(pub Said);
|
|
|
|
/// The attribute section. Either inline (claims about the subject) or a
|
|
/// SAID reference into a separate disclosure store (used by selective
|
|
/// disclosure when the inline attributes have been replaced with a
|
|
/// digest).
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
#[serde(untagged)]
|
|
pub enum AttributeSection {
|
|
/// Inline attribute map. The map's `d` field (if present) is the
|
|
/// section's SAID.
|
|
Inline(serde_json::Map<String, Value>),
|
|
/// Compact form: just the section SAID. Used by selective
|
|
/// disclosure to elide undisclosed attributes.
|
|
Compact(Said),
|
|
}
|
|
|
|
impl AttributeSection {
|
|
/// Borrow the inline map, if this is the inline form.
|
|
pub fn as_inline(&self) -> Option<&serde_json::Map<String, Value>> {
|
|
match self {
|
|
Self::Inline(m) => Some(m),
|
|
Self::Compact(_) => None,
|
|
}
|
|
}
|
|
|
|
/// Subject AID for a targeted ACDC, if present. The subject is the
|
|
/// inline attribute map's `i` field.
|
|
pub fn subject_aid(&self) -> Option<&str> {
|
|
self.as_inline()
|
|
.and_then(|m| m.get("i"))
|
|
.and_then(|v| v.as_str())
|
|
}
|
|
}
|
|
|
|
/// The edge section is a JSON object mapping local edge labels to edge
|
|
/// records. We store it as an opaque JSON map and let [`crate::edge`]
|
|
/// parse the structure when graph traversal is needed.
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)]
|
|
#[serde(transparent)]
|
|
pub struct EdgeSection(pub serde_json::Map<String, Value>);
|
|
|
|
/// An Authentic Chained Data Container.
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
pub struct Acdc {
|
|
/// Version string. Defaults to [`crate::VERSION_STRING`].
|
|
pub v: String,
|
|
/// Self-addressing identifier of the whole credential.
|
|
pub d: Said,
|
|
/// Issuer AID (KERI controller AID).
|
|
pub i: String,
|
|
/// Registry SAID for the credential's TEL, if any.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub ri: Option<Said>,
|
|
/// Schema section.
|
|
pub s: SchemaSection,
|
|
/// Attribute section.
|
|
pub a: AttributeSection,
|
|
/// Edge section (links to other ACDCs).
|
|
#[serde(default, skip_serializing_if = "is_empty_edges")]
|
|
pub e: EdgeSection,
|
|
/// Rule section.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub r: Option<serde_json::Map<String, Value>>,
|
|
}
|
|
|
|
fn is_empty_edges(e: &EdgeSection) -> bool {
|
|
e.0.is_empty()
|
|
}
|
|
|
|
impl Acdc {
|
|
/// True when the attribute section carries a subject AID (`a.i`).
|
|
pub fn is_targeted(&self) -> bool {
|
|
self.a.subject_aid().is_some()
|
|
}
|
|
|
|
/// Encode this ACDC as canonical JSON per RFC 8785 (JCS).
|
|
pub fn to_jcs(&self) -> Result<Vec<u8>> {
|
|
serde_jcs::to_vec(self).map_err(|e| AcdcError::Jcs(e.to_string()))
|
|
}
|
|
|
|
/// Recompute the SAID over the credential body with `d` replaced by
|
|
/// the spec placeholder, and verify it matches `self.d`.
|
|
pub fn verify_said(&self) -> Result<()> {
|
|
let computed = self.compute_said()?;
|
|
if computed != self.d {
|
|
return Err(AcdcError::SaidMismatch {
|
|
asserted: self.d,
|
|
computed,
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Compute the SAID over the body with `d` set to the placeholder.
|
|
pub fn compute_said(&self) -> Result<Said> {
|
|
let mut tmp = self.clone();
|
|
tmp.d = placeholder_said();
|
|
let bytes = tmp.to_jcs()?;
|
|
Ok(Said::hash(&bytes))
|
|
}
|
|
|
|
/// Parse from canonical JSON.
|
|
pub fn from_json(bytes: &[u8]) -> Result<Self> {
|
|
let acdc: Self = serde_json::from_slice(bytes)?;
|
|
Ok(acdc)
|
|
}
|
|
}
|
|
|
|
/// Build an [`Acdc`] from parts and seal it with its SAID.
|
|
#[derive(Default, Clone, Debug)]
|
|
pub struct AcdcBuilder {
|
|
issuer: Option<String>,
|
|
registry: Option<Said>,
|
|
schema: Option<SchemaSection>,
|
|
attributes: Option<AttributeSection>,
|
|
edges: EdgeSection,
|
|
rules: Option<serde_json::Map<String, Value>>,
|
|
}
|
|
|
|
impl AcdcBuilder {
|
|
/// New empty builder.
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Set the issuer AID.
|
|
pub fn issuer(mut self, aid: impl Into<String>) -> Self {
|
|
self.issuer = Some(aid.into());
|
|
self
|
|
}
|
|
|
|
/// Anchor to a registry SAID.
|
|
pub fn registry(mut self, said: Said) -> Self {
|
|
self.registry = Some(said);
|
|
self
|
|
}
|
|
|
|
/// Set the schema section.
|
|
pub fn schema(mut self, schema: SchemaSection) -> Self {
|
|
self.schema = Some(schema);
|
|
self
|
|
}
|
|
|
|
/// Set the attribute section.
|
|
pub fn attributes(mut self, attrs: AttributeSection) -> Self {
|
|
self.attributes = Some(attrs);
|
|
self
|
|
}
|
|
|
|
/// Replace the edge section.
|
|
pub fn edges(mut self, edges: EdgeSection) -> Self {
|
|
self.edges = edges;
|
|
self
|
|
}
|
|
|
|
/// Add one edge label -> JSON record.
|
|
pub fn edge(mut self, label: impl Into<String>, edge: Value) -> Self {
|
|
self.edges.0.insert(label.into(), edge);
|
|
self
|
|
}
|
|
|
|
/// Set the rule section.
|
|
pub fn rules(mut self, rules: serde_json::Map<String, Value>) -> Self {
|
|
self.rules = Some(rules);
|
|
self
|
|
}
|
|
|
|
/// Finish building. Computes the SAID and writes it into `d`.
|
|
pub fn build(self) -> Result<Acdc> {
|
|
let issuer = self.issuer.ok_or(AcdcError::MissingField("i"))?;
|
|
let schema = self.schema.ok_or(AcdcError::MissingField("s"))?;
|
|
let attributes = self.attributes.ok_or(AcdcError::MissingField("a"))?;
|
|
|
|
let mut acdc = Acdc {
|
|
v: crate::VERSION_STRING.to_string(),
|
|
d: placeholder_said(),
|
|
i: issuer,
|
|
ri: self.registry,
|
|
s: schema,
|
|
a: attributes,
|
|
e: self.edges,
|
|
r: self.rules,
|
|
};
|
|
let said = acdc.compute_said()?;
|
|
acdc.d = said;
|
|
Ok(acdc)
|
|
}
|
|
}
|
|
|
|
/// SAID placeholder used during derivation: 32 zero bytes. Because
|
|
/// [`crate::SAID_PLACEHOLDER`] is the *textual* placeholder used in
|
|
/// raw-JSON schemes, the binary equivalent inside our typed model is
|
|
/// simply a zero digest distinguishable from any real BLAKE3 output.
|
|
pub(crate) fn placeholder_said() -> Said {
|
|
Said([0u8; 32])
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn sample_schema() -> SchemaSection {
|
|
let mut m = serde_json::Map::new();
|
|
m.insert("$id".into(), Value::String("schema-1".into()));
|
|
m.insert("title".into(), Value::String("Sample".into()));
|
|
SchemaSection::Inline(m)
|
|
}
|
|
|
|
fn sample_attrs(targeted: bool) -> AttributeSection {
|
|
let mut m = serde_json::Map::new();
|
|
m.insert("name".into(), Value::String("Alice".into()));
|
|
if targeted {
|
|
m.insert("i".into(), Value::String("Bsubject".into()));
|
|
}
|
|
AttributeSection::Inline(m)
|
|
}
|
|
|
|
#[test]
|
|
fn build_and_verify_targeted() {
|
|
let acdc = AcdcBuilder::new()
|
|
.issuer("Bissuer")
|
|
.schema(sample_schema())
|
|
.attributes(sample_attrs(true))
|
|
.build()
|
|
.expect("build");
|
|
assert!(acdc.is_targeted());
|
|
acdc.verify_said().expect("verify");
|
|
}
|
|
|
|
#[test]
|
|
fn build_and_verify_untargeted() {
|
|
let acdc = AcdcBuilder::new()
|
|
.issuer("Bissuer")
|
|
.schema(sample_schema())
|
|
.attributes(sample_attrs(false))
|
|
.build()
|
|
.expect("build");
|
|
assert!(!acdc.is_targeted());
|
|
acdc.verify_said().expect("verify");
|
|
}
|
|
|
|
#[test]
|
|
fn tampered_credential_fails_verify() {
|
|
let mut acdc = AcdcBuilder::new()
|
|
.issuer("Bissuer")
|
|
.schema(sample_schema())
|
|
.attributes(sample_attrs(true))
|
|
.build()
|
|
.expect("build");
|
|
// Mutate the attributes after sealing.
|
|
if let AttributeSection::Inline(m) = &mut acdc.a {
|
|
m.insert("extra".into(), Value::String("tampered".into()));
|
|
}
|
|
assert!(matches!(
|
|
acdc.verify_said(),
|
|
Err(AcdcError::SaidMismatch { .. })
|
|
));
|
|
}
|
|
}
|