Direction check: should noir-ckb generate CKB Type Scripts from Noir circuits?

Gm! I’m looking for feedback on the direction I am considering for noir-ckb
before I commit to the next stage of development.

The proposed direction is a toolkit where a developer writes a supported Noir
circuit and a typed Cell-binding specification, then generates the CKB Type
Script, proof artifacts, adversarial tests, and versioned manifest needed to
make that proof authorize one specific Cell transition.

The security rule behind the idea is:

proof verifies mathematically
!=
proof authorizes the intended CKB state transition

The background and original problem are in
The Proof Is Valid. The Transition Might Not Be.
This post is specifically a direction check: whether the proposed generator is
useful, whether its boundary with existing CKB tools is correct, and whether
the proposed second circuit is a convincing way to test it.

What works today

The current supported example takes one pinned, public-first Noir circuit
through:

Noir source and ABI
-> ACIR, R1CS and witness checks
-> development BN254 Groth16 proof
-> strict arkworks and Molecule conversion
-> generic Groth16 verification in CKB-VM
-> Type Script binding to the actual Cell transition

The proof’s seven public values describe the Capsule identity, old state, old
nullifier, new state, action, new nullifier, and replay domain. The generic
verifier checks the proof, while the Capsule Type Script reconstructs that same
ordered statement from the transaction. Both scripts must accept.

The retained matrix contains one accepted transition and eleven expected
rejections. It rejects a valid proof paired with the wrong new state, Capsule
identity, or replay domain, as well as an invalid proof, missing verification
key, malformed data, a changed verifier lock, and ambiguous input groups.

The accepted path used approximately 101.6 million CKB-VM cycles in the retained
development runs. That is an observation for the exact pinned binaries and
fixtures, not a performance guarantee.

Repository: wamimi/noir-ckb-verifier

Reviewer instructions: Week 12 reviewer quickstart

How I understand the relationship with existing work

noir-ckb reuses Cecilia Mulandi’s groth16-ckbverification libraries and wire format.

Cecilia has also published [zk-Lock] (GitHub - CECILIA-MULANDI/zk-Lock: Reusable CKB lock script that conditions cell spending on a valid Groth16 proof. · GitHub),
a reusable Lock Script that gates Cell spending on a Groth16 proof. Its current
flow commits the verification-key hash and public-input hash into lock.args
when the Cell is created. At spending time, the proof becomes the authorization
to unlock that Cell.

The boundary I am exploring is different but complementary. The noir-ckb
prototype uses a Type Script to derive the public statement from the transaction
being attempted: the consumed state, created state, application identity,
action, and replay context. The Lock answers whether the Cell may be spent; the
Type Script answers whether this particular state change is valid. They can
occupy different script slots in the same transaction.

The proposed long-term product

Today, the transaction binding is described twice: once declaratively in
noir-ckb.toml, and once by hand in Rust.

[binding]
public_sources = [
  "type_args.capsule_id",
  "input_cell.data.state_commitment",
  "input_cell.data.nullifier",
  "output_cell.data.state_commitment",
  "constant.update_action_id",
  "output_cell.data.nullifier",
  "type_args.replay_domain",
]

The direction I am considering is to make the binding specification generate
the application layer.

A developer would write a supported Noir circuit and a typed binding manifest.
noir-ckb build would then:

  1. check the Noir ABI, public parameter names, order, and supported compiler
    profile;
  2. generate the Rust Type Script that reads the declared transaction fields and
    reconstructs the public vector;
  3. generate adversarial tests for changed state, identity, action, replay
    context, malformed encodings, and ambiguous groups; and
  4. emit a versioned manifest tying the circuit, verification key, binding, and
    generated script together.

The generated Type Script and test matrix would be the main deliverables. The
goal is to reduce the amount of hand-written byte-offset and public-input
mapping code, because a mistake at that boundary can leave a proof valid while
changing what the application believes it authorizes.

Why I am considering Battleship as the second circuit

