Layer 2/3 · The self-defending module
Canonical reference:
digstore-compiler(pipeline.rs,inject.rs,filler.rs,config.rs) for the module;digstore-guest::content+digstore-hostfor the serving guest. The data it carries is the DIGS data section.
Fixed-size obfuscation
Every module's injected DIGS blob is padded to exactly uniform_blob_len (default FIXED_BLOB_LEN = 128 MiB), so all production stores compile to the same module size — leaking nothing about content size (config.rs:11-15,38-44; override DIGSTORE_UNIFORM_BLOB_LEN for tests).
Pipeline math (pipeline.rs:106-136): encode the blob with an empty Filler body → blob_len_without_filler; reject if it exceeds the budget (never truncate); filler_len = budget − blob_len_without_filler. Filler (id 11, unreferenced) is the ONLY variable section — it never touches leaves or CurrentRoot, so padding changes nothing served or proven.
Filler bytes are deterministic: a ChaCha20 keystream with seed = SHA-256(store_id || roothash || "digstore-filler-v1"), key = seed, nonce = 12 zero bytes, positional (so a shorter request is a prefix of a longer one) — filler.rs:7-28. Determinism is what makes compilation byte-identical.
WASM injection & memory layout (BINDING D2)
DIGS_DATA_OFFSET = 0x0020_0000(2 MiB), chosen above the guest's rodata (the linker places rodata at the 1 MiB default global base).- The guest bump heap floats dynamically at
align_up(DIGS_DATA_OFFSET + blob_len, 64 KiB), so it never overlaps the data section for any blob size (datasection.rs:32-47). inject_data_sectionappends the DIGS blob as an active data segment ati32.const DIGS_DATA_OFFSETlast (later active segments win on overlap), drops the originalDataCountsection, and re-emits the Memory section withminimum = max(template_min, ceil((offset+blob)/65536))and alwaysmaximum = Some(6144)(= 384 MiB ceiling,MAX_MEMORY_PAGES); rejects if needed pages > 6144 (inject.rs:16-206).
History: the original 1 MiB offset collided with guest rodata and a fixed 8 MiB heap overlapped the chunk pool → the module dropped chunks and failed to self-serve; fixed by 2 MiB + a floating heap.
The self-serving guest pipeline
The capsule WASM self-serves: it runs its own serve flow for a requested retrieval key inside a bounded host runtime, and the host only decodes the envelope framing — never decrypting (content.rs:39-456).
The gate chain
opaque-true predicate
→ temporal window
→ attestation [DISABLED by default]
→ JWT [only when AuthInfo.requires_jwt]
→ KeyTable lookup → oblivious gather → ContentResponse
else → an indistinguishable, success-shaped decoy
The host-attestation gate is disabled by default so that any anonymous node can serve public content and the program hash stays network-stable (per-node trusted keys would change it). The privacy decoy path is independent. Any gate failure fails closed → an indistinguishable success-shaped decoy (content.rs:39-65,154-189).
Oblivious gather
The guest reads every slot in the access plan (cover + real), so the pool access pattern is uniform; real chunks are kept in order and concat_output produces the response ciphertext (content.rs:241-281, lib.rs:42-49).
The ContentResponse wire envelope
ciphertext(u32 len + bytes) || merkle_proof || roothash(32) || chunk_lens(Vec<u32> per-chunk CIPHERTEXT lengths)
chunk_lens lets a streaming client split + GCM-SIV-open each chunk (content.rs:321-382). The guest never decrypts — it relays ciphertext + proof; decryption is 100% client-side.
Deterministic compilation
Compilation is byte-identical: deterministic filler, a pinned committed guest template (the build never invokes cargo build for the guest), and wasm-opt intentionally skipped (not byte-stable) — lib.rs:1-21, config.rs:30-33. Optional obfuscation (nop insertion, an always-true opaque predicate, bogus dead functions, instruction substitution) is deterministic and behavior-preserving — security never rests on it (obfuscate.rs).
Related
- Capsule format — the DIGS data section the module carries
- Cryptography — the guest never decrypts
- Merkle inclusion proofs — the proof the guest emits
- The dig RPC — how a host runs the guest and streams the envelope
- The blind host model — the bounded host runtime + decoys