Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(preimage): OracleServer + HintReader #96

Merged
merged 2 commits into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading