Architecture

This page explains how the crate is organized internally and why. The protocol-level rationale for the three-layer split lives in RFC-ACDP-0001 §5 (identifiers, canonicalization, hashing, signatures) and RFC-ACDP-0002 (the body structure). This page maps that structure onto Rust modules and explains the one rule you must never break: a field's layer determines whether it is hashed, signed, or mutable.

The three layers

PublishRequest                                         src/types/publish.rs

  ├── Body                  ← immutable, JCS-canonicalized        src/types/body.rs
  │     │
  │     └── ProducerContent ← Body minus the §5.7 exclusion set
  │           │                (producer-controlled fields only)
  │           ├── content_hash  = sha256(JCS(ProducerContent))    src/crypto/hash.rs
  │           └── signature     = Ed25519( ASCII "sha256:<hex>" ) src/crypto/sign.rs
  │                                            ▲
  │                                            └─ ⚠️ the ASCII string, NOT the 32-byte digest

  ├── content_hash          ← echoed in the request for transport
  └── signature             ← the producer's Ed25519 signature

FullContext = Body + RegistryState                     ← retrieval shape

                     └── status, … (mutable, registry-derived)    src/types/body.rs

Three operations are protocol-critical and the crate implements them exactly:

OperationSpecImplementation
JCS canonicalizationRFC 8785src/crypto/jcs.rsin-house, handles -0.0
content_hashRFC-ACDP-0001 §5.7src/crypto/hash.rssha256(JCS(ProducerContent))
Ed25519 sign/verifyRFC-ACDP-0001 §5.8/§5.11src/crypto/{sign,verify}.rs

Three things that trip people up

  1. The signature preimage is the ASCII string "sha256:<hex>" — the 71-byte text — not the raw 32-byte digest. This is the single most common implementation mistake. See src/crypto/sign.rs.
  2. Option::is_none fields are skipped, not emitted as null. Emitting null for an unset field changes the JCS bytes and therefore the content_hash. This is load-bearing for the sig-001 golden vector.
  3. Body and RegistryState are split deliberately. status is not a body field — it's registry-derived and lives in RegistryState. Merging them would let mutable state into the signed preimage.

Module map

PathRole
src/lib.rsCrate root: module declarations, ACDP_VERSION, convenience re-exports.
src/types/Wire types: body, publish, search, data_ref, capabilities, primitives. Body / RegistryState are kept apart. Status, ContextType, Visibility are open enums — they preserve Other(String) for forward compat.
src/crypto/jcs (RFC 8785), hash (content_hash + lineage_id derivation), sign / verify (Ed25519, optional ECDSA-P256).
src/validation.rsThe one-stop schema validator: validate_publish_request, validate_body, validate_data_ref, validate_metadata, validate_capabilities. The builder runs this before emitting.
src/producer/Producer + RequestBuilder. Enforces v1-vs-v2+ rules, ms-truncates timestamps, validates, computes content_hash, then signs.
src/did/WebResolver for did:web (LRU-cached, SSRF-gated). v0.1.0 producers MUST use did:web.
src/safe_http.rsSsrfPolicy, the HTTPS guard, and SafeDnsResolver (the DNS-time IP filter). The only copy; src/registry/safe_http.rs re-exports it.
src/client/ (feature client)RegistryClient, VerifiedContext, VerificationPolicy/Report, CrossRegistryResolver, DataRefFetcher. Implements the acdp-consumer profile.
src/registry/ (feature server)RegistryServer, PublishValidator, RegistryStore, InMemoryStore. Building blocks for separate acdp-registry-* crates.
src/error.rsAcdpError — typed mapping of all RFC-ACDP-0007 §5 wire codes, plus is_transient.
src/profile.rsTyped profile vocabulary (acdp-consumer, acdp-registry-core, …).
src/bin/acdp.rs (feature cli)The CLI. Uses std::env::args directly — no clap by design.

Feature gating

The crate is single-crate (no workspace). Features add layers outward from a pure core:

              ┌─────────────────────────────────────────────┐
   cli  ───►  │ src/bin/acdp.rs                              │
              ├─────────────────────────────────────────────┤
 client ───►  │ client::{RegistryClient, VerifiedContext,   │
              │   CrossRegistryResolver}  ·  did::WebResolver│
              ├─────────────────────────────────────────────┤
 server ───►  │ registry::{RegistryServer, PublishValidator,│
              │   RegistryStore, InMemoryStore}              │
              ├─────────────────────────────────────────────┤
  core   ───► │ types · crypto · validation · producer ·    │  ← always present,
 (no feat)    │ error · profile · safe_http · did (types)   │    no HTTP stack
              └─────────────────────────────────────────────┘
  • client and server are independent. A consumer pulls client; a registry pulls server. They don't require each other.
  • The core has no async runtime and no HTTP. reqwest/tokio/rustls only arrive with client (or cli). This is what lets the bindings ship a thin wheel/.node — crypto in Rust, HTTP in the host language.

Where the work happens

Most behavior changes land in src/validation.rs (~44 KB). It is the single source of structural truth: the builder calls it before signing, the server feature calls it during publish, and VerifiedContext calls it during retrieval. If you're changing what counts as a valid context, that's the file.

The crypto layer is intentionally small and rarely changes — and when it does, it must keep passing the sig-001 / can-001 golden vectors (see Conformance & testing).

Design rules (repo conventions)

These are enforced and will fail CI or review if broken:

  • No unsafeunsafe_code = "forbid" at the crate root.
  • JCS is in-house — do not swap src/crypto/jcs.rs for an external crate; the -0.0 handling is pinned by proptest_jcs.rs and can-001.
  • No clap in the CLI — manual arg parsing keeps the dep graph identical to the library.
  • Option::is_none skip-serialization is load-bearing — never emit null for unset fields.
  • Producer timestamps are ms-truncated (time::trunc_ms) per RFC-ACDP-0001 §5.3.
  • Conventional Commitsrelease-plz derives versions from feat:/fix:/docs:/… prefixes.