Skip to main content

ContentId

Struct ContentId 

Source
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) — see DAG_CBOR_CODEC.
  • Multihash: BLAKE3 (0x1e) — see BLAKE3_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_dagcbor a ContentId is encoded as a CBOR tag-42 link (major-type-6 head 0xd8 0x2a, then a byte string carrying the 0x00 multibase-identity prefix followed by the CID binary form). This is the natural IPLD encoding and makes ContentId fields inside other content-addressed structures behave as real links — the crate’s IPLD-native thesis. See canonical.
  • Human-readable form (e.g. serde_json, TOML): the multibase base32 string. In a human-readable serializer a ContentId is the base32-lower b… CID string — the same text Display/FromStr produce — so JSON and config files carry a readable, portable CID rather than a raw byte array. The crate adds a thin is_human_readable branch over the inner Cid to 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:

FormMethodWhat it is
Canonical textDisplay / to_stringmultibase base32-lower (b…), the IPLD-canonical CID string
Binary envelopeto_bytes / from_bytesthe full CID binary form (version + codec + multihash + digest)
Bare digestdigest_bytesthe raw 32-byte BLAKE3 hash (no envelope)
Bare-digest-hexdigest_hexlowercase 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:

  1. bare-digest-hex — hex of the raw 32-byte BLAKE3 digest. This is what digest_hex returns (the “swarm” / kyln to_hex() convention: shortest, hash-only).
  2. full-CID-bytes-hex — hex of to_bytes (the whole CID envelope as base16). Deliberately not a method. Convention #2 is just hex::encode(id.to_bytes()); blessing it as cid_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-encodes to_bytes explicitly 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.)
  3. multibase base32-lower — the Display string. 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

Source

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:

  1. digest = BLAKE3(bytes) — a 32-byte hash.
  2. mh = Multihash::wrap(0x1e, digest) — tag the digest with the BLAKE3 multihash code.
  3. 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.

Source

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
Source

pub 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

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).

Source

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.

Source

pub fn as_cid(&self) -> &Cid

Borrow the underlying IPLD Cid.

Source

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.

Source

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-encodes to_bytes and owns that choice.
  • multibase base32-lower = the Display / to_string string (b…), the IPLD-canonical text form.

Part of the frozen presentation contract — see the presentation contract.

Source

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.

Source

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§

Source§

impl Clone for ContentId

Source§

fn clone(&self) -> ContentId

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Copy for ContentId

Source§

impl Debug for ContentId

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl<'de> Deserialize<'de> for ContentId

Source§

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

Source§

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.

Source§

impl Eq for ContentId

Source§

impl From<ContentId> for ClassifiedCid

Source§

fn from(id: ContentId) -> Self

Converts to this type from the input type.
Source§

impl From<ContentId> for Cid

Source§

fn from(id: ContentId) -> Self

Converts to this type from the input type.
Source§

impl FromStr for ContentId

Source§

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

The associated error which can be returned from parsing.
Source§

impl Hash for ContentId

Source§

fn hash<__H: Hasher>(&self, state: &mut __H)

Feeds this value into the given Hasher. Read more
1.3.0 · Source§

fn hash_slice<H>(data: &[Self], state: &mut H)
where H: Hasher, Self: Sized,

Feeds a slice of this type into the given Hasher. Read more
Source§

impl Ord for ContentId

Source§

fn cmp(&self, other: &ContentId) -> Ordering

This method returns an Ordering between self and other. Read more
1.21.0 (const: unstable) · Source§

fn max(self, other: Self) -> Self
where Self: Sized,

Compares and returns the maximum of two values. Read more
1.21.0 (const: unstable) · Source§

fn min(self, other: Self) -> Self
where Self: Sized,

Compares and returns the minimum of two values. Read more
1.50.0 (const: unstable) · Source§

fn clamp(self, min: Self, max: Self) -> Self
where Self: Sized,

Restrict a value to a certain interval. Read more
Source§

impl PartialEq for ContentId

Source§

fn eq(&self, other: &ContentId) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

impl PartialOrd for ContentId

Source§

fn partial_cmp(&self, other: &ContentId) -> Option<Ordering>

This method returns an ordering between self and other values if one exists. Read more
1.0.0 (const: unstable) · Source§

fn lt(&self, other: &Rhs) -> bool

Tests less than (for self and other) and is used by the < operator. Read more
1.0.0 (const: unstable) · Source§

fn le(&self, other: &Rhs) -> bool

Tests less than or equal to (for self and other) and is used by the <= operator. Read more
1.0.0 (const: unstable) · Source§

fn gt(&self, other: &Rhs) -> bool

Tests greater than (for self and other) and is used by the > operator. Read more
1.0.0 (const: unstable) · Source§

fn ge(&self, other: &Rhs) -> bool

Tests greater than or equal to (for self and other) and is used by the >= operator. Read more
Source§

impl Serialize for ContentId

Source§

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.

Source§

impl StructuralPartialEq for ContentId

Source§

impl TryFrom<Cid<64>> for ContentId

Source§

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.)

Source§

type Error = ContentError

The type returned in the event of a conversion error.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T> ToString for T
where T: Display + ?Sized,

Source§

fn to_string(&self) -> String

Converts the given value to a String. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.