Explorar o código

Add print functionality to Soroban contracts (#1659)

This PR adds static string print functionality to Soroban contracts.
This serves the following:

1. `print()` statements
2. Logging runtime errors.

However, the following findings might be interesting:
In both Solana and Polkadot, the VM execution capacity can grasp a call
to `vector_new` in the `stdlib`:
https://github.com/hyperledger/solang/blob/06798cdeac6fd62ee98f5ae7da38f3af4933dc0f/stdlib/stdlib.c#L167

However, Soroban doesn't. That's why Soroban would need Solang to
implement a more efficient way of printing dynamic strings.
@leighmcculloch

Signed-off-by: salaheldinsoliman <salaheldin_sameh@aucegypt.edu>
salaheldinsoliman hai 1 ano
pai
achega
420fbb8924

+ 2 - 0
integration/soroban/.gitignore

@@ -6,3 +6,5 @@
 !package.json
 node_modules
 package-lock.json
+*.txt
+*.toml

+ 9 - 0
integration/soroban/runtime_error.sol

@@ -0,0 +1,9 @@
+contract Error {
+    uint64  count = 1;
+
+    /// @notice Calling this function twice will cause an overflow
+    function decrement() public returns (uint64){
+        count -= 1;
+        return count;
+    }
+}

+ 54 - 43
integration/soroban/test_helpers.js

@@ -1,53 +1,64 @@
 import * as StellarSdk from '@stellar/stellar-sdk';
 
-
-
 export async function call_contract_function(method, server, keypair, contract) {
+    let res = null;
 
-    let res;
-    let builtTransaction = new StellarSdk.TransactionBuilder(await server.getAccount(keypair.publicKey()), {
-      fee: StellarSdk.BASE_FEE,
-      networkPassphrase: StellarSdk.Networks.TESTNET,
-    }).addOperation(contract.call(method)).setTimeout(30).build();
-  
-    let preparedTransaction = await server.prepareTransaction(builtTransaction);
-  
-    // Sign the transaction with the source account's keypair.
-    preparedTransaction.sign(keypair);
-  
     try {
-      let sendResponse = await server.sendTransaction(preparedTransaction);
-      if (sendResponse.status === "PENDING") {
-        let getResponse = await server.getTransaction(sendResponse.hash);
-        // Poll `getTransaction` until the status is not "NOT_FOUND"
-        while (getResponse.status === "NOT_FOUND") {
-          console.log("Waiting for transaction confirmation...");
-          // See if the transaction is complete
-          getResponse = await server.getTransaction(sendResponse.hash);
-          // Wait one second
-          await new Promise((resolve) => setTimeout(resolve, 1000));
-        }
-  
-        if (getResponse.status === "SUCCESS") {
-          // Make sure the transaction's resultMetaXDR is not empty
-          if (!getResponse.resultMetaXdr) {
-            throw "Empty resultMetaXDR in getTransaction response";
-          }
-          // Find the return value from the contract and return it
-          let transactionMeta = getResponse.resultMetaXdr;
-          let returnValue = transactionMeta.v3().sorobanMeta().returnValue();
-          console.log(`Transaction result: ${returnValue.value()}`);
-          res = returnValue.value();
+        let builtTransaction = new StellarSdk.TransactionBuilder(await server.getAccount(keypair.publicKey()), {
+            fee: StellarSdk.BASE_FEE,
+            networkPassphrase: StellarSdk.Networks.TESTNET,
+        }).addOperation(contract.call(method)).setTimeout(30).build();
+
+        let preparedTransaction = await server.prepareTransaction(builtTransaction);
+
+        // Sign the transaction with the source account's keypair.
+        preparedTransaction.sign(keypair);
+
+        let sendResponse = await server.sendTransaction(preparedTransaction);
+
+        if (sendResponse.status === "PENDING") {
+            let getResponse = await server.getTransaction(sendResponse.hash);
+            // Poll `getTransaction` until the status is not "NOT_FOUND"
+            while (getResponse.status === "NOT_FOUND") {
+                console.log("Waiting for transaction confirmation...");
+                // Wait one second
+                await new Promise((resolve) => setTimeout(resolve, 1000));
+                // See if the transaction is complete
+                getResponse = await server.getTransaction(sendResponse.hash);
+            }
+
+            if (getResponse.status === "SUCCESS") {
+                // Ensure the transaction's resultMetaXDR is not empty
+                if (!getResponse.resultMetaXdr) {
+                    throw "Empty resultMetaXDR in getTransaction response";
+                }
+                // Extract and return the return value from the contract
+                let transactionMeta = getResponse.resultMetaXdr;
+                let returnValue = transactionMeta.v3().sorobanMeta().returnValue();
+                console.log(`Transaction result: ${returnValue.value()}`);
+                res = returnValue.value();
+            } else {
+                throw `Transaction failed: ${getResponse.resultXdr}`;
+            }
+        } else if (sendResponse.status === "FAILED") {
+            // Handle expected failure and return the error message
+            if (sendResponse.errorResultXdr) {
+                const errorXdr = StellarSdk.xdr.TransactionResult.fromXDR(sendResponse.errorResultXdr, 'base64');
+                const errorRes = errorXdr.result().results()[0].tr().invokeHostFunctionResult().code().value;
+                console.log(`Transaction error: ${errorRes}`);
+                res = errorRes;
+            } else {
+                throw "Transaction failed but no errorResultXdr found";
+            }
         } else {
-          throw `Transaction failed: ${getResponse.resultXdr}`;
+            throw sendResponse.errorResultXdr;
         }
-      } else {
-        throw sendResponse.errorResultXdr;
-      }
     } catch (err) {
-      // Catch and report any errors we've thrown
-      console.log("Sending transaction failed");
-      console.log(err);
+        // Return the error as a string instead of failing the test
+        console.log("Transaction processing failed");
+        console.log(err);
+        res = err.toString();
     }
+
     return res;
-}
+}

+ 36 - 24
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;

+ 17 - 1
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: Some(initializer),
+                } = &to_print
+                {
+                    Expression::BytesLiteral {
+                        loc: *loc,
+                        ty: ty.clone(),
+                        value: initializer.to_vec(),
+                    }
+                } else {
+                    to_print
+                };
+
+                cfg.add(vartab, Instr::Print { expr: res });
             }
 
             Expression::Poison

