FPVM Backend
📖 Before reading this section of the book, it is advised to read the Fault Proof Program Environment section to familiarize yourself with the PreimageOracle IO pattern.
Kona is effectively split into two parts:
- OP Stack state transition logic (
kona-derive
,kona-executor
,kona-mpt
) - Fault Proof VM
IO and utilities
(
kona-common
,kona-common-proc
,kona-preimage
)
This section of the book focuses on the usage of kona-common
and kona-preimage
to facilitate host<->client
communication for programs running on top of the FPVM targets.
Host <-> Client Communication API
The FPVM system API is built on several layers. In this document, we'll cover these layers, from lowest-level to highest-level API.
kona-common
kona-common
implements raw syscall dispatch, a default global memory allocator, and a blocking async runtime.
kona-common
relies on a minimal linux backend to function, supporting only the syscalls required to implement the
PreimageOracle ABI (read
, write
, exit_group
).
These syscalls are exposed to the user through the io
module directly, with each supported platform implementing the
BasicKernelInterface
trait.
To directly dispatch these syscalls, the io
module
exposes a safe API:
use kona_common::{io, FileDescriptor};
// Print to `stdout`. Infallible, will panic if dispatch fails.
io::print("Hello, world!");
// Print to `stderr`. Infallible, will panic if dispatch fails.
io::print_err("Goodbye, world!");
// Read from or write to a specified file descriptor. Returns a result with the
// return value or syscall errno.
let _ = io::write(FileDescriptor::StdOut, "Hello, world!".as_bytes());
let mut buf = Vec::with_capacity(8);
let _ = io::read(FileDescriptor::StdIn, buf.as_mut_slice());
// Exit the program with a specified exit code.
io::exit(0);
With this library, you can implement a custom host<->client communication protocol, or extend the existing
PreimageOracle ABI. However, for most developers, we recommend sticking with kona-preimage
when developing programs that target the FPVMs, barring needs like printing directly to
stdout
.
kona-preimage
kona-preimage
is an implementation of the PreimageOracle ABI, built on top of kona-common
. This
crate enables synchronous communication between the host and client program, described in
Host <-> Client Communication in the FPP Dev environment section of the
book.
The crate is built around the PipeHandle
,
which serves as a single end of a bidirectional pipe (see: pipe
manpage).
Through this handle, the higher-level constructs can read and write data to the counterparty holding on to the other end of the pipe, following the protocol below:
sequenceDiagram Client->>+Host: Hint preimage (no-op on-chain / read-only mode) Host-->>-Client: Hint acknowledgement Client-->>+Host: Preimage Request Host-->>Host: Prepare Preimage Host-->>-Client: Preimage Data
The interfaces of each part of the above protocol are described by the following traits:
PreimageOracleClient
- To-spec implementation:
OracleReader
- To-spec implementation:
HintWriterClient
- To-spec implementation:
HintWriter
- To-spec implementation:
PreimageOracleServer
- To-spec implementation:
OracleServer
- To-spec implementation:
HintReaderServer
- To-spec implementation:
HintReader
- To-spec implementation:
Each of these traits, however, can be re-implemented to redefine the host<->client communication protocol if the needs of the consumer are not covered by the to-spec implementations.
kona-client
- Oracle-backed sources (example)
Finally, in kona-client
, implementations of data source traits from kona-derive
and kona-executor
are implemented
to pull in untyped data from the host by PreimageKey
. These data source traits are covered in more detail within
the Custom Backend section, but we'll quickly gloss over them here to build intuition.
Let's take, for example, OracleL1ChainProvider
.
The ChainProvider
trait in kona-derive
defines a simple interface for fetching information about the L1 chain. In the OracleL1ChainProvider
, this information
is pulled in over the PreimageOracle ABI. There are many other examples of these data source traits,
namely the L2ChainProvider
, BlobProvider
, TrieProvider
, and TrieHinter
, which enable the creation of different
data-source backends.
As an example, let's look at OracleL1ChainProvider::header_by_hash
, built on top of the CommsClient
trait, which
is a composition trait of the PreimageOracleClient + HintReaderServer
traits outlined above.
#[async_trait]
impl<T: CommsClient + Sync + Send> ChainProvider for OracleL1ChainProvider<T> {
type Error = anyhow::Error;
async fn header_by_hash(&mut self, hash: B256) -> Result<Header> {
// Send a hint for the block header.
self.oracle.write(&HintType::L1BlockHeader.encode_with(&[hash.as_ref()])).await?;
// Fetch the header RLP from the oracle.
let header_rlp =
self.oracle.get(PreimageKey::new(*hash, PreimageKeyType::Keccak256)).await?;
// Decode the header RLP into a Header.
Header::decode(&mut header_rlp.as_slice())
.map_err(|e| anyhow!("Failed to decode header RLP: {e}"))
}
// - snip -
}
In header_by_hash
, we use the inner HintWriter
to send a hint to the host to prepare the block hash preimage.
Then, once we've received an acknowledgement from the host that the preimage has been prepared, we reach out for
the RLP (which is the preimage of the hash). After the RLP is received, we decode the Header
type, and return
it to the user.