diff --git a/silverscript-lang/src/compiler.rs b/silverscript-lang/src/compiler.rs index a5cdf79..c4b450f 100644 --- a/silverscript-lang/src/compiler.rs +++ b/silverscript-lang/src/compiler.rs @@ -1,7 +1,7 @@ use std::collections::{HashMap, HashSet}; use kaspa_txscript::opcodes::codes::*; -use kaspa_txscript::script_builder::ScriptBuilder; +use kaspa_txscript::script_builder::{ScriptBuilder, ScriptBuilderError}; use kaspa_txscript::serialize_i64; use serde::{Deserialize, Serialize}; @@ -27,6 +27,23 @@ pub struct CompileOptions { pub record_debug_infos: bool, } +trait ScriptBuilderExt { + fn add_data_preserving_single_zero(&mut self, data: &[u8]) -> Result<&mut Self, ScriptBuilderError>; +} + +impl ScriptBuilderExt for ScriptBuilder { + fn add_data_preserving_single_zero(&mut self, data: &[u8]) -> Result<&mut Self, ScriptBuilderError> { + if data.len() == 1 && data[0] == 0 { + self.add_i64(0)?; + self.add_i64(1)?; + self.add_op(OpNum2Bin)?; + Ok(self) + } else { + self.add_data(data) + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct FunctionInputAbi { pub name: String, @@ -725,10 +742,10 @@ impl<'i> CompiledContract<'i> { .iter() .filter_map(|value| if let ExprKind::Byte(byte) = &value.kind { Some(*byte) } else { None }) .collect(); - builder.add_data(&bytes)?; + builder.add_data_preserving_single_zero(&bytes)?; } else { let bytes = encode_array_literal(&values, &input.type_name)?; - builder.add_data(&bytes)?; + builder.add_data_preserving_single_zero(&bytes)?; } } _ => { @@ -759,15 +776,15 @@ fn push_sigscript_arg<'i>(builder: &mut ScriptBuilder, arg: Expr<'i>) -> Result< builder.add_i64(if value { 1 } else { 0 })?; } ExprKind::String(value) => { - builder.add_data(value.as_bytes())?; + builder.add_data_preserving_single_zero(value.as_bytes())?; } ExprKind::Byte(value) => { - builder.add_data(&[value])?; + builder.add_data_preserving_single_zero(&[value])?; } ExprKind::Array(values) if values.iter().all(|value| matches!(&value.kind, ExprKind::Byte(_))) => { let bytes: Vec = values.iter().filter_map(|value| if let ExprKind::Byte(byte) = &value.kind { Some(*byte) } else { None }).collect(); - builder.add_data(&bytes)?; + builder.add_data_preserving_single_zero(&bytes)?; } ExprKind::DateLiteral(value) => { builder.add_i64(value)?; @@ -2431,21 +2448,21 @@ fn compile_expr<'i>( Ok(()) } ExprKind::Byte(byte) => { - builder.add_data(&[*byte])?; + builder.add_data_preserving_single_zero(&[*byte])?; *stack_depth += 1; Ok(()) } ExprKind::Array(values) => { // TODO: this particular handling should be done in encode_array_literal to unify the behavior if values.is_empty() { - builder.add_data(&[])?; + builder.add_data_preserving_single_zero(&[])?; *stack_depth += 1; return Ok(()); } let inferred_type = infer_fixed_array_literal_type(values) .ok_or_else(|| CompilerError::Unsupported("array literal type cannot be inferred".to_string()))?; let encoded = encode_array_literal(values, &inferred_type)?; - builder.add_data(&encoded)?; + builder.add_data_preserving_single_zero(&encoded)?; *stack_depth += 1; Ok(()) } @@ -2453,7 +2470,7 @@ fn compile_expr<'i>( Err(CompilerError::Unsupported("state object literals are only supported in validateOutputState".to_string())) } ExprKind::String(value) => { - builder.add_data(value.as_bytes())?; + builder.add_data_preserving_single_zero(value.as_bytes())?; *stack_depth += 1; Ok(()) } @@ -2466,7 +2483,7 @@ fn compile_expr<'i>( if let ExprKind::Array(values) = &expr.kind { if is_array_type(type_name) { let encoded = encode_array_literal(values, type_name)?; - builder.add_data(&encoded)?; + builder.add_data_preserving_single_zero(&encoded)?; *stack_depth += 1; visiting.remove(name); return Ok(()); @@ -3410,14 +3427,14 @@ fn compile_call_expr<'i>( } match &args[0].kind { ExprKind::String(value) => { - builder.add_data(value.as_bytes())?; + builder.add_data_preserving_single_zero(value.as_bytes())?; *stack_depth += 1; Ok(()) } ExprKind::Identifier(name) => { if let Some(expr) = scope.env.get(name) { if let ExprKind::String(value) = &expr.kind { - builder.add_data(value.as_bytes())?; + builder.add_data_preserving_single_zero(value.as_bytes())?; *stack_depth += 1; return Ok(()); } @@ -3779,10 +3796,10 @@ fn build_null_data_script<'i>(arg: &Expr<'i>) -> Result, CompilerError> .iter() .filter_map(|value| if let ExprKind::Byte(byte) = &value.kind { Some(*byte) } else { None }) .collect(); - builder.add_data(&bytes)?; + builder.add_data_preserving_single_zero(&bytes)?; } ExprKind::String(value) => { - builder.add_data(value.as_bytes())?; + builder.add_data_preserving_single_zero(value.as_bytes())?; } ExprKind::Call { name, args, .. } if name == "bytes" || name == "byte[]" => { if args.len() != 1 { @@ -3792,7 +3809,7 @@ fn build_null_data_script<'i>(arg: &Expr<'i>) -> Result, CompilerError> } match &args[0].kind { ExprKind::String(value) => { - builder.add_data(value.as_bytes())?; + builder.add_data_preserving_single_zero(value.as_bytes())?; } _ => { return Err(CompilerError::Unsupported( diff --git a/silverscript-lang/tests/compiler_tests.rs b/silverscript-lang/tests/compiler_tests.rs index d53f9ed..a9446ff 100644 --- a/silverscript-lang/tests/compiler_tests.rs +++ b/silverscript-lang/tests/compiler_tests.rs @@ -1065,6 +1065,64 @@ fn allows_concat_of_byte_arrays_with_plus() { assert!(result.is_ok(), "byte[] concatenation runtime failed: {}", result.unwrap_err()); } +#[test] +fn allows_concat_of_byte_arrays_with_zero_byte_between_them() { + let source = r#" + contract Arrays() { + entrypoint function main() { + byte[] prefix = 0x0102; + byte middle = 0x00; + byte[] suffix = 0x0304; + byte[5] out = prefix + middle + suffix; + + require(out.length == 5); + require(out == 0x0102000304); + } + } + "#; + + let options = CompileOptions::default(); + let compiled = compile_contract(source, &[], options).expect("compile succeeds"); + let sigscript = ScriptBuilder::new().drain(); + let result = run_script_with_sigscript(compiled.script, sigscript); + assert!(result.is_ok(), "byte[] + zero-byte + byte[] concatenation runtime failed: {}", result.unwrap_err()); +} + +#[test] +fn allows_concat_with_single_zero_byte_in_byte_fixed_and_dynamic_forms() { + let source = r#" + contract Arrays() { + entrypoint function main() { + byte[] prefix = 0xaa; + byte[] suffix = 0xbb; + + byte middle_byte = 0x00; + byte[1] middle_fixed = 0x00; + byte[] middle_dynamic = 0x00; + + byte[3] from_byte = prefix + middle_byte + suffix; + byte[3] from_fixed = prefix + middle_fixed + suffix; + byte[3] from_dynamic = prefix + middle_dynamic + suffix; + + require(middle_fixed.length == 1); + require(middle_dynamic.length == 1); + require(from_byte.length == 3); + require(from_fixed.length == 3); + require(from_dynamic.length == 3); + require(from_byte == 0xaa00bb); + require(from_fixed == 0xaa00bb); + require(from_dynamic == 0xaa00bb); + } + } + "#; + + let options = CompileOptions::default(); + let compiled = compile_contract(source, &[], options).expect("compile succeeds"); + let sigscript = ScriptBuilder::new().drain(); + let result = run_script_with_sigscript(compiled.script, sigscript); + assert!(result.is_ok(), "single-zero-byte concat in byte/byte[1]/byte[] forms failed: {}", result.unwrap_err()); +} + #[test] fn allows_concat_of_fixed_size_byte_array_elements_with_plus() { let source = r#"