I need a second, unrelated circuit to show that the generator is infrastructure
rather than a tool that only reproduces the existing Capsule fixture.

My current candidate is a small on-chain Battleship game. Each player commits
to a board and later proves that each hit-or-miss response is consistent with
that commitment. The proof must also be bound to the game, turn, coordinate,
and exact old-to-new game Cell transition.

It provides visible cheating tests like wrong coordinates, stale turns, cross-game
proofs, inconsistent responses, and altered state updates, without first
depending on an external issuer. The game would demonstrate the toolkit; it
would not define the whole product. Ecosystem feedback may point to a better
CKB-native example.

Important limitations

  • The Capsule circuit is a wiring fixture, not a private protocol. Its arithmetic
    relations expose simple functions of the private value and do not constitute
    a hiding commitment scheme.
  • The current path supports one pinned, public-first Noir compatibility profile,
    not arbitrary Noir circuits.
  • The public-first restriction is a workaround for a public-wire ordering issue
    in the experimental Noir-to-Groth16 backend. I retained the failure as a
    regression and reported it in
    Noir-Groth16 issue #1.
  • The Groth16 setup is development-only. There is no production ceremony,
    audit, mainnet deployment, or independent clean-clone result yet.
  • BN254 Groth16 is not post-quantum. This is application-layer verifiable
    computation, not a replacement for CKB’s work on post-quantum asset
    authorization.

How to test the current preview

The shortest path uses only public retained fixtures. It does not require a CKB
node, wallet, devnet deployment, or generation of trusted-setup material.

Please follow the
reviewer quickstart
and run:

./scripts/reviewer-smoke.sh

It builds both RISC-V scripts and runs the 12-case CKB-VM matrix. Hosted CI has
passed the retained fixture, but no independent external developer has returned
a clean-clone result yet. A complete failure log is as valuable as a pass
because independent reproduction is the next release gate.

Feedback I am looking for

  1. Is the Lock/Type boundary useful: proof-gated spending in a Lock Script and
    proof-bound state-transition validation in a Type Script?
  2. Would a typed binding manifest that generates the Type Script and adversarial
    tests be useful, or would CKB developers prefer to write that application
    binding directly?
  3. Is Battleship a convincing second-circuit test of the generator, or is there
    a better CKB-native problem that needs proof-bound transitions?
  4. Is approximately 101.6 million cycles practical for occasional proof-bound
    authorization, and what cycle target should the project design around?
  5. Which pieces should be contributed to groth16-ckb or the Noir-Groth16
    backend rather than maintained in noir-ckb?
  6. What is the smallest post-program milestone that would produce useful
    ecosystem evidence before any Community Fund proposal?

@Hanssen, @RetricSu, and @matt_ckb — Neon mentioned that your directional
guidance would be especially valuable. I would appreciate your thoughts on the
architecture boundary, reference application, and the smallest useful next
milestone. Tagging @neon.bit as well for the CKBuilder context.

Thanks for reading.

7 Likes

hi @xiaomao , this is very interesting work. Really glad to see you examining this and that you have put forth Battleship as an example to show it in action.

I do think that adding zk to the lock/type functionality opens up things that were not considered before, reading your post is the first time I ever thought about being able to validate two different zk proofs (with different inputs) atomically on CKB.

I am getting the sense that you have a strong grasp of what you’d like to accomplish here, but it does seem a little unclear on the surface. Can you provide some more explanation or examples about separation of concerns between lock (spending) and type (state transition) ?

If either fails, the transaction will not be valid, so the type controls spending conditions as well. I always thought that zk would just go in the lock (and no type would be necessary), but I can see that with what you’ve described here there may be design patterns that make use of a more generalized lock with more detailed conditions about the state transition in the type.

Really looking forward to learning more about the opportunities for innovation you see.

4 Likes

Thank you so much for the response!

You’re right that the Type script also gates spending. All applicable script groups must pass, so the distinction is not that the Lock can reject a transaction while the Type cannot. It is about execution scope and responsibility:

  • A Lock Script runs when a cell is consumed and normally answers whether that input is authorized to be spent.
  • A Type Script runs for typed inputs and outputs and normally answers whether that application state is being created, updated, or destroyed according to its rules.

