Skip to content

Expose OpZkPrecompile builtin to SilverScript front-end#125

Open
trillskillz wants to merge 2 commits into
kaspanet:masterfrom
trillskillz:feat/opzkprecompile-builtin
Open

Expose OpZkPrecompile builtin to SilverScript front-end#125
trillskillz wants to merge 2 commits into
kaspanet:masterfrom
trillskillz:feat/opzkprecompile-builtin

Conversation

@trillskillz

Copy link
Copy Markdown

Summary

Expose OpZkPrecompile as a callable SilverScript builtin.

This wires the existing engine-side KIP-16 opcode (0xa6) through the front-end with the minimal surface needed by downstream callers:

  • add the builtin dispatch row in silverscript-lang/src/compiler/compile.rs
  • add the debug value type row in silverscript-lang/src/compiler/debug_value_types.rs
  • add the stdlib declaration/docs in silverscript-lang/std/builtins.sil
  • add a minimal compile test (zk_minimal.sil) proving the builtin parses/builds through the compiler

Why

OpZkPrecompile is already implemented engine-side in rusty-kaspa via KIP-16, but the pinned silverscript-lang front-end snapshot still has no builtin row for it. That makes it impossible to express ZK-aware patterns in plain SilverScript without brittle raw-bytecode post-processing.

This PR intentionally keeps the builtin at 0 args. The opcode consumes its operands from the stack, and the operand schema varies by precompile tag, so pushing a tag-specific higher-arity wrapper into the compiler would leak verifier details into the front-end. Higher-level helpers can live in downstream SDKs.

Test plan

  • cargo test --manifest-path silverscript-lang/Cargo.toml --test examples_tests -- --nocapture

Context

OpenSilver tracking RFC / downstream context:

Reviewers who may care about the stack-shape/doc wording:

@trillskillz

Copy link
Copy Markdown
Author

Follow-up from downstream prototyping in OpenSilver:

I validated that this PR's 0-arg builtin exposure is necessary but not yet sufficient for authoring a real Phase-5 contract in current SilverScript syntax.

Concrete finding:

  • patched silverc accepts require(OpZkPrecompile())
  • but the language rejects raw expression statements intended to push operands before the opcode, e.g.:
entrypoint function run(byte[] proof, byte[32] a, byte[32] b) {
    a;
    b;
    2;
    proof;
    vk;
    0x20;
    require(OpZkPrecompile());
}

The parser errors at the first a; with:

expected expression_list, modifier, array_suffix, or Identifier

So unless there's another stack-shaping form I'm missing, downstream contracts still need one of:

  1. expression statements / explicit push syntax, or
  2. a higher-arity builtin surface that lowers the operands in the right order.

I'm leaving this PR up because it still exposes the opcode name cleanly, but I wanted to flag that the current front-end syntax appears insufficient for real contract authoring beyond the minimal compile test.

@trillskillz

Copy link
Copy Markdown
Author

Concrete follow-up from local prototyping:

I confirmed that compile_opcode_call() lowers builtin args in source order, then emits the opcode. That means a higher-arity surface can shape the stack deterministically.

I tested a local prototype by changing the builtin arity to 9 (for 5 Groth16 public inputs + n_inputs + proof + vk + tag) and compiling this contract successfully:

contract ShapeTest(byte[] init_vk) {
    byte[] vk = init_vk;

    entrypoint function run(byte[] proof, byte[32] a, byte[32] b, byte[32] c, byte[32] d, byte[32] e) {
        require(OpZkPrecompile(a, b, c, d, e, 5, proof, vk, 0x20));
    }
}

So the tradeoff is now clearer:

  • 0-arg builtin: clean name exposure, but current syntax cannot push operands beforehand.
  • Higher-arity builtin: mechanically viable today, but awkward because Groth16 arity depends on n_public_inputs.

That suggests the real design choice is probably one of:

  1. add expression statements / explicit push syntax (general solution), or
  2. add a tag-specific lowering surface that accepts a structured operand form and emits the canonical pushes internally.

