Skip to content

Commit

Permalink
docs: Document basic interactions with polkavm (#16)
Browse files Browse the repository at this point in the history
* docs: Document how to run poc and make clippy happy

* docs: add reference footnotes

* docs:  pass bytes between host and guest

* docs: add some links

* docs: update
  • Loading branch information
indirection42 authored May 13, 2024
1 parent dc57274 commit 6934c64
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 20 deletions.
71 changes: 64 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,70 @@
# XCQ

Cross-Consensus Query Language for Polkadot

## Setup
## Getting Started

### Prerequites

- Install [Rust toolchain targeting RISC-V RV32E](https://github.com/paritytech/rustc-rv32e-toolchain)
- Install [bun](https://bun.sh) (or npm or yarn) to run [Chopsticks](https://github.com/AcalaNetwork/chopsticks)
- Install [jq](https://stedolan.github.io/jq/)

### Run PoC

1. Install polkatool[^1](for relinking to .polkavm blob from a standard RV32E ELF) and chain-spec-builder[^2](for building chainspec from a wasm): `make tools`
2. Build a PolkaVM guest program[^1]: `make poc-guest`
3. Run a PoC and expected runtime structure:
- Run a simple host program which executes guest program (with trace turned on): `make poc-host`
- Run a runtime with `execute_query` api which executes guest program bytes via [chopsticks](https://github.com/AcalaNetwork/chopsticks): `make run`

## Explainations

### How guest program communicate with host?

Polkavm adopts a similar approach for guest accessing host functions to WASM.[^3]
In guest program, the host functions declarations are annotated with polkavm's proc-marco [`polkavm_import`](https://docs.rs/polkavm-derive/latest/polkavm_derive/attr.polkavm_import.html).
The definitions of guest functions are annotated with [`polkavm_export`](https://docs.rs/polkavm-derive/latest/polkavm_derive/attr.polkavm_export.html).
In host program, we register host functions through [`linker.func_wrap`](https://docs.rs/polkavm/latest/polkavm/struct.Linker.html#method.func_wrap)
Due to the limit of ABI, the signature of the those functions are limited to some primitive numeric types like `u32`, `i32`, `u64`(represented by two `u32` register).

### How to pass bytes from host to guest and vice versa?

In general, we can pass bytes between host and guest via guest's stack or heap. [^4][^5] The stack size of a guest program is 64KB, and the heap size is less than 4GB.

- If we need some space on the stack, it's easy for guest to define local variables on stack, and then pass pointer to host to have the host write data to it. However, it's not trivial to let host write data directly on the guest's stack without the guest's "guidance" because data written to an improper address might be overwritten later.

- If we need some space on the heap, Polkavm provides a dynamic allocation function both in host and guest through [`polkavm::Instance::sbrk`](https://docs.rs/polkavm/latest/polkavm/struct.Instance.html#method.sbrk) and [`polkavm_derive::sbrk`](https://docs.rs/polkavm-derive/latest/polkavm_derive/fn.sbrk.html) respectively.

According to the PolkaVM's doc[^6], memory allocated through `sbrk` can only be freed once the program finishes execution and its whole memory is cleared.

Note: Including a global allocator in guest will cause the guest program bloats, which is unacceptable because we need keep the guest program as small as possible to store it on chain compactly.

Specific Usages in Details:

- Pass arguements (at the entrypoint of the host function):
Currently we only support passing argumensts via heap memory.
Before calling guest function, host calls `sbrk` and [`polkavm::Instance::write_memory`](https://docs.rs/polkavm/latest/polkavm/struct.Instance.html#method.write_memory) to allocate and write memory, then pass ptr as argument to guest via [`polkavm::Instance::call_typed`](https://docs.rs/polkavm/latest/polkavm/struct.Instance.html#method.call_typed).

- Return value from guest to host (at the end of the host function):
In this case, it's viable to put the returned value on stack or heap. We recommend put the data on stack to prevent unnecessary memory allocation. The guest will return a `u64` which has the higher 32 bits as ptr and lower 32 bits as size due the limit of the ABI, and then have the host [`read_memory_into_vec`](https://docs.rs/polkavm/latest/polkavm/struct.Instance.html#method.read_memory_into_vec) to get the result.

- Call host function from guest, pass some data and get back some data (during the execution of the host function):
We construct arguments and returned values on the stack, then we pass the address of them to host to have the host read, process input and write output to the given address.

### How to pass non-primitive data types between guest and host?

Basically, if a data type contains no objects on the heap, then byte-to-byte copy is enough, and both guest and host should have the same layout of the type to interpret data correctly.

## References

- Install rv32e toolchain: https://github.com/paritytech/rustc-rv32e-toolchain
- Install tools: `make tools`
- Install bun (or npm or yarn) to run Chopsticks: https://bun.sh
- Install jq: https://stedolan.github.io/jq/
[PolkaVm](https://github.com/koute/polkavm) is a general purpose user-level RISC-V based virtual machine.

## Build
For more details, please refer to [PolkaVM Announcement on Polkadot Forum](https://forum.polkadot.network/t/announcing-polkavm-a-new-risc-v-based-vm-for-smart-contracts-and-possibly-more)

- Build guest: `make poc-guest`
[^1]: https://forum.polkadot.network/t/announcing-polkavm-a-new-risc-v-based-vm-for-smart-contracts-and-possibly-more/3811#the-compilation-pipeline-7 "The compilation pipeline"
[^2]: https://github.com/paritytech/polkadot-sdk/tree/master/substrate/bin/utils/chain-spec-builder "chain-spec-builder"
[^3]: https://forum.polkadot.network/t/announcing-polkavm-a-new-risc-v-based-vm-for-smart-contracts-and-possibly-more/3811#wasm-like-import-export-model-6 "WASM-like import-export model"
[^4]: https://forum.polkadot.network/t/announcing-polkavm-a-new-risc-v-based-vm-for-smart-contracts-and-possibly-more/3811#security-and-sandboxing-4 "Security and sandboxing"
[^5]: https://forum.polkadot.network/t/announcing-polkavm-a-new-risc-v-based-vm-for-smart-contracts-and-possibly-more/3811#guest-program-memory-map-13 "Guest program memory map"
[^6]: https://docs.rs/polkavm-derive/latest/polkavm_derive/fn.sbrk.html "polkavm_derive::sbrk"
8 changes: 4 additions & 4 deletions poc/executor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

extern crate alloc;

pub use polkavm::{Config, Engine, Linker, Module, ProgramBlob};
pub use alloc::vec::Vec;
pub use polkavm::{Config, Engine, Linker, Module, ProgramBlob};

pub trait XcqExecutorContext {
fn register_host_functions<T>(&mut self, linker: &mut Linker<T>);
Expand Down Expand Up @@ -53,15 +53,15 @@ impl<Ctx: XcqExecutorContext> XcqExecutor<Ctx> {
}

pub fn execute(&mut self, raw_blob: &[u8], input: &[u8]) -> Result<Vec<u8>, XcqExecutorError> {
let blob = ProgramBlob::parse(&raw_blob[..])?;
let blob = ProgramBlob::parse(raw_blob)?;
let module = Module::from_blob(&self.engine, &Default::default(), &blob)?;
let instance_pre = self.linker.instantiate_pre(&module)?;
let instance = instance_pre.instantiate()?;

let input_ptr = if input.len() > 0 {
let input_ptr = if !input.is_empty() {
let ptr = instance
.sbrk(input.len() as u32)?
.expect("sbrk must be able to allocate memoery here");
.expect("sbrk must be able to allocate memory here");
instance
.write_memory(ptr, input)
.map_err(|e| XcqExecutorError::ExecutionError(polkavm::ExecutionError::Trap(e)))?;
Expand Down
5 changes: 3 additions & 2 deletions poc/guest/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ extern "C" {
}

// return value is u64 instead of (u32, u32) due to https://github.com/koute/polkavm/issues/116
// higher 32bits are address, lower 32bits are size
#[polkavm_derive::polkavm_export]
extern "C" fn main(ptr: u32) -> u64 {
// ready first byte from ptr
Expand All @@ -29,8 +30,8 @@ extern "C" fn main(ptr: u32) -> u64 {
let val = b"test";
let val = Box::new(*val);
// leak val
let val = Box::into_raw(val);
(val as u32 as u64) << 32 | 4
let ptr = Box::into_raw(val);
(ptr as u32 as u64) << 32 | 4
}
1 => {
let val = unsafe { core::ptr::read_volatile((ptr + 1) as *const u8) };
Expand Down
16 changes: 9 additions & 7 deletions poc/host/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
use polkavm::{Config, Linker, Caller};
use polkavm::{Caller, Config, Linker};

struct HostFunctions;

impl poc_executor::XcqExecutorContext for HostFunctions {
fn register_host_functions<T>(&mut self, linker: &mut Linker<T>) {
linker.func_wrap("host_call", move |caller: Caller<_>, ptr: u32| -> u32 {
let mut data = [0u8];
let data = caller.read_memory_into_slice(ptr, &mut data).unwrap();
println!("host_call: {:?}", data);
return (data[0] + 1) as u32;
}).unwrap();
linker
.func_wrap("host_call", move |caller: Caller<_>, ptr: u32| -> u32 {
let mut data = [0u8];
let data = caller.read_memory_into_slice(ptr, &mut data).unwrap();
println!("host_call: {:?}", data);
(data[0] + 1) as u32
})
.unwrap();
}
}

Expand Down

0 comments on commit 6934c64

Please sign in to comment.