pub struct ContentId(/* private fields */);Expand description
The self-certifying identity of a value.
A ContentId wraps an IPLD CIDv1 whose codec is dag-cbor (0x71) and
whose multihash is BLAKE3 (0x1e) over the value’s canonical dag-cbor
bytes. See from_canonical_bytes.
Ordering is defined by the CID’s binary representation, which makes
ContentId usable as a key in ordered maps and sets with a stable,
content-derived sort.
§CID parameters (FROZEN at 0.1.0)
The three fields that determine every address this crate emits are a
fixed v1 contract, not configuration (gate items 3, 4, 5 — see
README.md):
- CID version:
V1(Cid::new_v1). There is no v0 / agility path. - Multicodec:
dag-cbor(0x71) — seeDAG_CBOR_CODEC. - Multihash:
BLAKE3(0x1e) — seeBLAKE3_HASH_CODE— over a fixed 32-byte digest (BLAKE3_DIGEST_LEN, 256 bits).
These values are not selectable. from_canonical_bytes
is the single mint site and always produces this shape. Changing the
version, codec, hash function, or digest length is a new representation =
major version bump, never a patch — every previously emitted address would
become unreachable. CIDs are self-describing, so a future hash/codec can
ship as a new representation under a new major without retrofitting
agility now. A raw Cid is admitted only if it matches this profile —
TryFrom<Cid> validates it — and the
inner CID is readable via as_cid.
§Serde representation (FROZEN at 0.1.0)
ContentId has two serde forms, chosen by the serializer via
is_human_readable, and both are a stability contract across the entire
0.1.x line — changing either is a major version bump. The committed
golden vectors (tests/vectors.json) plus the in-crate golden test pin
these exact bytes so a dependency bump cannot silently perturb them:
- Binary / IPLD form (e.g. dag-cbor): a tag-42 link. Through
serde_ipld_dagcboraContentIdis encoded as a CBOR tag-42 link (major-type-6 head0xd8 0x2a, then a byte string carrying the0x00multibase-identity prefix followed by the CID binary form). This is the natural IPLD encoding and makesContentIdfields inside other content-addressed structures behave as real links — the crate’s IPLD-native thesis. Seecanonical. - Human-readable form (e.g.
serde_json, TOML): the multibase base32 string. In a human-readable serializer aContentIdis the base32-lowerb…CID string — the same textDisplay/FromStrproduce — so JSON and config files carry a readable, portable CID rather than a raw byte array. The crate adds a thinis_human_readablebranch over the innerCidto guarantee this; the IPLD/binary path is unchanged.
§Presentation contract (FROZEN at 0.1.0)
Whatever string and byte forms this type emits become a byte/wire
contract at 0.1.0: changing any of them afterward is a major version
bump, never a patch (issue #6, gate item 2). The full presentation
surface is therefore named and frozen explicitly, with conformance vectors
(tests/vectors.json + the in-crate goldens) pinning the exact bytes. There
are four distinct presentation forms, each load-bearing for a different
reason — do not confuse them:
| Form | Method | What it is |
|---|---|---|
| Canonical text | Display / to_string | multibase base32-lower (b…), the IPLD-canonical CID string |
| Binary envelope | to_bytes / from_bytes | the full CID binary form (version + codec + multihash + digest) |
| Bare digest | digest_bytes | the raw 32-byte BLAKE3 hash (no envelope) |
| Bare-digest-hex | digest_hex | lowercase hex of the 32-byte digest (64 chars, no prefix) |
Three incompatible “hex” conventions exist in the wild for a CID; an adopter must pick one, so the crate names them to end the ambiguity:
- bare-digest-hex — hex of the raw 32-byte BLAKE3 digest. This is what
digest_hexreturns (the “swarm” / kylnto_hex()convention: shortest, hash-only). - full-CID-bytes-hex — hex of
to_bytes(the whole CID envelope as base16). Deliberately not a method. Convention #2 is justhex::encode(id.to_bytes()); blessing it ascid_hex()would add a third “hex” accessor that invites exactly the confusion this contract exists to end. A caller who genuinely needs CID-bytes-as-hex hex-encodesto_bytesexplicitly and owns that choice. (This non-decision is recorded so it is not re-litigated; it can be added later additively without breaking the frozen surface.) - multibase base32-lower — the
Displaystring. Use this, not a hex form, as the canonical text representation.
Display emits base32-lower and is the inverse of FromStr for that
form: the Display→FromStr round-trip is frozen and tested. FromStr
also tolerates other multibases (base58, base16, …) as a convenience,
not a contract — that tolerance may be tightened later without breaking
the frozen base32-lower round-trip.
Implementations§
Source§impl ContentId
impl ContentId
Sourcepub fn from_canonical_bytes(bytes: &[u8]) -> Self
pub fn from_canonical_bytes(bytes: &[u8]) -> Self
Compute the content id of bytes that are already canonical dag-cbor.
This is the crate’s fast, unchecked minting primitive — the hot path that
ContentAddressable::content_id
leans on. The pipeline is, end to end:
digest = BLAKE3(bytes)— a 32-byte hash.mh = Multihash::wrap(0x1e, digest)— tag the digest with the BLAKE3 multihash code.cid = Cid::new_v1(0x71, mh)— a CIDv1 declaring the bytes are dag-cbor.
§Precondition — CALLER MUST PASS CANONICAL DAG-CBOR
The bytes you pass MUST already be canonical dag-cbor — typically the
output of
to_canonical_dagcbor or, for a
whole value, ContentAddressable::canonical_form.
This function does not re-canonicalize and does not validate; it only
hashes whatever it is handed and stamps the result with the 0x71
(dag-cbor) codec.
Passing non-canonical CBOR (wrong map-key order, indefinite-length
items, non-smallest integers), hand-rolled CBOR, or arbitrary non-dag-cbor
bytes is a logic error, not handled at runtime: you get back a
perfectly valid-looking ContentId whose 0x71 codec is a lie —
the bytes it names are not canonical dag-cbor. A second party who
canonicalizes “the same value” will compute a different id, and
nothing here will have told you. This is silent and by design (validation
is not free, so it is opt-in — see below), so honor the precondition.
§Which door to use
- Have a value? Use
ContentAddressable::content_id(the documented default). It canonicalizes then hashes, so it can never hand this function non-canonical bytes — the precondition holds by construction and there is no per-call validation tax on the safe path. - Have bytes you canonicalized yourself (via
to_canonical_dagcbor)? This primitive is correct and fastest. - Have foreign / untrusted bytes you did not canonicalize? Use the
checked sibling
from_canonical_bytes_checked, which re-encodes and rejects non-canonical input with a typed error.
§Naming (FROZEN at 0.1.0)
This door keeps the name from_canonical_bytes (the fast primitive) and
is not renamed to _unchecked; the checked variant is the explicitly
suffixed from_canonical_bytes_checked.
This pairing is a frozen 0.1.0 decision (README gate item #6) — it
cannot change cheaply after 0.1.0.
§Panics
Does not panic. BLAKE3 always produces a 32-byte digest, which always
fits the multihash’s 64-byte capacity, so the wrap call cannot fail.
Sourcepub fn from_canonical_bytes_checked(bytes: &[u8]) -> Result<Self, ContentError>
pub fn from_canonical_bytes_checked(bytes: &[u8]) -> Result<Self, ContentError>
Compute the content id of bytes, first verifying they are canonical dag-cbor.
This is the opt-in, checked sibling of
from_canonical_bytes. It is for callers
minting an id from bytes they did not canonicalize themselves
(foreign, untrusted, hand-rolled, or stored input) and who therefore
cannot rely on the precondition by construction. It closes the silent
integrity hole the fast primitive leaves open, at the cost of a decode +
re-encode + compare on every call — pay it only when byte provenance is
not yours.
The check is a round-trip: decode the bytes as an
Ipld value, re-encode that value via
to_canonical_dagcbor, and
require the re-encoding to equal the input byte-for-byte. Because the
dag-cbor codec is canonical by construction (strict key order,
definite-length, smallest-form integers — see
canonical), equality proves the input was already
the unique canonical encoding of that value. On success the id is computed
over the (canonical) input bytes, so it is byte-identical to what
from_canonical_bytes would return for the
same bytes — the check changes the fallibility, never the emitted id.
§Errors
ContentError::DecodingErrorifbytesare not dag-cbor at all.ContentError::NonCanonicalifbytesdecode but are not the canonical encoding (wrong map-key order, indefinite-length items, non-smallest integers, …) — i.e. re-encoding differs from the input.ContentError::EncodingErrorin the unlikely event the decoded value cannot be re-encoded.
Sourcepub fn from_blake3_content_digest(digest: [u8; 32]) -> Self
👎Deprecated since 0.1.1: use RawContentId::from_blake3_digest for a digest of opaque bytes (the honest profile), or ContentId::from_dag_cbor_digest when the digest is known to be over canonical dag-cbor
pub fn from_blake3_content_digest(digest: [u8; 32]) -> Self
use RawContentId::from_blake3_digest for a digest of opaque bytes (the honest profile), or ContentId::from_dag_cbor_digest when the digest is known to be over canonical dag-cbor
Wrap an already-computed 32-byte BLAKE3 content digest as a
ContentId — without hashing.
This is a deliberately narrow, unchecked escape hatch for
BLAKE3-native upstreams that have already hashed their content and
hold only the 32-byte digest (a signature, an address) — not the
original canonical bytes. For them,
from_canonical_bytes is not just
wasteful, it is impossible: there are no bytes left to hash. This
constructor takes the digest they already have and wraps it directly:
Multihash::wrap(0x1e, digest) → Cid::new_v1(0x71, mh). No BLAKE3
step runs, and the result is byte-identical to what
from_canonical_bytes would have produced
had the caller hashed the real content correctly.
The [u8; 32] argument makes the 32-byte length a compile-time
guarantee: it is impossible to call with a wrong-length digest, so no
runtime length check, no fallible signature, and no new error variant
are needed. Callers holding a &[u8] convert with try_into() and own
that fallibility.
§⚠️ UNCHECKED ESCAPE HATCH — CALLER ASSERTS THE PRECONDITION ⚠️
This constructor does not hash and does not canonicalize. It trusts
the caller completely. By calling it you assert that digest is
exactly BLAKE3 over the value’s canonical dag-cbor bytes — the same
input domain from_canonical_bytes would
hash.
If that assertion is false — the digest was computed over non-canonical
bytes, over a different encoding, with a different hash, or is simply
arbitrary — the result is a ContentId that names content nothing
actually hashed. Such an id is silently wrong: there is no error, and
verifying it against the real bytes returns Ok(false), never an Err.
If you have the content bytes, use
from_canonical_bytes instead — it
hashes them for you and cannot be wrong this way. Do not reach for
this constructor merely to avoid a hash; reach for it only when you
genuinely hold a precomputed, canonical-dag-cbor BLAKE3 digest and the
content itself is gone.
§Motivation
The kyln-lore address.rs bridge (blake3_digest_to_content_id /
signature_to_content_id) does exactly this: a lore revision signature
is a raw 32-byte BLAKE3 digest of the content, so it becomes a kyln
ContentId by wrapping it — minus the blake3::hash step, because lore
already hashed the content. That bridge is what makes a projected git
commit’s provenance note content_id equal the originating lore
revision signature. kyln’s adoption (kyln #303) and any other
BLAKE3-native source needs this primitive so a system that already
hashed with BLAKE3 gets an exact ContentId with no re-hash.
§Panics
Does not panic. A 32-byte digest always fits the multihash’s 64-byte
capacity, so the wrap call cannot fail.
§Deprecated — the codec was a guess
This door stamps the dag-cbor (0x71) codec on a digest it cannot
know came from canonical dag-cbor. Every real caller (kyln-lore
signatures, bare blake3::hash outputs, blake3:<hex> digests) held a
digest of opaque bytes, for which the honest identity is
RawContentId::from_blake3_digest
(CIDv1 · raw · BLAKE3) — byte-identical to those systems’ existing raw
CIDs. If you genuinely hold a digest of canonical dag-cbor and the value
is gone, say so explicitly with
from_dag_cbor_digest. See decision record
docs/adr/0003 (question 5).
Sourcepub fn from_dag_cbor_digest(digest: [u8; 32]) -> Self
pub fn from_dag_cbor_digest(digest: [u8; 32]) -> Self
Wrap a precomputed 32-byte BLAKE3 digest of canonical dag-cbor bytes
as a ContentId, with no re-hash.
The name asserts the precondition: the digest MUST be BLAKE3 over the
canonical dag-cbor encoding of a value — the same bytes
from_canonical_bytes would have hashed.
This function cannot check that; a digest of anything else mints an id
whose dag-cbor codec is a lie (see the precondition discussion on
from_canonical_bytes). If your digest is
over opaque bytes, you want
RawContentId::from_blake3_digest.
Emits exactly the bytes from_canonical_bytes
emits for the same digest (one shared construction point), so
from_dag_cbor_digest(blake3(c)) == from_canonical_bytes(c).
§Panics
Does not panic: a 32-byte digest always fits the multihash’s 64-byte capacity.
Sourcepub fn digest_bytes(&self) -> [u8; 32]
pub fn digest_bytes(&self) -> [u8; 32]
The raw 32-byte BLAKE3 content digest, copied out of the multihash.
This is the bare hash — not the CID envelope. It is the sovereign
hash an adopter joins on across systems (the same digest a BLAKE3-native
upstream would hand to from_blake3_content_digest).
The length is a frozen, enforced invariant: every ContentId carries
exactly BLAKE3_DIGEST_LEN (32) digest bytes (see the
CID-parameters contract), so
this accessor is infallible and returns a fixed-size array. The copy
(try_into) can never fail; the expect is unreachable for any
ContentId — every ingress path (from_bytes, FromStr, TryFrom<Cid>,
binary Deserialize) validates the profile, so an off-profile CID can never
become a ContentId and reach this method.
Part of the frozen presentation contract — see the presentation contract.
Sourcepub fn digest_hex(&self) -> String
pub fn digest_hex(&self) -> String
Lowercase hex of the raw 32-byte BLAKE3 digest: 64 hex chars, no
0x or multibase prefix.
This is the “bare-digest-hex” convention (the shortest, hash-only
“hex” form an adopter reaches for, e.g. kyln’s to_hex()). It is hex of
digest_bytes — not hex of the full CID. The
three “hex” forms in the wild are mutually incompatible; this crate names
each so they cannot be confused:
- bare-digest-hex = this method =
digest_hex()(64 chars, hash only). - full-CID-bytes-hex =
hex::encode(id.to_bytes())— the whole CID envelope (version + codec + multihash header + digest) as base16. Deliberately not provided as a method (see the presentation contract); a caller who truly needs it hex-encodesto_bytesand owns that choice. - multibase base32-lower = the
Display/to_stringstring (b…), the IPLD-canonical text form.
Part of the frozen presentation contract — see the presentation contract.
Sourcepub fn to_bytes(&self) -> Vec<u8> ⓘ
pub fn to_bytes(&self) -> Vec<u8> ⓘ
Encode this id as its canonical CID binary form.
This is the full CID envelope: version + codec (0x71) + multihash
header + digest — not the bare hash. It round-trips through
from_bytes. For the hex of this form (“full-CID-
bytes-hex”), a caller hex-encodes the result explicitly; the crate
deliberately does not bless a cid_hex() method (see the
presentation contract).
Part of the frozen presentation contract — see the presentation contract.
Sourcepub fn from_bytes(bytes: &[u8]) -> Result<Self, ContentError>
pub fn from_bytes(bytes: &[u8]) -> Result<Self, ContentError>
Parse an id from its canonical CID binary form.
§Errors
Returns ContentError::InvalidCid if the bytes are not a valid CID.
Trait Implementations§
impl Copy for ContentId
Source§impl<'de> Deserialize<'de> for ContentId
impl<'de> Deserialize<'de> for ContentId
Source§fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error>
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error>
Deserialize per the frozen serde contract: a multibase base32 string from
human-readable formats, the inner Cid (a dag-cbor tag-42 link) from
binary/IPLD formats.
The accepted representation is frozen for the 0.1.x line; see the
type-level serde contract.
Source§impl Display for ContentId
impl Display for ContentId
Source§fn fmt(&self, f: &mut Formatter<'_>) -> Result
fn fmt(&self, f: &mut Formatter<'_>) -> Result
Render as multibase base32-lower (the b… CIDv1 string).
This is the crate’s canonical text form and is frozen for the
0.1.x line — see the
presentation contract.
It is the IPLD-canonical CID string and the same text the human-readable
serde form emits. It is the inverse of FromStr for base32-lower:
id.to_string().parse() == Ok(id) is a tested, frozen round-trip. A unit
test and the conformance vectors pin the exact string for fixed inputs,
so any drift in the inner CID’s rendering fails loudly. Changing this
form is a major version bump.
impl Eq for ContentId
Source§impl From<ContentId> for ClassifiedCid
impl From<ContentId> for ClassifiedCid
Source§impl FromStr for ContentId
impl FromStr for ContentId
Source§fn from_str(s: &str) -> Result<Self, Self::Err>
fn from_str(s: &str) -> Result<Self, Self::Err>
Parse an id from a multibase CID string.
The frozen contract is narrow: FromStr is the inverse of
Display for the base32-lower form, and that round-trip
(id.to_string().parse() == Ok(id)) is the part pinned by the
presentation contract.
As a convenience (not a contract) this also accepts other valid
multibases (base58, base16, …), since the inner Cid::from_str does;
that tolerance is not frozen and may be tightened later without
breaking the frozen base32-lower round-trip — so do not depend on parsing
non-base32 CID strings.
Source§type Err = ContentError
type Err = ContentError
Source§impl Ord for ContentId
impl Ord for ContentId
1.21.0 (const: unstable) · Source§fn max(self, other: Self) -> Selfwhere
Self: Sized,
fn max(self, other: Self) -> Selfwhere
Self: Sized,
Source§impl PartialOrd for ContentId
impl PartialOrd for ContentId
Source§impl Serialize for ContentId
impl Serialize for ContentId
Source§fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error>
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error>
Serialize per the frozen serde contract: the multibase base32 string in
human-readable formats (e.g. serde_json, TOML), the inner Cid (a
dag-cbor tag-42 link) in binary/IPLD formats.
This representation is frozen for the 0.1.x line; see the
type-level serde contract.
Changing it is a major version bump.
impl StructuralPartialEq for ContentId
Source§impl TryFrom<Cid<64>> for ContentId
impl TryFrom<Cid<64>> for ContentId
Source§fn try_from(cid: Cid) -> Result<Self, Self::Error>
fn try_from(cid: Cid) -> Result<Self, Self::Error>
Wrap a Cid iff it is this crate’s frozen profile; a foreign CID is
rejected with ContentError::InvalidCidProfile.
This is TryFrom, not From, on purpose: an infallible From<Cid> would
let an off-profile CID become a ContentId and later panic in a
presentation accessor. (The outward From<ContentId> for Cid stays
infallible — dropping the newtype is always safe.)