I wanted to post the positive result too, because it shows the compiler lowering path itself is not the problem.

@trillskillz

Copy link
Copy Markdown
Author

One more concrete narrowing result from reading the compiler:

Array arguments also won't solve this automatically.

Relevant behavior in compile.rs:

  • compile_opcode_call() compiles each builtin arg expression in source order, then emits the opcode.
  • compile_array_expr() lowers arrays as encoded/concatenated byte data on the stack, not as multiple stack items.

So a surface like:

OpGroth16Verify(vk, proof, publicInputs)

would still need custom lowering if publicInputs is meant to become:

  • public_input_{n-1} ... public_input_0
  • then n_inputs
  • then proof
  • then vk
  • then tag/opcode

In other words, the current front-end gives us:

  • 0-arg exposure: not enough
  • generic array arg: also not enough by itself
  • fixed higher arity: mechanically works, but awkward for variable n_public_inputs

That makes the likely viable upstream answers even narrower:

  1. explicit push / expression-statement syntax, or
  2. a purpose-built custom lowering helper (tag-specific / structured), not just a plain opcode builtin row.

@trillskillz

Copy link
Copy Markdown
Author

I pushed a concrete follow-up prototype onto the PR branch:

  • commit: ae065ef (Prototype Groth16 helper lowering for zk precompile)
  • branch: trillskillz:feat/opzkprecompile-builtin

What it does:

  • keeps OpZkPrecompile() as the minimal 0-arg builtin exposure from this PR
  • adds a tag-specific helper prototype: OpGroth16Verify(verifyingKey, proofBytes, [a, b, c, ...])
  • custom-lowers the third argument's array literal elements into separate public-input stack items
  • then appends n_public_inputs, proofBytes, verifyingKey, and fixed Groth16 tag 0x20
  • finally emits OpZkPrecompile

Why this seems interesting:

  • it avoids raw push/expression-statement syntax
  • it avoids a brittle OpGroth16Verify5 / OpGroth16Verify6 / ... family
  • it matches the real compiler limitation we found: plain array args compile as blob data, so they only help if the compiler deliberately flattens the array-literal AST during lowering

Validation on the prototype branch:

  • cargo test --manifest-path silverscript-lang/Cargo.toml --test examples_tests -- --nocapture ✅ (27/27)
  • includes a new compile-only example: zk_groth16_helper.sil

I’m not claiming this is definitely the right final API, but it is now a concrete working option if the maintainers prefer a structured helper over broader explicit-push syntax.

trillskillz added a commit to trillskillz/OpenSilver that referenced this pull request May 24, 2026
First real Phase-5 contract: Verified Computation compiles via the
local OpenSilver patch lane (npm run patch:silverc:zk) and runs an
actual Groth16 proof through kaspa-txscript's TxScriptEngine.

contracts/zk/verified-computation.sil
- Stateless covenant that releases funds to a recipient on receipt
  of a valid Groth16 proof + prover signature.
- Verifying key, recipient pubkey, prover pubkey, public-input
  count are all deploy-time contract state — never witness-supplied.
- Two-tier auth: proof attests *what was computed*; prover signature
  attests *who is submitting it*. Conservative default; drop
  requireProver(...) for open-redeem semantics.
- Fixed N=5 public inputs to match the vendored Groth16 fixture; for
  other circuits, recompile a sibling contract with the correct N.

Compiler patch correctness fix:
The earlier patches/silverscript-opzkprecompile.patch pushed public
inputs onto the stack in source order (pi0 first/bottom, pi4 last/
top). But Groth16Precompile::verify_zk pops n_inputs times from the
top and treats them as unprepared_public_inputs[0..n], so the pop
order needs to give the engine [pi0..pi4] not [pi4..pi0]. The fix
is to push in REVERSE source order — mirrors the engine-side
reference test in rusty-kaspa/crypto/txscript/src/zk_precompiles/
groth16/mod.rs::try_verify_stack which pushes input{n-1} first and
input0 last.

