Manifest
The WeftManifest type — fields, validation, signing, and the canonical encoding.
What this page covers
WeftManifest is the signed commitment a producer makes when publishing a payload. This page documents every field, the validation rules, and the canonical signing payload.
Schema and version
pub const WEFT_MANIFEST_SCHEMA: &str = "weave.weft.manifest.v1";
pub const WEFT_MANIFEST_VERSION: u16 = 1;Manifests with a different schema or version are rejected by WeftManifest::validate.
Changing WEFT_MANIFEST_VERSION is a breaking change. Old consumers will reject new manifests. Plan version bumps as coordinated migrations: keep producing v1 manifests until consumers are updated.
Fields
pub struct WeftManifest {
pub schema: String,
pub version: u16,
pub root: WeftHash,
pub chunk_size: u64,
pub chunk_count: u64,
pub total_len: u64,
pub chunks: Vec<ChunkDescriptor>,
pub parity: Option<ParitySpec>,
pub producer: ManifestProducer,
pub artifact: ArtifactMetadata,
}| Field | Type | Meaning |
|---|---|---|
schema | String | Always weave.weft.manifest.v1 for this version |
version | u16 | Always 1 |
root | WeftHash | Merkle root over the ordered chunk ids |
chunk_size | u64 | Bytes per chunk (final chunk may be shorter) |
chunk_count | u64 | Number of chunks (chunks.len() as u64) |
total_len | u64 | Original payload length in bytes |
chunks | Vec<ChunkDescriptor> | Ordered chunk descriptors |
parity | Option<ParitySpec> | Reserved; must be None today |
producer | ManifestProducer | DID + signature |
artifact | ArtifactMetadata | Media type, schema, optional registry pointers |
ChunkDescriptor
pub struct ChunkDescriptor {
pub index: u64, // 0-based position in the manifest
pub offset: u64, // byte offset within the original payload
pub len: u32, // length of this chunk in bytes
pub id: WeftHash, // BLAKE3 chunk id
}ManifestProducer
pub struct ManifestProducer {
pub did: String,
pub signing_key_id: Option<String>,
pub signature: Vec<u8>,
}ArtifactMetadata
pub struct ArtifactMetadata {
pub artifact_id: Option<String>, // optional registry id (e.g. MARS)
pub media_type: String, // required, e.g. "application/octet-stream"
pub schema: String, // required, semantic schema id
pub mars_asset_id: Option<String>, // optional MARS asset id
pub sigil_commitment_cid: Option<String>, // optional Sigil commitment CID
}Validation
WeftManifest::validate runs every check below and returns the first failure. Both from_bytes and from_canonical_bytes call validate before returning, so an invalid manifest is never observable.
| Check | Failure |
|---|---|
schema == WEFT_MANIFEST_SCHEMA | Manifest("unsupported schema ...") |
version == WEFT_MANIFEST_VERSION | Manifest("unsupported version ...") |
0 < chunk_size <= MAX_CHUNK_SIZE | Manifest("chunk_size ... out of bounds") |
parity.is_none() | Unsupported("...erasure coding is reserved...") |
chunk_count == chunks.len() as u64 | Manifest("chunk_count mismatch") |
!producer.did.trim().is_empty() | Manifest("producer DID is required") |
!producer.signature.is_empty() | Manifest("producer signature is required") |
artifact.media_type and artifact.schema non-empty | Manifest("artifact media_type and schema are required") |
chunks[i].index == i for each i | Manifest("chunk index mismatch at ...") |
chunks[i].offset == sum(chunks[0..i].len) | Manifest("chunk ... offset ..., expected ...") |
chunks[i].len > 0 | Manifest("chunk ... has zero length") |
chunks[i].len <= chunk_size | Manifest("chunk ... length exceeds chunk_size") |
Non-final chunks have len == chunk_size | Manifest("non-final chunk ... is short") |
sum(chunks.len) == total_len | Manifest("total_len ..., expected ...") |
Merkle root over chunks[*].id equals root | Manifest("root mismatch") |
Canonical bytes
to_canonical_bytes() uses serde_json::to_vec. The encoding is stable across producers running the same crate version — the field order is fixed by the struct declaration and serde_json preserves it.
let manifest = WeftManifest::from_bytes(&payload, cfg, producer, artifact)?;
let bytes = manifest.to_canonical_bytes()?;
let round_trip = WeftManifest::from_canonical_bytes(&bytes)?;
assert_eq!(manifest, round_trip);The canonical bytes are what goes on the wire to peers, what is stored on disk, and what is committed on Sigil.
Signing payload
The producer signs manifest.signing_payload(), which returns the canonical bytes of the manifest with the signature field cleared:
fn signing_payload(&self) -> Result<Vec<u8>> {
let mut unsigned = self.clone();
unsigned.producer.signature.clear();
unsigned.to_canonical_bytes()
}The producer signs these bytes with their Ed25519 key, then replaces producer.signature with the 64-byte signature before publishing.
Verifying a signature
use ed25519_dalek::{Signature, VerifyingKey, Verifier};
use weft::manifest::WeftManifest;
fn verify(
manifest: &WeftManifest,
pubkey: &VerifyingKey,
) -> Result<(), Box<dyn std::error::Error>> {
let payload = manifest.signing_payload()?;
let sig = Signature::from_slice(&manifest.producer.signature)?;
pubkey.verify(&payload, &sig)?;
Ok(())
}The verifier needs to obtain the public key for the producer's DID. This typically happens through Weave's identity layer (weave-identity) — see DHT Binding for the resolution flow.
Forward compatibility
The schema constant pins weave.weft.manifest.v1. Adding fields is a breaking change for verifiers; bump WEFT_MANIFEST_VERSION and gate parsing on the version.
The parity field is reserved for a future erasure-coding extension. Setting it today is a hard error; this is intentional — old verifiers cannot reconstruct from parity, so old producers must not emit it.