Skip to content

Commit

Permalink
Add print functionality to Soroban contracts (#1659)
Browse files Browse the repository at this point in the history
This PR adds static string print functionality to Soroban contracts.
This serves the following:

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

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

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

Signed-off-by: salaheldinsoliman <salaheldin_sameh@aucegypt.edu>
  • Loading branch information
salaheldinsoliman authored Sep 12, 2024
1 parent df692d5 commit 420fbb8
Show file tree
Hide file tree
Showing 14 changed files with 326 additions and 80 deletions.
2 changes: 2 additions & 0 deletions integration/soroban/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@
!package.json
node_modules
package-lock.json
*.txt
*.toml
9 changes: 9 additions & 0 deletions integration/soroban/runtime_error.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
97 changes: 54 additions & 43 deletions integration/soroban/test_helpers.js
Original file line number Diff line number Diff line change
@@ -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;
}
}
60 changes: 36 additions & 24 deletions src/codegen/dispatch/soroban.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
18 changes: 17 additions & 1 deletion src/codegen/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 27 additions & 1 deletion src/emit/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions src/emit/soroban/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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) {
Expand Down
64 changes: 62 additions & 2 deletions src/emit/soroban/target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
7 changes: 3 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

Expand Down
Loading

0 comments on commit 420fbb8

Please sign in to comment.