Seismic is a privacy focused L1 that currently maintains a fork of revm.
We uphold privacy by changing storage values to be (uint256, bool) where the bool tracks whether the slot is shielded or not. We added CSTORE/CLOAD opcodes which are similar to SSTORE/SLOAD except that CSTORE flips the is_shielded bit to be true, and then SLOAD can't read it, and rpcs such as eth_getStorageAt and eth_getProof also zero out shielded slots.
Most of our fork diff is due to changing storage values across the entire stack, since revm/reth are not generic over the storageValue type.
I've been playing with this idea and have a PR showing how this could work in revm by using a StorageValueTr trait. Before I sink more time into it, and especially adapt reth to work with this, was wondering if the revm/reth maintainers would be open to merging this kind of change. I'm sure there are more use cases for this, but the main one I have in mind is ours, which can be thought of as somewhat analogical to a database row-level security.
Claude plan I'm working with for reth next steps in case this is useful
revm is well-architected for extensions now — Database, Host, InstructionProvider,
PrecompileProvider, and Handler are all properly generic/pluggable. Seismic slots in cleanly
at this layer.
reth is where the gaps are — FlaggedStorage is hardcoded in StateProvider, EvmStateProvider,
ExecutionOutcome, and the trie, rather than flowing generically from NodeTypes::Storage.
Making that single associated type thread through the entire reth stack would turn Seismic
from "a collection of fork patches" into "a clean instantiation of NodeTypes { Storage =
FlaggedStorage }."
Gaps -- Interfaces Needed for SDK-Like Seismic
Gap: StorageValue not threaded through reth
Where: reth/storage/storage-api/
Current State: StateProvider::storage() returns FlaggedStorage concretely
Ideal Interface: StateProvider<S: StorageValueTr> with fn storage() → Option<S>
Gap: NodeTypes::Storage is a marker
Where: reth/node/types/
Current State: Storage: Default + Send + Sync (no real constraints)
Ideal Interface: Storage: StorageValueTr threaded into StateProvider, ConfigureEvm, and EvmStateProvider
Gap: reth-revm bridge hardcodes FS
Where: reth/revm/database.rs
Current State: type StorageValue = FlaggedStorage
Ideal Interface: Generic: type StorageValue = <Node as NodeTypes>::Storage
────────────────────────────────────────
Gap: Trie assumes privacy flag
Where: seismic-trie/
Current State: LeafNode.is_private is structural
Ideal Interface: trait TrieStorageValue: StorageValueTr { fn metadata() → LeafMetadata }
────────────────────────────────────────
Gap: No TEE/KeyProvider interface
Where: reth/seismic/node/
Current State: purpose_keys: &'static GetPurposeKeysResponse passed ad-hoc
Ideal Interface: trait KeyProvider { fn tx_io_key(); fn rng_keypair(); } on ConfigureEvm or
NodeTypes
────────────────────────────────────────
Gap: RPC storage filtering is ad-hoc
Where: reth/rpc/
Current State: Seismic overrides storage_at() inline
Ideal Interface: trait StorageVisibility { fn filter(StorageValue) → RpcValue }
────────────────────────────────────────
Gap: ExecutionOutcome uses FS directly
Where: reth/evm/execution-types/
Current State: BundleStateInit has (FlaggedStorage, FlaggedStorage)
Ideal Interface: Generic over StorageValueTr via BundleState<SV>
Ideal SDK Architecture (if we could modify upstream)
┌─────────────────────────────────────────┐
│ NodeTypes (reth) │
│ ┌───────────────────────────────────┐ │
│ │ type Primitives: NodePrimitives │ │
│ │ type ChainSpec: EthChainSpec │ │
│ │ type Storage: StorageValueTr ◀────│──│── THIS IS THE KEY
│ │ type Payload: PayloadTypes │ │
│ │ type KeyProvider: KeyProviderTr │◀─│── NEW (for TEE)
│ └───────────────────────────────────┘ │
└───────┬─────────┬──────────┬────────────┘
│ │ │
┌─────────────┘ │ └──────────────┐
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌───────────────────┐
│ StateProvider<S> │ │ ConfigureEvm │ │ RpcConfig │
│ where │ │ where │ │ where │
│ S = N::Storage │ │ N::Storage used │ │ S = N::Storage │
│ │ │ in EvmFactory │ │ fn filter(S)→U256│
│ fn storage()→S │ │ and BlockExec │ │ │
└──────────────────┘ └──────────────────┘ └───────────────────┘
│ │ │
▼ ▼ │
┌──────────────────┐ ┌──────────────────┐ │
│ reth-revm bridge │ │ revm Database │ │
│ DatabaseRef { │ │ { type SV = S } │ │
│ type SV = S │ │ │ │
│ } │ │ Host { type SV=S }│ │
└──────────────────┘ └──────────────────┘ │
│ │ │
▼ ▼ │
┌──────────────────┐ ┌──────────────────┐ │
│ Trie<S> │ │ BundleState<S> │ │
│ where │ │ Account<S> │ │
│ S: TrieEncodable│ │ EvmStorage<S> │ │
│ + StorageValueTr│ │ ExecutionOutcome<S>│ │
└──────────────────┘ └──────────────────┘ │
│
For Ethereum: S = U256 (default, no privacy) │
For Seismic: S = FlaggedStorage (with privacy flag) │
For Future: S = EncryptedStorage? (FHE, MPC, etc.) │
The core insight: NodeTypes::Storage should be the single generic that threads through the
entire stack — from primitives through revm through reth storage through trie through RPC.
Today it's a marker type. If it were properly constrained to StorageValueTr and threaded
through StateProvider, EvmStateProvider, ExecutionOutcome, and Trie, then Seismic would be a
clean instantiation of NodeTypes { Storage = FlaggedStorage } rather than a collection of fork
patches.
The revm layer is already there (Database and Host are properly generic over StorageValue).
The gap is in reth, where FlaggedStorage is hardcoded in concrete types rather than flowing
from the NodeTypes generic.
Before I sink more time into this, and especially adapt reth to work with these generic types, was wondering if the revm/reth maintainers would be open to merging this kind of change upstream, which would unlock us to use revm and reth as an sdk instead of having to fork it.
Seismic is a privacy focused L1 that currently maintains a fork of revm.
We uphold privacy by changing storage values to be
(uint256, bool)where the bool tracks whether the slot is shielded or not. We added CSTORE/CLOAD opcodes which are similar to SSTORE/SLOAD except that CSTORE flips theis_shieldedbit to be true, and then SLOAD can't read it, and rpcs such aseth_getStorageAtandeth_getProofalso zero out shielded slots.Most of our fork diff is due to changing storage values across the entire stack, since revm/reth are not generic over the storageValue type.
I've been playing with this idea and have a PR showing how this could work in revm by using a StorageValueTr trait. Before I sink more time into it, and especially adapt reth to work with this, was wondering if the revm/reth maintainers would be open to merging this kind of change. I'm sure there are more use cases for this, but the main one I have in mind is ours, which can be thought of as somewhat analogical to a database row-level security.
Claude plan I'm working with for reth next steps in case this is useful
Before I sink more time into this, and especially adapt reth to work with these generic types, was wondering if the revm/reth maintainers would be open to merging this kind of change upstream, which would unlock us to use revm and reth as an sdk instead of having to fork it.