+ 27 - 1
src/emit/expression.rs

@@ -126,7 +126,33 @@ 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.i64_type().into(),
+                    ],
+                    false,
+                );
+
+                return ty
+                    .const_named_struct(&[
+                        data.into(),
+                        bin.context
+                            .i64_type()
+                            .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

+ 12 - 0
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) {

+ 62 - 2
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) {

+ 3 - 4
src/lib.rs

@@ -95,12 +95,11 @@ impl Target {
 
     /// Size of a pointer in bits
     pub fn ptr_size(&self) -> u16 {
-        if *self == Target::Solana {
+        match *self {
             // Solana is BPF, which is 64 bit
-            64
-        } else {
+            Target::Solana => 64,
             // All others are WebAssembly in 32 bit mode
-            32
+            _ => 32,
         }
     }
 

+ 3 - 3
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<u8> {
     let dir = tempdir().expect("failed to create temp directory for linking");
@@ -82,7 +81,7 @@ fn generate_module(input: &[u8]) -> Vec<u8> {
     module.finish()
 }
 
-/// Resolve all pallet contracts runtime imports
+/// Resolve all soroban contracts runtime imports
 fn generate_import_section(section: SectionLimited<Import>, 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<Import>, 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

+ 1 - 1
src/sema/namespace.rs

@@ -41,7 +41,7 @@ impl Namespace {
                 value_length,
             } => (address_length, value_length),
             Target::Solana => (32, 8),
-            Target::Soroban => (32, 8),
+            Target::Soroban => (32, 64),
         };
 
         let mut ns = Namespace {

+ 22 - 1
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<Val>,
+    ) -> Vec<String> {
+        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::<Val, Val>(addr, &func, args_soroban);
+
+        self.env.logs().all()
+    }
 }
 
 impl Default for SorobanEnv {

+ 1 - 0
tests/soroban_testcases/mod.rs

@@ -1,3 +1,4 @@
 // SPDX-License-Identifier: Apache-2.0
 mod math;
+mod print;
 mod storage;

+ 77 - 0
tests/soroban_testcases/print.rs

@@ -0,0 +1,77 @@
+// SPDX-License-Identifier: Apache-2.0
+
+use crate::build_solidity;
+use soroban_sdk::testutils::Logs;
+
+#[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"));
+}