Skip to content

Commit 420fbb8

Browse files
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 <[email protected]>
1 parent df692d5 commit 420fbb8

File tree

14 files changed

+326
-80
lines changed

14 files changed

+326
-80
lines changed

integration/soroban/.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@
66
!package.json
77
node_modules
88
package-lock.json
9+
*.txt
10+
*.toml

integration/soroban/runtime_error.sol

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
contract Error {
2+
uint64 count = 1;
3+
4+
/// @notice Calling this function twice will cause an overflow
5+
function decrement() public returns (uint64){
6+
count -= 1;
7+
return count;
8+
}
9+
}

integration/soroban/test_helpers.js

+54-43
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,64 @@
11
import * as StellarSdk from '@stellar/stellar-sdk';
22

3-
4-
53
export async function call_contract_function(method, server, keypair, contract) {
4+
let res = null;
65

7-
let res;
8-
let builtTransaction = new StellarSdk.TransactionBuilder(await server.getAccount(keypair.publicKey()), {
9-
fee: StellarSdk.BASE_FEE,
10-
networkPassphrase: StellarSdk.Networks.TESTNET,
11-
}).addOperation(contract.call(method)).setTimeout(30).build();
12-
13-
let preparedTransaction = await server.prepareTransaction(builtTransaction);
14-
15-
// Sign the transaction with the source account's keypair.
16-
preparedTransaction.sign(keypair);
17-
186
try {
19-
let sendResponse = await server.sendTransaction(preparedTransaction);
20-
if (sendResponse.status === "PENDING") {
21-
let getResponse = await server.getTransaction(sendResponse.hash);
22-
// Poll `getTransaction` until the status is not "NOT_FOUND"
23-
while (getResponse.status === "NOT_FOUND") {
24-
console.log("Waiting for transaction confirmation...");
25-
// See if the transaction is complete
26-
getResponse = await server.getTransaction(sendResponse.hash);
27-
// Wait one second
28-
await new Promise((resolve) => setTimeout(resolve, 1000));
29-
}
30-
31-
if (getResponse.status === "SUCCESS") {
32-
// Make sure the transaction's resultMetaXDR is not empty
33-
if (!getResponse.resultMetaXdr) {
34-
throw "Empty resultMetaXDR in getTransaction response";
35-
}
36-
// Find the return value from the contract and return it
37-
let transactionMeta = getResponse.resultMetaXdr;
38-
let returnValue = transactionMeta.v3().sorobanMeta().returnValue();
39-
console.log(`Transaction result: ${returnValue.value()}`);
40-
res = returnValue.value();
7+
let builtTransaction = new StellarSdk.TransactionBuilder(await server.getAccount(keypair.publicKey()), {
8+
fee: StellarSdk.BASE_FEE,
9+
networkPassphrase: StellarSdk.Networks.TESTNET,
10+
}).addOperation(contract.call(method)).setTimeout(30).build();
11+
12+
let preparedTransaction = await server.prepareTransaction(builtTransaction);
13+
14+
// Sign the transaction with the source account's keypair.
15+
preparedTransaction.sign(keypair);
16+
17+
let sendResponse = await server.sendTransaction(preparedTransaction);
18+
19+
if (sendResponse.status === "PENDING") {
20+
let getResponse = await server.getTransaction(sendResponse.hash);
21+
// Poll `getTransaction` until the status is not "NOT_FOUND"
22+
while (getResponse.status === "NOT_FOUND") {
23+
console.log("Waiting for transaction confirmation...");
24+
// Wait one second
25+
await new Promise((resolve) => setTimeout(resolve, 1000));
26+
// See if the transaction is complete
27+
getResponse = await server.getTransaction(sendResponse.hash);
28+
}
29+
30+
if (getResponse.status === "SUCCESS") {
31+
// Ensure the transaction's resultMetaXDR is not empty
32+
if (!getResponse.resultMetaXdr) {
33+
throw "Empty resultMetaXDR in getTransaction response";
34+
}
35+
// Extract and return the return value from the contract
36+
let transactionMeta = getResponse.resultMetaXdr;
37+
let returnValue = transactionMeta.v3().sorobanMeta().returnValue();
38+
console.log(`Transaction result: ${returnValue.value()}`);
39+
res = returnValue.value();
40+
} else {
41+
throw `Transaction failed: ${getResponse.resultXdr}`;
42+
}
43+
} else if (sendResponse.status === "FAILED") {
44+
// Handle expected failure and return the error message
45+
if (sendResponse.errorResultXdr) {
46+
const errorXdr = StellarSdk.xdr.TransactionResult.fromXDR(sendResponse.errorResultXdr, 'base64');
47+
const errorRes = errorXdr.result().results()[0].tr().invokeHostFunctionResult().code().value;
48+
console.log(`Transaction error: ${errorRes}`);
49+
res = errorRes;
50+
} else {
51+
throw "Transaction failed but no errorResultXdr found";
52+
}
4153
} else {
42-
throw `Transaction failed: ${getResponse.resultXdr}`;
54+
throw sendResponse.errorResultXdr;
4355
}
44-
} else {
45-
throw sendResponse.errorResultXdr;
46-
}
4756
} catch (err) {
48-
// Catch and report any errors we've thrown
49-
console.log("Sending transaction failed");
50-
console.log(err);
57+
// Return the error as a string instead of failing the test
58+
console.log("Transaction processing failed");
59+
console.log(err);
60+
res = err.toString();
5161
}
62+
5263
return res;
53-
}
64+
}

