From 64a3e8c21a8ae93cb307342648bbc05e22bbcaa9 Mon Sep 17 00:00:00 2001 From: elldeeone <73735118+elldeeone@users.noreply.github.com> Date: Tue, 23 Jun 2026 07:46:57 +0000 Subject: [PATCH 1/2] Implement real checkDataSig lowering --- Cargo.lock | 1 + docs/TUTORIAL.md | 10 +++++ silverscript-lang/Cargo.toml | 1 + silverscript-lang/src/compiler/compile.rs | 16 +++---- .../tests/cashc_valid_examples_tests.rs | 44 +++++++++++++------ silverscript-lang/tests/compiler_tests.rs | 38 ++++++++++++++++ silverscript-lang/tests/examples_tests.rs | 4 +- 7 files changed, 91 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6b25cd0..40b194f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3055,6 +3055,7 @@ dependencies = [ "semver", "serde", "serde_json", + "sha2", "thiserror 1.0.69", ] diff --git a/docs/TUTORIAL.md b/docs/TUTORIAL.md index 849542f..74a68d9 100644 --- a/docs/TUTORIAL.md +++ b/docs/TUTORIAL.md @@ -848,6 +848,16 @@ Verify a signature against a public key: require(checkSig(s, pk)); ``` +**`checkDataSig(datasig signature, byte[] message, pubkey publicKey): bool`** + +Verify a 64-byte Schnorr data signature against a public key. The signature is +checked over `sha256(message)`, so off-chain signers must sign the SHA-256 hash +of the message bytes supplied to the contract: + +```javascript +require(checkDataSig(oracleSig, oracleMessage, oraclePk)); +``` + ### Type Conversion Functions **`byte[](value): bytes`** diff --git a/silverscript-lang/Cargo.toml b/silverscript-lang/Cargo.toml index 5f31458..81c1ae4 100644 --- a/silverscript-lang/Cargo.toml +++ b/silverscript-lang/Cargo.toml @@ -31,3 +31,4 @@ semver = "1.0" [dev-dependencies] kaspa-addresses.workspace = true +sha2 = "0.10" diff --git a/silverscript-lang/src/compiler/compile.rs b/silverscript-lang/src/compiler/compile.rs index 988aae4..2f56204 100644 --- a/silverscript-lang/src/compiler/compile.rs +++ b/silverscript-lang/src/compiler/compile.rs @@ -3801,15 +3801,15 @@ fn compile_checksig_call<'i>(ctx: &mut CompileCallContext<'_, 'i>, args: &[Expr< } fn compile_checkdatasig_call<'i>(ctx: &mut CompileCallContext<'_, 'i>, args: &[Expr<'i>]) -> Result<(), CompilerError> { - for arg in args { - compile_call_arg_with_context(ctx, arg)?; - } - for _ in 0..args.len() { - ctx.builder.add_op(OpDrop)?; - *ctx.stack_depth -= 1; + if args.len() != 3 { + return Err(CompilerError::Unsupported("checkDataSig() expects 3 arguments (signature, message, publicKey)".to_string())); } - ctx.builder.add_op(OpTrue)?; - *ctx.stack_depth += 1; + compile_call_arg_with_context(ctx, &args[0])?; + compile_call_arg_with_context(ctx, &args[1])?; + ctx.builder.add_op(OpSHA256)?; + compile_call_arg_with_context(ctx, &args[2])?; + ctx.builder.add_op(OpCheckSigFromStack)?; + *ctx.stack_depth -= 2; Ok(()) } diff --git a/silverscript-lang/tests/cashc_valid_examples_tests.rs b/silverscript-lang/tests/cashc_valid_examples_tests.rs index abcd555..0ddb687 100644 --- a/silverscript-lang/tests/cashc_valid_examples_tests.rs +++ b/silverscript-lang/tests/cashc_valid_examples_tests.rs @@ -700,20 +700,36 @@ fn runs_cashc_valid_examples() { assert!(result.is_err(), "{example} should fail"); } "simple_checkdatasig.sil" => { - let constructor_args = vec![vec![0u8; 64].into(), vec![1u8; 32].into()]; - let compiled = compile_contract(&source, &constructor_args, CompileOptions::default()).expect("compile succeeds"); - let selector = selector_for_compiled(&compiled, "cds"); - let sigscript = build_sigscript(&[ArgValue::Bytes(b"data".to_vec())], selector); - let (mut tx, utxo, reused) = build_tx_context( - compiled.script.clone(), - vec![(1_000, compiled.script.clone()), (1_000, compiled.script.clone())], - 2_000, - 0, - 1, - ); - tx.tx.inputs[0].signature_script = sigscript; - let result = execute_tx(tx, utxo, reused); - assert!(result.is_ok(), "{example} failed: {}", result.unwrap_err()); + use sha2::{Digest, Sha256}; + + let keypair = random_keypair(); + let pubkey_bytes = keypair.x_only_public_key().0.serialize().to_vec(); + let message = b"data".to_vec(); + let message_hash = Sha256::digest(&message); + let signed = Message::from_digest_slice(&message_hash).expect("sha256 digest is 32 bytes"); + let valid_sig = keypair.sign_schnorr(signed).as_ref().to_vec(); + assert_eq!(valid_sig.len(), 64, "datasig is a bare schnorr signature"); + + let run = |signature: Vec| { + let constructor_args = vec![signature.into(), pubkey_bytes.clone().into()]; + let compiled = compile_contract(&source, &constructor_args, CompileOptions::default()).expect("compile succeeds"); + let selector = selector_for_compiled(&compiled, "cds"); + let sigscript = build_sigscript(&[ArgValue::Bytes(message.clone())], selector); + let (mut tx, utxo, reused) = build_tx_context( + compiled.script.clone(), + vec![(1_000, compiled.script.clone()), (1_000, compiled.script.clone())], + 2_000, + 0, + 1, + ); + tx.tx.inputs[0].signature_script = sigscript; + execute_tx(tx, utxo, reused) + }; + + assert!(run(valid_sig.clone()).is_ok(), "{example}: valid data signature should pass"); + let mut forged_sig = valid_sig; + forged_sig[0] ^= 0x01; + assert!(run(forged_sig).is_err(), "{example}: forged data signature should fail"); } "simple_constant.sil" => { let constructor_args = vec![]; diff --git a/silverscript-lang/tests/compiler_tests.rs b/silverscript-lang/tests/compiler_tests.rs index f789c02..76c8e27 100644 --- a/silverscript-lang/tests/compiler_tests.rs +++ b/silverscript-lang/tests/compiler_tests.rs @@ -7329,6 +7329,44 @@ fn assert_compiled_body(source: &str, body: Vec) { assert_eq!(compiled.script, expected); } +#[test] +fn checkdatasig_lowers_to_checksigfromstack_over_sha256_message() { + let source = r#" + contract DataSig(datasig signature, byte[] message, pubkey publicKey) { + entrypoint function main() { + require(checkDataSig(signature, message, publicKey)); + } + } + "#; + let signature = vec![0x11; 64]; + let message = b"authorize".to_vec(); + let public_key = vec![0x22; 32]; + let compiled = compile_contract( + source, + &[signature.clone().into(), message.clone().into(), public_key.clone().into()], + CompileOptions::default(), + ) + .expect("compile succeeds"); + + let expected = ScriptBuilder::new() + .add_data_with_push_opcode(&signature) + .unwrap() + .add_data_with_push_opcode(&message) + .unwrap() + .add_op(OpSHA256) + .unwrap() + .add_data_with_push_opcode(&public_key) + .unwrap() + .add_op(OpCheckSigFromStack) + .unwrap() + .add_op(OpVerify) + .unwrap() + .add_op(OpTrue) + .unwrap() + .drain(); + assert_eq!(compiled.script, expected); +} + #[test] fn canonicalizes_bool_comparison_operands_for_equality_and_inequality() { let cases = [(("=="), OpNumEqual), (("!="), OpNumNotEqual)]; diff --git a/silverscript-lang/tests/examples_tests.rs b/silverscript-lang/tests/examples_tests.rs index 522ff51..902ae55 100644 --- a/silverscript-lang/tests/examples_tests.rs +++ b/silverscript-lang/tests/examples_tests.rs @@ -484,7 +484,9 @@ fn compiles_hodl_vault_example_and_verifies() { let block_height = 1000u32; let price = 20u32; let oracle_message = [block_height.to_le_bytes(), price.to_le_bytes()].concat(); - let oracle_sig = vec![0u8; 64]; + let oracle_message_hash = ::digest(&oracle_message); + let oracle_signed = secp256k1::Message::from_digest_slice(&oracle_message_hash).unwrap(); + let oracle_sig = oracle.sign_schnorr(oracle_signed).as_ref().to_vec(); let constructor_args = vec![owner_pk.to_vec().into(), oracle_pk.to_vec().into(), min_block.into(), price_target.into()]; let compiled = compile_contract(&source, &constructor_args, CompileOptions::default()).expect("compile succeeds"); From b10d872d77a6d4425dc5033d5772907d80147e13 Mon Sep 17 00:00:00 2001 From: elldeeone <73735118+elldeeone@users.noreply.github.com> Date: Wed, 24 Jun 2026 05:22:59 +0000 Subject: [PATCH 2/2] Expose typed signature stack builtins --- docs/TUTORIAL.md | 19 +- .../queries/silverscript/highlights.scm | 2 +- extensions/vscode/queries/highlights.scm | 3 +- .../zed/languages/silverscript/highlights.scm | 2 +- silverscript-lang/src/compiler/compile.rs | 17 +- .../src/compiler/debug_value_types.rs | 2 +- .../src/compiler/static_check.rs | 223 +++++++++++-- .../tests/cashc_valid_examples_tests.rs | 40 ++- silverscript-lang/tests/compiler_tests.rs | 310 +++++++++++++++++- .../tests/examples/hodl_vault.sil | 2 +- ...tasig.sil => simple_checksigfromstack.sil} | 2 +- .../simple_checksigfromstack_ecdsa.sil | 7 + .../tests/examples/trailing_comma.sil | 4 +- tree-sitter/queries/highlights.scm | 2 +- 14 files changed, 570 insertions(+), 65 deletions(-) rename silverscript-lang/tests/examples/{simple_checkdatasig.sil => simple_checksigfromstack.sil} (67%) create mode 100644 silverscript-lang/tests/examples/simple_checksigfromstack_ecdsa.sil diff --git a/docs/TUTORIAL.md b/docs/TUTORIAL.md index 74a68d9..fa8084d 100644 --- a/docs/TUTORIAL.md +++ b/docs/TUTORIAL.md @@ -848,14 +848,23 @@ Verify a signature against a public key: require(checkSig(s, pk)); ``` -**`checkDataSig(datasig signature, byte[] message, pubkey publicKey): bool`** +**`CheckSigFromStack(datasig signature, byte[32] digest, pubkey publicKey): bool`** -Verify a 64-byte Schnorr data signature against a public key. The signature is -checked over `sha256(message)`, so off-chain signers must sign the SHA-256 hash -of the message bytes supplied to the contract: +Verify a 64-byte Schnorr signature against a 32-byte digest supplied by the +contract. Hash the message explicitly with the hash function required by your +protocol: ```javascript -require(checkDataSig(oracleSig, oracleMessage, oraclePk)); +require(CheckSigFromStack(oracleSig, sha256(oracleMessage), oraclePk)); +``` + +**`CheckSigFromStackECDSA(datasig signature, byte[32] digest, byte[33] publicKey): bool`** + +Verify a compact 64-byte ECDSA signature against a 32-byte digest and compressed +33-byte ECDSA public key: + +```javascript +require(CheckSigFromStackECDSA(oracleSig, sha256(oracleMessage), oraclePk)); ``` ### Type Conversion Functions diff --git a/extensions/silverscript.nvim/queries/silverscript/highlights.scm b/extensions/silverscript.nvim/queries/silverscript/highlights.scm index d5d7a56..e39f591 100644 --- a/extensions/silverscript.nvim/queries/silverscript/highlights.scm +++ b/extensions/silverscript.nvim/queries/silverscript/highlights.scm @@ -72,7 +72,7 @@ (function_call (identifier) @function.builtin (#match? @function.builtin - "^(readInputState|readInputStateWithTemplate|validateOutputState|validateOutputStateWithTemplate|verifyOutputState|verifyOutputStates|OpSha256|sha256|OpTxSubnetId|OpTxGas|OpTxPayloadLen|OpTxPayloadSubstr|OpOutpointTxId|OpOutpointIndex|OpTxInputScriptSigLen|OpTxInputScriptSigSubstr|OpTxInputSeq|OpTxInputIsCoinbase|OpTxInputSpkLen|OpTxInputSpkSubstr|OpTxOutputSpkLen|OpTxOutputSpkSubstr|OpAuthOutputCount|OpAuthOutputIdx|OpInputCovenantId|OpOutputCovenantId|OpCovInputCount|OpCovInputIdx|OpCovOutputCount|OpCovOutputIdx|OpNum2Bin|OpBin2Num|OpChainblockSeqCommit|checkDataSig|checkSig|checkMultiSig|blake2b)$")) + "^(readInputState|readInputStateWithTemplate|validateOutputState|validateOutputStateWithTemplate|verifyOutputState|verifyOutputStates|OpSha256|sha256|OpTxSubnetId|OpTxGas|OpTxPayloadLen|OpTxPayloadSubstr|OpOutpointTxId|OpOutpointIndex|OpTxInputScriptSigLen|OpTxInputScriptSigSubstr|OpTxInputSeq|OpTxInputIsCoinbase|OpTxInputSpkLen|OpTxInputSpkSubstr|OpTxOutputSpkLen|OpTxOutputSpkSubstr|OpAuthOutputCount|OpAuthOutputIdx|OpInputCovenantId|OpOutputCovenantId|OpCovInputCount|OpCovInputIdx|OpCovOutputCount|OpCovOutputIdx|OpNum2Bin|OpBin2Num|OpChainblockSeqCommit|CheckSigFromStack|CheckSigFromStackECDSA|checkSig|checkMultiSig|blake2b)$")) (unary_suffix) @property diff --git a/extensions/vscode/queries/highlights.scm b/extensions/vscode/queries/highlights.scm index b135bf3..e39f591 100644 --- a/extensions/vscode/queries/highlights.scm +++ b/extensions/vscode/queries/highlights.scm @@ -72,7 +72,7 @@ (function_call (identifier) @function.builtin (#match? @function.builtin - "^(readInputState|readInputStateWithTemplate|validateOutputState|validateOutputStateWithTemplate|verifyOutputState|verifyOutputStates|OpSha256|sha256|OpTxSubnetId|OpTxGas|OpTxPayloadLen|OpTxPayloadSubstr|OpOutpointTxId|OpOutpointIndex|OpTxInputScriptSigLen|OpTxInputScriptSigSubstr|OpTxInputSeq|OpTxInputIsCoinbase|OpTxInputSpkLen|OpTxInputSpkSubstr|OpTxOutputSpkLen|OpTxOutputSpkSubstr|OpAuthOutputCount|OpAuthOutputIdx|OpInputCovenantId|OpOutputCovenantId|OpCovInputCount|OpCovInputIdx|OpCovOutputCount|OpCovOutputIdx|OpNum2Bin|OpBin2Num|OpChainblockSeqCommit|checkDataSig|checkSig|checkMultiSig|blake2b)$")) + "^(readInputState|readInputStateWithTemplate|validateOutputState|validateOutputStateWithTemplate|verifyOutputState|verifyOutputStates|OpSha256|sha256|OpTxSubnetId|OpTxGas|OpTxPayloadLen|OpTxPayloadSubstr|OpOutpointTxId|OpOutpointIndex|OpTxInputScriptSigLen|OpTxInputScriptSigSubstr|OpTxInputSeq|OpTxInputIsCoinbase|OpTxInputSpkLen|OpTxInputSpkSubstr|OpTxOutputSpkLen|OpTxOutputSpkSubstr|OpAuthOutputCount|OpAuthOutputIdx|OpInputCovenantId|OpOutputCovenantId|OpCovInputCount|OpCovInputIdx|OpCovOutputCount|OpCovOutputIdx|OpNum2Bin|OpBin2Num|OpChainblockSeqCommit|CheckSigFromStack|CheckSigFromStackECDSA|checkSig|checkMultiSig|blake2b)$")) (unary_suffix) @property @@ -100,7 +100,6 @@ "new" "require" "return" - "yield" "console.log" ] @keyword diff --git a/extensions/zed/languages/silverscript/highlights.scm b/extensions/zed/languages/silverscript/highlights.scm index d5d7a56..e39f591 100644 --- a/extensions/zed/languages/silverscript/highlights.scm +++ b/extensions/zed/languages/silverscript/highlights.scm @@ -72,7 +72,7 @@ (function_call (identifier) @function.builtin (#match? @function.builtin - "^(readInputState|readInputStateWithTemplate|validateOutputState|validateOutputStateWithTemplate|verifyOutputState|verifyOutputStates|OpSha256|sha256|OpTxSubnetId|OpTxGas|OpTxPayloadLen|OpTxPayloadSubstr|OpOutpointTxId|OpOutpointIndex|OpTxInputScriptSigLen|OpTxInputScriptSigSubstr|OpTxInputSeq|OpTxInputIsCoinbase|OpTxInputSpkLen|OpTxInputSpkSubstr|OpTxOutputSpkLen|OpTxOutputSpkSubstr|OpAuthOutputCount|OpAuthOutputIdx|OpInputCovenantId|OpOutputCovenantId|OpCovInputCount|OpCovInputIdx|OpCovOutputCount|OpCovOutputIdx|OpNum2Bin|OpBin2Num|OpChainblockSeqCommit|checkDataSig|checkSig|checkMultiSig|blake2b)$")) + "^(readInputState|readInputStateWithTemplate|validateOutputState|validateOutputStateWithTemplate|verifyOutputState|verifyOutputStates|OpSha256|sha256|OpTxSubnetId|OpTxGas|OpTxPayloadLen|OpTxPayloadSubstr|OpOutpointTxId|OpOutpointIndex|OpTxInputScriptSigLen|OpTxInputScriptSigSubstr|OpTxInputSeq|OpTxInputIsCoinbase|OpTxInputSpkLen|OpTxInputSpkSubstr|OpTxOutputSpkLen|OpTxOutputSpkSubstr|OpAuthOutputCount|OpAuthOutputIdx|OpInputCovenantId|OpOutputCovenantId|OpCovInputCount|OpCovInputIdx|OpCovOutputCount|OpCovOutputIdx|OpNum2Bin|OpBin2Num|OpChainblockSeqCommit|CheckSigFromStack|CheckSigFromStackECDSA|checkSig|checkMultiSig|blake2b)$")) (unary_suffix) @property diff --git a/silverscript-lang/src/compiler/compile.rs b/silverscript-lang/src/compiler/compile.rs index 2f56204..da2f956 100644 --- a/silverscript-lang/src/compiler/compile.rs +++ b/silverscript-lang/src/compiler/compile.rs @@ -469,6 +469,8 @@ fn infer_expr_type_ref_for_comparison<'i>( | "ScriptPubKeyP2SHFromRedeemScript" | "OpInputCovenantId" | "OpOutputCovenantId" + | "CheckSigFromStack" + | "CheckSigFromStackECDSA" | "OpTxGas" | "OpTxPayloadLen" | "OpTxInputIndex" @@ -3589,7 +3591,8 @@ fn compile_call_expr<'i>( } "blake2b" => compile_blake2b_call(&mut ctx, args), "checkSig" => compile_checksig_call(&mut ctx, args), - "checkDataSig" => compile_checkdatasig_call(&mut ctx, args), + "CheckSigFromStack" => compile_checksigfromstack_call(&mut ctx, name, args, OpCheckSigFromStack), + "CheckSigFromStackECDSA" => compile_checksigfromstack_call(&mut ctx, name, args, OpCheckSigFromStackECDSA), _ => compile_unknown_function_call(name), } } @@ -3800,15 +3803,19 @@ fn compile_checksig_call<'i>(ctx: &mut CompileCallContext<'_, 'i>, args: &[Expr< Ok(()) } -fn compile_checkdatasig_call<'i>(ctx: &mut CompileCallContext<'_, 'i>, args: &[Expr<'i>]) -> Result<(), CompilerError> { +fn compile_checksigfromstack_call<'i>( + ctx: &mut CompileCallContext<'_, 'i>, + name: &str, + args: &[Expr<'i>], + opcode: u8, +) -> Result<(), CompilerError> { if args.len() != 3 { - return Err(CompilerError::Unsupported("checkDataSig() expects 3 arguments (signature, message, publicKey)".to_string())); + return Err(CompilerError::Unsupported(format!("{name}() expects 3 arguments (signature, digest, publicKey)"))); } compile_call_arg_with_context(ctx, &args[0])?; compile_call_arg_with_context(ctx, &args[1])?; - ctx.builder.add_op(OpSHA256)?; compile_call_arg_with_context(ctx, &args[2])?; - ctx.builder.add_op(OpCheckSigFromStack)?; + ctx.builder.add_op(opcode)?; *ctx.stack_depth -= 2; Ok(()) } diff --git a/silverscript-lang/src/compiler/debug_value_types.rs b/silverscript-lang/src/compiler/debug_value_types.rs index a3ea590..183fdc3 100644 --- a/silverscript-lang/src/compiler/debug_value_types.rs +++ b/silverscript-lang/src/compiler/debug_value_types.rs @@ -54,7 +54,7 @@ fn builtin_call_value_type(name: &str) -> &'static str { | "OpCovInputIdx" | "OpCovOutputCount" | "OpCovOutputIdx" => "int", - "OpTxInputIsCoinbase" => "bool", + "OpTxInputIsCoinbase" | "checkSig" | "CheckSigFromStack" | "CheckSigFromStackECDSA" => "bool", "blake2b" | "sha256" | "OpSha256" => "byte[32]", "bytes" | "OpTxSubnetId" diff --git a/silverscript-lang/src/compiler/static_check.rs b/silverscript-lang/src/compiler/static_check.rs index ce4ca70..8660725 100644 --- a/silverscript-lang/src/compiler/static_check.rs +++ b/silverscript-lang/src/compiler/static_check.rs @@ -162,7 +162,16 @@ fn validate_contract_field_initializers<'i>( for field in &contract.fields { let type_name = type_name_from_ref(&field.type_ref); - validate_expr_semantics(&field.expr, constants, &HashSet::new(), &types, structs, &HashMap::new(), &contract.fields)?; + validate_expr_semantics( + &field.expr, + constants, + &HashSet::new(), + &types, + structs, + constants, + &HashMap::new(), + &contract.fields, + )?; ensure_array_elements_have_known_size(&field.type_ref, structs, &type_name)?; validate_expr_assignable_to_type(&field.expr, &field.type_ref, &types, structs, constants, &HashMap::new(), &contract.fields) .map_err(|_| CompilerError::Unsupported(format!("contract field '{}' expects {}", field.name, type_name)))?; @@ -262,6 +271,7 @@ fn validate_variable_definition_statement_shape<'i>( ctx.prefer_env_for_comparison, ctx.types, ctx.structs, + ctx.constants, ctx.functions, ctx.contract_fields, )?; @@ -331,7 +341,16 @@ fn validate_tuple_assignment_statement_shape<'i>( right_name: &str, expr: &Expr<'i>, ) -> Result<(), CompilerError> { - validate_expr_semantics(expr, ctx.env, ctx.prefer_env_for_comparison, ctx.types, ctx.structs, ctx.functions, ctx.contract_fields)?; + validate_expr_semantics( + expr, + ctx.env, + ctx.prefer_env_for_comparison, + ctx.types, + ctx.structs, + ctx.constants, + ctx.functions, + ctx.contract_fields, + )?; ensure_array_elements_have_known_size(left_type_ref, ctx.structs, &type_name_from_ref(left_type_ref))?; ensure_array_elements_have_known_size(right_type_ref, ctx.structs, &type_name_from_ref(right_type_ref))?; if let ExprKind::Split { source, index, span: split_span, .. } = &expr.kind { @@ -365,6 +384,7 @@ fn validate_state_function_call_assign_statement_shape<'i>( ctx.prefer_env_for_comparison, ctx.types, ctx.structs, + ctx.constants, ctx.functions, ctx.contract_fields, )?; @@ -382,7 +402,16 @@ fn validate_struct_destructure_statement_shape<'i>( bindings: &[StateBindingAst<'i>], expr: &Expr<'i>, ) -> Result<(), CompilerError> { - validate_expr_semantics(expr, ctx.env, ctx.prefer_env_for_comparison, ctx.types, ctx.structs, ctx.functions, ctx.contract_fields)?; + validate_expr_semantics( + expr, + ctx.env, + ctx.prefer_env_for_comparison, + ctx.types, + ctx.structs, + ctx.constants, + ctx.functions, + ctx.contract_fields, + )?; validate_struct_destructure_bindings(bindings, expr, ctx.types, ctx.structs, ctx.contract_fields)?; for binding in bindings { ensure_array_elements_have_known_size(&binding.type_ref, ctx.structs, &type_name_from_ref(&binding.type_ref))?; @@ -403,10 +432,22 @@ fn validate_function_call_statement_shape<'i>( ctx.prefer_env_for_comparison, ctx.types, ctx.structs, + ctx.constants, ctx.functions, ctx.contract_fields, )?; } + validate_builtin_call( + name, + args, + ctx.env, + ctx.prefer_env_for_comparison, + ctx.types, + ctx.structs, + ctx.constants, + ctx.functions, + ctx.contract_fields, + )?; if ctx.functions.contains_key(name) { validate_internal_call(name, args, ctx.types, ctx.structs, ctx.constants, ctx.functions, ctx.contract_fields)?; } @@ -426,6 +467,7 @@ fn validate_function_call_assign_statement_shape<'i>( ctx.prefer_env_for_comparison, ctx.types, ctx.structs, + ctx.constants, ctx.functions, ctx.contract_fields, )?; @@ -458,6 +500,7 @@ fn validate_return_statement_shape<'i>( ctx.prefer_env_for_comparison, ctx.types, ctx.structs, + ctx.constants, ctx.functions, ctx.contract_fields, )?; @@ -469,14 +512,32 @@ fn validate_require_statement_shape<'i>( ctx: &mut ValidateStatementShapesContext<'_, 'i>, expr: &Expr<'i>, ) -> Result<(), CompilerError> { - validate_expr_semantics(expr, ctx.env, ctx.prefer_env_for_comparison, ctx.types, ctx.structs, ctx.functions, ctx.contract_fields) + validate_expr_semantics( + expr, + ctx.env, + ctx.prefer_env_for_comparison, + ctx.types, + ctx.structs, + ctx.constants, + ctx.functions, + ctx.contract_fields, + ) } fn validate_time_op_statement_shape<'i>( ctx: &mut ValidateStatementShapesContext<'_, 'i>, expr: &Expr<'i>, ) -> Result<(), CompilerError> { - validate_expr_semantics(expr, ctx.env, ctx.prefer_env_for_comparison, ctx.types, ctx.structs, ctx.functions, ctx.contract_fields) + validate_expr_semantics( + expr, + ctx.env, + ctx.prefer_env_for_comparison, + ctx.types, + ctx.structs, + ctx.constants, + ctx.functions, + ctx.contract_fields, + ) } fn validate_console_statement_shape<'i>( @@ -490,6 +551,7 @@ fn validate_console_statement_shape<'i>( ctx.prefer_env_for_comparison, ctx.types, ctx.structs, + ctx.constants, ctx.functions, ctx.contract_fields, )?; @@ -502,7 +564,16 @@ fn validate_assign_statement_shape<'i>( name: &str, expr: &Expr<'i>, ) -> Result<(), CompilerError> { - validate_expr_semantics(expr, ctx.env, ctx.prefer_env_for_comparison, ctx.types, ctx.structs, ctx.functions, ctx.contract_fields)?; + validate_expr_semantics( + expr, + ctx.env, + ctx.prefer_env_for_comparison, + ctx.types, + ctx.structs, + ctx.constants, + ctx.functions, + ctx.contract_fields, + )?; if let Some(type_name) = ctx.types.get(name).cloned() { let type_ref = parse_type_ref(&type_name)?; validate_expr_assignable_to_type(expr, &type_ref, ctx.types, ctx.structs, ctx.constants, ctx.functions, ctx.contract_fields) @@ -528,6 +599,7 @@ fn validate_if_statement_shape<'i>( ctx.prefer_env_for_comparison, ctx.types, ctx.structs, + ctx.constants, ctx.functions, ctx.contract_fields, )?; @@ -599,16 +671,27 @@ fn validate_for_statement_shape<'i>( ctx.prefer_env_for_comparison, ctx.types, ctx.structs, + ctx.constants, + ctx.functions, + ctx.contract_fields, + )?; + validate_expr_semantics( + end, + ctx.env, + ctx.prefer_env_for_comparison, + ctx.types, + ctx.structs, + ctx.constants, ctx.functions, ctx.contract_fields, )?; - validate_expr_semantics(end, ctx.env, ctx.prefer_env_for_comparison, ctx.types, ctx.structs, ctx.functions, ctx.contract_fields)?; validate_expr_semantics( max_iterations, ctx.env, ctx.prefer_env_for_comparison, ctx.types, ctx.structs, + ctx.constants, ctx.functions, ctx.contract_fields, )?; @@ -759,13 +842,14 @@ fn validate_expr_semantics<'i>( prefer_env_for_comparison: &HashSet, types: &HashMap, structs: &StructRegistry, + constants: &HashMap>, functions: &HashMap>, contract_fields: &[ContractFieldAst<'i>], ) -> Result<(), CompilerError> { match &expr.kind { ExprKind::Binary { op, left, right } => { - validate_expr_semantics(left, env, prefer_env_for_comparison, types, structs, functions, contract_fields)?; - validate_expr_semantics(right, env, prefer_env_for_comparison, types, structs, functions, contract_fields)?; + validate_expr_semantics(left, env, prefer_env_for_comparison, types, structs, constants, functions, contract_fields)?; + validate_expr_semantics(right, env, prefer_env_for_comparison, types, structs, constants, functions, contract_fields)?; let left_value_type = super::debug_value_types::infer_debug_expr_value_type(left, env, types, &mut HashSet::new()).ok(); let right_value_type = super::debug_value_types::infer_debug_expr_value_type(right, env, types, &mut HashSet::new()).ok(); if matches!(op, BinaryOp::Add) @@ -806,12 +890,12 @@ fn validate_expr_semantics<'i>( Ok(()) } ExprKind::Unary { expr, .. } => { - validate_expr_semantics(expr, env, prefer_env_for_comparison, types, structs, functions, contract_fields) + validate_expr_semantics(expr, env, prefer_env_for_comparison, types, structs, constants, functions, contract_fields) } ExprKind::IfElse { condition, then_expr, else_expr } => { - validate_expr_semantics(condition, env, prefer_env_for_comparison, types, structs, functions, contract_fields)?; - validate_expr_semantics(then_expr, env, prefer_env_for_comparison, types, structs, functions, contract_fields)?; - validate_expr_semantics(else_expr, env, prefer_env_for_comparison, types, structs, functions, contract_fields)?; + validate_expr_semantics(condition, env, prefer_env_for_comparison, types, structs, constants, functions, contract_fields)?; + validate_expr_semantics(then_expr, env, prefer_env_for_comparison, types, structs, constants, functions, contract_fields)?; + validate_expr_semantics(else_expr, env, prefer_env_for_comparison, types, structs, constants, functions, contract_fields)?; let then_type = infer_expr_type_ref_for_comparison_ref( then_expr, env, @@ -843,14 +927,15 @@ fn validate_expr_semantics<'i>( } ExprKind::Array(values) => { for value in values { - validate_expr_semantics(value, env, prefer_env_for_comparison, types, structs, functions, contract_fields)?; + validate_expr_semantics(value, env, prefer_env_for_comparison, types, structs, constants, functions, contract_fields)?; } Ok(()) } ExprKind::Call { name, args, .. } => { for arg in args { - validate_expr_semantics(arg, env, prefer_env_for_comparison, types, structs, functions, contract_fields)?; + validate_expr_semantics(arg, env, prefer_env_for_comparison, types, structs, constants, functions, contract_fields)?; } + validate_builtin_call(name, args, env, prefer_env_for_comparison, types, structs, constants, functions, contract_fields)?; if let Some(function) = functions.get(name) { if function.entrypoint { return Err(CompilerError::Unsupported(format!("entrypoint function '{}' cannot be called", name))); @@ -872,21 +957,21 @@ fn validate_expr_semantics<'i>( } ExprKind::New { args, .. } => { for arg in args { - validate_expr_semantics(arg, env, prefer_env_for_comparison, types, structs, functions, contract_fields)?; + validate_expr_semantics(arg, env, prefer_env_for_comparison, types, structs, constants, functions, contract_fields)?; } Ok(()) } ExprKind::Split { source, index, .. } => { - validate_expr_semantics(source, env, prefer_env_for_comparison, types, structs, functions, contract_fields)?; - validate_expr_semantics(index, env, prefer_env_for_comparison, types, structs, functions, contract_fields) + validate_expr_semantics(source, env, prefer_env_for_comparison, types, structs, constants, functions, contract_fields)?; + validate_expr_semantics(index, env, prefer_env_for_comparison, types, structs, constants, functions, contract_fields) } ExprKind::Slice { source, start, end, .. } => { - validate_expr_semantics(source, env, prefer_env_for_comparison, types, structs, functions, contract_fields)?; - validate_expr_semantics(start, env, prefer_env_for_comparison, types, structs, functions, contract_fields)?; - validate_expr_semantics(end, env, prefer_env_for_comparison, types, structs, functions, contract_fields) + validate_expr_semantics(source, env, prefer_env_for_comparison, types, structs, constants, functions, contract_fields)?; + validate_expr_semantics(start, env, prefer_env_for_comparison, types, structs, constants, functions, contract_fields)?; + validate_expr_semantics(end, env, prefer_env_for_comparison, types, structs, constants, functions, contract_fields) } ExprKind::Append { source, args, .. } => { - validate_expr_semantics(source, env, prefer_env_for_comparison, types, structs, functions, contract_fields)?; + validate_expr_semantics(source, env, prefer_env_for_comparison, types, structs, constants, functions, contract_fields)?; let source_type = infer_expr_type_ref_for_comparison_ref( source, env, @@ -901,7 +986,7 @@ fn validate_expr_semantics<'i>( return Err(CompilerError::Unsupported("append target must be an array".to_string())); }; for arg in args { - validate_expr_semantics(arg, env, prefer_env_for_comparison, types, structs, functions, contract_fields)?; + validate_expr_semantics(arg, env, prefer_env_for_comparison, types, structs, constants, functions, contract_fields)?; validate_expr_assignable_to_type(arg, &element_type, types, structs, &HashMap::new(), functions, contract_fields) .map_err(|_| { CompilerError::Unsupported(format!( @@ -913,15 +998,24 @@ fn validate_expr_semantics<'i>( Ok(()) } ExprKind::ArrayIndex { source, index } => { - validate_expr_semantics(source, env, prefer_env_for_comparison, types, structs, functions, contract_fields)?; - validate_expr_semantics(index, env, prefer_env_for_comparison, types, structs, functions, contract_fields) + validate_expr_semantics(source, env, prefer_env_for_comparison, types, structs, constants, functions, contract_fields)?; + validate_expr_semantics(index, env, prefer_env_for_comparison, types, structs, constants, functions, contract_fields) } ExprKind::Introspection { index, .. } => { - validate_expr_semantics(index, env, prefer_env_for_comparison, types, structs, functions, contract_fields) + validate_expr_semantics(index, env, prefer_env_for_comparison, types, structs, constants, functions, contract_fields) } ExprKind::StateObject(fields) => { for field in fields { - validate_expr_semantics(&field.expr, env, prefer_env_for_comparison, types, structs, functions, contract_fields)?; + validate_expr_semantics( + &field.expr, + env, + prefer_env_for_comparison, + types, + structs, + constants, + functions, + contract_fields, + )?; } Ok(()) } @@ -934,14 +1028,15 @@ fn validate_expr_semantics<'i>( prefer_env_for_comparison, types, structs, + constants, functions, contract_fields, ); } - validate_expr_semantics(source, env, prefer_env_for_comparison, types, structs, functions, contract_fields) + validate_expr_semantics(source, env, prefer_env_for_comparison, types, structs, constants, functions, contract_fields) } ExprKind::UnarySuffix { source, .. } => { - validate_expr_semantics(source, env, prefer_env_for_comparison, types, structs, functions, contract_fields) + validate_expr_semantics(source, env, prefer_env_for_comparison, types, structs, constants, functions, contract_fields) } ExprKind::Identifier(name) => { if types.contains_key(name) || env.contains_key(name) { @@ -1023,11 +1118,14 @@ fn infer_expr_type_ref_for_comparison_ref<'i>( Some(TypeRef { base: TypeBase::Custom(STATE_TYPE_NAME.to_string()), array_dims: Vec::new() }) } ExprKind::Call { name, .. } => { - let function = functions.get(name)?; - if function.entrypoint || function.returns_tuple || function.return_types.len() != 1 { - return None; + if let Some(function) = functions.get(name) { + if function.entrypoint || function.returns_tuple || function.return_types.len() != 1 { + return None; + } + return Some(function.return_types[0].clone()); } - Some(function.return_types[0].clone()) + let type_name = super::debug_value_types::infer_debug_expr_value_type(expr, env, types, &mut HashSet::new()).ok()?; + parse_type_ref(&type_name).ok() } _ => { let type_name = super::debug_value_types::infer_debug_expr_value_type(expr, env, types, &mut HashSet::new()).ok()?; @@ -1063,6 +1161,7 @@ fn validate_tuple_field_access<'i>( prefer_env_for_comparison: &HashSet, types: &HashMap, structs: &StructRegistry, + constants: &HashMap>, functions: &HashMap>, contract_fields: &[ContractFieldAst<'i>], ) -> Result<(), CompilerError> { @@ -1070,7 +1169,7 @@ fn validate_tuple_field_access<'i>( return Err(CompilerError::Unsupported("tuple field access requires a tuple-returning function call".to_string())); }; for arg in args { - validate_expr_semantics(arg, env, prefer_env_for_comparison, types, structs, functions, contract_fields)?; + validate_expr_semantics(arg, env, prefer_env_for_comparison, types, structs, constants, functions, contract_fields)?; } let Some(function) = functions.get(name) else { return Err(CompilerError::Unsupported(format!("function '{}' not found", name))); @@ -1267,6 +1366,49 @@ fn validate_internal_call<'i>( Ok(function) } +fn validate_builtin_call<'i>( + name: &str, + args: &[Expr<'i>], + env: &HashMap>, + prefer_env_for_comparison: &HashSet, + types: &HashMap, + structs: &StructRegistry, + constants: &HashMap>, + functions: &HashMap>, + contract_fields: &[ContractFieldAst<'i>], +) -> Result<(), CompilerError> { + let expected_args = match name { + "CheckSigFromStack" => [("signature", "datasig"), ("digest", "byte[32]"), ("publicKey", "pubkey")], + "CheckSigFromStackECDSA" => [("signature", "datasig"), ("digest", "byte[32]"), ("publicKey", "byte[33]")], + _ => return Ok(()), + }; + if args.len() != expected_args.len() { + return Err(CompilerError::Unsupported(format!("{name}() expects {} arguments", expected_args.len()))); + } + + for (arg, (arg_name, expected_type_name)) in args.iter().zip(expected_args) { + let expected_type = parse_type_ref(expected_type_name)?; + let actual_type = + infer_expr_type_ref_for_comparison_ref(arg, env, prefer_env_for_comparison, types, structs, functions, contract_fields) + .ok_or_else(|| CompilerError::Unsupported(format!("{name}() argument '{arg_name}' expects {expected_type_name}")))?; + if !is_type_assignable_ref(&actual_type, &expected_type, constants) && !expr_matches_type_ref(arg, &expected_type) { + return Err(CompilerError::Unsupported(format!( + "{name}() argument '{arg_name}' expects {expected_type_name}, got {}", + type_name_from_ref(&actual_type) + ))); + } + } + + Ok(()) +} + +fn typed_builtin_return_type_ref(name: &str) -> Option { + match name { + "CheckSigFromStack" | "CheckSigFromStackECDSA" => parse_type_ref("bool").ok(), + _ => None, + } +} + fn validate_expr_assignable_to_type<'i>( expr: &Expr<'i>, type_ref: &TypeRef, @@ -1300,6 +1442,16 @@ fn validate_expr_assignable_to_type<'i>( return Err(CompilerError::Unsupported("type mismatch".to_string())); } + if let ExprKind::Call { name, .. } = &expr.kind + && let Some(actual_type) = typed_builtin_return_type_ref(name) + { + return if is_type_assignable_ref(&actual_type, type_ref, constants) { + Ok(()) + } else { + Err(CompilerError::Unsupported("type mismatch".to_string())) + }; + } + if matches!(type_ref.base, TypeBase::Byte) && type_ref.array_dims.is_empty() && matches!(expr.kind, ExprKind::Int(value) if (0..=255).contains(&value)) @@ -1621,6 +1773,9 @@ pub(super) fn expr_matches_return_type_ref<'i>( ExprKind::Int(_) | ExprKind::DateLiteral(_) | ExprKind::Bool(_) | ExprKind::Byte(_) | ExprKind::String(_) => { expr_matches_type_ref(expr, type_ref) } + ExprKind::Call { name, .. } => typed_builtin_return_type_ref(name) + .map(|actual_type| is_type_assignable_ref(&actual_type, type_ref, constants)) + .unwrap_or(true), _ => true, } } diff --git a/silverscript-lang/tests/cashc_valid_examples_tests.rs b/silverscript-lang/tests/cashc_valid_examples_tests.rs index 0ddb687..ffc7b44 100644 --- a/silverscript-lang/tests/cashc_valid_examples_tests.rs +++ b/silverscript-lang/tests/cashc_valid_examples_tests.rs @@ -228,7 +228,8 @@ fn runs_cashc_valid_examples() { "p2pkh_with_cast.sil", "reassignment.sil", "simple_cast.sil", - "simple_checkdatasig.sil", + "simple_checksigfromstack.sil", + "simple_checksigfromstack_ecdsa.sil", "simple_constant.sil", "simple_covenant.sil", "simple_functions.sil", @@ -699,7 +700,7 @@ fn runs_cashc_valid_examples() { let result = execute_tx(tx, utxo, reused); assert!(result.is_err(), "{example} should fail"); } - "simple_checkdatasig.sil" => { + "simple_checksigfromstack.sil" => { use sha2::{Digest, Sha256}; let keypair = random_keypair(); @@ -731,6 +732,38 @@ fn runs_cashc_valid_examples() { forged_sig[0] ^= 0x01; assert!(run(forged_sig).is_err(), "{example}: forged data signature should fail"); } + "simple_checksigfromstack_ecdsa.sil" => { + use sha2::{Digest, Sha256}; + + let keypair = random_keypair(); + let pubkey_bytes = keypair.public_key().serialize().to_vec(); + let message = b"data".to_vec(); + let message_hash = Sha256::digest(&message); + let signed = Message::from_digest_slice(&message_hash).expect("sha256 digest is 32 bytes"); + let valid_sig = keypair.secret_key().sign_ecdsa(signed).serialize_compact().to_vec(); + assert_eq!(valid_sig.len(), 64, "datasig is a compact ECDSA signature"); + + let run = |signature: Vec| { + let constructor_args = vec![signature.into(), pubkey_bytes.clone().into()]; + let compiled = compile_contract(&source, &constructor_args, CompileOptions::default()).expect("compile succeeds"); + let selector = selector_for_compiled(&compiled, "cds"); + let sigscript = build_sigscript(&[ArgValue::Bytes(message.clone())], selector); + let (mut tx, utxo, reused) = build_tx_context( + compiled.script.clone(), + vec![(1_000, compiled.script.clone()), (1_000, compiled.script.clone())], + 2_000, + 0, + 1, + ); + tx.tx.inputs[0].signature_script = sigscript; + execute_tx(tx, utxo, reused) + }; + + assert!(run(valid_sig.clone()).is_ok(), "{example}: valid ECDSA data signature should pass"); + let mut forged_sig = valid_sig; + forged_sig[0] ^= 0x01; + assert!(run(forged_sig).is_err(), "{example}: forged ECDSA data signature should fail"); + } "simple_constant.sil" => { let constructor_args = vec![]; let compiled = compile_contract(&source, &constructor_args, CompileOptions::default()).expect("compile succeeds"); @@ -1093,7 +1126,8 @@ fn compiles_cashc_valid_examples() { "p2pkh_with_cast.sil", "reassignment.sil", "simple_cast.sil", - "simple_checkdatasig.sil", + "simple_checksigfromstack.sil", + "simple_checksigfromstack_ecdsa.sil", "simple_constant.sil", "simple_covenant.sil", "simple_functions.sil", diff --git a/silverscript-lang/tests/compiler_tests.rs b/silverscript-lang/tests/compiler_tests.rs index 76c8e27..1374b5a 100644 --- a/silverscript-lang/tests/compiler_tests.rs +++ b/silverscript-lang/tests/compiler_tests.rs @@ -7330,20 +7330,33 @@ fn assert_compiled_body(source: &str, body: Vec) { } #[test] -fn checkdatasig_lowers_to_checksigfromstack_over_sha256_message() { +fn checksig_result_can_be_used_in_bool_comparisons() { let source = r#" - contract DataSig(datasig signature, byte[] message, pubkey publicKey) { + contract P2PK(sig signature, pubkey publicKey) { + entrypoint function main() { + require(checkSig(signature, publicKey) == true); + } + } + "#; + compile_contract(source, &[vec![0x11u8; 65].into(), vec![0x22u8; 32].into()], CompileOptions::default()) + .expect("checkSig bool comparison should compile"); +} + +#[test] +fn checksigfromstack_lowers_to_matching_opcode() { + let source = r#" + contract DataSig(datasig signature, byte[32] digest, pubkey publicKey) { entrypoint function main() { - require(checkDataSig(signature, message, publicKey)); + require(CheckSigFromStack(signature, digest, publicKey)); } } "#; let signature = vec![0x11; 64]; - let message = b"authorize".to_vec(); + let digest = vec![0x33; 32]; let public_key = vec![0x22; 32]; let compiled = compile_contract( source, - &[signature.clone().into(), message.clone().into(), public_key.clone().into()], + &[signature.clone().into(), digest.clone().into(), public_key.clone().into()], CompileOptions::default(), ) .expect("compile succeeds"); @@ -7351,9 +7364,7 @@ fn checkdatasig_lowers_to_checksigfromstack_over_sha256_message() { let expected = ScriptBuilder::new() .add_data_with_push_opcode(&signature) .unwrap() - .add_data_with_push_opcode(&message) - .unwrap() - .add_op(OpSHA256) + .add_data_with_push_opcode(&digest) .unwrap() .add_data_with_push_opcode(&public_key) .unwrap() @@ -7367,6 +7378,289 @@ fn checkdatasig_lowers_to_checksigfromstack_over_sha256_message() { assert_eq!(compiled.script, expected); } +#[test] +fn checksigfromstackecdsa_lowers_to_matching_opcode() { + let source = r#" + contract DataSig(datasig signature, byte[32] digest, byte[33] publicKey) { + entrypoint function main() { + require(CheckSigFromStackECDSA(signature, digest, publicKey)); + } + } + "#; + let signature = vec![0x11; 64]; + let digest = vec![0x33; 32]; + let public_key = vec![0x22; 33]; + let compiled = compile_contract( + source, + &[signature.clone().into(), digest.clone().into(), public_key.clone().into()], + CompileOptions::default(), + ) + .expect("compile succeeds"); + + let expected = ScriptBuilder::new() + .add_data_with_push_opcode(&signature) + .unwrap() + .add_data_with_push_opcode(&digest) + .unwrap() + .add_data_with_push_opcode(&public_key) + .unwrap() + .add_op(OpCheckSigFromStackECDSA) + .unwrap() + .add_op(OpVerify) + .unwrap() + .add_op(OpTrue) + .unwrap() + .drain(); + assert_eq!(compiled.script, expected); +} + +#[test] +fn checksigfromstack_requires_datasig_and_32_byte_digest_types() { + let raw_message = r#" + contract DataSig(datasig signature, byte[] message, pubkey publicKey) { + entrypoint function main() { + require(CheckSigFromStack(signature, message, publicKey)); + } + } + "#; + let raw_message_err = compile_contract( + raw_message, + &[vec![0x11u8; 64].into(), b"authorize".to_vec().into(), vec![0x22u8; 32].into()], + CompileOptions::default(), + ) + .expect_err("raw byte[] message should fail"); + assert!(raw_message_err.to_string().contains("argument 'digest' expects byte[32]"), "unexpected error: {raw_message_err}"); + + let local_size_identifier = r#" + contract DataSig(datasig signature, pubkey publicKey) { + entrypoint function main() { + int N = 32; + byte[N] digest = 0x010203; + require(CheckSigFromStack(signature, digest, publicKey)); + } + } + "#; + let local_size_identifier_err = + compile_contract(local_size_identifier, &[vec![0x11u8; 64].into(), vec![0x22u8; 32].into()], CompileOptions::default()) + .expect_err("local runtime size identifier should not satisfy byte[32]"); + assert!( + local_size_identifier_err.to_string().contains("argument 'digest' expects byte[32]"), + "unexpected error: {local_size_identifier_err}" + ); + + let contract_constant_size = r#" + contract DataSig(datasig signature, pubkey publicKey) { + int constant N = 32; + + entrypoint function main(byte[N] digest) { + require(CheckSigFromStack(signature, digest, publicKey)); + } + } + "#; + compile_contract(contract_constant_size, &[vec![0x11u8; 64].into(), vec![0x22u8; 32].into()], CompileOptions::default()) + .expect("contract constants should satisfy byte[32]"); + + let signature_literal = format!("0x{}", "11".repeat(64)); + let digest_literal = format!("0x{}", "33".repeat(32)); + let public_key_literal = format!("0x{}", "22".repeat(32)); + let literal_args = format!( + r#" + contract DataSig() {{ + entrypoint function main() {{ + require(CheckSigFromStack({signature_literal}, {digest_literal}, {public_key_literal})); + }} + }} + "# + ); + compile_contract(&literal_args, &[], CompileOptions::default()).expect("literal datasig, digest, and pubkey args should compile"); + + let byte_pubkey_variable = format!( + r#" + contract DataSig(datasig signature) {{ + entrypoint function main() {{ + byte[32] digest = {digest_literal}; + byte[32] publicKey = {public_key_literal}; + require(CheckSigFromStack(signature, digest, publicKey)); + }} + }} + "# + ); + let byte_pubkey_variable_err = compile_contract(&byte_pubkey_variable, &[vec![0x11u8; 64].into()], CompileOptions::default()) + .expect_err("byte[32] variable should not be promoted to pubkey"); + assert!( + byte_pubkey_variable_err.to_string().contains("argument 'publicKey' expects pubkey"), + "unexpected error: {byte_pubkey_variable_err}" + ); + + let tx_signature = r#" + contract DataSig(sig signature, byte[32] digest, pubkey publicKey) { + entrypoint function main() { + require(CheckSigFromStack(signature, digest, publicKey)); + } + } + "#; + let tx_signature_err = compile_contract( + tx_signature, + &[vec![0x11u8; 65].into(), vec![0x33u8; 32].into(), vec![0x22u8; 32].into()], + CompileOptions::default(), + ) + .expect_err("65-byte sig should fail"); + assert!(tx_signature_err.to_string().contains("argument 'signature' expects datasig"), "unexpected error: {tx_signature_err}"); + + let schnorr_pubkey_for_ecdsa = r#" + contract DataSig(datasig signature, byte[32] digest, pubkey publicKey) { + entrypoint function main() { + require(CheckSigFromStackECDSA(signature, digest, publicKey)); + } + } + "#; + let schnorr_pubkey_err = compile_contract( + schnorr_pubkey_for_ecdsa, + &[vec![0x11u8; 64].into(), vec![0x33u8; 32].into(), vec![0x22u8; 32].into()], + CompileOptions::default(), + ) + .expect_err("32-byte Schnorr pubkey should fail for ECDSA"); + assert!( + schnorr_pubkey_err.to_string().contains("argument 'publicKey' expects byte[33]"), + "unexpected error: {schnorr_pubkey_err}" + ); +} + +#[test] +fn checksigfromstack_result_is_checked_as_bool() { + let bool_assignment = r#" + contract DataSig(datasig signature, byte[32] digest, pubkey publicKey) { + entrypoint function main() { + bool ok = CheckSigFromStack(signature, digest, publicKey); + require(ok); + } + } + "#; + compile_contract( + bool_assignment, + &[vec![0x11u8; 64].into(), vec![0x33u8; 32].into(), vec![0x22u8; 32].into()], + CompileOptions::default(), + ) + .expect("bool assignment should compile"); + + let byte_assignment = r#" + contract DataSig(datasig signature, byte[32] digest, pubkey publicKey) { + entrypoint function main() { + byte[32] ok = CheckSigFromStack(signature, digest, publicKey); + require(true); + } + } + "#; + let byte_assignment_err = compile_contract( + byte_assignment, + &[vec![0x11u8; 64].into(), vec![0x33u8; 32].into(), vec![0x22u8; 32].into()], + CompileOptions::default(), + ) + .expect_err("builtin bool result should not assign to byte[32]"); + assert!(byte_assignment_err.to_string().contains("variable 'ok' expects byte[32]"), "unexpected error: {byte_assignment_err}"); + + let bool_return = r#" + contract DataSig(datasig signature, byte[32] digest, pubkey publicKey) { + function ok() : bool { + return CheckSigFromStack(signature, digest, publicKey); + } + + entrypoint function main() { + require(ok()); + } + } + "#; + compile_contract( + bool_return, + &[vec![0x11u8; 64].into(), vec![0x33u8; 32].into(), vec![0x22u8; 32].into()], + CompileOptions::default(), + ) + .expect("bool return should compile"); + + let byte_return = r#" + contract DataSig(datasig signature, byte[32] digest, pubkey publicKey) { + function bad() : byte[32] { + return CheckSigFromStack(signature, digest, publicKey); + } + + entrypoint function main() { + require(true); + } + } + "#; + let byte_return_err = compile_contract( + byte_return, + &[vec![0x11u8; 64].into(), vec![0x33u8; 32].into(), vec![0x22u8; 32].into()], + CompileOptions::default(), + ) + .expect_err("builtin bool result should not return byte[32]"); + assert!(byte_return_err.to_string().contains("return value expects byte[32]"), "unexpected error: {byte_return_err}"); +} + +#[test] +fn checksigfromstack_executes_schnorr_signature_verification() { + let source = r#" + contract DataSig(datasig signature, byte[32] digest, pubkey publicKey) { + entrypoint function main() { + require(CheckSigFromStack(signature, digest, publicKey)); + } + } + "#; + let keypair = secp256k1::Keypair::from_seckey_slice(secp256k1::SECP256K1, &[7u8; 32]).unwrap(); + let public_key = keypair.x_only_public_key().0.serialize().to_vec(); + let digest = Hash::from_bytes([3u8; 32]); + let message = secp256k1::Message::from_digest(digest.into()); + let valid_signature = keypair.sign_schnorr(message).as_ref().to_vec(); + + let run = |signature: Vec| { + let compiled = compile_contract( + source, + &[signature.into(), digest.as_bytes().to_vec().into(), public_key.clone().into()], + CompileOptions::default(), + ) + .expect("compile succeeds"); + let selector = selector_for(&compiled, "main"); + run_script_with_selector(compiled.script, selector) + }; + + assert!(run(valid_signature.clone()).is_ok(), "valid Schnorr data signature should pass"); + let mut forged_signature = valid_signature; + forged_signature[0] ^= 0x01; + assert!(run(forged_signature).is_err(), "forged Schnorr data signature should fail"); +} + +#[test] +fn checksigfromstackecdsa_executes_ecdsa_signature_verification() { + let source = r#" + contract DataSig(datasig signature, byte[32] digest, byte[33] publicKey) { + entrypoint function main() { + require(CheckSigFromStackECDSA(signature, digest, publicKey)); + } + } + "#; + let keypair = secp256k1::Keypair::from_seckey_slice(secp256k1::SECP256K1, &[9u8; 32]).unwrap(); + let public_key = keypair.public_key().serialize().to_vec(); + let digest = Hash::from_bytes([5u8; 32]); + let message = secp256k1::Message::from_digest(digest.into()); + let valid_signature = keypair.secret_key().sign_ecdsa(message).serialize_compact().to_vec(); + + let run = |signature: Vec| { + let compiled = compile_contract( + source, + &[signature.into(), digest.as_bytes().to_vec().into(), public_key.clone().into()], + CompileOptions::default(), + ) + .expect("compile succeeds"); + let selector = selector_for(&compiled, "main"); + run_script_with_selector(compiled.script, selector) + }; + + assert!(run(valid_signature.clone()).is_ok(), "valid ECDSA data signature should pass"); + let mut forged_signature = valid_signature; + forged_signature[0] ^= 0x01; + assert!(run(forged_signature).is_err(), "forged ECDSA data signature should fail"); +} + #[test] fn canonicalizes_bool_comparison_operands_for_equality_and_inequality() { let cases = [(("=="), OpNumEqual), (("!="), OpNumNotEqual)]; diff --git a/silverscript-lang/tests/examples/hodl_vault.sil b/silverscript-lang/tests/examples/hodl_vault.sil index 10ba66f..d680347 100644 --- a/silverscript-lang/tests/examples/hodl_vault.sil +++ b/silverscript-lang/tests/examples/hodl_vault.sil @@ -15,7 +15,7 @@ contract HodlVault( require(tx.time >= blockHeight); require(price >= priceTarget); - require(checkDataSig(oracleSig, oracleMessage, oraclePk)); + require(CheckSigFromStack(oracleSig, sha256(oracleMessage), oraclePk)); require(checkSig(ownerSig, ownerPk)); } } diff --git a/silverscript-lang/tests/examples/simple_checkdatasig.sil b/silverscript-lang/tests/examples/simple_checksigfromstack.sil similarity index 67% rename from silverscript-lang/tests/examples/simple_checkdatasig.sil rename to silverscript-lang/tests/examples/simple_checksigfromstack.sil index 064cadf..2495eb5 100644 --- a/silverscript-lang/tests/examples/simple_checkdatasig.sil +++ b/silverscript-lang/tests/examples/simple_checksigfromstack.sil @@ -2,6 +2,6 @@ pragma silverscript ^0.1.0; contract Test(datasig s, pubkey pk) { entrypoint function cds(byte[] data) { - require(checkDataSig(s, data, pk)); + require(CheckSigFromStack(s, sha256(data), pk)); } } diff --git a/silverscript-lang/tests/examples/simple_checksigfromstack_ecdsa.sil b/silverscript-lang/tests/examples/simple_checksigfromstack_ecdsa.sil new file mode 100644 index 0000000..54babba --- /dev/null +++ b/silverscript-lang/tests/examples/simple_checksigfromstack_ecdsa.sil @@ -0,0 +1,7 @@ +pragma silverscript ^0.1.0; + +contract Test(datasig s, byte[33] pk) { + entrypoint function cds(byte[] data) { + require(CheckSigFromStackECDSA(s, sha256(data), pk)); + } +} diff --git a/silverscript-lang/tests/examples/trailing_comma.sil b/silverscript-lang/tests/examples/trailing_comma.sil index 60a5dca..89da41a 100644 --- a/silverscript-lang/tests/examples/trailing_comma.sil +++ b/silverscript-lang/tests/examples/trailing_comma.sil @@ -10,9 +10,9 @@ contract Contract( sig oracleTxSig, ) { byte[] oracleMessage = byte[]('Spend') + byte[](12, 10,); - require(checkDataSig( + require(CheckSigFromStack( oracleMsgSig, - oracleMessage, + sha256(oracleMessage), oraclePk, )); require(checkMultiSig([ diff --git a/tree-sitter/queries/highlights.scm b/tree-sitter/queries/highlights.scm index d5d7a56..e39f591 100644 --- a/tree-sitter/queries/highlights.scm +++ b/tree-sitter/queries/highlights.scm @@ -72,7 +72,7 @@ (function_call (identifier) @function.builtin (#match? @function.builtin - "^(readInputState|readInputStateWithTemplate|validateOutputState|validateOutputStateWithTemplate|verifyOutputState|verifyOutputStates|OpSha256|sha256|OpTxSubnetId|OpTxGas|OpTxPayloadLen|OpTxPayloadSubstr|OpOutpointTxId|OpOutpointIndex|OpTxInputScriptSigLen|OpTxInputScriptSigSubstr|OpTxInputSeq|OpTxInputIsCoinbase|OpTxInputSpkLen|OpTxInputSpkSubstr|OpTxOutputSpkLen|OpTxOutputSpkSubstr|OpAuthOutputCount|OpAuthOutputIdx|OpInputCovenantId|OpOutputCovenantId|OpCovInputCount|OpCovInputIdx|OpCovOutputCount|OpCovOutputIdx|OpNum2Bin|OpBin2Num|OpChainblockSeqCommit|checkDataSig|checkSig|checkMultiSig|blake2b)$")) + "^(readInputState|readInputStateWithTemplate|validateOutputState|validateOutputStateWithTemplate|verifyOutputState|verifyOutputStates|OpSha256|sha256|OpTxSubnetId|OpTxGas|OpTxPayloadLen|OpTxPayloadSubstr|OpOutpointTxId|OpOutpointIndex|OpTxInputScriptSigLen|OpTxInputScriptSigSubstr|OpTxInputSeq|OpTxInputIsCoinbase|OpTxInputSpkLen|OpTxInputSpkSubstr|OpTxOutputSpkLen|OpTxOutputSpkSubstr|OpAuthOutputCount|OpAuthOutputIdx|OpInputCovenantId|OpOutputCovenantId|OpCovInputCount|OpCovInputIdx|OpCovOutputCount|OpCovOutputIdx|OpNum2Bin|OpBin2Num|OpChainblockSeqCommit|CheckSigFromStack|CheckSigFromStackECDSA|checkSig|checkMultiSig|blake2b)$")) (unary_suffix) @property