Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions silverscript-lang/src/compiler/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@ fn infer_expr_type_ref_for_comparison<'i>(
| "OpNum2Bin"
| "OpBin2Num"
| "OpChainblockSeqCommit"
| "OpGroth16Verify"
| "LockingBytecodeNullData"
| "ScriptPubKeyP2PK"
| "ScriptPubKeyP2SH"
Expand Down Expand Up @@ -3582,6 +3583,8 @@ 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),

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.

"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),
Expand Down Expand Up @@ -3791,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(

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

"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()));
Expand Down
1 change: 1 addition & 0 deletions silverscript-lang/src/compiler/debug_value_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ fn builtin_call_value_type(name: &str) -> &'static str {
| "ScriptPubKeyP2PK"
| "ScriptPubKeyP2SH"
| "ScriptPubKeyP2SHFromRedeemScript" => "byte[]",
"OpZkPrecompile" | "OpGroth16Verify" => "bool",
"OpInputCovenantId" | "OpOutputCovenantId" => "byte[32]",
_ => "byte[]",
}
Expand Down
53 changes: 53 additions & 0 deletions silverscript-lang/std/builtins.sil
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,56 @@ 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
* `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;
13 changes: 13 additions & 0 deletions silverscript-lang/tests/examples/zk_groth16_helper.sil
Original file line number Diff line number Diff line change
@@ -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]));
}
}
7 changes: 7 additions & 0 deletions silverscript-lang/tests/examples/zk_minimal.sil
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
pragma silverscript ^0.1.0;

contract ZkMinimal() {
entrypoint function verify() {
require(OpZkPrecompile());
}
}
17 changes: 17 additions & 0 deletions silverscript-lang/tests/examples_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1532,3 +1532,20 @@ 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");
}

#[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");
}