From 399c199923d52d45ebab088091a55b04f9f09c40 Mon Sep 17 00:00:00 2001 From: salaheldinsoliman <49910731+salaheldinsoliman@users.noreply.github.com> Date: Tue, 25 Jun 2024 12:08:20 +0200 Subject: [PATCH 1/7] Soroban counter.sol example (#1645) This PR aims to make Solang support a simple counter.sol example on Soroban, where a storage variable is instatiated, modified and retrieved. The counter contract is only limited to `uint64` data types, and only supports `instance` soroban storage. This can be considered a "skeleton" for supporting more data and storage types, as well as more host function invokations. - [x] Support Soroban storage function calls `put_contract_data`, `get_contract_data` and `has_contract_data` - [x] Implement a wrapper `init` for `storage_initializer` - [x] Implement wrappers for public functions - [x] Insert decoding/encoding instructions into the wrapper functions - [x] Soroban doesn't have function return codes. This needs to be handled all over emit - [x] Add integration tests and MockVm tests --------- Signed-off-by: salaheldinsoliman --- .github/workflows/test.yml | 42 +++++++ integration/soroban/.gitignore | 8 ++ integration/soroban/counter.sol | 13 +++ integration/soroban/counter.spec.js | 55 +++++++++ integration/soroban/package.json | 23 ++++ integration/soroban/setup.js | 63 ++++++++++ integration/soroban/test_helpers.js | 53 +++++++++ src/bin/cli/mod.rs | 16 ++- src/bin/cli/test.rs | 18 ++- src/bin/solang.rs | 8 +- src/codegen/dispatch/mod.rs | 5 +- src/codegen/dispatch/soroban.rs | 141 +++++++++++++++++++++++ src/codegen/mod.rs | 4 +- src/emit/binary.rs | 2 +- src/emit/instructions.rs | 87 +++++++------- src/emit/soroban/mod.rs | 158 +++++++++++++++++++------- src/emit/soroban/target.rs | 61 ++++++++-- src/emit/storage.rs | 2 +- src/linker/soroban_wasm.rs | 67 ++++++++++- src/sema/contracts.rs | 3 +- tests/soroban.rs | 1 + tests/soroban_testcases/math.rs | 47 ++++---- tests/soroban_testcases/mod.rs | 1 + tests/soroban_testcases/storage.rs | 41 +++++++ tests/undefined_variable_detection.rs | 1 + 25 files changed, 794 insertions(+), 126 deletions(-) create mode 100644 integration/soroban/.gitignore create mode 100644 integration/soroban/counter.sol create mode 100644 integration/soroban/counter.spec.js create mode 100644 integration/soroban/package.json create mode 100644 integration/soroban/setup.js create mode 100644 integration/soroban/test_helpers.js create mode 100644 src/codegen/dispatch/soroban.rs create mode 100644 tests/soroban_testcases/storage.rs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0065a5a44..39f17f1e0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -306,6 +306,48 @@ jobs: with: name: anchor-tests path: ./target/*.profraw + + soroban: + name: Soroban Integration test + runs-on: solang-ubuntu-latest + container: ghcr.io/hyperledger/solang-llvm:ci-7 + needs: linux-x86-64 + steps: + - name: Checkout sources + uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: '16' + - uses: dtolnay/rust-toolchain@1.74.0 + - uses: actions/download-artifact@v3 + with: + name: solang-linux-x86-64 + path: bin + - name: Solang Compiler + run: | + chmod 755 ./bin/solang + echo "$(pwd)/bin" >> $GITHUB_PATH + + - name: Install Soroban + run: cargo install --locked soroban-cli --version 21.0.0-rc.1 + - name: Add cargo install location to PATH + run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH + - run: npm install + working-directory: ./integration/soroban + - name: Build Solang contracts + run: npm run build + working-directory: ./integration/soroban + - name: Setup Soroban enivronment + run: npm run setup + working-directory: ./integration/soroban + - name: Deploy and test contracts + run: npm run test + working-directory: ./integration/soroban + - name: Upload test coverage files + uses: actions/upload-artifact@v3.1.0 + with: + name: soroban-tests + path: ./target/*.profraw solana: name: Solana Integration test diff --git a/integration/soroban/.gitignore b/integration/soroban/.gitignore new file mode 100644 index 000000000..d33bf9529 --- /dev/null +++ b/integration/soroban/.gitignore @@ -0,0 +1,8 @@ +*.js +*.so +*.key +*.json +!tsconfig.json +!package.json +node_modules +package-lock.json diff --git a/integration/soroban/counter.sol b/integration/soroban/counter.sol new file mode 100644 index 000000000..3d289ba33 --- /dev/null +++ b/integration/soroban/counter.sol @@ -0,0 +1,13 @@ +contract counter { + uint64 public count = 10; + + function increment() public returns (uint64) { + count += 1; + return count; + } + + function decrement() public returns (uint64) { + count -= 1; + return count; + } +} diff --git a/integration/soroban/counter.spec.js b/integration/soroban/counter.spec.js new file mode 100644 index 000000000..53f3ed103 --- /dev/null +++ b/integration/soroban/counter.spec.js @@ -0,0 +1,55 @@ +import * as StellarSdk from '@stellar/stellar-sdk'; +import { readFileSync } from 'fs'; +import { expect } from 'chai'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { call_contract_function } from './test_helpers.js'; + +const __filename = fileURLToPath(import.meta.url); +const dirname = path.dirname(__filename); + +describe('Counter', () => { + let keypair; + const server = new StellarSdk.SorobanRpc.Server( + "https://soroban-testnet.stellar.org:443", + ); + + let contractAddr; + let contract; + before(async () => { + + console.log('Setting up counter contract tests...'); + + // read secret from file + const secret = readFileSync('alice.txt', 'utf8').trim(); + keypair = StellarSdk.Keypair.fromSecret(secret); + + let contractIdFile = path.join(dirname, '.soroban', 'contract-ids', 'counter.txt'); + // read contract address from file + contractAddr = readFileSync(contractIdFile, 'utf8').trim().toString(); + + // load contract + contract = new StellarSdk.Contract(contractAddr); + + // initialize the contract + await call_contract_function("init", server, keypair, contract); + + }); + + it('get correct initial counter', async () => { + // get the count + let count = await call_contract_function("count", server, keypair, contract); + expect(count.toString()).eq("10"); + }); + + it('increment counter', async () => { + // increment the counter + await call_contract_function("increment", server, keypair, contract); + + // get the count + let count = await call_contract_function("count", server, keypair, contract); + expect(count.toString()).eq("11"); + }); +}); + + diff --git a/integration/soroban/package.json b/integration/soroban/package.json new file mode 100644 index 000000000..dc8303de0 --- /dev/null +++ b/integration/soroban/package.json @@ -0,0 +1,23 @@ +{ + "type": "module", + "dependencies": { + "@stellar/stellar-sdk": "^12.0.1", + "chai": "^5.1.1", + "dotenv": "^16.4.5", + "mocha": "^10.4.0" + }, + "scripts": { + "build": "solang compile *.sol --target soroban", + "setup": "node setup.js", + "test": "mocha *.spec.js --timeout 20000" + }, + "devDependencies": { + "@eslint/js": "^9.4.0", + "@types/mocha": "^10.0.6", + "eslint": "^9.4.0", + "expect": "^29.7.0", + "globals": "^15.4.0", + "typescript": "^5.4.5" + } + } + \ No newline at end of file diff --git a/integration/soroban/setup.js b/integration/soroban/setup.js new file mode 100644 index 000000000..7bdb32a8b --- /dev/null +++ b/integration/soroban/setup.js @@ -0,0 +1,63 @@ + +import 'dotenv/config'; +import { mkdirSync, readdirSync} from 'fs'; +import { execSync } from 'child_process'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +console.log("###################### Initializing ########################"); + +// Get dirname (equivalent to the Bash version) +const __filename = fileURLToPath(import.meta.url); +const dirname = path.dirname(__filename); + +// variable for later setting pinned version of soroban in "$(dirname/target/bin/soroban)" +const soroban = "soroban" + +// Function to execute and log shell commands +function exe(command) { + console.log(command); + execSync(command, { stdio: 'inherit' }); +} + +function generate_alice() { + exe(`${soroban} keys generate alice --network testnet`); + + // get the secret key of alice and put it in alice.txt + exe(`${soroban} keys show alice > alice.txt`); +} + + +function filenameNoExtension(filename) { + return path.basename(filename, path.extname(filename)); +} + +function deploy(wasm) { + + let contractId = path.join(dirname, '.soroban', 'contract-ids', filenameNoExtension(wasm) + '.txt'); + + exe(`(${soroban} contract deploy --wasm ${wasm} --ignore-checks --source-account alice --network testnet) > ${contractId}`); +} + +function deploy_all() { + const contractsDir = path.join(dirname, '.soroban', 'contract-ids'); + mkdirSync(contractsDir, { recursive: true }); + + const wasmFiles = readdirSync(`${dirname}`).filter(file => file.endsWith('.wasm')); + + wasmFiles.forEach(wasmFile => { + deploy(path.join(dirname, wasmFile)); + }); +} + +function add_testnet() { + + exe(`${soroban} network add \ + --global testnet \ + --rpc-url https://soroban-testnet.stellar.org:443 \ + --network-passphrase "Test SDF Network ; September 2015"`); +} + +add_testnet(); +generate_alice(); +deploy_all(); diff --git a/integration/soroban/test_helpers.js b/integration/soroban/test_helpers.js new file mode 100644 index 000000000..ebafc8f81 --- /dev/null +++ b/integration/soroban/test_helpers.js @@ -0,0 +1,53 @@ +import * as StellarSdk from '@stellar/stellar-sdk'; + + + +export async function call_contract_function(method, server, keypair, contract) { + + 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(); + } else { + throw `Transaction failed: ${getResponse.resultXdr}`; + } + } else { + throw sendResponse.errorResultXdr; + } + } catch (err) { + // Catch and report any errors we've thrown + console.log("Sending transaction failed"); + console.log(err); + } + return res; +} \ No newline at end of file diff --git a/src/bin/cli/mod.rs b/src/bin/cli/mod.rs index 3e0eafa3e..dc7add7bc 100644 --- a/src/bin/cli/mod.rs +++ b/src/bin/cli/mod.rs @@ -321,6 +321,15 @@ pub struct CompilePackage { #[arg(name = "VERSION", help = "specify contracts version", long = "version", num_args = 1, value_parser = ValueParser::new(parse_version))] #[serde(default, deserialize_with = "deserialize_version")] pub version: Option, + + #[arg( + name = "SOROBAN-VERSION", + help = "specify soroban contracts pre-release number", + short = 's', + long = "soroban-version", + num_args = 1 + )] + pub soroban_version: Option, } #[derive(Args, Deserialize, Debug, PartialEq)] @@ -545,7 +554,11 @@ pub fn imports_arg(package: &T) -> FileResolver { resolver } -pub fn options_arg(debug: &DebugFeatures, optimizations: &Optimizations) -> Options { +pub fn options_arg( + debug: &DebugFeatures, + optimizations: &Optimizations, + compiler_inputs: &CompilePackage, +) -> Options { let opt_level = if let Some(level) = &optimizations.opt_level { match level.as_str() { "none" => OptimizationLevel::None, @@ -574,6 +587,7 @@ pub fn options_arg(debug: &DebugFeatures, optimizations: &Optimizations) -> Opti } else { None }), + soroban_version: compiler_inputs.soroban_version, } } diff --git a/src/bin/cli/test.rs b/src/bin/cli/test.rs index d1ebb9ed1..ebdb84e6e 100644 --- a/src/bin/cli/test.rs +++ b/src/bin/cli/test.rs @@ -101,7 +101,17 @@ mod tests { let default_optimize: cli::Optimizations = toml::from_str("").unwrap(); - let opt = options_arg(&default_debug, &default_optimize); + let compiler_package = cli::CompilePackage { + input: Some(vec![PathBuf::from("flipper.sol")]), + contracts: Some(vec!["flipper".to_owned()]), + import_path: Some(vec![]), + import_map: Some(vec![]), + authors: None, + version: Some("0.1.0".to_string()), + soroban_version: None, + }; + + let opt = options_arg(&default_debug, &default_optimize, &compiler_package); assert_eq!(opt, Options::default()); @@ -185,7 +195,8 @@ mod tests { import_path: Some(vec![]), import_map: Some(vec![]), authors: None, - version: Some("0.1.0".to_string()) + version: Some("0.1.0".to_string()), + soroban_version: None }, compiler_output: cli::CompilerOutput { emit: None, @@ -239,7 +250,8 @@ mod tests { import_path: Some(vec![]), import_map: Some(vec![]), authors: Some(vec!["not_sesa".to_owned()]), - version: Some("0.1.0".to_string()) + version: Some("0.1.0".to_string()), + soroban_version: None }, compiler_output: cli::CompilerOutput { emit: None, diff --git a/src/bin/solang.rs b/src/bin/solang.rs index ad1c44d1d..f07ae2d28 100644 --- a/src/bin/solang.rs +++ b/src/bin/solang.rs @@ -171,7 +171,13 @@ fn compile(compile_args: &Compile) { let mut resolver = imports_arg(&compile_args.package); - let opt = options_arg(&compile_args.debug_features, &compile_args.optimizations); + let compile_package = &compile_args.package; + + let opt = options_arg( + &compile_args.debug_features, + &compile_args.optimizations, + compile_package, + ); let mut namespaces = Vec::new(); diff --git a/src/codegen/dispatch/mod.rs b/src/codegen/dispatch/mod.rs index 764740b0c..be718d9ec 100644 --- a/src/codegen/dispatch/mod.rs +++ b/src/codegen/dispatch/mod.rs @@ -5,10 +5,11 @@ use crate::{sema::ast::Namespace, Target}; pub(crate) mod polkadot; pub(super) mod solana; +pub(super) mod soroban; pub(super) fn function_dispatch( contract_no: usize, - all_cfg: &[ControlFlowGraph], + all_cfg: &mut [ControlFlowGraph], ns: &mut Namespace, opt: &Options, ) -> Vec { @@ -17,6 +18,6 @@ pub(super) fn function_dispatch( Target::Polkadot { .. } | Target::EVM => { polkadot::function_dispatch(contract_no, all_cfg, ns, opt) } - Target::Soroban => vec![], + Target::Soroban => soroban::function_dispatch(contract_no, all_cfg, ns, opt), } } diff --git a/src/codegen/dispatch/soroban.rs b/src/codegen/dispatch/soroban.rs new file mode 100644 index 000000000..94959edd3 --- /dev/null +++ b/src/codegen/dispatch/soroban.rs @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: Apache-2.0 + +use num_bigint::BigInt; +use solang_parser::pt::{self}; + +use crate::sema::ast; +use crate::{ + codegen::{ + cfg::{ASTFunction, ControlFlowGraph, Instr, InternalCallTy}, + vartable::Vartable, + Expression, Options, + }, + sema::ast::{Namespace, Type}, +}; + +pub fn function_dispatch( + contract_no: usize, + all_cfg: &mut [ControlFlowGraph], + ns: &mut Namespace, + _opt: &Options, +) -> Vec { + // For each function in all_cfg, we will generate a wrapper function that will call the function + // The wrapper function will call abi_encode to encode the arguments, and then call the function + + let mut wrapper_cfgs = Vec::new(); + + for cfg in all_cfg.iter_mut() { + let function = match &cfg.function_no { + ASTFunction::SolidityFunction(no) => &ns.functions[*no], + _ => continue, + }; + + let wrapper_name = { + if cfg.public { + if function.mangled_name_contracts.contains(&contract_no) { + function.mangled_name.clone() + } else { + function.id.name.clone() + } + } else { + continue; + } + }; + + let mut wrapper_cfg = ControlFlowGraph::new(wrapper_name.to_string(), ASTFunction::None); + + wrapper_cfg.params = function.params.clone(); + + let param = ast::Parameter::new_default(Type::Uint(64)); + wrapper_cfg.returns = vec![param].into(); + wrapper_cfg.public = true; + + let mut vartab = Vartable::from_symbol_table(&function.symtable, ns.next_id); + + let mut value = Vec::new(); + let mut return_tys = Vec::new(); + + let mut call_returns = Vec::new(); + for arg in function.returns.iter() { + let new = vartab.temp_anonymous(&arg.ty); + value.push(Expression::Variable { + loc: arg.loc, + ty: arg.ty.clone(), + var_no: new, + }); + return_tys.push(arg.ty.clone()); + call_returns.push(new); + } + + let cfg_no = match cfg.function_no { + ASTFunction::SolidityFunction(no) => no, + _ => 0, + }; + let placeholder = Instr::Call { + res: call_returns, + call: InternalCallTy::Static { cfg_no }, + return_tys, + args: function + .params + .iter() + .enumerate() + .map(|(i, p)| Expression::ShiftRight { + loc: pt::Loc::Codegen, + ty: Type::Uint(64), + left: Expression::FunctionArg { + loc: p.loc, + ty: p.ty.clone(), + arg_no: i, + } + .into(), + right: Expression::NumberLiteral { + loc: pt::Loc::Codegen, + ty: Type::Uint(64), + value: BigInt::from(8_u64), + } + .into(), + + signed: false, + }) + .collect(), + }; + + 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 { + 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 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] }); + + vartab.finalize(ns, &mut wrapper_cfg); + cfg.public = false; + wrapper_cfgs.push(wrapper_cfg); + } + + wrapper_cfgs +} diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index 7d544ac16..0484cfc01 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -107,6 +107,7 @@ pub struct Options { pub log_prints: bool, #[cfg(feature = "wasm_opt")] pub wasm_opt: Option, + pub soroban_version: Option, } impl Default for Options { @@ -123,6 +124,7 @@ impl Default for Options { log_prints: true, #[cfg(feature = "wasm_opt")] wasm_opt: None, + soroban_version: None, } } } @@ -247,7 +249,7 @@ fn contract(contract_no: usize, ns: &mut Namespace, opt: &Options) { ns.contracts[contract_no].default_constructor = Some((func, cfg_no)); } - for mut dispatch_cfg in function_dispatch(contract_no, &all_cfg, ns, opt) { + for mut dispatch_cfg in function_dispatch(contract_no, &mut all_cfg, ns, opt) { optimize_and_check_cfg(&mut dispatch_cfg, ns, ASTFunction::None, opt); all_cfg.push(dispatch_cfg); } diff --git a/src/emit/binary.rs b/src/emit/binary.rs index 06a669378..a81035329 100644 --- a/src/emit/binary.rs +++ b/src/emit/binary.rs @@ -1227,7 +1227,7 @@ impl<'a> Binary<'a> { ns: &Namespace, code: PanicCode, ) -> (PointerValue<'a>, IntValue<'a>) { - if ns.target == Target::Solana { + if ns.target == Target::Solana || ns.target == Target::Soroban { return ( self.context .i8_type() diff --git a/src/emit/instructions.rs b/src/emit/instructions.rs index 13c475065..c36b266b3 100644 --- a/src/emit/instructions.rs +++ b/src/emit/instructions.rs @@ -461,7 +461,8 @@ pub(super) fn process_instruction<'a, T: TargetRuntime<'a> + ?Sized>( .map(|p| expression(target, bin, p, &w.vars, function, ns).into()) .collect::>(); - if !res.is_empty() { + // Soroban doesn't write return values to imported memory + if !res.is_empty() && ns.target != Target::Soroban { for v in f.returns.iter() { parms.push(if ns.target == Target::Solana { bin.build_alloca(function, bin.llvm_var_ty(&v.ty, ns), v.name_as_str()) @@ -484,54 +485,58 @@ pub(super) fn process_instruction<'a, T: TargetRuntime<'a> + ?Sized>( .build_call(bin.functions[cfg_no], &parms, "") .unwrap() .try_as_basic_value() - .left() - .unwrap(); - - let success = bin - .builder - .build_int_compare( - IntPredicate::EQ, - ret.into_int_value(), - bin.return_values[&ReturnCode::Success], - "success", - ) - .unwrap(); + .left(); - let success_block = bin.context.append_basic_block(function, "success"); - let bail_block = bin.context.append_basic_block(function, "bail"); - bin.builder - .build_conditional_branch(success, success_block, bail_block) - .unwrap(); + // Soroban doesn't have return codes, and only returns a single i64 value + if ns.target != Target::Soroban { + let success = bin + .builder + .build_int_compare( + IntPredicate::EQ, + ret.unwrap().into_int_value(), + bin.return_values[&ReturnCode::Success], + "success", + ) + .unwrap(); - bin.builder.position_at_end(bail_block); + let success_block = bin.context.append_basic_block(function, "success"); + let bail_block = bin.context.append_basic_block(function, "bail"); + bin.builder + .build_conditional_branch(success, success_block, bail_block) + .unwrap(); - bin.builder.build_return(Some(&ret)).unwrap(); - bin.builder.position_at_end(success_block); + bin.builder.position_at_end(bail_block); - if !res.is_empty() { - for (i, v) in f.returns.iter().enumerate() { - let load_ty = bin.llvm_var_ty(&v.ty, ns); - let val = bin - .builder - .build_load( - load_ty, - parms[args.len() + i].into_pointer_value(), - v.name_as_str(), - ) - .unwrap(); - let dest = w.vars[&res[i]].value; + bin.builder.build_return(Some(&ret.unwrap())).unwrap(); + bin.builder.position_at_end(success_block); - if dest.is_pointer_value() - && !(v.ty.is_reference_type(ns) - || matches!(v.ty, Type::ExternalFunction { .. })) - { - bin.builder - .build_store(dest.into_pointer_value(), val) + if !res.is_empty() { + for (i, v) in f.returns.iter().enumerate() { + let load_ty = bin.llvm_var_ty(&v.ty, ns); + let val = bin + .builder + .build_load( + load_ty, + parms[args.len() + i].into_pointer_value(), + v.name_as_str(), + ) .unwrap(); - } else { - w.vars.get_mut(&res[i]).unwrap().value = val; + let dest = w.vars[&res[i]].value; + + if dest.is_pointer_value() + && !(v.ty.is_reference_type(ns) + || matches!(v.ty, Type::ExternalFunction { .. })) + { + bin.builder + .build_store(dest.into_pointer_value(), val) + .unwrap(); + } else { + w.vars.get_mut(&res[i]).unwrap().value = val; + } } } + } else if let Some(value) = ret { + w.vars.get_mut(&res[0]).unwrap().value = value; } } Instr::Call { diff --git a/src/emit/soroban/mod.rs b/src/emit/soroban/mod.rs index 074f3ab41..51191ea37 100644 --- a/src/emit/soroban/mod.rs +++ b/src/emit/soroban/mod.rs @@ -1,13 +1,13 @@ // SPDX-License-Identifier: Apache-2.0 pub(super) mod target; -use crate::codegen::cfg::ControlFlowGraph; -use crate::emit::cfg::emit_cfg; -use crate::{ - codegen::{cfg::ASTFunction, Options}, - emit::Binary, - sema::ast, +use crate::codegen::{ + cfg::{ASTFunction, ControlFlowGraph}, + Options, STORAGE_INITIALIZER, }; + +use crate::emit::cfg::emit_cfg; +use crate::{emit::Binary, sema::ast}; use inkwell::{ context::Context, module::{Linkage, Module}, @@ -16,8 +16,12 @@ use soroban_sdk::xdr::{ DepthLimitedWrite, ScEnvMetaEntry, ScSpecEntry, ScSpecFunctionInputV0, ScSpecFunctionV0, ScSpecTypeDef, StringM, WriteXdr, }; +use std::ffi::CString; +use std::sync; -const SOROBAN_ENV_INTERFACE_VERSION: u64 = 85899345977; +const SOROBAN_ENV_INTERFACE_VERSION: u64 = 90194313216; +pub const PUT_CONTRACT_DATA: &str = "l._"; +pub const GET_CONTRACT_DATA: &str = "l.1"; pub struct SorobanTarget; @@ -41,8 +45,21 @@ impl SorobanTarget { None, ); - Self::emit_functions_with_spec(contract, &mut binary, ns, context, contract_no); - Self::emit_env_meta_entries(context, &mut binary); + let mut export_list = Vec::new(); + Self::declare_externals(&mut binary); + Self::emit_functions_with_spec( + contract, + &mut binary, + ns, + context, + contract_no, + &mut export_list, + ); + binary.internalize(export_list.as_slice()); + + Self::emit_initializer(&mut binary, ns); + + Self::emit_env_meta_entries(context, &mut binary, opt); binary } @@ -54,7 +71,8 @@ impl SorobanTarget { binary: &mut Binary<'a>, ns: &'a ast::Namespace, context: &'a Context, - contract_no: usize, + _contract_no: usize, + export_list: &mut Vec<&'a str>, ) { let mut defines = Vec::new(); @@ -68,41 +86,30 @@ impl SorobanTarget { // For each function, determine the name and the linkage // Soroban has no dispatcher, so all externally addressable functions are exported and should be named the same as the original function name in the source code. // If there are duplicate function names, then the function name in the source is mangled to include the signature. - let default_constructor = ns.default_constructor(contract_no); - let name = { - if cfg.public { - let f = match &cfg.function_no { - ASTFunction::SolidityFunction(no) | ASTFunction::YulFunction(no) => { - &ns.functions[*no] - } - _ => &default_constructor, - }; - - if f.mangled_name_contracts.contains(&contract_no) { - &f.mangled_name - } else { - &f.id.name - } - } else { - &cfg.name - } - }; - Self::emit_function_spec_entry(context, cfg, name.clone(), binary); + // if func is a default constructor, then the function name is the contract name let linkage = if cfg.public { + let name = if cfg.name.contains("::") { + // get the third part of the name which is the function name + cfg.name.split("::").collect::>()[2] + } else { + &cfg.name + }; + Self::emit_function_spec_entry(context, cfg, name.to_string(), binary); + export_list.push(name); Linkage::External } else { Linkage::Internal }; - let func_decl = if let Some(func) = binary.module.get_function(name) { + let func_decl = if let Some(func) = binary.module.get_function(&cfg.name) { // must not have a body yet assert_eq!(func.get_first_basic_block(), None); func } else { - binary.module.add_function(name, ftype, Some(linkage)) + binary.module.add_function(&cfg.name, ftype, Some(linkage)) }; binary.functions.insert(cfg_no, func_decl); @@ -110,14 +117,21 @@ impl SorobanTarget { defines.push((func_decl, cfg)); } + let init_type = context.i64_type().fn_type(&[], false); + binary + .module + .add_function("storage_initializer", init_type, None); + for (func_decl, cfg) in defines { emit_cfg(&mut SorobanTarget, binary, contract, cfg, func_decl, ns); } } - fn emit_env_meta_entries<'a>(context: &'a Context, binary: &mut Binary<'a>) { + fn emit_env_meta_entries<'a>(context: &'a Context, binary: &mut Binary<'a>, opt: &'a Options) { let mut meta = DepthLimitedWrite::new(Vec::new(), 10); - ScEnvMetaEntry::ScEnvMetaKindInterfaceVersion(SOROBAN_ENV_INTERFACE_VERSION) + let soroban_env_interface_version = + opt.soroban_version.unwrap_or(SOROBAN_ENV_INTERFACE_VERSION); + ScEnvMetaEntry::ScEnvMetaKindInterfaceVersion(soroban_env_interface_version) .write_xdr(&mut meta) .expect("writing env meta interface version to xdr"); Self::add_custom_section(context, &binary.module, "contractenvmetav0", meta.inner); @@ -125,12 +139,12 @@ impl SorobanTarget { fn emit_function_spec_entry<'a>( context: &'a Context, - cfg: &'a ControlFlowGraph, + cfg: &ControlFlowGraph, name: String, binary: &mut Binary<'a>, ) { if cfg.public && !cfg.is_placeholder() { - // TODO: Emit custom type spec entries. + // TODO: Emit custom type spec entries let mut spec = DepthLimitedWrite::new(Vec::new(), 10); ScSpecEntry::FunctionV0(ScSpecFunctionV0 { name: name @@ -148,7 +162,7 @@ impl SorobanTarget { .unwrap_or_else(|| i.to_string()) .try_into() .expect("function input name exceeds limit"), - type_: ScSpecTypeDef::U32, // TODO: Map type. + type_: ScSpecTypeDef::U64, // TODO: Map type. doc: StringM::default(), // TODO: Add doc. }) .collect::>() @@ -157,7 +171,20 @@ impl SorobanTarget { outputs: cfg .returns .iter() - .map(|_| ScSpecTypeDef::U32) // TODO: Map type. + .map(|return_type| { + let ty = return_type.ty.clone(); + match ty { + ast::Type::Uint(32) => ScSpecTypeDef::U32, + ast::Type::Uint(64) => ScSpecTypeDef::U64, + ast::Type::Int(_) => ScSpecTypeDef::I32, + ast::Type::Bool => ScSpecTypeDef::Bool, + ast::Type::Address(_) => ScSpecTypeDef::Address, + ast::Type::Bytes(_) => ScSpecTypeDef::Bytes, + ast::Type::String => ScSpecTypeDef::String, + ast::Type::Void => ScSpecTypeDef::Void, + _ => panic!("unsupported return type {:?}", ty), + } + }) // TODO: Map type. .collect::>() .try_into() .expect("function output count exceeds limit"), @@ -192,4 +219,59 @@ impl SorobanTarget { ) .expect("adding spec as metadata"); } + + fn declare_externals(binary: &mut Binary) { + let ty = binary.context.i64_type(); + let function_ty_1 = binary + .context + .i64_type() + .fn_type(&[ty.into(), ty.into(), ty.into()], false); + let function_ty = binary + .context + .i64_type() + .fn_type(&[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)); + } + + fn emit_initializer(binary: &mut Binary, _ns: &ast::Namespace) { + let mut cfg = ControlFlowGraph::new("init".to_string(), ASTFunction::None); + + cfg.public = true; + let void_param = ast::Parameter::new_default(ast::Type::Void); + cfg.returns = sync::Arc::new(vec![void_param]); + + Self::emit_function_spec_entry(binary.context, &cfg, "init".to_string(), binary); + + let function_name = CString::new(STORAGE_INITIALIZER).unwrap(); + let mut storage_initializers = binary + .functions + .values() + .filter(|f: &&inkwell::values::FunctionValue| f.get_name() == function_name.as_c_str()); + let storage_initializer = *storage_initializers + .next() + .expect("storage initializer is always present"); + assert!(storage_initializers.next().is_none()); + + let void_type = binary.context.i64_type().fn_type(&[], false); + let init = binary + .module + .add_function("init", void_type, Some(Linkage::External)); + let entry = binary.context.append_basic_block(init, "entry"); + + binary.builder.position_at_end(entry); + binary + .builder + .build_call(storage_initializer, &[], "storage_initializer") + .unwrap(); + + // return zero + let zero_val = binary.context.i64_type().const_int(2, false); + binary.builder.build_return(Some(&zero_val)).unwrap(); + } } diff --git a/src/emit/soroban/target.rs b/src/emit/soroban/target.rs index 56fb02f87..cc50591f7 100644 --- a/src/emit/soroban/target.rs +++ b/src/emit/soroban/target.rs @@ -3,17 +3,22 @@ use crate::codegen::cfg::HashTy; use crate::codegen::Expression; use crate::emit::binary::Binary; -use crate::emit::soroban::SorobanTarget; +use crate::emit::soroban::{SorobanTarget, GET_CONTRACT_DATA, PUT_CONTRACT_DATA}; use crate::emit::ContractArgs; use crate::emit::{TargetRuntime, Variable}; +use crate::emit_context; use crate::sema::ast; use crate::sema::ast::CallTy; use crate::sema::ast::{Function, Namespace, Type}; + use inkwell::types::{BasicTypeEnum, IntType}; use inkwell::values::{ - ArrayValue, BasicMetadataValueEnum, BasicValueEnum, FunctionValue, IntValue, PointerValue, + ArrayValue, BasicMetadataValueEnum, BasicValue, BasicValueEnum, FunctionValue, IntValue, + PointerValue, }; + use solang_parser::pt::Loc; + use std::collections::HashMap; // TODO: Implement TargetRuntime for SorobanTarget. @@ -21,12 +26,12 @@ use std::collections::HashMap; impl<'a> TargetRuntime<'a> for SorobanTarget { fn get_storage_int( &self, - bin: &Binary<'a>, + binary: &Binary<'a>, function: FunctionValue, slot: PointerValue<'a>, ty: IntType<'a>, ) -> IntValue<'a> { - unimplemented!() + todo!() } fn storage_load( @@ -37,7 +42,23 @@ impl<'a> TargetRuntime<'a> for SorobanTarget { function: FunctionValue<'a>, ns: &ast::Namespace, ) -> BasicValueEnum<'a> { - unimplemented!() + emit_context!(binary); + let ret = call!( + GET_CONTRACT_DATA, + &[ + slot.as_basic_value_enum() + .into_int_value() + .const_cast(binary.context.i64_type(), false) + .into(), + i64_const!(2).into() + ] + ) + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); + + ret.into() } /// Recursively store a type to storage @@ -51,7 +72,28 @@ impl<'a> TargetRuntime<'a> for SorobanTarget { function: FunctionValue<'a>, ns: &ast::Namespace, ) { - unimplemented!() + emit_context!(binary); + let function_value = binary.module.get_function(PUT_CONTRACT_DATA).unwrap(); + + let value = binary + .builder + .build_call( + function_value, + &[ + slot.as_basic_value_enum() + .into_int_value() + .const_cast(binary.context.i64_type(), false) + .into(), + dest.into(), + binary.context.i64_type().const_int(2, false).into(), + ], + PUT_CONTRACT_DATA, + ) + .unwrap() + .try_as_basic_value() + .left() + .unwrap() + .into_int_value(); } /// Recursively clear storage. The default implementation is for slot-based storage @@ -193,9 +235,8 @@ impl<'a> TargetRuntime<'a> for SorobanTarget { } /// Prints a string - fn print(&self, bin: &Binary, string: PointerValue, length: IntValue) { - unimplemented!() - } + /// TODO: Implement this function, with a call to the `log` function in the Soroban runtime. + fn print(&self, bin: &Binary, string: PointerValue, length: IntValue) {} /// Return success without any result fn return_empty_abi(&self, bin: &Binary) { @@ -209,7 +250,7 @@ impl<'a> TargetRuntime<'a> for SorobanTarget { /// Return failure without any result fn assert_failure(&self, bin: &Binary, data: PointerValue, length: IntValue) { - unimplemented!() + bin.builder.build_unreachable().unwrap(); } fn builtin_function( diff --git a/src/emit/storage.rs b/src/emit/storage.rs index 52b0248e6..db61956e6 100644 --- a/src/emit/storage.rs +++ b/src/emit/storage.rs @@ -5,7 +5,7 @@ use crate::sema::ast::{Namespace, Type}; use inkwell::types::BasicTypeEnum; use inkwell::values::{ArrayValue, BasicValueEnum, FunctionValue, IntValue, PointerValue}; -/// This trait species the methods for managing storage on slot based environments +/// This trait specifies the methods for managing storage on slot based environments pub(super) trait StorageSlot { fn set_storage( &self, diff --git a/src/linker/soroban_wasm.rs b/src/linker/soroban_wasm.rs index cfa79ec91..0a6be8bc6 100644 --- a/src/linker/soroban_wasm.rs +++ b/src/linker/soroban_wasm.rs @@ -5,6 +5,14 @@ use std::fs::File; use std::io::Read; use std::io::Write; use tempfile::tempdir; +use wasm_encoder::{ + ConstExpr, EntityType, GlobalSection, GlobalType, ImportSection, MemoryType, Module, + RawSection, ValType, +}; +use wasmparser::{Global, Import, Parser, Payload::*, SectionLimited, TypeRef}; + +use crate::emit::soroban::GET_CONTRACT_DATA; +use crate::emit::soroban::PUT_CONTRACT_DATA; pub fn link(input: &[u8], name: &str) -> Vec { let dir = tempdir().expect("failed to create temp directory for linking"); @@ -50,5 +58,62 @@ pub fn link(input: &[u8], name: &str) -> Vec { .read_to_end(&mut output) .expect("failed to read output file"); - output + //output + generate_module(&output) +} + +fn generate_module(input: &[u8]) -> Vec { + let mut module = Module::new(); + for payload in Parser::new(0).parse_all(input).map(|s| s.unwrap()) { + match payload { + ImportSection(s) => generate_import_section(s, &mut module), + GlobalSection(s) => generate_global_section(s, &mut module), + ModuleSection { .. } | ComponentSection { .. } => panic!("nested WASM module"), + _ => { + if let Some((id, range)) = payload.as_section() { + module.section(&RawSection { + id, + data: &input[range], + }); + } + } + } + } + module.finish() +} + +/// Resolve all pallet 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()) { + let import_type = match import.ty { + TypeRef::Func(n) => EntityType::Function(n), + TypeRef::Memory(m) => EntityType::Memory(MemoryType { + maximum: m.maximum, + minimum: m.initial, + memory64: m.memory64, + shared: m.shared, + }), + _ => panic!("unexpected WASM import section {:?}", import), + }; + let module_name = match import.name { + GET_CONTRACT_DATA | PUT_CONTRACT_DATA => "l", + _ => panic!("got func {:?}", import), + }; + // parse the import name to all string after the the first dot + let import_name = import.name.split('.').nth(1).unwrap(); + imports.import(module_name, import_name, import_type); + } + module.section(&imports); +} + +/// Set the stack pointer to 64k (this is the only global) +fn generate_global_section(_section: SectionLimited, module: &mut Module) { + let mut globals = GlobalSection::new(); + let global_type = GlobalType { + val_type: ValType::I32, + mutable: true, + }; + globals.global(global_type, &ConstExpr::i32_const(1048576)); + module.section(&globals); } diff --git a/src/sema/contracts.rs b/src/sema/contracts.rs index 3025f3732..8c6f2b624 100644 --- a/src/sema/contracts.rs +++ b/src/sema/contracts.rs @@ -1,7 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 use super::{ - annotions_not_allowed, ast, + annotions_not_allowed, + ast::{self}, diagnostics::Diagnostics, expression::{compatible_mutability, ExprContext}, functions, statements, diff --git a/tests/soroban.rs b/tests/soroban.rs index 8086f0c6d..841a8dc8d 100644 --- a/tests/soroban.rs +++ b/tests/soroban.rs @@ -31,6 +31,7 @@ pub fn build_solidity(src: &str) -> SorobanEnv { log_prints: true, #[cfg(feature = "wasm_opt")] wasm_opt: Some(contract_build::OptimizationPasses::Z), + soroban_version: Some(85899345977), ..Default::default() }, std::vec!["unknown".to_string()], diff --git a/tests/soroban_testcases/math.rs b/tests/soroban_testcases/math.rs index db50d0b6e..00220937b 100644 --- a/tests/soroban_testcases/math.rs +++ b/tests/soroban_testcases/math.rs @@ -1,11 +1,11 @@ // SPDX-License-Identifier: Apache-2.0 use crate::build_solidity; -use soroban_sdk::Val; +use soroban_sdk::{IntoVal, Val}; #[test] fn math() { - let env = build_solidity( + let runtime = build_solidity( r#"contract math { function max(uint64 a, uint64 b) public returns (uint64) { if (a > b) { @@ -17,13 +17,14 @@ fn math() { }"#, ); - let addr = env.contracts.last().unwrap(); - let res = env.invoke_contract( - addr, - "max", - vec![*Val::from_u32(4).as_val(), *Val::from_u32(5).as_val()], - ); - assert!(Val::from_u32(5).as_val().shallow_eq(&res)) + let arg: Val = 5_u64.into_val(&runtime.env); + let arg2: Val = 4_u64.into_val(&runtime.env); + + let addr = runtime.contracts.last().unwrap(); + let res = runtime.invoke_contract(addr, "max", vec![arg, arg2]); + + let expected: Val = 5_u64.into_val(&runtime.env); + assert!(expected.shallow_eq(&res)); } #[test] @@ -58,21 +59,17 @@ fn math_same_name() { ); let addr = src.contracts.last().unwrap(); - let res = src.invoke_contract( - addr, - "max_uint64_uint64", - vec![*Val::from_u32(4).as_val(), *Val::from_u32(5).as_val()], - ); - assert!(Val::from_u32(5).as_val().shallow_eq(&res)); - let res = src.invoke_contract( - addr, - "max_uint64_uint64_uint64", - vec![ - *Val::from_u32(4).as_val(), - *Val::from_u32(5).as_val(), - *Val::from_u32(6).as_val(), - ], - ); - assert!(Val::from_u32(6).as_val().shallow_eq(&res)); + let arg1: Val = 5_u64.into_val(&src.env); + let arg2: Val = 4_u64.into_val(&src.env); + let res = src.invoke_contract(addr, "max_uint64_uint64", vec![arg1, arg2]); + let expected: Val = 5_u64.into_val(&src.env); + assert!(expected.shallow_eq(&res)); + + let arg1: Val = 5_u64.into_val(&src.env); + let arg2: Val = 4_u64.into_val(&src.env); + let arg3: Val = 6_u64.into_val(&src.env); + let res = src.invoke_contract(addr, "max_uint64_uint64_uint64", vec![arg1, arg2, arg3]); + let expected: Val = 6_u64.into_val(&src.env); + assert!(expected.shallow_eq(&res)); } diff --git a/tests/soroban_testcases/mod.rs b/tests/soroban_testcases/mod.rs index 88e88a31e..abe0ca498 100644 --- a/tests/soroban_testcases/mod.rs +++ b/tests/soroban_testcases/mod.rs @@ -1,2 +1,3 @@ // SPDX-License-Identifier: Apache-2.0 mod math; +mod storage; diff --git a/tests/soroban_testcases/storage.rs b/tests/soroban_testcases/storage.rs new file mode 100644 index 000000000..d5923a946 --- /dev/null +++ b/tests/soroban_testcases/storage.rs @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 + +use crate::build_solidity; +use soroban_sdk::{IntoVal, Val}; + +#[test] +fn counter() { + let src = build_solidity( + r#"contract counter { + uint64 public count = 10; + + function increment() public returns (uint64){ + count += 1; + return count; + } + + function decrement() public returns (uint64){ + count -= 1; + return count; + } + }"#, + ); + + let addr = src.contracts.last().unwrap(); + + let _res = src.invoke_contract(addr, "init", vec![]); + + let res = src.invoke_contract(addr, "count", vec![]); + let expected: Val = 10_u64.into_val(&src.env); + assert!(expected.shallow_eq(&res)); + + src.invoke_contract(addr, "increment", vec![]); + let res = src.invoke_contract(addr, "count", vec![]); + let expected: Val = 11_u64.into_val(&src.env); + assert!(expected.shallow_eq(&res)); + + src.invoke_contract(addr, "decrement", vec![]); + let res = src.invoke_contract(addr, "count", vec![]); + let expected: Val = 10_u64.into_val(&src.env); + assert!(expected.shallow_eq(&res)); +} diff --git a/tests/undefined_variable_detection.rs b/tests/undefined_variable_detection.rs index 7fb23ea5f..0e745284e 100644 --- a/tests/undefined_variable_detection.rs +++ b/tests/undefined_variable_detection.rs @@ -27,6 +27,7 @@ fn parse_and_codegen(src: &'static str) -> Namespace { log_prints: true, #[cfg(feature = "wasm_opt")] wasm_opt: None, + soroban_version: None, }; codegen(&mut ns, &opt); From b00f6178c717bc2aa4d7c33b2ee2c9f5407f1b7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Szab=C3=B3?= Date: Tue, 25 Jun 2024 20:45:34 +0300 Subject: [PATCH 2/7] It is better to use the repository field in Cargo.toml (#1655) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To allow [Crates.io](https://crates.io/) , [lib.rs](https://lib.rs/) and the [Rust Digger](https://rust-digger.code-maven.com/) to link to it. See [the manifest](https://doc.rust-lang.org/cargo/reference/manifest.html#the-repository-field) for the explanation. Signed-off-by: Gábor Szabó --- Cargo.toml | 2 +- solang-parser/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2c10d3f47..534ab0ac5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "solang" version = "0.3.3" authors = ["Sean Young ", "Lucas Steuernagel ", "Cyrill Leutwiler "] -homepage = "https://github.com/hyperledger/solang" +repository = "https://github.com/hyperledger/solang" documentation = "https://solang.readthedocs.io/" license = "Apache-2.0" build = "build.rs" diff --git a/solang-parser/Cargo.toml b/solang-parser/Cargo.toml index 043739246..03c48d8d5 100644 --- a/solang-parser/Cargo.toml +++ b/solang-parser/Cargo.toml @@ -2,7 +2,7 @@ name = "solang-parser" version = "0.3.3" authors = ["Sean Young ", "Lucas Steuernagel ", "Cyrill Leutwiler "] -homepage = "https://github.com/hyperledger/solang" +repository = "https://github.com/hyperledger/solang" documentation = "https://solang.readthedocs.io/" license = "Apache-2.0" build = "build.rs" From d8217522c77f6b4b464f2124b6997e097da0a8c7 Mon Sep 17 00:00:00 2001 From: Sean Young Date: Thu, 27 Jun 2024 14:24:01 +0100 Subject: [PATCH 3/7] macOS 11 runners are being removed (#1657) See: https://github.blog/changelog/2024-05-20-actions-upcoming-changes-to-github-hosted-macos-runners/ We should try and build on the oldest supported platform, so that the binaries run on as many platforms as possible. Signed-off-by: Sean Young --- .github/workflows/release.yml | 5 +---- .github/workflows/test.yml | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 484629694..722ac9690 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -118,10 +118,7 @@ jobs: mac-intel: name: Mac Intel - # The Hyperledger self-hosted intel macs have the label macos-latest - # and run macos 12. We don't want to build llvm on macos 12, because - # then it can't be used on macos 11. - runs-on: macos-11 + runs-on: macos-12 steps: - name: Checkout sources uses: actions/checkout@v3 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 39f17f1e0..6c7fadd46 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -199,7 +199,7 @@ jobs: mac-intel: name: Mac Intel - runs-on: macos-11 + runs-on: macos-12 steps: - name: Checkout sources uses: actions/checkout@v3 @@ -223,7 +223,7 @@ jobs: mac-universal: name: Mac Universal Binary - runs-on: macos-11 + runs-on: macos-12 needs: [mac-arm, mac-intel] steps: - uses: actions/download-artifact@v3 From 06798cdeac6fd62ee98f5ae7da38f3af4933dc0f Mon Sep 17 00:00:00 2001 From: kyscott18 <43524469+kyscott18@users.noreply.github.com> Date: Mon, 1 Jul 2024 16:47:00 -0400 Subject: [PATCH 4/7] fix: shift right display macro (#1656) Signed-off-by: Kyle Scott --- solang-parser/src/lexer.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/solang-parser/src/lexer.rs b/solang-parser/src/lexer.rs index 73700cd7c..55795eea0 100644 --- a/solang-parser/src/lexer.rs +++ b/solang-parser/src/lexer.rs @@ -246,8 +246,8 @@ impl<'input> fmt::Display for Token<'input> { Token::CloseBracket => write!(f, "]"), Token::BitwiseNot => write!(f, "~"), Token::Question => write!(f, "?"), - Token::ShiftRightAssign => write!(f, "<<="), - Token::ShiftRight => write!(f, "<<"), + Token::ShiftRightAssign => write!(f, ">>="), + Token::ShiftRight => write!(f, ">>"), Token::Less => write!(f, "<"), Token::LessEqual => write!(f, "<="), Token::Bool => write!(f, "bool"), From 25f06afa247f6a6e1c9d9103dafa55d6885951b0 Mon Sep 17 00:00:00 2001 From: Sean Young Date: Wed, 21 Aug 2024 08:53:28 +0100 Subject: [PATCH 5/7] Appease rust 1.80 clippies (#1663) Signed-off-by: Sean Young --- src/codegen/polkadot.rs | 2 +- src/codegen/statements/mod.rs | 10 +++------- src/sema/ast.rs | 2 +- src/sema/external_functions.rs | 4 ++-- tests/solana.rs | 4 ++-- 5 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/codegen/polkadot.rs b/src/codegen/polkadot.rs index 511eec0c9..65871d260 100644 --- a/src/codegen/polkadot.rs +++ b/src/codegen/polkadot.rs @@ -98,7 +98,7 @@ impl RetCodeCheckBuilder { impl RetCodeCheck { /// Handles all cases from the [RetBlock] accordingly. - /// * On success, nothing is done and the execution continues at the success block. + /// On success, nothing is done and the execution continues at the success block. /// If the callee reverted and output was supplied, it will be bubble up. /// Otherwise, a revert without data will be inserted. pub(crate) fn handle_cases( diff --git a/src/codegen/statements/mod.rs b/src/codegen/statements/mod.rs index ef1172ed5..e8d3782e3 100644 --- a/src/codegen/statements/mod.rs +++ b/src/codegen/statements/mod.rs @@ -1208,9 +1208,7 @@ pub fn process_side_effects_expressions( } ast::Expression::Builtin { - kind: builtin_type, .. - } => match &builtin_type { - ast::Builtin::PayableSend + kind: ast::Builtin::PayableSend | ast::Builtin::ArrayPush | ast::Builtin::ArrayPop // PayableTransfer, Revert, Require and SelfDestruct do not occur inside an expression @@ -1229,12 +1227,10 @@ pub fn process_side_effects_expressions( | ast::Builtin::WriteUint64LE | ast::Builtin::WriteUint128LE | ast::Builtin::WriteUint256LE - | ast::Builtin::WriteAddress => { + | ast::Builtin::WriteAddress, .. + } => { let _ = expression(exp, ctx.cfg, ctx.contract_no, ctx.func, ctx.ns, ctx.vartab, ctx.opt); false - } - - _ => true, }, _ => true, diff --git a/src/sema/ast.rs b/src/sema/ast.rs index 9a66a385f..1cfdf3a56 100644 --- a/src/sema/ast.rs +++ b/src/sema/ast.rs @@ -1281,7 +1281,7 @@ pub struct CallArgs { /// This enum manages the accounts in an external call on Solana. There can be three options: /// 1. The developer explicitly specifies there are not accounts for the call (`NoAccount`). /// 2. The accounts call argument is absent, in which case we attempt to generate the AccountMetas -/// vector automatically (`AbsentArgumet`). +/// vector automatically (`AbsentArgumet`). /// 3. There are accounts specified in the accounts call argument (Present). #[derive(PartialEq, Eq, Clone, Debug, Default)] pub enum ExternalCallAccounts { diff --git a/src/sema/external_functions.rs b/src/sema/external_functions.rs index a8e1fdf5d..d46270ade 100644 --- a/src/sema/external_functions.rs +++ b/src/sema/external_functions.rs @@ -169,8 +169,8 @@ fn check_statement(stmt: &Statement, call_list: &mut CallList) -> bool { } } } - Statement::Return(_, exprs) => { - for e in exprs { + Statement::Return(_, expr) => { + if let Some(e) = expr { e.recurse(call_list, check_expression); } } diff --git a/tests/solana.rs b/tests/solana.rs index 512ab9a45..da054ede5 100644 --- a/tests/solana.rs +++ b/tests/solana.rs @@ -917,9 +917,9 @@ fn sol_log_data( result ); - print!(" {}", hex::encode(&event)); - events.push(event.to_vec()); + + print!(" {}", hex::encode(event)); } println!(); From df692d5bc92ae24e095add07cef5e2187a49d792 Mon Sep 17 00:00:00 2001 From: Sean Young Date: Wed, 4 Sep 2024 14:27:21 +0100 Subject: [PATCH 6/7] Update github actions to newer versions (#1666) Fixes https://github.com/hyperledger/solang/security/dependabot/5 Signed-off-by: Sean Young --- .github/workflows/release.yml | 12 ++--- .github/workflows/test.yml | 96 +++++++++++++++++------------------ 2 files changed, 54 insertions(+), 54 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 722ac9690..da4314230 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ jobs: container: ghcr.io/hyperledger/solang-llvm:ci-7 steps: - name: Checkout sources - uses: actions/checkout@v3.1.0 + uses: actions/checkout@v4 with: submodules: recursive - uses: dtolnay/rust-toolchain@1.74.0 @@ -34,7 +34,7 @@ jobs: if: ${{ github.repository_owner == 'hyperledger' }} steps: - name: Checkout sources - uses: actions/checkout@v3.1.0 + uses: actions/checkout@v4 with: submodules: recursive - name: Basic build tools @@ -65,7 +65,7 @@ jobs: runs-on: windows-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive - name: Download LLVM @@ -94,7 +94,7 @@ jobs: runs-on: macos-13-xlarge steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive - uses: dtolnay/rust-toolchain@1.74.0 @@ -121,7 +121,7 @@ jobs: runs-on: macos-12 steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive - uses: dtolnay/rust-toolchain@1.74.0 @@ -165,7 +165,7 @@ jobs: runs-on: solang-ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - run: | echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u $GITHUB_ACTOR --password-stdin docker buildx build . \ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6c7fadd46..253871cfb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,7 +15,7 @@ jobs: runs-on: solang-ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Run repolinter run: npx repolinter --rulesetUrl https://raw.githubusercontent.com/hyperledger-labs/hyperledger-community-management-tools/master/repo_structure/repolint.json - uses: enarx/spdx@master @@ -32,7 +32,7 @@ jobs: apt-get update apt-get install -y python3-pip git pkg-config libcairo-dev latexmk - name: Checkout sources - uses: actions/checkout@v3.1.0 + uses: actions/checkout@v4 with: # docs/conf.py uses `git describe --tags` which requires full history fetch-depth: 0 @@ -41,7 +41,7 @@ jobs: run: | # Without the --add safe.directory we get the following error: # fatal: detected dubious ownership in repository at '/__w/solang/solang' - # actions/checkout@v3.1.0 is supposed to fix this, but it does not work + # actions/checkout@v4 is supposed to fix this, but it does not work git config --global --add safe.directory "${GITHUB_WORKSPACE}" pip install -r requirements.txt make html epub @@ -57,7 +57,7 @@ jobs: CARGO_LLVM_COV_TARGET_DIR: ${{ github.workspace }}/target steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive - name: Install Rust @@ -91,7 +91,7 @@ jobs: if: always() run: cargo llvm-cov --all-features --workspace --no-report --jobs 2 - name: Upload binary - uses: actions/upload-artifact@v3.1.0 + uses: actions/upload-artifact@v4.4.0 with: name: solang-linux-x86-64 path: ./target/debug/solang @@ -102,7 +102,7 @@ jobs: tar -czvf rust-tests.tar.gz * working-directory: ./target - name: Upload test coverage files - uses: actions/upload-artifact@v3.1.0 + uses: actions/upload-artifact@v4.4.0 with: name: rust-tests.tar.gz path: ./target/rust-tests.tar.gz @@ -113,7 +113,7 @@ jobs: if: ${{ github.repository_owner == 'hyperledger' }} steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive - name: Basic build tools @@ -131,7 +131,7 @@ jobs: run: cargo build --verbose - name: Run tests run: cargo test --verbose --workspace - - uses: actions/upload-artifact@v3.1.0 + - uses: actions/upload-artifact@v4.4.0 with: name: solang-linux-arm64 path: ./target/debug/solang @@ -141,7 +141,7 @@ jobs: runs-on: windows-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive - name: Download LLVM @@ -163,7 +163,7 @@ jobs: run: cargo build --verbose - name: Run tests run: cargo test --verbose --workspace - - uses: actions/upload-artifact@v3.1.0 + - uses: actions/upload-artifact@v4.4.0 with: name: solang.exe path: C:/target/debug/solang.exe @@ -176,7 +176,7 @@ jobs: runs-on: macos-13-xlarge steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive - uses: dtolnay/rust-toolchain@1.74.0 @@ -192,7 +192,7 @@ jobs: run: cargo test --verbose --workspace - name: Run tests without wasm_opt run: cargo test --verbose --workspace --no-default-features --features language_server,llvm,soroban - - uses: actions/upload-artifact@v3.1.0 + - uses: actions/upload-artifact@v4.4.0 with: name: solang-mac-arm path: ./target/debug/solang @@ -202,7 +202,7 @@ jobs: runs-on: macos-12 steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive - uses: dtolnay/rust-toolchain@1.74.0 @@ -216,7 +216,7 @@ jobs: run: cargo build --verbose - name: Run tests run: cargo test --verbose --workspace - - uses: actions/upload-artifact@v3.1.0 + - uses: actions/upload-artifact@v4.4.0 with: name: solang-mac-intel path: ./target/debug/solang @@ -226,16 +226,16 @@ jobs: runs-on: macos-12 needs: [mac-arm, mac-intel] steps: - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4.1.8 with: name: solang-mac-intel - run: mv solang solang-mac-intel - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4.1.8 with: name: solang-mac-arm - run: mv solang solang-mac-arm - run: lipo -create -output solang-mac solang-mac-intel solang-mac-arm - - uses: actions/upload-artifact@v3.1.0 + - uses: actions/upload-artifact@v4.4.0 with: name: solang-mac path: solang-mac @@ -245,7 +245,7 @@ jobs: runs-on: solang-ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - run: echo "push=--push" >> $GITHUB_OUTPUT id: push if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} @@ -263,14 +263,14 @@ jobs: needs: linux-x86-64 steps: - name: Checkout sources - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: '16' - uses: dtolnay/rust-toolchain@1.74.0 - name: Setup yarn run: npm install -g yarn - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4.1.8 with: name: solang-linux-x86-64 path: bin @@ -302,7 +302,7 @@ jobs: run: anchor test --skip-local-validator working-directory: ./integration/anchor - name: Upload test coverage files - uses: actions/upload-artifact@v3.1.0 + uses: actions/upload-artifact@v4.4.0 with: name: anchor-tests path: ./target/*.profraw @@ -314,12 +314,12 @@ jobs: needs: linux-x86-64 steps: - name: Checkout sources - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: '16' - uses: dtolnay/rust-toolchain@1.74.0 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4.1.8 with: name: solang-linux-x86-64 path: bin @@ -344,7 +344,7 @@ jobs: run: npm run test working-directory: ./integration/soroban - name: Upload test coverage files - uses: actions/upload-artifact@v3.1.0 + uses: actions/upload-artifact@v4.4.0 with: name: soroban-tests path: ./target/*.profraw @@ -356,12 +356,12 @@ jobs: needs: linux-x86-64 steps: - name: Checkout sources - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: '16' - uses: dtolnay/rust-toolchain@1.74.0 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4.1.8 with: name: solang-linux-x86-64 path: bin @@ -382,7 +382,7 @@ jobs: run: npm run test working-directory: ./integration/solana - name: Upload test coverage files - uses: actions/upload-artifact@v3.1.0 + uses: actions/upload-artifact@v4.4.0 with: name: solana-tests path: ./target/*.profraw @@ -393,7 +393,7 @@ jobs: needs: linux-x86-64 steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive # We can't run substrate node as a github actions service, since it requires @@ -401,14 +401,14 @@ jobs: - name: Start substrate contracts node run: echo id=$(docker run -d -p 9944:9944 ghcr.io/hyperledger/solang-substrate-ci:62a8a6c substrate-contracts-node --dev --rpc-external -lwarn,runtime::contracts=trace) >> $GITHUB_OUTPUT id: substrate - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4.1.8 with: name: solang-linux-x86-64 path: bin - run: | chmod 755 ./bin/solang echo "$(pwd)/bin" >> $GITHUB_PATH - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: '16' - run: npm install @@ -423,7 +423,7 @@ jobs: run: npm run test working-directory: ./integration/polkadot - name: Upload test coverage files - uses: actions/upload-artifact@v3.1.0 + uses: actions/upload-artifact@v4.4.0 with: name: polkadot-tests path: ./target/*.profraw @@ -443,7 +443,7 @@ jobs: - name: Start substrate run: echo id=$(docker run -d -p 9944:9944 ghcr.io/hyperledger/solang-substrate-ci:62a8a6c substrate-contracts-node --dev --rpc-external -lwarn,runtime::contracts=trace) >> $GITHUB_OUTPUT id: substrate - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4.1.8 with: name: solang-linux-x86-64 path: bin @@ -463,7 +463,7 @@ jobs: if: always() run: docker kill ${{steps.substrate.outputs.id}} - name: Upload test coverage files - uses: actions/upload-artifact@v3.1.0 + uses: actions/upload-artifact@v4.4.0 with: name: polkadot-subxt-tests path: ./target/*.profraw @@ -474,8 +474,8 @@ jobs: needs: linux-x86-64 steps: - name: Checkout - uses: actions/checkout@v3 - - uses: actions/download-artifact@v3 + uses: actions/checkout@v4 + - uses: actions/download-artifact@v4.1.8 with: name: solang-linux-x86-64 path: bin @@ -483,7 +483,7 @@ jobs: chmod 755 ./bin/solang echo "$(pwd)/bin" >> $GITHUB_PATH - name: Install Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: '16' - run: npm install @@ -498,7 +498,7 @@ jobs: - run: vsce package working-directory: ./vscode - name: Upload test coverage files - uses: actions/upload-artifact@v3.1.0 + uses: actions/upload-artifact@v4.4.0 with: name: vscode-tests path: ./target/*.profraw @@ -508,7 +508,7 @@ jobs: runs-on: solang-ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Lints for stdlib run: | make lint @@ -531,7 +531,7 @@ jobs: CARGO_LLVM_COV_TARGET_DIR: ${{ github.workspace }}/target steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive - name: Install Rust @@ -542,32 +542,32 @@ jobs: - name: Install cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov - name: Download Rust coverage files - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4.1.8 with: name: rust-tests.tar.gz path: ./target - name: Download Solana coverage files - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4.1.8 with: name: solana-tests path: ./target - name: Download Polkadot coverage files - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4.1.8 with: name: polkadot-tests path: ./target - name: Download Polkadot subxt coverage files - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4.1.8 with: name: polkadot-subxt-tests path: ./target - name: Download Anchor coverage files - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4.1.8 with: name: anchor-tests path: ./target - name: Download VSCode coverage files - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4.1.8 with: name: vscode-tests path: ./target From 420fbb8924804183f219fe2c375da435dab049d0 Mon Sep 17 00:00:00 2001 From: salaheldinsoliman <49910731+salaheldinsoliman@users.noreply.github.com> Date: Thu, 12 Sep 2024 23:28:04 +0200 Subject: [PATCH 7/7] 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 --- integration/soroban/.gitignore | 2 + integration/soroban/runtime_error.sol | 9 +++ integration/soroban/test_helpers.js | 97 +++++++++++++++------------ src/codegen/dispatch/soroban.rs | 60 ++++++++++------- src/codegen/expression.rs | 18 ++++- src/emit/expression.rs | 28 +++++++- src/emit/soroban/mod.rs | 12 ++++ src/emit/soroban/target.rs | 64 +++++++++++++++++- src/lib.rs | 7 +- src/linker/soroban_wasm.rs | 6 +- src/sema/namespace.rs | 2 +- tests/soroban.rs | 23 ++++++- tests/soroban_testcases/mod.rs | 1 + tests/soroban_testcases/print.rs | 77 +++++++++++++++++++++ 14 files changed, 326 insertions(+), 80 deletions(-) create mode 100644 integration/soroban/runtime_error.sol create mode 100644 tests/soroban_testcases/print.rs diff --git a/integration/soroban/.gitignore b/integration/soroban/.gitignore index d33bf9529..ee0ea4517 100644 --- a/integration/soroban/.gitignore +++ b/integration/soroban/.gitignore @@ -6,3 +6,5 @@ !package.json node_modules package-lock.json +*.txt +*.toml diff --git a/integration/soroban/runtime_error.sol b/integration/soroban/runtime_error.sol new file mode 100644 index 000000000..6e6853062 --- /dev/null +++ b/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; + } +} \ No newline at end of file diff --git a/integration/soroban/test_helpers.js b/integration/soroban/test_helpers.js index ebafc8f81..1421a6add 100644 --- a/integration/soroban/test_helpers.js +++ b/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; -} \ No newline at end of file +} 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..6ed45ced0 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: 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 diff --git a/src/emit/expression.rs b/src/emit/expression.rs index 600cfee51..696ecc928 100644 --- a/src/emit/expression.rs +++ b/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 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..aac266aba 100644 --- a/src/lib.rs +++ b/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, } } 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/src/sema/namespace.rs b/src/sema/namespace.rs index 0b78408b3..d56577f30 100644 --- a/src/sema/namespace.rs +++ b/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 { 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..8dc919906 --- /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; + +#[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")); +}