From ea7342221f03279a23c9e936270bcff5d3362f5a Mon Sep 17 00:00:00 2001 From: trillskillz Date: Sun, 24 May 2026 12:53:02 -0500 Subject: [PATCH 1/2] Expose OpZkPrecompile builtin to SilverScript front-end --- silverscript-lang/src/compiler/compile.rs | 1 + .../src/compiler/debug_value_types.rs | 1 + silverscript-lang/std/builtins.sil | 33 +++++++++++++++++++ .../tests/examples/zk_minimal.sil | 7 ++++ silverscript-lang/tests/examples_tests.rs | 10 ++++++ 5 files changed, 52 insertions(+) create mode 100644 silverscript-lang/tests/examples/zk_minimal.sil diff --git a/silverscript-lang/src/compiler/compile.rs b/silverscript-lang/src/compiler/compile.rs index cbca2471..bd6d3d4e 100644 --- a/silverscript-lang/src/compiler/compile.rs +++ b/silverscript-lang/src/compiler/compile.rs @@ -3582,6 +3582,7 @@ fn compile_call_expr<'i>( "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), + "OpZkPrecompile" => compile_opcode_builtin_call(&mut ctx, name, args, 0, OpZkPrecompile), "bytes" => compile_bytes_call(&mut ctx, args), "length" => compile_length_call(&mut ctx, args), "int" | "byte" | "bool" | "string" | "sig" | "pubkey" | "datasig" => compile_passthrough_cast_call(&mut ctx, name, args), diff --git a/silverscript-lang/src/compiler/debug_value_types.rs b/silverscript-lang/src/compiler/debug_value_types.rs index a3ea5903..ba1ac844 100644 --- a/silverscript-lang/src/compiler/debug_value_types.rs +++ b/silverscript-lang/src/compiler/debug_value_types.rs @@ -70,6 +70,7 @@ fn builtin_call_value_type(name: &str) -> &'static str { | "ScriptPubKeyP2PK" | "ScriptPubKeyP2SH" | "ScriptPubKeyP2SHFromRedeemScript" => "byte[]", + "OpZkPrecompile" => "bool", "OpInputCovenantId" | "OpOutputCovenantId" => "byte[32]", _ => "byte[]", } diff --git a/silverscript-lang/std/builtins.sil b/silverscript-lang/std/builtins.sil index 4df97a1b..44676539 100644 --- a/silverscript-lang/std/builtins.sil +++ b/silverscript-lang/std/builtins.sil @@ -137,3 +137,36 @@ function readInputStateWithTemplate( int templateSuffixLen, byte[32] expectedTemplateHash ) : (object); + +/** + * Role: + * Verify a ZK proof via the KIP-16 precompile dispatcher. + * + * Definition: + * The KIP-16 OpZkPrecompile opcode (0xa6) reads a tag byte from the + * stack top, dispatches to the corresponding verifier + * (0x20 = Groth16, 0x21 = RISC0-Succinct), and pushes `true` if the + * proof verifies. Tag-specific operands are consumed from the stack + * in the order documented per precompile. + * + * Stack shape (Groth16, tag 0x20, top-to-bottom): + * [..., public_input_{n-1}, ..., public_input_0, + * n_public_inputs (i32), + * proof_bytes, + * uncompressed_verifying_key, + * tag (0x20)] + * + * Stack shape (RISC0-Succinct, tag 0x21, top-to-bottom): + * [claim, control_index, control_digests, seal, journal, + * image_id, control_id, hashfn, tag (0x21)] + * + * Security notes: + * - Verifying key, image_id, and any other identity-binding inputs + * should come from contract state or a verified protocol + * commitment, never from caller witness. Same trusted-source rule as + * `expectedTemplateHash` in `validateOutputStateWithTemplate`. + * - Failure surfaces as `TxScriptError::ZkIntegrity(String)`. + * SilverScript cannot discriminate "verify failed" from + * "operand deserialise failed". + * - Both precompiles are gated on `vm.flags.covenants_enabled`. They + * activate with the rest of Toccata; pre-activation calls fail with diff --git a/silverscript-lang/tests/examples/zk_minimal.sil b/silverscript-lang/tests/examples/zk_minimal.sil new file mode 100644 index 00000000..bc6589d9 --- /dev/null +++ b/silverscript-lang/tests/examples/zk_minimal.sil @@ -0,0 +1,7 @@ +pragma silverscript ^0.1.0; + +contract ZkMinimal() { + entrypoint function verify() { + require(OpZkPrecompile()); + } +} diff --git a/silverscript-lang/tests/examples_tests.rs b/silverscript-lang/tests/examples_tests.rs index 522ff513..eb2f6222 100644 --- a/silverscript-lang/tests/examples_tests.rs +++ b/silverscript-lang/tests/examples_tests.rs @@ -1532,3 +1532,13 @@ fn compiles_many_assignments_example_under_500_bytes() { // variable should be stored on the stack once and reused by later steps. assert!(compiled.script.len() < 500, "long.sil should compile to less than 500 bytes, got {}", compiled.script.len()); } + +#[test] +fn compiles_zk_minimal_example() { + let source = load_example_source("zk_minimal.sil"); + + let compiled = compile_contract(&source, &[], CompileOptions::default()).expect("zk_minimal should compile"); + compiled.build_sig_script("verify", vec![]).expect("sigscript builds"); + + assert!(!compiled.script.is_empty(), "compiled script should not be empty"); +} From ae065efeabaf923979bf9902b06db0652206460b Mon Sep 17 00:00:00 2001 From: trillskillz Date: Sun, 24 May 2026 13:32:09 -0500 Subject: [PATCH 2/2] Prototype Groth16 helper lowering for zk precompile --- silverscript-lang/src/compiler/compile.rs | 29 +++++++++++++++++++ .../src/compiler/debug_value_types.rs | 2 +- silverscript-lang/std/builtins.sil | 20 +++++++++++++ .../tests/examples/zk_groth16_helper.sil | 13 +++++++++ silverscript-lang/tests/examples_tests.rs | 7 +++++ 5 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 silverscript-lang/tests/examples/zk_groth16_helper.sil diff --git a/silverscript-lang/src/compiler/compile.rs b/silverscript-lang/src/compiler/compile.rs index bd6d3d4e..8f9c90b8 100644 --- a/silverscript-lang/src/compiler/compile.rs +++ b/silverscript-lang/src/compiler/compile.rs @@ -458,6 +458,7 @@ fn infer_expr_type_ref_for_comparison<'i>( | "OpNum2Bin" | "OpBin2Num" | "OpChainblockSeqCommit" + | "OpGroth16Verify" | "LockingBytecodeNullData" | "ScriptPubKeyP2PK" | "ScriptPubKeyP2SH" @@ -3582,6 +3583,7 @@ fn compile_call_expr<'i>( "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), "OpZkPrecompile" => compile_opcode_builtin_call(&mut ctx, name, args, 0, OpZkPrecompile), "bytes" => compile_bytes_call(&mut ctx, args), "length" => compile_length_call(&mut ctx, args), @@ -3792,6 +3794,33 @@ fn compile_blake2b_call<'i>(ctx: &mut CompileCallContext<'_, 'i>, args: &[Expr<' Ok(()) } +fn compile_groth16_verify_call<'i>(ctx: &mut CompileCallContext<'_, 'i>, args: &[Expr<'i>]) -> Result<(), CompilerError> { + if args.len() != 3 { + 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( + "OpGroth16Verify() expects the third argument to be an array literal like [a, b, c]".to_string(), + )); + }; + if public_inputs.is_empty() { + return Err(CompilerError::Unsupported("OpGroth16Verify() expects at least one public input".to_string())); + } + + for public_input in public_inputs { + compile_call_arg_with_context(ctx, public_input)?; + } + ctx.builder.add_i64(public_inputs.len() as i64)?; + *ctx.stack_depth += 1; + compile_call_arg_with_context(ctx, &args[1])?; + compile_call_arg_with_context(ctx, &args[0])?; + ctx.builder.add_data_with_push_opcode(&[0x20])?; + *ctx.stack_depth += 1; + ctx.builder.add_op(OpZkPrecompile)?; + *ctx.stack_depth += 1 - (public_inputs.len() as i64 + 4); + Ok(()) +} + fn compile_checksig_call<'i>(ctx: &mut CompileCallContext<'_, 'i>, args: &[Expr<'i>]) -> Result<(), CompilerError> { if args.len() != 2 { return Err(CompilerError::Unsupported("checkSig() expects 2 arguments".to_string())); diff --git a/silverscript-lang/src/compiler/debug_value_types.rs b/silverscript-lang/src/compiler/debug_value_types.rs index ba1ac844..5b95d73a 100644 --- a/silverscript-lang/src/compiler/debug_value_types.rs +++ b/silverscript-lang/src/compiler/debug_value_types.rs @@ -70,7 +70,7 @@ fn builtin_call_value_type(name: &str) -> &'static str { | "ScriptPubKeyP2PK" | "ScriptPubKeyP2SH" | "ScriptPubKeyP2SHFromRedeemScript" => "byte[]", - "OpZkPrecompile" => "bool", + "OpZkPrecompile" | "OpGroth16Verify" => "bool", "OpInputCovenantId" | "OpOutputCovenantId" => "byte[32]", _ => "byte[]", } diff --git a/silverscript-lang/std/builtins.sil b/silverscript-lang/std/builtins.sil index 44676539..54271c7b 100644 --- a/silverscript-lang/std/builtins.sil +++ b/silverscript-lang/std/builtins.sil @@ -170,3 +170,23 @@ function readInputStateWithTemplate( * "operand deserialise failed". * - Both precompiles are gated on `vm.flags.covenants_enabled`. They * activate with the rest of Toccata; pre-activation calls fail with + * `TxScriptError::ZkPrecompileDisabled`. + */ +function OpZkPrecompile() : bool; + +/** + * Role: + * Groth16-specific helper surface over `OpZkPrecompile`. + * + * Definition: + * `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 the fixed Groth16 tag (`0x20`) before emitting + * `OpZkPrecompile`. + * + * Current prototype constraint: + * - `publicInputs` must be written as an array literal so the front-end can + * flatten its elements during lowering (for example `[a, b, c]`). + */ +function OpGroth16Verify(byte[] verifyingKey, byte[] proofBytes, byte[32][] publicInputs) : bool; diff --git a/silverscript-lang/tests/examples/zk_groth16_helper.sil b/silverscript-lang/tests/examples/zk_groth16_helper.sil new file mode 100644 index 00000000..6baac528 --- /dev/null +++ b/silverscript-lang/tests/examples/zk_groth16_helper.sil @@ -0,0 +1,13 @@ +contract ZkGroth16Helper() { + entrypoint function verify( + byte[32] a, + byte[32] b, + byte[32] c, + byte[32] d, + byte[32] e, + byte[] proof, + byte[] vk + ) { + require(OpGroth16Verify(vk, proof, [a, b, c, d, e])); + } +} diff --git a/silverscript-lang/tests/examples_tests.rs b/silverscript-lang/tests/examples_tests.rs index eb2f6222..7f14cd21 100644 --- a/silverscript-lang/tests/examples_tests.rs +++ b/silverscript-lang/tests/examples_tests.rs @@ -1542,3 +1542,10 @@ fn compiles_zk_minimal_example() { assert!(!compiled.script.is_empty(), "compiled script should not be empty"); } + +#[test] +fn compiles_zk_groth16_helper_example() { + let source = load_example_source("zk_groth16_helper.sil"); + let compiled = compile_contract(&source, &[], CompileOptions::default()).expect("Groth16 helper example should compile"); + assert!(!compiled.script.is_empty(), "compiled script should not be empty"); +}