A new output cell’s own Lock does not execute when that cell is created; its Type does. Type groups also naturally bring the old and new typed cells into the same validation context. That makes Types a natural home for rules that must persist across state transitions.

In the current prototype, one proof is split across two checks:

Generic verifier Lock:
  verify that the proof is mathematically valid
  for its public-input vector

Capsule Type Script:
  derive the expected public-input vector from
  the actual input cell, output cell, identity,
  action and replay domain and compare

The Type does not repeat the Groth16 verification. It checks that the values verified by the Lock actually describe the transaction being attempted.

For example, suppose a proof is valid for:

Capsule 11, old state A, new state B,
update action 1, replay domain 13

If someone attaches it to a transaction that performs A -> C, the proof remains mathematically valid for A -> B, so the verifier Lock accepts it. The Type reads the actual output, reconstructs A -> C, sees the mismatch, and rejects.

invalid proof
  -> Lock rejects

valid proof + intended transition
  -> both accept

valid proof + different transition
  -> Lock accepts mathematically
  -> Type rejects the binding

That third case is the main result the current CKB-VM test matrix demonstrates.

As I think about this more, I see at least four possible patterns. The appropriate one depends on whether the Cell holds evolving application state and whether ownership authorization and state-transition validity should be checked separately.

  1. Proof-only Lock for a stateless cell. Here, possession of a valid proof is the authorization to consume the cell. There may be no continuing application state and therefore no need to create a successor cell with the same rules. This is suitable for something like a proof-gated vault, one-time claim, faucet or private eligibility check. In this case, adding a Type Script may only introduce unnecessary complexity.

  2. Generic verifier Lock plus binding Type, which is what the current prototype implements. The Lock verifies one proof against its verification key and public-input vector. The Type does not verify the proof again; it reads the actual old and new cells and checks that the proof’s public values describe this exact transition. This separates mathematical proof validity from application meaning. It is useful when several applications can reuse the same verifier code while defining their own state-transition rules.

  3. Normal ownership Lock plus proof-verifying Type. A conventional Lock, such as a signature or multisig, controls who may consume the Cell. The Type then verifies the ZK proof and binds it to the old and new application state. This may be more natural for a persistent application such as Battleship: the player authorizes the move through the Lock, while the Type proves that the private game rules were followed and that the resulting game state is valid.

  4. Two-proof composition. The Lock verifies one proof for private authorization, while the Type verifies a different proof for the state transition. For example, proof A could establish that the spender belongs to an authorized set without revealing their identity, while proof B establishes that the committed application state changed correctly. Both proofs would have to pass atomically. They would also need shared public context such as application identity, cell identities, action, and replay domain; otherwise, they could be two valid but unrelated proofs in the same transaction.

The current project establishes the second pattern. I do not yet know whether the generator should support all four. Each has different ownership assumptions, witness layouts, verification-key placement, and adversarial tests. The next question is which additional patterns solve real CKB application needs and which would only increase complexity and security surface.

For Battleship, the third pattern may be more natural than my current layout. A normal player Lock could control the game cell. The circuit would prove that a hit-or-miss response is consistent with the committed board, while the game Type would verify and bind that proof to the game identity, turn, coordinate, old state, and new state. That is proposed, not implemented, but it shows why proof placement should depend on the application.

I also find your two-proof observation very interesting… CKB can require several script groups to pass atomically, so one proof could establish private authorization while another proves a legal state transition.

However, atomicity alone is not enough. Both proofs would need shared public context such as application identity, cell identities, state commitments, action, and replay domain. Otherwise, the transaction only establishes:

proof A is valid AND proof B is valid

not:

proof A and proof B authorize the same operation

Your input has made the next design question much clearer:

which of these additional patterns would solve real CKB application needs, and which would only add complexity and security surface to the generator?

3 Likes