src/codegen/dispatch/soroban.rs

+36-24
Original file line numberDiff line numberDiff line change
@@ -102,35 +102,47 @@ pub fn function_dispatch(
102102

103103
wrapper_cfg.add(&mut vartab, placeholder);
104104

105-
// set the msb 8 bits of the return value to 6, the return value is 64 bits.
106-
// FIXME: this assumes that the solidity function always returns one value.
107-
let shifted = Expression::ShiftLeft {
108-
loc: pt::Loc::Codegen,
109-
ty: Type::Uint(64),
110-
left: value[0].clone().into(),
111-
right: Expression::NumberLiteral {
105+
// TODO: support multiple returns
106+
if value.len() == 1 {
107+
// set the msb 8 bits of the return value to 6, the return value is 64 bits.
108+
// FIXME: this assumes that the solidity function always returns one value.
109+
let shifted = Expression::ShiftLeft {
112110
loc: pt::Loc::Codegen,
113111
ty: Type::Uint(64),
114-
value: BigInt::from(8_u64),
115-
}
116-
.into(),
117-
};
112+
left: value[0].clone().into(),
113+
right: Expression::NumberLiteral {
114+
loc: pt::Loc::Codegen,
115+
ty: Type::Uint(64),
116+
value: BigInt::from(8_u64),
117+
}
118+
.into(),
119+
};
118120

119-
let tag = Expression::NumberLiteral {
120-
loc: pt::Loc::Codegen,
121-
ty: Type::Uint(64),
122-
value: BigInt::from(6_u64),
123-
};
121+
let tag = Expression::NumberLiteral {
122+
loc: pt::Loc::Codegen,
123+
ty: Type::Uint(64),
124+
value: BigInt::from(6_u64),
125+
};
124126

125-
let added = Expression::Add {
126-
loc: pt::Loc::Codegen,
127-
ty: Type::Uint(64),
128-
overflowing: false,
129-
left: shifted.into(),
130-
right: tag.into(),
131-
};
127+
let added = Expression::Add {
128+
loc: pt::Loc::Codegen,
129+
ty: Type::Uint(64),
130+
overflowing: false,
131+
left: shifted.into(),
132+
right: tag.into(),
133+
};
134+
135+
wrapper_cfg.add(&mut vartab, Instr::Return { value: vec![added] });
136+
} else {
137+
// Return 2 as numberliteral. 2 is the soroban Void type encoded.
138+
let two = Expression::NumberLiteral {
139+
loc: pt::Loc::Codegen,
140+
ty: Type::Uint(64),
141+
value: BigInt::from(2_u64),
142+
};
132143

133-
wrapper_cfg.add(&mut vartab, Instr::Return { value: vec![added] });
144+
wrapper_cfg.add(&mut vartab, Instr::Return { value: vec![two] });
145+
}
134146

135147
vartab.finalize(ns, &mut wrapper_cfg);
136148
cfg.public = false;

src/codegen/expression.rs

+17-1
Original file line numberDiff line numberDiff line change
@@ -939,7 +939,23 @@ pub fn expression(
939939
expr
940940
};
941941

942-
cfg.add(vartab, Instr::Print { expr: to_print });
942+
let res = if let Expression::AllocDynamicBytes {
943+
loc,
944+
ty,
945+
size: _,
946+
initializer: Some(initializer),
947+
} = &to_print
948+
{
949+
Expression::BytesLiteral {
950+
loc: *loc,
951+
ty: ty.clone(),
952+
value: initializer.to_vec(),
953+
}
954+
} else {
955+
to_print
956+
};
957+
958+
cfg.add(vartab, Instr::Print { expr: res });
943959
}
944960