This bug was undetectable from the AST-only smoke test (which only
proves the shape parses) — it took a real Groth16 proof to surface.
Caught before the upstream PR merges, so the fix can be folded
into the open PR (kaspanet/silverscript#125).

runtime-tests/tests/zk_runtime.rs — 3 tests, all green:
- verified_computation_accepts_valid_groth16_proof
  Compiles the contract with the real fixture VK, builds a tx that
  supplies the fixture proof + 5 public inputs + a prover sig, and
  asserts kaspa-txscript's engine accepts it. The headline proof
  that Phase 5 actually works.
- verified_computation_rejects_tampered_proof
  Same shape but with one byte of the proof flipped — engine
  rejects with ZkIntegrity (or VerifyError/EvalFalse).
- verified_computation_rejects_wrong_prover_signature
  Valid proof but the prover slot is filled with attacker
  credentials — the require(prover_pk == prover) gate fires.

Also adds:
- tests/zk/verified-computation-compile.test.ts — vitest compile
  scaffold asserting AST presence of OpGroth16Verify +
  requireProver + requireExactPayout.
- runtime-tests/Cargo.toml — serde + serde_json deps for the
  fixture parser.

Test counts: 28/28 vitest files (72 tests), 61/61 cargo runtime
(51 core + 7 kcc20 + 3 zk), 0 ignored. The patch lane
(npm run patch:silverc:zk) must run before cargo test on the zk
suite — documented in the test file's header.
trillskillz added a commit to trillskillz/OpenSilver that referenced this pull request May 24, 2026
Reflects the headline 5.1 milestone landed in the previous commit:
contracts/zk/verified-computation.sil now compiles via the local
patch lane and runtime-verifies a real Groth16 proof end-to-end.

Updates:
- STATUS.md: PHASE_5_STATUS flipped from 'authoring-surface
  blocked' to '5.1 LIVE LOCALLY'. Test counts bumped to
  28 vitest files (72 tests) and 61 cargo runtime tests
  (51 core + 7 kcc20 + 3 zk).
- README.md: Phase 5 entry rewritten — 5.1 is scaffolded +
  runtime-verified; 5.2/5.3/5.4 still design-only. Notes the
  stack-order correctness fix that needs folding into upstream
  PR kaspanet/silverscript#125.
- docs/patterns/zk/verified-computation.md: Status flipped from
  DESIGN to SCAFFOLDED + RUNTIME-VERIFIED. New section
  enumerates the three runtime tests by name and documents the
  patch correctness fix.
- sdk/src/index.ts: manifest entry for
  zk-aware.verified-computation flipped from 'planned' to
  'scaffolded' with a contractPath now pointing at the real
  source. CLI 'opensilver list' / 'opensilver get
  zk-aware.verified-computation' now show the correct status.
- NEXT_SESSION.md: implementation order updated — 5.1 marked
  DONE, 5.3 ZK-Verified Oracle promoted to NEXT (reuses
  Groth16 fixture, adds M-of-N committee composition).
return Err(CompilerError::Unsupported("OpGroth16Verify() expects 3 arguments: verifying_key, proof, public_inputs".to_string()));
}
let ExprKind::Array(public_inputs) = &args[2].kind else {
return Err(CompilerError::Unsupported(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no opcode called opgroth16verify. Please ensure errors are representative of the actual opcode names

"OpNum2Bin" => compile_opcode_builtin_call(&mut ctx, name, args, 2, OpNum2Bin),
"OpBin2Num" => compile_opcode_builtin_call(&mut ctx, name, args, 1, OpBin2Num),
"OpChainblockSeqCommit" => compile_opcode_builtin_call(&mut ctx, name, args, 1, OpChainblockSeqCommit),
"OpGroth16Verify" => compile_groth16_verify_call(&mut ctx, args),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should not be such an opcode. It should be invoked via opzkprecompile and maybe we should pass args as a vector in order to ensure all of the tags are supported.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants