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: revmc-worker #73

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
457 changes: 382 additions & 75 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[workspace]
members = ["crates/*", "examples/*", "fuzz"]
default-members = ["crates/revmc-cli"]
default-members = ["crates/revmc-cli", "crates/revmc-worker"]
resolver = "2"

[workspace.package]
Expand Down Expand Up @@ -42,6 +42,7 @@ revmc-builtins = { version = "0.1.0", path = "crates/revmc-builtins", default-fe
revmc-context = { version = "0.1.0", path = "crates/revmc-context", default-features = false }
revmc-cranelift = { version = "0.1.0", path = "crates/revmc-cranelift", default-features = false }
revmc-llvm = { version = "0.1.0", path = "crates/revmc-llvm", default-features = false }
revmc-worker = { version = "0.1.0", path = "crates/revmc-worker", default-features = false }

alloy-primitives = { version = "0.8", default-features = false }
revm = { version = "19.0", default-features = false }
Expand Down
39 changes: 39 additions & 0 deletions crates/revmc-worker/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
[package]
name = "revmc-worker"
description = "EVM AOT Compile Worker for using machine code in hot contracts within a node runtime"

version.workspace = true
authors.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
categories.workspace = true
keywords.workspace = true
repository.workspace = true
exclude.workspace = true

[package.metadata.docs.rs]
no-default-features = true
rustdoc-args = ["--cfg", "docsrs"]

[lints]
workspace = true

[dependencies]

revmc.workspace = true
revm.workspace = true
revm-primitives.workspace = true

revmc-backend.workspace = true
revmc-llvm.workspace = true

tokio = { version = "1.41.1", features = ["full"] }
thiserror = "1.0.34"
tracing = { workspace = true, optional = true }
libloading = "0.8"
lru = "0.12.5"
rocksdb = { version = "0.23.0", features = ["multi-threaded-cf"] }

[build-dependencies]
revmc-build.workspace = true
3 changes: 3 additions & 0 deletions crates/revmc-worker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# revmc-worker

EVM aot compilation worker within node runtime implementation.
5 changes: 5 additions & 0 deletions crates/revmc-worker/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#![allow(missing_docs)]

fn main() {
revmc_build::emit();
}
4 changes: 4 additions & 0 deletions crates/revmc-worker/src/error/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#![allow(missing_docs)]
mod worker;

pub use worker::Error;
30 changes: 30 additions & 0 deletions crates/revmc-worker/src/error/worker.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use libloading::Error as LibLoadingError;
use rocksdb::Error as DbError;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum Error {
#[error("Database error: {0}")]
Database(#[from] DbError),

#[error("Backend init error, err: {err}")]
BackendInit { err: String },

#[error("File I/O error, err: {err}")]
FileIO { err: String },

#[error("Bytecode translation error, err: {err}")]
BytecodeTranslation { err: String },

#[error("Link error, err: {err}")]
Link { err: String },

#[error("Lib loading error: {0}")]
LibLoading(#[from] LibLoadingError),

#[error("Get symbol error: {err}")]
GetSymbol { err: String },

#[error("RwLock poison error: {err}")]
RwLockPoison { err: String },
}
113 changes: 113 additions & 0 deletions crates/revmc-worker/src/external.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
use std::{
fmt::{self, Debug},
num::NonZeroUsize,
sync::{Arc, RwLock, TryLockError},
};

use crate::{
error::Error,
module_name,
worker::{store_path, AotCompileWorkerPool, HotCodeCounter},
};
use libloading::{Library, Symbol};
use lru::LruCache;
use revm_primitives::{Bytes, SpecId, B256};
use revmc::EvmCompilerFn;

#[derive(PartialEq, Debug)]
pub enum FetchedFnResult {
Found(EvmCompilerFn),
NotFound,
}

impl Debug for EXTCompileWorker {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("EXTCompileWorker")
.field("worker_pool", &self.worker_pool)
.field("cache", &self.cache)
.finish()
}
}

#[derive(Debug)]
pub struct EvmCompilerFnTuple((EvmCompilerFn, Arc<Library>));
/// Compiler Worker as an external context.
///
/// External function fetching is optimized by using an LRU Cache.
/// In many cases, a contract that is called will likely be called again,
/// so the cache helps reduce library loading cost.
pub struct EXTCompileWorker {
worker_pool: AotCompileWorkerPool,
pub cache: RwLock<LruCache<B256, EvmCompilerFnTuple>>,
}

impl EXTCompileWorker {
pub fn new(
primary: bool,
threshold: u64,
worker_pool_size: usize,
cache_size_words: usize,
) -> Result<Self, Error> {
let hot_code_counter = HotCodeCounter::new(primary, worker_pool_size)?;
let worker_pool = AotCompileWorkerPool::new(threshold, hot_code_counter, worker_pool_size);

Ok(Self {
worker_pool,
cache: RwLock::new(LruCache::new(NonZeroUsize::new(cache_size_words).unwrap())),
})
}

/// Fetches the compiled function from disk, if exists
pub fn get_function(&self, code_hash: &B256) -> Result<FetchedFnResult, Error> {
if code_hash.is_zero() {
return Ok(FetchedFnResult::NotFound);
}
// Write locks are required for reading from LRU Cache
{
let mut acq = true;

let cache = match self.cache.try_write() {
Ok(c) => Some(c),
Err(err) => match err {
/* in this case, read from file instead of cache */
TryLockError::WouldBlock => {
acq = false;
None
}
TryLockError::Poisoned(err) => Some(err.into_inner()),
},
};

if acq {
if let Some(t) = cache.unwrap().get(code_hash) {
return Ok(FetchedFnResult::Found(t.0 .0));
}
}
}

let name = module_name();
let so = store_path().join(code_hash.to_string()).join("a.so");
if so.try_exists().unwrap_or(false) {
{
let lib = Arc::new((unsafe { Library::new(so) })?);
let f: Symbol<'_, revmc::EvmCompilerFn> = unsafe { lib.get(name.as_bytes())? };

let tuple = EvmCompilerFnTuple((*f, lib.clone()));
let mut cache = self
.cache
.write()
.map_err(|err| Error::RwLockPoison { err: err.to_string() })?;
cache.put(*code_hash, tuple);

return Ok(FetchedFnResult::Found(*f));
}
}
Ok(FetchedFnResult::NotFound)
}

/// Spwan AOT compile the byecode referred by code_hash
pub fn spwan(&self, spec_id: SpecId, code_hash: B256, bytecode: Bytes) -> Result<(), Error> {
self.worker_pool.spwan(spec_id, code_hash, bytecode)?;
Ok(())
}
}
56 changes: 56 additions & 0 deletions crates/revmc-worker/src/handler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use std::{
panic::{catch_unwind, AssertUnwindSafe},
sync::Arc,
};

use revm::{handler::register::EvmHandler, Database};

use crate::{EXTCompileWorker, FetchedFnResult};

// Register handler for external context to support background compile worker in node runtime
pub fn register_handler<DB: Database + 'static>(
handler: &mut EvmHandler<'_, Arc<EXTCompileWorker>, DB>,
) {
let prev = handler.execution.execute_frame.clone();
handler.execution.execute_frame = Arc::new(move |frame, memory, tables, context| {
let interpreter = frame.interpreter_mut();
let code_hash = interpreter.contract.hash.unwrap_or_default();

match context.external.get_function(&code_hash) {
Ok(FetchedFnResult::NotFound) => {
let spec_id = context.evm.inner.spec_id();
let bytecode = context.evm.db.code_by_hash(code_hash).unwrap_or_default();
let _res = context.external.spwan(spec_id, code_hash, bytecode.original_bytes());
#[cfg(feature = "tracing")]
if let Err(err) = _res {
tracing::error!("Worker failed: with bytecode hash {}: {:#?}", code_hash, err);
}

prev(frame, memory, tables, context)
}

Ok(FetchedFnResult::Found(f)) => {
let res = catch_unwind(AssertUnwindSafe(|| unsafe {
f.call_with_interpreter_and_memory(interpreter, memory, context)
}));

#[cfg(feature = "tracing")]
if let Err(err) = &res {
tracing::error!(
"AOT function call error: with bytecode hash {} {:#?}",
code_hash,
err
);
}

Ok(res.unwrap())
}

Err(_err) => {
#[cfg(feature = "tracing")]
tracing::error!("Error occurred in handler: {:?}", _err);
prev(frame, memory, tables, context)
}
}
});
}
15 changes: 15 additions & 0 deletions crates/revmc-worker/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//! EVM runtime AOT worker implementation.
//!
//! Jit

#![allow(missing_docs)]
pub mod error;
mod external;
mod handler;
#[cfg(test)]
mod tests;
mod worker;

pub use external::*;
pub use handler::*;
pub use worker::*;
3 changes: 3 additions & 0 deletions crates/revmc-worker/src/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#![allow(missing_docs)]
mod utils;
mod worker;
46 changes: 46 additions & 0 deletions crates/revmc-worker/src/tests/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use std::fs;

use crate::{db_path, sc_db_path, store_path};

pub(super) struct TestEnvGuard;

impl TestEnvGuard {
pub(super) fn new() -> Self {
let store = store_path();
let db = db_path();
let sc_db = sc_db_path();

if store.exists() {
let _ = fs::remove_dir_all(&store);
}

if db.exists() {
let _ = fs::remove_dir_all(&db);
}

if sc_db.exists() {
let _ = fs::remove_dir_all(&sc_db);
}
Self
}
}

impl Drop for TestEnvGuard {
fn drop(&mut self) {
let store = store_path();
let db = db_path();
let sc_db = sc_db_path();

if store.exists() {
let _ = fs::remove_dir_all(&store);
}

if db.exists() {
let _ = fs::remove_dir_all(&db);
}

if sc_db.exists() {
let _ = fs::remove_dir_all(&sc_db);
}
}
}
Loading
Loading