Skip to content

Commit

Permalink
Merge pull request #47 from athenavm/e2effitest
Browse files Browse the repository at this point in the history
E2E FFI recursive host test
  • Loading branch information
lrettig authored Jul 20, 2024
2 parents b584363 + 5a91fa7 commit 19f2ff6
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 85 deletions.
3 changes: 3 additions & 0 deletions ffi/athcon/bindings/rust/athcon-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ extern "C" {
fn athcon_create_athenavmwrapper() -> *mut ffi::athcon_vm;
}

// In principle it's safe to clone these handles, but the caller needs to be very careful to
// ensure the memory is freed properly, isn't double-freed, etc.
#[derive(Clone)]
pub struct AthconVm {
handle: *mut ffi::athcon_vm,
host_interface: *mut ffi::athcon_host_interface,
Expand Down
1 change: 1 addition & 0 deletions ffi/ffitest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ edition.workspace = true

[dev-dependencies]
athcon-client = { path = "../athcon/bindings/rust/athcon-client" }
athena-interface = { path = "../../interface" }
athcon-sys = { path = "../athcon/bindings/rust/athcon-sys" }
athena-vmlib = { path = "../vmlib" }
hex = "0.4"
87 changes: 65 additions & 22 deletions ffi/ffitest/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,20 @@
mod ffi_tests {
use std::collections::BTreeMap;

use athcon_client::create;
use athcon_client::host::HostContext as HostInterface;
use athcon_client::types::{
Address, Bytes, Bytes32, MessageKind, Revision, StatusCode, StorageStatus, ADDRESS_LENGTH,
BYTES32_LENGTH,
};
use athcon_client::{create, AthconVm};
use athcon_sys as ffi;
use athena_interface::ADDRESS_ALICE;
use athena_vmlib;

const CONTRACT_CODE: &[u8] =
include_bytes!("../../../tests/recursive_call/elf/recursive-call-test");
const EMPTY_ADDRESS: Address = [0u8; ADDRESS_LENGTH];

// Declare the external functions you want to test
extern "C" {
fn athcon_create_athenavmwrapper() -> *mut ffi::athcon_vm;
Expand All @@ -31,17 +36,20 @@ mod ffi_tests {

struct HostContext {
storage: BTreeMap<Bytes32, Bytes32>,
vm: AthconVm,
}

impl HostContext {
fn new() -> HostContext {
fn new(vm: AthconVm) -> HostContext {
HostContext {
storage: BTreeMap::new(),
vm,
}
}
}

// test all of the host functions the VM can handle
// An extremely simplistic host implementation. Note that we cannot use the MockHost
// from athena-interface because we need to work with FFI types here.
impl HostInterface for HostContext {
fn account_exists(&self, _addr: &Address) -> bool {
println!("Host: account_exists");
Expand Down Expand Up @@ -76,7 +84,7 @@ mod ffi_tests {
println!("Host: get_tx_context");
return (
[0u8; BYTES32_LENGTH],
[0u8; ADDRESS_LENGTH],
EMPTY_ADDRESS,
0,
0,
0,
Expand All @@ -91,21 +99,52 @@ mod ffi_tests {

fn call(
&mut self,
_kind: MessageKind,
_destination: &Address,
_sender: &Address,
_value: &Bytes32,
_input: &Bytes,
_gas: i64,
_depth: i32,
kind: MessageKind,
destination: &Address,
sender: &Address,
value: &Bytes32,
input: &Bytes,
gas: i64,
depth: i32,
) -> (Vec<u8>, i64, Address, StatusCode) {
println!("Host: call");
return (
vec![0u8; BYTES32_LENGTH],
_gas,
[0u8; ADDRESS_LENGTH],
StatusCode::ATHCON_SUCCESS,
// check depth
if depth > 10 {
return (
vec![0u8; BYTES32_LENGTH],
0,
EMPTY_ADDRESS,
StatusCode::ATHCON_CALL_DEPTH_EXCEEDED,
);
}

// we recognize one destination address
if destination != &ADDRESS_ALICE {
return (
vec![0u8; BYTES32_LENGTH],
0,
EMPTY_ADDRESS,
StatusCode::ATHCON_CONTRACT_VALIDATION_FAILURE,
);
}

// Create an owned copy of VM here to avoid borrow issues when passing self into execute
// Note: this clone duplicates the FFI handles, but we don't attempt to destroy them here.
// That'll be done using the original handles.
let vm = self.vm.clone();
let res = vm.execute(
self,
Revision::ATHCON_FRONTIER,
kind,
depth + 1,
gas,
destination,
sender,
input,
value,
CONTRACT_CODE,
);
return (res.0.to_vec(), res.1, EMPTY_ADDRESS, res.2);
}
}

Expand All @@ -123,26 +162,30 @@ mod ffi_tests {
/// it allows us to test talking to the VM via FFI, and that the host bindings work as expected.
#[test]
fn test_rust_host() {
let code = include_bytes!("../../../examples/hello_world/program/elf/hello-world-program");
let vm = create();
println!("Instantiate: {:?}", (vm.get_name(), vm.get_version()));
let mut host = HostContext::new();

// Same proviso as above: we're cloning the pointers here, which is fine as long as we
// don't attempt to destroy them twice, or use the clone after we destroy the original.
let mut host = HostContext::new(vm.clone());
let (output, gas_left, status_code) = vm.execute(
&mut host,
Revision::ATHCON_FRONTIER,
MessageKind::ATHCON_CALL,
123,
0,
50000000,
&[32u8; ADDRESS_LENGTH],
&ADDRESS_ALICE,
&[128u8; ADDRESS_LENGTH],
&[0u8; 0],
// the value 3 as little-endian u32
3u32.to_le_bytes().to_vec().as_slice(),
&[0u8; BYTES32_LENGTH],
&code[..],
CONTRACT_CODE,
);
println!("Output: {:?}", hex::encode(output));
println!("GasLeft: {:?}", gas_left);
println!("Status: {:?}", status_code);
assert_eq!(status_code, StatusCode::ATHCON_SUCCESS);
assert_eq!(output, 2u32.to_le_bytes().to_vec().as_slice());
vm.destroy();
}
}
154 changes: 91 additions & 63 deletions ffi/vmlib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,38 +168,62 @@ impl From<ffi::athcon_message> for AthenaMessageWrapper {
}
}

impl From<AthenaMessageWrapper> for ffi::athcon_message {
// probably not needed, but keeping it here for reference for now
// note: this code is memory safe, but would require manually freeing the input_data and code pointers.
// impl From<AthenaMessageWrapper> for ffi::athcon_message {
// fn from(item: AthenaMessageWrapper) -> Self {
// let (input_data, input_size) = if let Some(data) = item.0.input_data {
// // need to transfer ownership of the data to the FFI
// let boxed_data = data.into_boxed_slice();
// let data_len = boxed_data.len();
// let data_ptr = Box::into_raw(boxed_data) as *const u8;
// (data_ptr, data_len)
// } else {
// (std::ptr::null(), 0)
// };
// let boxed_code = item.0.code.into_boxed_slice();
// let code_size = boxed_code.len();
// let code_ptr = Box::into_raw(boxed_code) as *const u8;
// let kind = match item.0.kind {
// MessageKind::Call => ffi::athcon_call_kind::ATHCON_CALL,
// };
// let value: Bytes32AsU64 = item.0.value.into();
// ffi::athcon_message {
// kind,
// depth: item.0.depth as i32,
// gas: item.0.gas as i64,
// recipient: AddressWrapper(item.0.recipient).into(),
// sender: AddressWrapper(item.0.sender).into(),
// input_data,
// input_size,
// value: Bytes32Wrapper(value.into()).into(),
// code: code_ptr,
// code_size,
// }
// }
// }

impl From<AthenaMessageWrapper> for AthconExecutionMessage {
fn from(item: AthenaMessageWrapper) -> Self {
let (input_data, input_size) = if let Some(data) = item.0.input_data {
(data.as_ptr(), data.len())
} else {
(std::ptr::null(), 0)
};
let code = item.0.code.as_ptr();
let code_size = item.0.code.len();
let kind = match item.0.kind {
MessageKind::Call => ffi::athcon_call_kind::ATHCON_CALL,
};
let value: Bytes32AsU64 = item.0.value.into();
ffi::athcon_message {
let code = if item.0.code.len() > 0 {
Some(item.0.code.as_slice())
} else {
None
};
AthconExecutionMessage::new(
kind,
depth: item.0.depth as i32,
gas: item.0.gas as i64,
recipient: AddressWrapper(item.0.recipient).into(),
sender: AddressWrapper(item.0.sender).into(),
input_data,
input_size,
value: Bytes32Wrapper(value.into()).into(),
item.0.depth as i32,
item.0.gas as i64,
AddressWrapper(item.0.recipient).into(),
AddressWrapper(item.0.sender).into(),
item.0.input_data.as_deref(),
Bytes32Wrapper(value.into()).into(),
code,
code_size,
}
}
}

impl From<AthenaMessageWrapper> for AthconExecutionMessage {
fn from(item: AthenaMessageWrapper) -> Self {
// conversion is already implemented on the other side; utilize this
AthconExecutionMessage::from(&ffi::athcon_message::from(item))
)
}
}

Expand Down Expand Up @@ -311,41 +335,47 @@ impl From<AthconExecutionResult> for ExecutionResultWrapper {

impl From<ExecutionResultWrapper> for AthconExecutionResult {
fn from(wrapper: ExecutionResultWrapper) -> Self {
// use conversion implemented on the other side
ffi::athcon_result::from(wrapper).into()
AthconExecutionResult::new(
StatusCodeWrapper(wrapper.0.status_code).into(),
wrapper.0.gas_left as i64,
wrapper.0.output.as_deref(),
)
}
}

impl From<ExecutionResultWrapper> for ffi::athcon_result {
fn from(value: ExecutionResultWrapper) -> Self {
let output = value.0.output.unwrap_or_else(Vec::new);
let output_size = output.len();

// in order to ensure that a slice can be reconstructed from empty output,
// we need some trickery here. see std::slice::from_raw_parts for more details.
let output_data = if output_size > 0 {
output.as_ptr()
} else {
core::ptr::NonNull::<u8>::dangling().as_ptr()
};

let gas_left = value.0.gas_left as i64;
let create_address = value.0.create_address.map_or_else(
|| ffi::athcon_address::default(),
|address| AddressWrapper(address).into(),
);
let status_code = StatusCodeWrapper(value.0.status_code).into();
let release = None;
ffi::athcon_result {
output_data,
output_size,
gas_left,
create_address,
status_code,
release,
}
}
}
// probably not needed, but keeping it here for reference for now
// note: this code is NOT MEMORY SAFE. it assumes that output lives at least as long as the result.
// otherwise, output_data will be a dangling pointer.
// impl From<ExecutionResultWrapper> for ffi::athcon_result {
// fn from(value: ExecutionResultWrapper) -> Self {
// let output = value.0.output.unwrap_or_else(Vec::new);
// let output_size = output.len();

// // in order to ensure that a slice can be reconstructed from empty output,
// // we need some trickery here. see std::slice::from_raw_parts for more details.
// let output_data = if output_size > 0 {
// output.as_ptr()
// } else {
// core::ptr::NonNull::<u8>::dangling().as_ptr()
// };

// let gas_left = value.0.gas_left as i64;
// let create_address = value.0.create_address.map_or_else(
// || ffi::athcon_address::default(),
// |address| AddressWrapper(address).into(),
// );
// let status_code = StatusCodeWrapper(value.0.status_code).into();
// let release = None;
// ffi::athcon_result {
// output_data,
// output_size,
// gas_left,
// create_address,
// status_code,
// release,
// }
// }
// }

struct AthenaVmWrapper {
base: ffi::athcon_vm,
Expand Down Expand Up @@ -425,12 +455,10 @@ impl<'a> HostInterface for WrappedHostInterface<'a> {
Bytes32Wrapper::from(self.context.get_block_hash(number)).into()
}
fn call(&mut self, msg: AthenaMessage) -> ExecutionResult {
ExecutionResultWrapper::from(
self
.context
.call(&AthconExecutionMessage::from(AthenaMessageWrapper(msg))),
)
.into()
let execmsg = AthconExecutionMessage::from(AthenaMessageWrapper(msg));
let res = ExecutionResultWrapper::from(self.context.call(&execmsg));
// the execution message contains raw pointers that were passed over FFI and now need to be freed
res.into()
}
}

Expand Down

0 comments on commit 19f2ff6

Please sign in to comment.