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] 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);