Skip to content

Commit

Permalink
feat(preimage): OracleServer + HintReader (#96)
Browse files Browse the repository at this point in the history
* feat(preimage): `OracleServer` + `HintReader`

Adds the host end of the `PreimageOracle` ABI plumbing. This includes
two new traits:
* `PreimageOracleServer`
* `HintReaderServer`

as well as implementations of both of them that compliment the existing
client handles, the `OracleReader` and `HintWriter`.

* feat(preimage): tracing
  • Loading branch information
clabby authored Apr 8, 2024
1 parent 2dba60b commit 9f9a724
Show file tree
Hide file tree
Showing 9 changed files with 314 additions and 36 deletions.
18 changes: 18 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ exclude = ["**/target", "benches/", "tests"]

[workspace.dependencies]
anyhow = { version = "1.0.79", default-features = false }
tracing = "0.1.40"
tracing = { version = "0.1.40", default-features = false }
cfg-if = "1.0.0"

[profile.dev]
Expand Down
4 changes: 2 additions & 2 deletions crates/common/src/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ mod native_io {
.write(buf)
.map_err(|e| anyhow!("Error writing to buffer to file descriptor: {e}"))?;

// Reset the cursor back to 0 for the reader.
file.seek(SeekFrom::Start(0))
// Reset the cursor back to before the data we just wrote for the reader's consumption.
file.seek(SeekFrom::Current(-(buf.len() as i64)))
.map_err(|e| anyhow!("Failed to reset file cursor to 0: {e}"))?;

// forget the file descriptor so that the `Drop` impl doesn't close it.
Expand Down
4 changes: 4 additions & 0 deletions crates/preimage/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@ homepage.workspace = true
# workspace
anyhow.workspace = true
cfg-if.workspace = true
tracing.workspace = true

# local
kona-common = { path = "../common", version = "0.0.1" }

# External
alloy-primitives = { version = "0.7.0", default-features = false }

[dev-dependencies]
tokio = { version = "1.36.0", features = ["full"] }
tempfile = "3.10.0"
120 changes: 118 additions & 2 deletions crates/preimage/src/hint.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::{traits::HintWriterClient, PipeHandle};
use alloc::vec;
use crate::{traits::HintWriterClient, HintReaderServer, PipeHandle};
use alloc::{string::String, vec};
use anyhow::Result;
use tracing::{debug, error};

/// A [HintWriter] is a high-level interface to the hint pipe. It provides a way to write hints to
/// the host.
Expand All @@ -26,13 +27,128 @@ impl HintWriterClient for HintWriter {
hint_bytes[0..4].copy_from_slice(u32::to_be_bytes(hint.len() as u32).as_ref());
hint_bytes[4..].copy_from_slice(hint.as_bytes());

debug!(target: "hint_writer", "Writing hint \"{hint}\"");

// Write the hint to the host.
self.pipe_handle.write(&hint_bytes)?;

debug!(target: "hint_writer", "Successfully wrote hint");

// Read the hint acknowledgement from the host.
let mut hint_ack = [0u8; 1];
self.pipe_handle.read_exact(&mut hint_ack)?;

debug!(target: "hint_writer", "Received hint acknowledgement");

Ok(())
}
}

/// A [HintReader] is a router for hints sent by the [HintWriter] from the client program. It
/// provides a way for the host to prepare preimages for reading.
#[derive(Debug, Clone, Copy)]
pub struct HintReader {
pipe_handle: PipeHandle,
}

impl HintReader {
/// Create a new [HintReader] from a [PipeHandle].
pub fn new(pipe_handle: PipeHandle) -> Self {
Self { pipe_handle }
}
}

impl HintReaderServer for HintReader {
fn next_hint(&self, mut route_hint: impl FnMut(String) -> Result<()>) -> Result<()> {
// Read the length of the raw hint payload.
let mut len_buf = [0u8; 4];
self.pipe_handle.read_exact(&mut len_buf)?;
let len = u32::from_be_bytes(len_buf);

// Read the raw hint payload.
let mut raw_payload = vec![0u8; len as usize];
self.pipe_handle.read_exact(raw_payload.as_mut_slice())?;
let payload = String::from_utf8(raw_payload)
.map_err(|e| anyhow::anyhow!("Failed to decode hint payload: {e}"))?;

debug!(target: "hint_reader", "Successfully read hint: \"{payload}\"");

// Route the hint
if let Err(e) = route_hint(payload) {
// Write back on error to prevent blocking the client.
self.pipe_handle.write(&[0x00])?;

error!("Failed to route hint: {e}");
anyhow::bail!("Failed to rout hint: {e}");
}

// Write back an acknowledgement to the client to unblock their process.
self.pipe_handle.write(&[0x00])?;

debug!(target: "hint_reader", "Successfully routed and acknowledged hint");

Ok(())
}
}
#[cfg(test)]
mod test {
extern crate std;

use super::*;
use alloc::vec::Vec;
use kona_common::FileDescriptor;
use std::{fs::File, os::fd::AsRawFd};
use tempfile::tempfile;

/// Test struct containing the [HintReader] and [HintWriter]. The [File]s are stored in this
/// struct so that they are not dropped until the end of the test.
#[derive(Debug)]
struct ClientAndHost {
hint_writer: HintWriter,
hint_reader: HintReader,
_read_file: File,
_write_file: File,
}

/// Helper for creating a new [HintReader] and [HintWriter] for testing. The file channel is
/// over two temporary files.
fn client_and_host() -> ClientAndHost {
let (read_file, write_file) = (tempfile().unwrap(), tempfile().unwrap());
let (read_fd, write_fd) = (
FileDescriptor::Wildcard(read_file.as_raw_fd().try_into().unwrap()),
FileDescriptor::Wildcard(write_file.as_raw_fd().try_into().unwrap()),
);
let client_handle = PipeHandle::new(read_fd, write_fd);
let host_handle = PipeHandle::new(write_fd, read_fd);

let hint_writer = HintWriter::new(client_handle);
let hint_reader = HintReader::new(host_handle);

ClientAndHost { hint_writer, hint_reader, _read_file: read_file, _write_file: write_file }
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_hint_client_and_host() {
const MOCK_DATA: &str = "test-hint 0xfacade";

let sys = client_and_host();
let (hint_writer, hint_reader) = (sys.hint_writer, sys.hint_reader);

let client = tokio::task::spawn(async move { hint_writer.write(MOCK_DATA) });
let host = tokio::task::spawn(async move {
let mut v = Vec::new();
let route_hint = |hint: String| {
v.push(hint.clone());
Ok(())
};
hint_reader.next_hint(route_hint).unwrap();

assert_eq!(v.len(), 1);

v.remove(0)
});

let (_, h) = tokio::join!(client, host);
assert_eq!(h.unwrap(), MOCK_DATA);
}
}
37 changes: 35 additions & 2 deletions crates/preimage/src/key.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
//! Contains the [PreimageKey] type, which is used to identify preimages that may be fetched from
//! the preimage oracle.
use alloy_primitives::B256;

/// <https://specs.optimism.io/experimental/fault-proof/index.html#pre-image-key-types>
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[repr(u8)]
pub enum PreimageKeyType {
/// Local key types are local to a given instance of a fault-proof and context dependent.
Expand All @@ -23,6 +25,21 @@ pub enum PreimageKeyType {
Blob = 5,
}

impl TryFrom<u8> for PreimageKeyType {
type Error = anyhow::Error;

fn try_from(value: u8) -> Result<Self, Self::Error> {
Ok(match value {
1 => PreimageKeyType::Local,
2 => PreimageKeyType::Keccak256,
3 => PreimageKeyType::GlobalGeneric,
4 => PreimageKeyType::Sha256,
5 => PreimageKeyType::Blob,
_ => anyhow::bail!("Invalid preimage key type"),
})
}
}

/// A preimage key is a 32-byte value that identifies a preimage that may be fetched from the
/// oracle.
///
Expand All @@ -31,7 +48,7 @@ pub enum PreimageKeyType {
/// |---------|-------------|
/// | [0, 1) | Type byte |
/// | [1, 32) | Data |
#[derive(Debug, Default, Clone, Copy)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
pub struct PreimageKey {
data: [u8; 31],
key_type: PreimageKeyType,
Expand Down Expand Up @@ -69,6 +86,22 @@ impl From<PreimageKey> for [u8; 32] {
}
}

impl TryFrom<[u8; 32]> for PreimageKey {
type Error = anyhow::Error;

fn try_from(value: [u8; 32]) -> Result<Self, Self::Error> {
let key_type = PreimageKeyType::try_from(value[0])?;
Ok(Self::new(value, key_type))
}
}

impl core::fmt::Display for PreimageKey {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let raw: [u8; 32] = (*self).into();
write!(f, "{}", B256::from(raw))
}
}

#[cfg(test)]
mod test {
use super::*;
Expand Down
6 changes: 3 additions & 3 deletions crates/preimage/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ mod key;
pub use key::{PreimageKey, PreimageKeyType};

mod oracle;
pub use oracle::OracleReader;
pub use oracle::{OracleReader, OracleServer};

mod hint;
pub use hint::HintWriter;
pub use hint::{HintReader, HintWriter};

mod pipe;
pub use pipe::PipeHandle;

mod traits;
pub use traits::{HintWriterClient, PreimageOracleClient};
pub use traits::{HintReaderServer, HintWriterClient, PreimageOracleClient, PreimageOracleServer};
Loading

0 comments on commit 9f9a724

Please sign in to comment.