From e4ddaac66ec404b78914d55e0605ba7a01009eed Mon Sep 17 00:00:00 2001 From: salaheldinsoliman Date: Mon, 12 Aug 2024 00:28:24 +0200 Subject: [PATCH] feat: Add static print functionality to Soroban contracts Signed-off-by: salaheldinsoliman --- src/codegen/dispatch/soroban.rs | 60 +++++++++++++++---------- src/codegen/expression.rs | 18 +++++++- src/emit/expression.rs | 30 ++++++++++++- src/emit/soroban/mod.rs | 12 +++++ src/emit/soroban/target.rs | 64 +++++++++++++++++++++++++- src/lib.rs | 2 +- src/linker/soroban_wasm.rs | 6 +-- tests/soroban.rs | 23 +++++++++- tests/soroban_testcases/mod.rs | 1 + tests/soroban_testcases/print.rs | 77 ++++++++++++++++++++++++++++++++ 10 files changed, 260 insertions(+), 33 deletions(-) create mode 100644 tests/soroban_testcases/print.rs diff --git a/src/codegen/dispatch/soroban.rs b/src/codegen/dispatch/soroban.rs index 94959edd3..83c2fd4cd 100644 --- a/src/codegen/dispatch/soroban.rs +++ b/src/codegen/dispatch/soroban.rs @@ -102,35 +102,47 @@ pub fn function_dispatch( wrapper_cfg.add(&mut vartab, placeholder); - // set the msb 8 bits of the return value to 6, the return value is 64 bits. - // FIXME: this assumes that the solidity function always returns one value. - let shifted = Expression::ShiftLeft { - loc: pt::Loc::Codegen, - ty: Type::Uint(64), - left: value[0].clone().into(), - right: Expression::NumberLiteral { + // TODO: support multiple returns + if value.len() == 1 { + // set the msb 8 bits of the return value to 6, the return value is 64 bits. + // FIXME: this assumes that the solidity function always returns one value. + let shifted = Expression::ShiftLeft { loc: pt::Loc::Codegen, ty: Type::Uint(64), - value: BigInt::from(8_u64), - } - .into(), - }; + left: value[0].clone().into(), + right: Expression::NumberLiteral { + loc: pt::Loc::Codegen, + ty: Type::Uint(64), + value: BigInt::from(8_u64), + } + .into(), + }; - let tag = Expression::NumberLiteral { - loc: pt::Loc::Codegen, - ty: Type::Uint(64), - value: BigInt::from(6_u64), - }; + let tag = Expression::NumberLiteral { + loc: pt::Loc::Codegen, + ty: Type::Uint(64), + value: BigInt::from(6_u64), + }; - let added = Expression::Add { - loc: pt::Loc::Codegen, - ty: Type::Uint(64), - overflowing: false, - left: shifted.into(), - right: tag.into(), - }; + let added = Expression::Add { + loc: pt::Loc::Codegen, + ty: Type::Uint(64), + overflowing: false, + left: shifted.into(), + right: tag.into(), + }; + + wrapper_cfg.add(&mut vartab, Instr::Return { value: vec![added] }); + } else { + // Return 2 as numberliteral. 2 is the soroban Void type encoded. + let two = Expression::NumberLiteral { + loc: pt::Loc::Codegen, + ty: Type::Uint(64), + value: BigInt::from(2_u64), + }; - wrapper_cfg.add(&mut vartab, Instr::Return { value: vec![added] }); + wrapper_cfg.add(&mut vartab, Instr::Return { value: vec![two] }); + } vartab.finalize(ns, &mut wrapper_cfg); cfg.public = false; diff --git a/src/codegen/expression.rs b/src/codegen/expression.rs index c70818301..47aa85542 100644 --- a/src/codegen/expression.rs +++ b/src/codegen/expression.rs @@ -939,7 +939,23 @@ pub fn expression( expr }; - cfg.add(vartab, Instr::Print { expr: to_print }); + let res = if let Expression::AllocDynamicBytes { + loc, + ty, + size: _, + initializer, + } = &to_print + { + Expression::BytesLiteral { + loc: *loc, + ty: ty.clone(), + value: initializer.clone().unwrap(), + } + } else { + to_print + }; + + cfg.add(vartab, Instr::Print { expr: res }); } Expression::Poison diff --git a/src/emit/expression.rs b/src/emit/expression.rs index 600cfee51..18604e924 100644 --- a/src/emit/expression.rs +++ b/src/emit/expression.rs @@ -126,7 +126,35 @@ pub(super) fn expression<'a, T: TargetRuntime<'a> + ?Sized>( s.into() } - Expression::BytesLiteral { value: bs, .. } => { + Expression::BytesLiteral { value: bs, ty, .. } => { + // If the type of a BytesLiteral is a String, embedd the bytes in the binary. + if ty == &Type::String { + let data = bin.emit_global_string("const_string", bs, true); + + // A constant string, or array, is represented by a struct with two fields: a pointer to the data, and its length. + let ty = bin.context.struct_type( + &[ + bin.llvm_type(&Type::Bytes(bs.len() as u8), ns) + .ptr_type(AddressSpace::default()) + .into(), + bin.context + .custom_width_int_type(ns.target.ptr_size().into()) + .into(), + ], + false, + ); + + return ty + .const_named_struct(&[ + data.into(), + bin.context + .custom_width_int_type(ns.target.ptr_size().into()) + .const_int(bs.len() as u64, false) + .into(), + ]) + .into(); + } + let ty = bin.context.custom_width_int_type((bs.len() * 8) as u32); // hex"11223344" should become i32 0x11223344 diff --git a/src/emit/soroban/mod.rs b/src/emit/soroban/mod.rs index 51191ea37..3b81b5373 100644 --- a/src/emit/soroban/mod.rs +++ b/src/emit/soroban/mod.rs @@ -22,6 +22,7 @@ use std::sync; const SOROBAN_ENV_INTERFACE_VERSION: u64 = 90194313216; pub const PUT_CONTRACT_DATA: &str = "l._"; pub const GET_CONTRACT_DATA: &str = "l.1"; +pub const LOG_FROM_LINEAR_MEMORY: &str = "x._"; pub struct SorobanTarget; @@ -231,12 +232,23 @@ impl SorobanTarget { .i64_type() .fn_type(&[ty.into(), ty.into()], false); + let log_function_ty = binary + .context + .i64_type() + .fn_type(&[ty.into(), ty.into(), ty.into(), ty.into()], false); + binary .module .add_function(PUT_CONTRACT_DATA, function_ty_1, Some(Linkage::External)); binary .module .add_function(GET_CONTRACT_DATA, function_ty, Some(Linkage::External)); + + binary.module.add_function( + LOG_FROM_LINEAR_MEMORY, + log_function_ty, + Some(Linkage::External), + ); } fn emit_initializer(binary: &mut Binary, _ns: &ast::Namespace) { diff --git a/src/emit/soroban/target.rs b/src/emit/soroban/target.rs index cc50591f7..76dd8a398 100644 --- a/src/emit/soroban/target.rs +++ b/src/emit/soroban/target.rs @@ -3,7 +3,9 @@ use crate::codegen::cfg::HashTy; use crate::codegen::Expression; use crate::emit::binary::Binary; -use crate::emit::soroban::{SorobanTarget, GET_CONTRACT_DATA, PUT_CONTRACT_DATA}; +use crate::emit::soroban::{ + SorobanTarget, GET_CONTRACT_DATA, LOG_FROM_LINEAR_MEMORY, PUT_CONTRACT_DATA, +}; use crate::emit::ContractArgs; use crate::emit::{TargetRuntime, Variable}; use crate::emit_context; @@ -236,7 +238,65 @@ impl<'a> TargetRuntime<'a> for SorobanTarget { /// Prints a string /// TODO: Implement this function, with a call to the `log` function in the Soroban runtime. - fn print(&self, bin: &Binary, string: PointerValue, length: IntValue) {} + fn print(&self, bin: &Binary, string: PointerValue, length: IntValue) { + if string.is_const() && length.is_const() { + let msg_pos = bin + .builder + .build_ptr_to_int(string, bin.context.i64_type(), "msg_pos") + .unwrap(); + let msg_pos = msg_pos.const_cast(bin.context.i64_type(), false); + + let length = length.const_cast(bin.context.i64_type(), false); + + let eight = bin.context.i64_type().const_int(8, false); + let four = bin.context.i64_type().const_int(4, false); + let zero = bin.context.i64_type().const_int(0, false); + let thirty_two = bin.context.i64_type().const_int(32, false); + + // XDR encode msg_pos and length + let msg_pos_encoded = bin + .builder + .build_left_shift(msg_pos, thirty_two, "temp") + .unwrap(); + let msg_pos_encoded = bin + .builder + .build_int_add(msg_pos_encoded, four, "msg_pos_encoded") + .unwrap(); + + let length_encoded = bin + .builder + .build_left_shift(length, thirty_two, "temp") + .unwrap(); + let length_encoded = bin + .builder + .build_int_add(length_encoded, four, "length_encoded") + .unwrap(); + + let zero_encoded = bin.builder.build_left_shift(zero, eight, "temp").unwrap(); + + let eight_encoded = bin.builder.build_left_shift(eight, eight, "temp").unwrap(); + let eight_encoded = bin + .builder + .build_int_add(eight_encoded, four, "eight_encoded") + .unwrap(); + + let call_res = bin + .builder + .build_call( + bin.module.get_function(LOG_FROM_LINEAR_MEMORY).unwrap(), + &[ + msg_pos_encoded.into(), + length_encoded.into(), + msg_pos_encoded.into(), + four.into(), + ], + "log", + ) + .unwrap(); + } else { + todo!("Dynamic String printing is not yet supported") + } + } /// Return success without any result fn return_empty_abi(&self, bin: &Binary) { diff --git a/src/lib.rs b/src/lib.rs index 46abbf7e3..b2be2b36b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -95,7 +95,7 @@ impl Target { /// Size of a pointer in bits pub fn ptr_size(&self) -> u16 { - if *self == Target::Solana { + if *self == Target::Solana || *self == Target::Soroban { // Solana is BPF, which is 64 bit 64 } else { diff --git a/src/linker/soroban_wasm.rs b/src/linker/soroban_wasm.rs index 0a6be8bc6..6208a6947 100644 --- a/src/linker/soroban_wasm.rs +++ b/src/linker/soroban_wasm.rs @@ -11,8 +11,7 @@ use wasm_encoder::{ }; use wasmparser::{Global, Import, Parser, Payload::*, SectionLimited, TypeRef}; -use crate::emit::soroban::GET_CONTRACT_DATA; -use crate::emit::soroban::PUT_CONTRACT_DATA; +use crate::emit::soroban::{GET_CONTRACT_DATA, LOG_FROM_LINEAR_MEMORY, PUT_CONTRACT_DATA}; pub fn link(input: &[u8], name: &str) -> Vec { let dir = tempdir().expect("failed to create temp directory for linking"); @@ -82,7 +81,7 @@ fn generate_module(input: &[u8]) -> Vec { module.finish() } -/// Resolve all pallet contracts runtime imports +/// Resolve all soroban contracts runtime imports fn generate_import_section(section: SectionLimited, module: &mut Module) { let mut imports = ImportSection::new(); for import in section.into_iter().map(|import| import.unwrap()) { @@ -98,6 +97,7 @@ fn generate_import_section(section: SectionLimited, module: &mut Module) }; let module_name = match import.name { GET_CONTRACT_DATA | PUT_CONTRACT_DATA => "l", + LOG_FROM_LINEAR_MEMORY => "x", _ => panic!("got func {:?}", import), }; // parse the import name to all string after the the first dot diff --git a/tests/soroban.rs b/tests/soroban.rs index 841a8dc8d..fee9e43e8 100644 --- a/tests/soroban.rs +++ b/tests/soroban.rs @@ -6,6 +6,7 @@ pub mod soroban_testcases; use solang::codegen::Options; use solang::file_resolver::FileResolver; use solang::{compile, Target}; +use soroban_sdk::testutils::Logs; use soroban_sdk::{vec, Address, Env, Symbol, Val}; use std::ffi::OsStr; @@ -27,7 +28,7 @@ pub fn build_solidity(src: &str) -> SorobanEnv { target, &Options { opt_level: opt.into(), - log_runtime_errors: false, + log_runtime_errors: true, log_prints: true, #[cfg(feature = "wasm_opt")] wasm_opt: Some(contract_build::OptimizationPasses::Z), @@ -74,6 +75,26 @@ impl SorobanEnv { println!("args_soroban: {:?}", args_soroban); self.env.invoke_contract(addr, &func, args_soroban) } + + /// Invoke a contract and expect an error. Returns the logs. + pub fn invoke_contract_expect_error( + &self, + addr: &Address, + function_name: &str, + args: Vec, + ) -> Vec { + let func = Symbol::new(&self.env, function_name); + let mut args_soroban = vec![&self.env]; + for arg in args { + args_soroban.push_back(arg) + } + + let _ = self + .env + .try_invoke_contract::(addr, &func, args_soroban); + + self.env.logs().all() + } } impl Default for SorobanEnv { diff --git a/tests/soroban_testcases/mod.rs b/tests/soroban_testcases/mod.rs index abe0ca498..080ab8938 100644 --- a/tests/soroban_testcases/mod.rs +++ b/tests/soroban_testcases/mod.rs @@ -1,3 +1,4 @@ // SPDX-License-Identifier: Apache-2.0 mod math; +mod print; mod storage; diff --git a/tests/soroban_testcases/print.rs b/tests/soroban_testcases/print.rs new file mode 100644 index 000000000..37af17bb1 --- /dev/null +++ b/tests/soroban_testcases/print.rs @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: Apache-2.0 + +use crate::build_solidity; +use soroban_sdk::{testutils::Logs, IntoVal, Val}; + +#[test] +fn log_runtime_error() { + let src = build_solidity( + r#"contract counter { + uint64 public count = 1; + + function decrement() public returns (uint64){ + count -= 1; + return count; + } + }"#, + ); + + let addr = src.contracts.last().unwrap(); + + let _res = src.invoke_contract(addr, "init", vec![]); + + src.invoke_contract(addr, "decrement", vec![]); + + let logs = src.invoke_contract_expect_error(addr, "decrement", vec![]); + + assert!(logs[0].contains("runtime_error: math overflow in test.sol:5:17-27")); +} + +#[test] +fn print() { + let src = build_solidity( + r#"contract Printer { + + function print() public { + print("Hello, World!"); + } + }"#, + ); + + let addr = src.contracts.last().unwrap(); + + let _res = src.invoke_contract(addr, "init", vec![]); + + src.invoke_contract(addr, "print", vec![]); + + let logs = src.env.logs().all(); + + assert!(logs[0].contains("Hello, World!")); +} + +#[test] +fn print_then_runtime_error() { + let src = build_solidity( + r#"contract counter { + uint64 public count = 1; + + function decrement() public returns (uint64){ + print("Second call will FAIL!"); + count -= 1; + return count; + } + }"#, + ); + + let addr = src.contracts.last().unwrap(); + + let _res = src.invoke_contract(addr, "init", vec![]); + + src.invoke_contract(addr, "decrement", vec![]); + + let logs = src.invoke_contract_expect_error(addr, "decrement", vec![]); + + assert!(logs[0].contains("Second call will FAIL!")); + assert!(logs[1].contains("Second call will FAIL!")); + assert!(logs[2].contains("runtime_error: math overflow in test.sol:6:17-27")); +}