From the latest edition of the DarkWow Book, with Deepseek, following recent breakthroughs. The repository is also migrating to Github, following recent Codeberg terms of service changes.
1. Introduction
Object-capability (O-Cap) security models provide a principled foundation for authorization: capabilities are unforgeable tokens that grant authority, and they can be passed, delegated, and consumed. In reference-mode systems (e.g., Agoric’s ERTP), a capability is an object reference; authority is checked by the runtime whenever a method is invoked. However, when porting this model to a private blockchain, a fundamental tension arises: if the verifier must know which capability is being exercised to enforce authorization, how can the exercise remain private?
DarkWow resolves this through the Authorization Inversion Theorem: an ACL-based authorization system A(p, r, s) can be inverted to a privacy-preserving capability scheme A′(π, r, s) if and only if there exists a zero-knowledge proof system for the language L_{r,s} = {w : P_{r,s}(w) = 1} with proofs simulatable without knowledge of w [1]. In practice, the capability type is the predicate language – the ZK circuit defines the type.
This paper presents the first implementation of two fundamental O-Cap primitives – Box (capability delegation) and Purse (fungible value container) – within a Halo2-based zkVM with full privacy (L1 level). Unlike prior ZK systems that reveal resource identities as public inputs (L2), our approach hides the resource identity inside the ZK witness, proving Merkle inclusion against a known root. An observer learns only a nullifier and a Merkle root – not which object was operated on, by whom, or how much. This is achieved through a combination of:
Consume+create state transitions: each operation nullifies the old state and creates a new Merkle leaf, keeping the active object count bounded.
Domain-separated Poseidon hashing: restores type distinctions that Poseidon’s output otherwise erases, preventing cross-type confusion attacks.
A four-component architecture (Circuit → Params → Metadata → Exec/Apply) that eliminates metadata-circuit drift, ensuring the public inputs match the circuit constraints exactly.
Additive O-Cap composition: state spaces add rather than multiply, preventing combinatorial explosion across composed contracts.
We prove that both Box and Purse satisfy the safe L1 classification bounds (P ≤ 9, W ≤ 13, O ≤ 3) derived from a formal combinatorial analysis [2], and all circuit constraints are verified in Lean4 with zero axioms.
2. The Challenge: Private Capabilities in ZK
2.1 What Must Remain Hidden
For a transferable capability (e.g., a coin, a delegation token, a purse balance), the following must be private:
The identity of the object (which box, which purse)
The identity of the holder (public key link)
The value or contents (for a purse, the balance)
The relationship between consumed and produced states (unlinkability)
At the same time, the system must enforce:
Single-use: a capability cannot be exercised twice (nullifier)
Existence: the object must be in the current state set (Merkle inclusion)
Authority: the exerciser must prove knowledge of the secret that controls the capability (ZK proof of secret key)
Conservation: for value-bearing capabilities, total value must be conserved (Pedersen homomorphism)
2.2 Why Prior Approaches Fail
L2 (instance privacy): resource IDs are public inputs, enabling direct KV lookup. This leaks which object is being operated on, destroying transaction unlinkability – an observer can build social graphs.
Reference-mode (Agoric): capabilities are object references – if the reference leaks, so does the authority. Privacy is not a goal.
Simple ZK with Merkle proofs: while Merkle inclusion hides which leaf, without domain separation and proper binding of public inputs, the prover can forge proofs by reusing nullifiers or misusing hash outputs. The Zcash Orchard bug [3] is a canonical example: an under-constrained EC multiplication allowed unlimited minting for four years because there was no independent supply audit.
2.3 The Innovation: A Fully Private L1 Capability Model
DarkWow’s Box and Purse achieve full privacy through:
Nullifier as exclusive consumption evidence:
nf = poseidon_hash(DOMAIN_NULLIFIER, owner_secret, object_id, state_nonce). The nullifier is a unique, deterministic fingerprint of a specific state transition. It is published on-chain; the verifier checks it hasn’t been used before (SMT lookup). Becauseowner_secretandobject_idare witnesses, the nullifier reveals nothing about the object.Merkle inclusion with hidden leaves: the circuit proves that the leaf (which encodes object ID, contents, and state nonce) exists in the Merkle tree at a known root. The leaf value is also a
poseidon_hashwith domain separation; the verifier only sees the root.Domain separation: every hash function call in the circuit is prefixed with a domain constant (e.g.,
DOMAIN_NULLIFIER,DOMAIN_MERKLE_LEAF). This ensures that a nullifier hash is not accidentally equal to a leaf hash or a commitment hash, preventing cross-type substitution attacks.Derivation constraint: every public input is derived in-circuit from a witness and constrained with
constrain_equal_base. A prover cannot supply an arbitrary public input – it must match the circuit-computed value.Consume+create invariant: each non-terminal operation consumes exactly one old state (by publishing its nullifier) and creates exactly one new state (by appending a new leaf). This keeps the anonymity set size
Nconstant over time, preventing unbounded growth that would degrade privacy.
3. Box: The Capability Delegation Primitive
3.1 Data Model
A Box is a container for an arbitrary capability. Its state is represented by:
box_leaf = poseidon_hash(DOMAIN_MERKLE_LEAF, box_id, contents_commit, state_nonce)
nullifier = poseidon_hash(DOMAIN_NULLIFIER, owner_secret, box_id, state_nonce)
owner_pub = poseidon_hash(DOMAIN_SIGNATURE_SECRET, owner_secret)Where:
box_id: a unique identifier for the box (witness, never revealed)contents_commit: a commitment to the capability being delegated (e.g., a token ID, a function ID, or another box ID)state_nonce: a monotonically increasing counter to ensure each state transition has a unique leaf and nullifierowner_secret: the secret key that authorisesPutandTakeoperationsDOMAIN_MERKLE_LEAF,DOMAIN_NULLIFIER,DOMAIN_SIGNATURE_SECRET: domain constants (defined aswitness_base(5),witness_base(1),witness_base(7))
3.2 Circuit Constraints (Put.zk)
The put.zk circuit enforces:
// 1. Derive owner public key from owner_secret
pub = ec_mul_base(owner_secret, NULLIFIER_K);
owner_pub_x = ec_get_x(pub);
owner_pub_y = ec_get_y(pub);
// 2. Bind to witness public key (params)
constrain_equal_base(owner_pub_x, witness_owner_pub_x);
constrain_equal_base(owner_pub_y, witness_owner_pub_y);
// 3. Compute nullifier from owner_secret, box_id, old_state_nonce
nullifier = poseidon_hash(DOMAIN_NULLIFIER, owner_secret, box_id, old_state_nonce);
constrain_equal_base(nullifier, witness_nullifier);
// 4. Compute old leaf and prove Merkle inclusion
old_leaf = poseidon_hash(DOMAIN_MERKLE_LEAF, box_id, old_contents_commit, old_state_nonce);
root = merkle_root(leaf_pos, merkle_path, old_leaf);
constrain_equal_base(root, witness_expected_root); // expected root is a public input
// 5. Compute new leaf with updated contents and incremented state_nonce
new_state_nonce = base_add(old_state_nonce, 1);
new_leaf = poseidon_hash(DOMAIN_MERKLE_LEAF, box_id, new_contents_commit, new_state_nonce);
// 6. Publish new leaf as a public input (to be appended in Apply)
constrain_instance(new_leaf); // this becomes the new commitment
constrain_instance(nullifier); // this is the consumption evidence
constrain_instance(root); // the Merkle root against which inclusion was provenKey innovation: The circuit does not compute the new leaf’s position or path – that is handled by the Apply phase, which appends the new leaf to the tree. The circuit only proves that the new leaf is correctly formed from the witnesses and that it satisfies the same invariants. The constrain_instance values are all derived in-circuit; there is no free witness.
3.3 Metadata-Echo Pattern
The get_metadata function for Box does no computation – it simply echoes the params fields in the order they appear in the circuit’s constrain_instance calls:
fn get_metadata(cid, ix) -> Vec<u8> {
let params = decode::<PutParams>(ix);
let mut public_inputs = vec![];
public_inputs.push(params.new_leaf);
public_inputs.push(params.nullifier);
public_inputs.push(params.expected_root);
// ... remaining public inputs in exact order
serialize(&public_inputs)
}This guarantees that the circuit’s instance column matches the metadata vector exactly – a crucial invariant that prevents the Orchard-class failure where metadata and circuit disagree on public input order [3].
4. Purse: The Fungible Capability Container
4.1 Data Model
A Purse holds a balance of a fungible token. Its state is:
purse_leaf = poseidon_hash(DOMAIN_MERKLE_LEAF, purse_id, balance_commit, state_nonce)
balance_commit = pedersen_commit(balance, balance_blind) // Pedersen, not Poseidon
nullifier = poseidon_hash(DOMAIN_NULLIFIER, owner_secret, purse_id, state_nonce)
owner_pub = poseidon_hash(DOMAIN_SIGNATURE_SECRET, owner_secret)Pedersen commitments are used for balances because they are additively homomorphic – a critical property for proving value conservation without revealing amounts.
4.2 Circuit Constraints (Deposit.zk)
// 1. Derive owner public key (same as Box)
// 2. Compute nullifier (same as Box)
// 3. Prove old leaf inclusion (same as Box)
// 4. Compute old balance commitment from witness
old_balance_commit = pedersen_commit(old_balance, old_blind);
constrain_equal_base(old_balance_commit, witness_old_balance_commit);
// 5. Compute deposit commitment
deposit_commit = pedersen_commit(deposit_amount, deposit_blind);
constrain_equal_base(deposit_commit, witness_deposit_commit);
// 6. Compute new balance commitment via Pedersen addition
new_balance_commit_x = ec_add(old_balance_commit_x, deposit_commit_x); // field addition
new_balance_commit_y = ec_add(old_balance_commit_y, deposit_commit_y);
// This is equivalent to pedersen_commit(old_balance + deposit_amount, old_blind + deposit_blind)
// but we let the caller provide the new_balance_commit as a witness and constrain equality:
constrain_equal_base(new_balance_commit_x, witness_new_balance_commit_x);
constrain_equal_base(new_balance_commit_y, witness_new_balance_commit_y);
// 7. Compute new leaf
new_state_nonce = base_add(old_state_nonce, 1);
new_leaf = poseidon_hash(DOMAIN_MERKLE_LEAF, purse_id, new_balance_commit, new_state_nonce);
// 8. Publish as instances
constrain_instance(new_leaf);
constrain_instance(nullifier);
constrain_instance(root);
// Also publish value-related commitments for supply audit
constrain_instance(deposit_commit_x);
constrain_instance(deposit_commit_y);Withdraw adds two extra constraints:
range_check(64, withdraw_amount); // amount fits in u64
less_than_strict(withdraw_amount, old_balance + 1); // ensures withdraw_amount <= old_balanceThe conservation check becomes new_balance_commit = old_balance_commit - withdraw_commit, enforced by Pedersen subtraction.
Balance is a read-only operation: it proves Merkle inclusion of the current leaf but publishes no nullifier – the purse is not consumed. This allows users to prove ownership of a Purse without spending it.
4.3 Why Pedersen, Not Poseidon, for Balances
Poseidon has no homomorphic property; it cannot be used to add commitments without revealing the plaintext. Pedersen commitments allow:
C(old) + C(deposit) = C(old + deposit) // additive homomorphismThe circuit verifies this equality using EC addition (ec_add) of the commitment coordinates. This is the core of the value conservation proof.
5. The Breakthrough: Why This Was Not Possible Before
5.1 Prior State: L2 Only, or Broken L1
Earlier attempts at L1 capability containers either:
Revealed the object ID as a public input (L2), breaking unlinkability.
Used Poseidon-only commitments without domain separation, allowing hash collisions across types (e.g., a nullifier could be misused as a commitment).
Had unconstrained public inputs – the Zcash Orchard bug [3] is canonical; a
constrain_instancewith no derivation constraint gave the prover arbitrary power.Lacked a consume+create invariant, causing anonymity sets to grow unboundedly and eventually making scanning impractical.
5.2 DarkWow’s Specific Innovations
Domain separation as type restoration: By prepending a domain constant to every
poseidon_hash, we ensure thatposeidon_hash(DOMAIN_NULLIFIER, ...)andposeidon_hash(DOMAIN_MERKLE_LEAF, ...)produce outputs that are not equal for the same inputs. This prevents the prover from reusing a hash intended for one purpose in another context. The domain constants arewitness_base(N)where N is a unique integer per purpose (1=nullifier, 5=leaf, etc.). This is enforced at the circuit level – not just documentation.The four-component architecture eliminates metadata-circuit drift: In prior ZK systems, the metadata function (which returns public inputs) often recomputed values using different hash implementations (e.g., Poseidon without domain constants) or in a different order than the circuit’s
constrain_instancecalls. This led to silent proof failures. DarkWow’s metadata is a pure echo of the params struct, guaranteeing that every public input is exactly what the circuit constrained. This pattern is codified as a design rule for all L1 contracts.Consume+create as a structural invariant: Every L1 operation must consume exactly one old state (by publishing its nullifier) and create exactly one new leaf. This is enforced by the circuit structure – the Apply phase appends the new leaf and marks the nullifier as spent. The anonymity set size
Nremains constant, ensuring that the wallet scan boundN ≤ scan_rate × block_intervalholds. Without this invariant, stale objects would accumulate, degrading privacy for all users.Compositional additivity: When Box and Purse compose (e.g., a Box containing a Purse), the state spaces add, not multiply. This is because each contract has its own Merkle tree; the composition does not create new trajectories. Mathematically:
T(Box ∘ Purse) = T(Box) + T(Purse). This prevents the combinatorial explosion that would make L1 scaling impossible. The proof of additivity is formalized in Lean4 [2].
5.3 Formal Verification of Critical Properties
All Box and Purse circuits have been verified in Lean4 against the Orchard-class detection rule: every constrain_instance(X) has a corresponding derivation constraint X = f(witnesses) in the circuit. The verification suite [4] includes:
Pareto-efficiency: all primitive types have pairwise distinct barb sets – no accidental unification.
Barb preservation: composing capabilities never erases a barb.
Authorization inversion type-level: a capability type exists iff its primitives cover the required barbs.
Wallet construction soundness:
walletConstructreturns a typed capability iff the barbs are covered.
The proof files for Box and Purse are at proofs/lean/src/DarkFi/Capability/Composition.lean and Circuits/Token.lean; they are machine-checked with zero sorry.
6. Performance and Bounds
The combinatorial theorem [2] states that for an L1 contract with N objects and K operations, the number of valid state trajectories is N^K. To keep the system practical, DarkWow imposes bounds on P (public inputs), W (witness values), and O (operations per contract). Box and Purse fall well within the safe region:
ContractPWOSafe?Box Put591✓Box Take471✓Purse Deposit9131✓Purse Withdraw9131✓Purse Balance7111✓
The bounds are P_CEILING = 9, W_CEILING = 13, O_CEILING = 3 – derived from Halo2’s circuit structure (k≤15, instance column proportion 1/7). The practical anonymity set size is bounded by wallet scan rate: N ≤ 120,000 for mobile clients at 120s block intervals.
7. Conclusion
We have presented the first implementation of Box and Purse as fully private object capability primitives in a Halo2 zkVM. The key innovations that enable this breakthrough are:
Domain-separated hashing to restore type distinctions and prevent cross-type attacks.
A four-component architecture that guarantees metadata-circuit agreement, eliminating the Orchard-class vulnerability of unconstrained public inputs.
Consume+create state transitions that keep the anonymity set bounded and wallet scanning feasible.
Additive composition that prevents combinatorial explosion across composed contracts.
These primitives are not just theoretical – they are deployed in DarkWow’s genesis contracts, have undergone adversarial HAZOP analysis, and are formally verified in Lean4. The architectural patterns established here provide a blueprint for building any L1 transferable capability in a privacy-preserving blockchain, enabling a new generation of private DeFi, governance, and identity systems.
References
[1] “The Zero-Knowledge Authorization Inversion Theorem,” https://technologytruth.substack.com/p/the-zero-knowledge-authorization.
[2] DarkWow Combinatorial Analysis, proofs/lean/src/DarkFi/Combinatorial/ – theorems l1TrajectoryCount, safe_l1_classification_sound, ocap_additive_composition.
[3] Zcash Orchard Vulnerability Disclosure, May 2026.
[4] DarkWow Opcode Verification, proofs/lean/src/DarkFi/ – Pareto.lean, Composition.lean, Inversion.lean, Wallet.lean.

