Lesson 1 — OpenHlEvmFactory — hooking into every EVM creation
Question
Reth creates a new EVM instance for every block. OpenHlEvmFactory is the hook — it lets you inject custom precompiles into every EVM creation. Build it as a ~115-line scaffold.
Principle (minimum model)
EvmFactorytrait. Reth's extension point:fn create_evm<DB: Database>(&self, db: DB, input: EvmEnv) -> Self::Evm<DB, NoOpInspector>. Implement once; Reth calls it everywhere.OpenHlEvmFactorystruct. A field-less unit struct (#[non_exhaustive]). It holds no CLOB or fill-sink references — the precompiles reach CLOB state through process-global storage that later lessons add.- Precompile injection. Inside
create_evm, build the EVM and call.with_precompiles(PrecompilesMap::from_static(precompiles_for(spec))). The set is the hardfork's standard precompiles extended with the custom CLOB ones. - No custom Database.
create_evmpasses Reth'sdbstraight through; the factory only changes the precompile set. - NodeBuilder integration.
OpenHlExecutorBuilderreturnsEthEvmConfig::new_with_evm_factory(ctx.chain_spec(), OpenHlEvmFactory); Lesson 3 plugs it in withEthereumNode::components().executor(OpenHlExecutorBuilder). - Tests. None in this lesson (
cargo checkonly). Lesson 3 adds the Reth dev-node launch test and the registry unit tests. - Why a factory, not a one-off injection. Every block creates a new EVM; the factory pattern means the injection is centralised + correct everywhere.
Worked example + steps
Lesson 1 — OpenHlEvmFactory — hooking into every EVM creation
Goal
Concepts you'll grasp in this lesson:
EvmFactory+ExecutorBuilderas Reth's "swap one slot" seam — every EVM that Reth constructs (payload build, block validation, eth_call RPC, debug RPC) goes through one factory, so registering custom precompiles once propagates everywhere.alloy-evm(abstract traits) vsreth-evm(concrete wiring) — why both deps are required: the trait layer expresses what an EVM is, the executor layer expresses how Reth runs it.- Per-spec
OnceLockcaching ofPrecompiles— building a precompile set is expensive (hashing addresses),create_evmis hot, so each hardfork tier's set is constructed once and shared as&'static. - Stub-with-stable-signature as an incremental-construction tactic — the passthrough
openhl_precompiles(base) -> Precompileslets the factory wire up now while Lesson 2 fills the body later, with no call-site rewrites.
Verification:
cargo check -p openhl-evm
…compiles cleanly.
Specific changes:
You'll have two new modules in crates/evm/src/:
openhl_evm.rs—OpenHlEvmFactory(Reth'sEvmFactoryslot) +OpenHlExecutorBuilder(Reth'sExecutorBuilderslot) + per-hardfork precompile dispatch viaOnceLock. About 115 lines (doc comments included).precompiles/mod.rs— a stubopenhl_precompiles(base) -> Precompilesthat passes through unchanged. Lesson 2 fills in the actual read precompile.
You also add 6 dependency lines (1 workspace-level alloy-evm; 5 in crates/evm [dependencies] — including 1 new git-pinned dep for reth-node-api).
After Lesson 1, the custom EVM structure exists end-to-end. Reth can construct EVM instances via our factory; the factory's job (register custom precompiles) just doesn't do anything yet because Lesson 2 is what defines those precompiles.
Recap
After the openhl CLOB course, crates/evm/src/ has:
crates/evm/src/
├── lib.rs
├── engine.rs consensus course: RethEvmBridge
├── in_memory.rs consensus course: InMemoryEvmBridge
├── reth_node.rs consensus course: bootstrap proof (tests only)
└── live_node.rs consensus + CLOB courses: LiveRethEvmBridge<P>
cargo test -p openhl-evm clob_fills_flow_into_payload --release passes. The bridge owns a CLOB and routes fills through build_payload. But smart contracts running inside the bridge's Reth node can't see the CLOB — that's the gap Lesson 1 starts closing.
Plan
Seven things:
- Add
alloy-evm = "0.34"to the workspaceCargo.toml. This is the publicalloy-evmcrate (not git-pinned to Reth) that providesEvmFactory,Database,EvmEnv, etc. - Add 5 deps to
crates/evm/Cargo.toml[dependencies]:reth-evm,reth-evm-ethereum,reth-node-api(new git dep — same SHA as the rest),reth-node-builder(until now only a dev-dep), andalloy-evm. - Create
crates/evm/src/openhl_evm.rswithOpenHlEvmFactory+OpenHlExecutorBuilder+precompiles_for(spec). - Create
crates/evm/src/precompiles/mod.rsas a passthrough stub. - Wire
pub mod openhl_evm; pub mod precompiles;intocrates/evm/src/lib.rs. - Re-export
OpenHlEvmFactoryandOpenHlExecutorBuilderat crate root for Lesson 3's NodeBuilder integration. cargo check -p openhl-evm— clean.
This is the dependency-heaviest lesson in this course. Once the scaffold compiles, Lesson 2 adds the actual precompile content; Lesson 3 wires it into NodeBuilder and tests that the precompile is reachable from EVM execution.
Walk-through
Step 1: Add alloy-evm to the workspace
Open the root Cargo.toml. The Reth block (after the consensus course's live-Reth boot lesson and the end of the CLOB course) ends with:
reth-primitives-traits = "0.3"
alloy-genesis = { version = "2.0", default-features = false }
Add one line right after alloy-genesis:
alloy-evm = { version = "0.34", default-features = false }
alloy-evm is the public alloy crate that provides REVM's abstractions at the trait level (EvmFactory, Database, EvmEnv). It's a stable crates.io dependency, not git-pinned to Reth — same status as alloy-genesis.
Step 2: Update crates/evm/Cargo.toml
Open crates/evm/Cargo.toml. After the CLOB course's bridge-fields lesson + the consensus course's live-bridge lesson, the [dependencies] section has 13 entries. We add 5 more:
[dependencies]
openhl-consensus = { workspace = true }
openhl-types = { workspace = true }
openhl-clob = { workspace = true }
async-trait = { workspace = true }
eyre = { workspace = true }
alloy-primitives = { workspace = true }
alloy-consensus = { workspace = true }
reth-ethereum-primitives = { workspace = true }
reth-storage-api = { workspace = true }
reth-consensus = { workspace = true }
reth-ethereum-consensus = { workspace = true }
reth-primitives-traits = { workspace = true }
reth-chainspec = { workspace = true }
reth-evm = { workspace = true } # NEW
reth-evm-ethereum = { workspace = true } # NEW
reth-node-api = { git = "https://github.com/paradigmxyz/reth", rev = "88505c7fcbfdebfd3b56d88c86b62e950043c6c4" } # NEW (1-off git dep)
reth-node-builder = { workspace = true } # NEW (also stays a dev-dep)
alloy-evm = { workspace = true } # NEW
reth-node-builder is added to [dependencies] — production code (OpenHlExecutorBuilder) now uses it. Leave the [dev-dependencies] entry in place: the reference keeps reth-node-builder = { workspace = true, features = ["test-utils"] } there, because the dev-node tests need the test-utils feature (testing_node). [dev-dependencies] is unchanged in this lesson.
reth-node-api is a one-off direct git dep (not via workspace). The workspace Cargo.toml doesn't declare it; we inline the git+rev directly. This is intentional: reth-node-api is used in exactly one crate (openhl-evm), and the rest of the workspace doesn't need it. Promoting it to a workspace dep would force every crate's build graph to know about it.
⚠️ The
revmust match the rest of thereth-*crates exactly. When you inlinerev = "88505c7...", the commit hash has to be identical to the one already pinned by the otherreth-*crates in the workspace. Reth shares types (FullNodeTypes,NodeTypes,BuilderContext, etc.) strictly across its internal crates, and Cargo's same-version-same-source rule means even a one-character mismatch causes those types to be treated as different identities — producing cryptic errors likeexpected ChainSpec, found ChainSpec. The temptation to "just bumpreth-node-apito a newer rev" is strong; resist it unless you're upgrading everyreth-*rev together. Operationally, confirm aftercargo check -p openhl-evmthat rootCargo.lockresolves one coherent graph. A stray rev here causes Cargo to resolve duplicate Reth trees, which increases build time and explodes into type-identity mismatches.
Step 3: Create crates/evm/src/precompiles/mod.rs (stub)
Before writing openhl_evm.rs, we need the precompile module to exist (because openhl_evm.rs imports from it). Create the directory + file:
mkdir -p crates/evm/src/precompiles
touch crates/evm/src/precompiles/mod.rs
Open crates/evm/src/precompiles/mod.rs and write:
//! Custom REVM precompiles that expose CLOB state to EVM execution.
//!
//! Stage 9a — scout commit. Lesson 2 adds the first real precompile
//! (`clob_read_best_bid` at 0x...0c1b) that returns a hardcoded best-bid
//! response so smart contracts can prove the precompile is reachable.
//! Lesson 4+ wires it to live CLOB state.
use alloy_evm::revm::precompile::Precompiles;
/// Wraps Reth's spec-default precompile set, adding openhl's CLOB precompiles.
///
/// Lesson 1 (this lesson): passthrough — clones the base unchanged.
/// Lesson 2: registers `clob_read_best_bid`.
/// Lesson 7+: registers `clob_place_order`.
#[must_use]
pub fn openhl_precompiles(base: &Precompiles) -> Precompiles {
// Lesson 2 will replace this with `let mut precompiles = base.clone();
// precompiles.extend([...]); precompiles`.
base.clone()
}
3 lines of body. The function takes a Precompiles set (Reth's default for the current hardfork) and returns it unchanged. Lesson 2 inserts the actual clob_read_best_bid between base and the return.
The function signature is the stable contract the EVM factory depends on. Lessons 2–11 will change the contents of this function, but openhl_precompiles(base: &Precompiles) -> Precompiles stays the same shape throughout.
Step 4: Create crates/evm/src/openhl_evm.rs
The main file. Top of the file:
//! `OpenHlEvmFactory` + `OpenHlExecutorBuilder` — Reth's `ConfigureEvm` slot,
//! filled with our custom-precompile EVM.
//!
//! Stage 9a (scout commit) — modelled on Reth's `examples/custom-evm/src/main.rs`
//! pattern. The factory's `create_evm` installs `openhl_precompiles(...)` so
//! any EVM execution path (RPC call, payload assembly, validation) sees the
//! CLOB precompile registered at `CLOB_READ_BEST_BID`.
use alloy_evm::{
eth::EthEvmContext,
precompiles::PrecompilesMap,
revm::{
context::{BlockEnv, Context, TxEnv},
context_interface::result::{EVMError, HaltReason},
handler::EthPrecompiles,
inspector::{Inspector, NoOpInspector},
interpreter::interpreter::EthInterpreter,
precompile::Precompiles,
primitives::hardfork::SpecId,
MainBuilder, MainContext,
},
Database, EvmEnv, EvmFactory,
};
use reth_chainspec::ChainSpec;
use reth_ethereum_primitives::EthPrimitives;
use reth_evm_ethereum::{EthEvm, EthEvmConfig};
use reth_node_api::{FullNodeTypes, NodeTypes};
use reth_node_builder::{components::ExecutorBuilder, BuilderContext};
use std::sync::OnceLock;
use crate::precompiles::openhl_precompiles;
20-ish imports. Most are REVM internals via alloy-evm's re-exports. Worth scanning, not memorizing:
EvmFactory— the trait we'll implement. Reth calls our factory'screate_evmwhenever it needs an EVM instance.ExecutorBuilder— the trait we'll implement forOpenHlExecutorBuilder. Reth'sNodeBuilderuses it to construct an EVM config.Precompiles— REVM's collection of precompiled contracts. We add to this.OnceLock— std's once-init primitive. We cache the per-spec precompile sets.
Now the factory struct:
/// EVM factory that registers openhl's custom precompiles on every EVM
/// instance Reth constructs (for payload assembly, block validation, RPC
/// state queries, etc.).
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct OpenHlEvmFactory;
impl EvmFactory for OpenHlEvmFactory {
type Evm<DB: Database, I: Inspector<EthEvmContext<DB>, EthInterpreter>> =
EthEvm<DB, I, Self::Precompiles>;
type Tx = TxEnv;
type Error<DBError: core::error::Error + Send + Sync + 'static> = EVMError<DBError>;
type HaltReason = HaltReason;
type Context<DB: Database> = EthEvmContext<DB>;
type Spec = SpecId;
type BlockEnv = BlockEnv;
type Precompiles = PrecompilesMap;
fn create_evm<DB: Database>(&self, db: DB, input: EvmEnv) -> Self::Evm<DB, NoOpInspector> {
let spec = input.cfg_env.spec;
let evm = Context::mainnet()
.with_db(db)
.with_cfg(input.cfg_env)
.with_block(input.block_env)
.build_mainnet_with_inspector(NoOpInspector {})
.with_precompiles(PrecompilesMap::from_static(precompiles_for(spec)));
EthEvm::new(evm, false)
}
fn create_evm_with_inspector<DB: Database, I: Inspector<Self::Context<DB>, EthInterpreter>>(
&self,
db: DB,
input: EvmEnv,
inspector: I,
) -> Self::Evm<DB, I> {
EthEvm::new(
self.create_evm(db, input).into_inner().with_inspector(inspector),
true,
)
}
}
The 8 associated types are scaffolding — every EvmFactory impl needs them, and most are the same as Reth's defaults. The interesting part is create_evm. Five steps:
Context::mainnet()— REVM's "Ethereum mainnet" preset (gas constants, etc.)..with_db(db)+.with_cfg(input.cfg_env)+.with_block(input.block_env)— install the database, config, and block env passed in..build_mainnet_with_inspector(NoOpInspector {})— construct the EVM with a no-op inspector (no tracing)..with_precompiles(PrecompilesMap::from_static(precompiles_for(spec)))— install our precompiles.precompiles_for(spec)returns the right precompile set for the current Ethereum hardfork.EthEvm::new(evm, false)— wrap in Reth's EthEvm type.
create_evm_with_inspector is the same path with a custom inspector instead of the no-op. Most callers use create_evm; the inspector variant is for debug RPC.
Step 5: Add the precompiles_for(spec) helper
Below the factory impl:
/// Lazily-initialised per-spec precompile sets. `OnceLock` ensures we build
/// each set once and share the static reference across every `create_evm` call,
/// matching the pattern in Reth's custom-evm example. Shanghai/Paris/London
/// don't add new precompiles, so they fall through to the Berlin set.
fn precompiles_for(spec: SpecId) -> &'static Precompiles {
static PRAGUE: OnceLock<Precompiles> = OnceLock::new();
static CANCUN: OnceLock<Precompiles> = OnceLock::new();
static FALLBACK: OnceLock<Precompiles> = OnceLock::new();
match spec {
SpecId::PRAGUE | SpecId::OSAKA => {
PRAGUE.get_or_init(|| openhl_precompiles(Precompiles::prague()))
}
SpecId::CANCUN => CANCUN.get_or_init(|| openhl_precompiles(Precompiles::cancun())),
// For older hardforks (Berlin/London/Paris/Shanghai), use the Berlin
// set as the most-recent-additions-cutoff base plus ours.
_ => FALLBACK.get_or_init(|| {
let base = EthPrecompiles::new(spec).precompiles;
openhl_precompiles(base)
}),
}
}
Each Ethereum hardfork has a different set of standard precompiles (ECDSA recovery, SHA-256, ModExp, EC-pairing, etc.). Cancun adds the point evaluation precompile for blobs. Prague will add yet more. Our wrapper openhl_precompiles injects our custom precompile(s) into whichever base set is active.
Three OnceLocks, one per hardfork tier:
PRAGUE— covers Prague + Osaka (Osaka inherits Prague's precompiles for now).CANCUN— covers Cancun.FALLBACK— Berlin/London/Paris/Shanghai, usingEthPrecompiles::new(spec)to get whatever set Reth thinks is right for that spec.
Why OnceLock instead of computing per call? Because Precompiles is a HashMap-backed structure that's expensive to construct (hashing every precompile address). Computing it once per spec + caching is one of the optimizations Reth's custom-evm example demonstrates. Caching matters because create_evm is called very frequently — every RPC eth_call, every block validation, every block build.
Step 6: Add the OpenHlExecutorBuilder
At the bottom of openhl_evm.rs:
/// Executor builder that swaps in `OpenHlEvmFactory` while keeping all other
/// Reth `EthereumNode` components at default.
#[derive(Debug, Default, Clone, Copy)]
#[non_exhaustive]
pub struct OpenHlExecutorBuilder;
impl<Node> ExecutorBuilder<Node> for OpenHlExecutorBuilder
where
Node: FullNodeTypes<Types: NodeTypes<ChainSpec = ChainSpec, Primitives = EthPrimitives>>,
{
type EVM = EthEvmConfig<ChainSpec, OpenHlEvmFactory>;
async fn build_evm(self, ctx: &BuilderContext<Node>) -> eyre::Result<Self::EVM> {
Ok(EthEvmConfig::new_with_evm_factory(
ctx.chain_spec(),
OpenHlEvmFactory,
))
}
}
10 lines. The ExecutorBuilder trait is Reth's hook for swapping the EVM config used by EthereumNode. The associated type EVM = EthEvmConfig<ChainSpec, OpenHlEvmFactory> says "use Reth's standard EthEvmConfig, but parameterize over our factory." build_evm constructs that config.
The trait bound Node: FullNodeTypes<Types: NodeTypes<ChainSpec = ChainSpec, Primitives = EthPrimitives>> constrains what kind of node this builder works with — Ethereum mainnet primitives, our ChainSpec. Anything more exotic (Optimism, OP Stack) wouldn't satisfy these bounds; that's by design.
#[non_exhaustive] on both structs lets us add fields later without a breaking API change. They're unit structs now; if openhl ever needs them to carry configuration, the attribute means consumers can't construct them via OpenHlExecutorBuilder {} literal.
Step 7: Wire into crates/evm/src/lib.rs
Open crates/evm/src/lib.rs. Currently it has the engine, in_memory, live_node and reth_node modules from the earlier openhl courses. Add three lines:
//! ... existing module doc ...
pub mod engine; // existing
pub mod in_memory; // existing
pub mod live_node; // existing (consensus course)
pub mod openhl_evm; // NEW
pub mod precompiles; // NEW
pub mod reth_node; // existing (its contents are tests only)
pub use engine::RethEvmBridge; // existing
pub use in_memory::InMemoryEvmBridge; // existing
pub use live_node::LiveRethEvmBridge; // existing
pub use openhl_evm::{OpenHlEvmFactory, OpenHlExecutorBuilder}; // NEW
Two changes:
pub mod openhl_evm— visible to consumers.pub mod precompiles— public, as in the reference. Smart contracts call precompiles by address, but the module also carries the address constants and (from Lesson 4) theinstall_clob/current_best_bidhelpers that bridge code and tests call.
The re-export at the bottom (pub use openhl_evm::{OpenHlEvmFactory, OpenHlExecutorBuilder}) makes the two types accessible as openhl_evm::OpenHlEvmFactory from consumer code. Lesson 3's NodeBuilder integration will use them.
Test
cargo check -p openhl-evm
First run is slow — alloy-evm + the new Reth crates pull in non-trivial code. Expect ~30-60 seconds. Subsequent runs use the cache.
Expected:
Compiling openhl-evm v0.1.0 (.../crates/evm)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 32.45s
No warnings (the import list is long but every item is used). No errors.
Regression check for existing tests — Lesson 1 doesn't add tests for the new modules yet, but you should confirm that the dep additions and the lib.rs edits haven't broken the 12 openhl-evm tests carried over from prior courses (the consensus course's bridges / live_node and the CLOB course's CLOB-bridge integration):
cargo test -p openhl-evm --release
12 tests should still pass. This isn't a test of Lesson 1's new logic — it's purely a regression check that the structural changes haven't broken existing behaviour. The first tests for the new modules land in Lesson 3 (reth_dev_node_with_openhl_executor and registered_precompile_is_invokable_via_registry). If you only want to confirm the structure compiles at Lesson 1, cargo check -p openhl-evm alone is enough.
Common errors and fixes:
error[E0432]: unresolved import 'reth_node_api'— the inline git dep wasn't added. Re-check Step 2.error[E0277]: 'EvmFactory' is not implemented for 'OpenHlEvmFactory'(some associated type) — typo in one of the 8 associated types. Compare against the reference at SHA1761d4d. Most common:type Spec = SpecIdvstype Spec = u64or similar.error[E0282]: type annotations needed for 'PrecompilesMap'—PrecompilesMap::from_staticreturns a generic; the call site needs to know the type. In our case, thewith_precompiles(...)call provides the inference. If the compiler complains, double-check the imports.unused import: 'openhl_precompiles'— the function is referenced inprecompiles_for's closures. If you see this warning, you may have writtenPrecompiles::prague()directly instead ofopenhl_precompiles(Precompiles::prague()). Wrap each base set inopenhl_precompiles(...).
Design reflection
Three load-bearing decisions encoded here:
-
The factory pattern matches Reth's "many EVM instances" reality. Reth doesn't construct one EVM and reuse it — it creates a fresh EVM for every RPC call, every block validation, every payload build. The
EvmFactorytrait is how we hook into "every EVM creation" in one place. One factory, many EVMs, consistent precompile registration everywhere. -
OnceLockper spec is the right caching shape. Building aPrecompilesset is non-trivial (hashing addresses, inserting fns). Doing it percreate_evmcall would burn cycles. Per-spec caching means each hardfork tier (Prague, Cancun, fallback) is constructed once. TheOnceLockensures thread-safe lazy init. -
The passthrough
openhl_precompilesstub keeps Lesson 1 isolated. The function exists with the right signature; it does nothing yet. Lesson 2 fills the body. A stub with the right signature is a contract: callers (the factory) can be wired now, and the implementation can land later without changing the call site. This is incremental construction with no rewrites required.
Answer key
cd ~/code/openhl-reference
git checkout 1761d4d
diff -u ~/code/my-openhl/crates/evm/src/openhl_evm.rs ./crates/evm/src/openhl_evm.rs
diff -u ~/code/my-openhl/crates/evm/Cargo.toml ./crates/evm/Cargo.toml
diff -u ~/code/my-openhl/Cargo.toml ./Cargo.toml
The reference at 1761d4d has the full precompiles/mod.rs (Stage 9a's read precompile). Your stubbed version differs:
diff -u ~/code/my-openhl/crates/evm/src/precompiles/mod.rs ./crates/evm/src/precompiles/mod.rs
# Expected: your stub is much shorter than the reference; you'll see the
# difference as a big addition in the reference. Lesson 2 adds the missing content.
openhl_evm.rs should match closely (the factory structure is identical; only doc-comment wording may differ).
Return:
git checkout main
Common questions
Q: Why are both pub mod openhl_evm and pub mod precompiles public?
OpenHlEvmFactory / OpenHlExecutorBuilder are the API consumers plug into NodeBuilder (Lesson 3). precompiles is public in the reference too: it carries the address constants (CLOB_READ_BEST_BID, later CLOB_PLACE_ORDER) and, from Lesson 4, install_clob / uninstall_clob / current_best_bid, which bridge code and tests call. The precompile functions themselves (read_best_bid, place_order) stay non-pub.
Q: What's the difference between PrecompilesMap::from_static and building a fresh set?
PrecompilesMap::from_static takes a &'static Precompiles reference — meaning the precompile set is one we've cached and reuse. Precompiles::default() would build a new (empty) instance. Our create_evm uses from_static because the OnceLock-cached set is 'static. Caching + static reference = no allocation per EVM creation.
Q: Why is PRAGUE covering OSAKA too?
Because the reference maps SpecId::OSAKA to the Prague set (Precompiles::prague()). If Osaka-specific precompiles need to be active, this match arm would split into separate OSAKA and PRAGUE branches.
Q: Does OpenHlExecutorBuilder need to be Clone?
The trait doesn't require Clone, but #[derive(Clone, Copy)] is cheap (it's a unit struct, zero-sized) and matches Reth's pattern. If you later add fields to the struct, you'd want to keep Clone for ergonomic API.
Next lesson (Lesson 2)
The factory is wired but the precompile module is a passthrough — Reth's standard precompiles are installed, no extra ones. Lesson 2 adds the first real precompile: clob_read_best_bid at address 0x...0c1b. It returns hardcoded values for now (the same approach as openhl Stage 9a). Lessons 4–5 will wire it to live CLOB state; this lesson just gets the function defined, registered, and reachable via the registry.
Summary (3 lines)
OpenHlEvmFactory= the hook for injecting custom precompiles into every Reth EVM creation (per-spec sets cached inOnceLock). ~115-line scaffold.OpenHlExecutorBuilderwraps it inEthEvmConfig::new_with_evm_factory; Lesson 3 plugs it in viaEthereumNode::components().executor(OpenHlExecutorBuilder).- The factory is a field-less unit struct;
openhl_precompiles(base: &Precompiles) -> Precompilesis the stable stub contract. Next: first real precompile.