945961
Expression::Poison

src/emit/expression.rs

+27-1
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,33 @@ pub(super) fn expression<'a, T: TargetRuntime<'a> + ?Sized>(
126126

127127
s.into()
128128
}
129-
Expression::BytesLiteral { value: bs, .. } => {
129+
Expression::BytesLiteral { value: bs, ty, .. } => {
130+
// If the type of a BytesLiteral is a String, embedd the bytes in the binary.
131+
if ty == &Type::String {
132+
let data = bin.emit_global_string("const_string", bs, true);
133+
134+
// A constant string, or array, is represented by a struct with two fields: a pointer to the data, and its length.
135+
let ty = bin.context.struct_type(
136+
&[
137+
bin.llvm_type(&Type::Bytes(bs.len() as u8), ns)
138+
.ptr_type(AddressSpace::default())
139+
.into(),
140+
bin.context.i64_type().into(),
141+
],
142+
false,
143+
);
144+
145+
return ty
146+
.const_named_struct(&[
147+
data.into(),
148+
bin.context
149+
.i64_type()
150+
.const_int(bs.len() as u64, false)
151+
.into(),
152+
])
153+
.into();
154+
}
155+
130156
let ty = bin.context.custom_width_int_type((bs.len() * 8) as u32);
131157

132158
// hex"11223344" should become i32 0x11223344

src/emit/soroban/mod.rs

+12
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use std::sync;
2222
const SOROBAN_ENV_INTERFACE_VERSION: u64 = 90194313216;
2323
pub const PUT_CONTRACT_DATA: &str = "l._";
2424
pub const GET_CONTRACT_DATA: &str = "l.1";
25+
pub const LOG_FROM_LINEAR_MEMORY: &str = "x._";
2526

2627
pub struct SorobanTarget;
2728

@@ -231,12 +232,23 @@ impl SorobanTarget {
231232
.i64_type()
232233
.fn_type(&[ty.into(), ty.into()], false);
233234

235+
let log_function_ty = binary
236+
.context
237+
.i64_type()
238+
.fn_type(&[ty.into(), ty.into(), ty.into(), ty.into()], false);
239+
234240
binary
235241
.module
236242
.add_function(PUT_CONTRACT_DATA, function_ty_1, Some(Linkage::External));
237243
binary
238244
.module
239245
.add_function(GET_CONTRACT_DATA, function_ty, Some(Linkage::External));
246+
247+
binary.module.add_function(
248+
LOG_FROM_LINEAR_MEMORY,
249+
log_function_ty,
250+
Some(Linkage::External),
251+
);
240252
}
241253

242254
fn emit_initializer(binary: &mut Binary, _ns: &ast::Namespace) {

src/emit/soroban/target.rs

+62-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
use crate::codegen::cfg::HashTy;
44
use crate::codegen::Expression;
55
use crate::emit::binary::Binary;
6-
use crate::emit::soroban::{SorobanTarget, GET_CONTRACT_DATA, PUT_CONTRACT_DATA};
6+
use crate::emit::soroban::{
7+
SorobanTarget, GET_CONTRACT_DATA, LOG_FROM_LINEAR_MEMORY, PUT_CONTRACT_DATA,
8+
};
79
use crate::emit::ContractArgs;
810
use crate::emit::{TargetRuntime, Variable};
911
use crate::emit_context;
@@ -236,7 +238,65 @@ impl<'a> TargetRuntime<'a> for SorobanTarget {
236238

237239
/// Prints a string
238240
/// TODO: Implement this function, with a call to the `log` function in the Soroban runtime.
239-
fn print(&self, bin: &Binary, string: PointerValue, length: IntValue) {}
241+
fn print(&self, bin: &Binary, string: PointerValue, length: IntValue) {
242+
if string.is_const() && length.is_const() {
243+
let msg_pos = bin
244+
.builder
245+
.build_ptr_to_int(string, bin.context.i64_type(), "msg_pos")
246+
.unwrap();
247+
let msg_pos = msg_pos.const_cast(bin.context.i64_type(), false);
248+
249+
let length = length.const_cast(bin.context.i64_type(), false);
250+
251+
let eight = bin.context.i64_type().const_int(8, false);
252+
let four = bin.context.i64_type().const_int(4, false);
253+
let zero = bin.context.i64_type().const_int(0, false);
254+
let thirty_two = bin.context.i64_type().const_int(32, false);
255+
256+
// XDR encode msg_pos and length
257+
let msg_pos_encoded = bin
258+
.builder
259+
.build_left_shift(msg_pos, thirty_two, "temp")
260+
.unwrap();
261+
let msg_pos_encoded = bin
262+
.builder
263+
.build_int_add(msg_pos_encoded, four, "msg_pos_encoded")
264+
.unwrap();
265+
266+
let length_encoded = bin
267+
.builder
268+
.build_left_shift(length, thirty_two, "temp")
269+
.unwrap();
270+
let length_encoded = bin
271+
.builder
272+
.build_int_add(length_encoded, four, "length_encoded")
273+
.unwrap();
274+
275+
let zero_encoded = bin.builder.build_left_shift(zero, eight, "temp").unwrap();
276+
277+
let eight_encoded = bin.builder.build_left_shift(eight, eight, "temp").unwrap();
278+
let eight_encoded = bin
279+
.builder
280+
.build_int_add(eight_encoded, four, "eight_encoded")
281+
.unwrap();
282+
283+
let call_res = bin
284+
.builder
285+
.build_call(
286+
bin.module.get_function(LOG_FROM_LINEAR_MEMORY).unwrap(),
287+
&[
288+
msg_pos_encoded.into(),
289+
length_encoded.into(),
290+
msg_pos_encoded.into(),
291+
four.into(),
292+
],
293+
"log",
294+
)
295+
.unwrap();
296+
} else {
297+
todo!("Dynamic String printing is not yet supported")
298+
}
299+
}
240300

241301
/// Return success without any result
242302
fn return_empty_abi(&self, bin: &Binary) {

src/lib.rs

+3-4
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,11 @@ impl Target {
9595

9696
/// Size of a pointer in bits
9797
pub fn ptr_size(&self) -> u16 {
98-
if *self == Target::Solana {
98+
match *self {
9999
// Solana is BPF, which is 64 bit
100-
64
101-
} else {
100+
Target::Solana => 64,
102101
// All others are WebAssembly in 32 bit mode
103-
32
102+
_ => 32,
104103
}
105104
}
106105

0 commit comments

Comments
 (0)