feat(l1+evm+precompiles): add L1-backed zone db#698
Conversation
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
0394607 to
0c94b43
Compare
0c94b43 to
604908a
Compare
|
cyclops audit |
|
cc @0xrusowsky Cyclops audit event published. View workflow run Config: config: |
| fn execute_inner( | ||
| &mut self, | ||
| execute: impl FnOnce( | ||
| &mut TempoEvm<AnchoredZoneDb<DB, L1>, I>, | ||
| ) -> Result<TempoResult, AdaptedEvmError<DB::Error>>, | ||
| ) -> Result<TempoResult, ZoneEvmError<DB::Error>> { | ||
| let snapshot = self.anchor_controller().phase(); | ||
| let mut result = match execute(&mut self.inner) { | ||
| Ok(result) => result, | ||
| Err(error) => { | ||
| self.anchor_controller().restore(snapshot); | ||
| return Err(map_adapter_error(error)); | ||
| } | ||
| }; | ||
| if !result.result.is_success() { | ||
| self.anchor_controller().restore(snapshot); | ||
| return Ok(result); | ||
| } | ||
| if let Err(error) = self.inner.db().sanitize_state(&mut result.state) { | ||
| self.anchor_controller().restore(snapshot); | ||
| return Err(error.into_evm_error()); | ||
| } | ||
| Ok(result) | ||
| } | ||
| } |
There was a problem hiding this comment.
can we not avoid this logic? we always have advanceTempo as first tx in the block so i think entire block's execution would use the same L1 state always?
There was a problem hiding this comment.
simplified state machine logic b62c49f (this PR)
| if let Some(account) = state.get(&TIP403_REGISTRY_ADDRESS) { | ||
| if account.info != account.original_info() { | ||
| return l1_write(TIP403_REGISTRY_ADDRESS, U256::ZERO); | ||
| } | ||
| for (slot, value) in &account.storage { | ||
| if value.is_changed() { | ||
| return l1_write(TIP403_REGISTRY_ADDRESS, *slot); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
is this actually reachable given that we ban all mutating selectors? it would be kinda unfortunate if txs can execute all the way to here and only then get rejected
There was a problem hiding this comment.
should be unreachable yeah, i only added it cause it feels like an invariant worth enforcing?
There was a problem hiding this comment.
Yeah seems like a reasonable defense in depth.
| /// Restores execution-local L1 anchor state when a simulated transaction is not committed. | ||
| fn execute_transaction_with_commit_condition( | ||
| &mut self, | ||
| tx: impl ExecutableTx<Self>, | ||
| f: impl FnOnce(&Self::Result) -> CommitChanges, | ||
| ) -> Result<Option<GasOutput>, BlockExecutionError> { | ||
| let snapshot = self.evm().anchor_controller().phase(); | ||
| let output = self.execute_transaction_without_commit(tx)?; | ||
| if !f(&output).should_commit() { | ||
| self.evm().anchor_controller().restore(snapshot); | ||
| return Ok(None); | ||
| } | ||
| Ok(Some(self.commit_transaction(output))) | ||
| } |
There was a problem hiding this comment.
execute_transaction_without_commit should not advance any state either. it is expected that execute_transaction_without_commit might be called multiple times in a row with different transactions
83cb69c to
18a7ded
Compare
| /// Per-address mutation barriers. A value at V cannot serve a read at N when a barrier exists | ||
| /// in `(V, N]`. | ||
| invalidations: HashMap<Address, BTreeSet<u64>>, |
There was a problem hiding this comment.
can we just clear the invalidated addresses from slots mapping instead of tracking an extra map?
|
Hi @klkvr — your review approval was detected by Voight-Kampff but no live Voight-Kampff agent connection received it, so Voight-Kampff sent a push fallback. The push was not approved before the approval window closed. Your review did not count toward this PR. You can also try approving the PR directly via CLI with |
👁️ Cyclops Security Review
🧭 Auditing · mode=
Findings
⚙️ Controls
📜 33 events🔍 |
| .max_sync_attempts | ||
| .is_some_and(|max_attempts| attempt >= max_attempts) | ||
| { | ||
| return Err(eyre::eyre!( |
There was a problem hiding this comment.
should add metric for grafana?
| sync_attempts: u32, | ||
| ) -> Self { | ||
| assert!( | ||
| sync_attempts > 0, | ||
| "at least one synchronous attempt is required" | ||
| ); |
| fn execute_inner( | ||
| &mut self, | ||
| execute: impl FnOnce( |
There was a problem hiding this comment.
nit: can we add doc comments on this function?
| } | ||
|
|
||
| /// Clears database-adapter bookkeeping left by the current transaction attempt. | ||
| pub(crate) fn reset_transaction_state(&mut self) { |
There was a problem hiding this comment.
nit: consider renaming to clear_l1_anchor or similar for clarity as this is only clearing the db adapter state.
| L1: L1StorageReader, | ||
| I: Inspector<TempoCtx<AnchoredZoneDb<DB, L1>>>, | ||
| { | ||
| fn execute_inner( |
There was a problem hiding this comment.
nit: consider renaming to execute_and_sanitize or similar for clarity. Right now this is a bit generic.
| /// Per-address mutation barriers. A value at V cannot serve a read at N when a barrier exists | ||
| /// in `(V, N]`. | ||
| invalidations: HashMap<Address, BTreeSet<u64>>, |
| let policy_provider = PolicyProvider::new(self.policy_cache, policy_l1, runtime_handle); | ||
| evm_config = evm_config.with_policy_provider(policy_provider); | ||
| info!(target: "reth::cli", "Zone EVM initialized with TempoState + TIP-403 proxy precompiles"); | ||
| let evm_config = ZoneEvmConfig::new(ctx.chain_spec(), tempo_chain_spec, l1_provider); |
There was a problem hiding this comment.
Block execution now uses the anchored L1 overlay, but pool construction still replaces this with a plain TempoEvmConfig at line 1078. When anchored L1 policy differs from Zone-local state, eth_sendRawTransaction can reject a transaction with PolicyForbids even though block execution would accept it.
replaces #626
High-level goal
before this PR stack, Zones had two representations of Tempo L1 state:
PolicyCache/PolicyProvider, populated from typed TIP-403 and TIP-20 events with RPC fallback. CustomZoneTip20TokenandZoneTip403ProxyRegistryimplementations used it to reproduce upstream policy behavior.(address, slot), used byTempoStateand Zone system contracts for portal storage reads.this duplicated policy semantics and left two L1-state paths that had to remain aligned in block anchoring, event decoding, invalidation, RPC fallback, and upstream behavior. The goal is to remove the semantic policy implementation from the EVM path and execute upstream Tempo code against one factory-installed L1-aware database. The database wraps the normal caller-provided EVM database and:
TempoStatetransferPolicyIdfield in TIP-20’s packed slot, preserving Zone-local fieldsfrom upstream Tempo's perspective these remain ordinary EVM storage operations, but selected slots are virtualized to finalized L1 state. This makes upstream Tempo code the source of execution semantics rather than maintaining Zone-specific policy implementations or installing a separate storage provider around each precompile call.
EVM Database
database.rsaddsL1OverlayDB, a database adapter installed byZoneEvmFactoryfor every EVM created for payload building, canonical import, RPC calls, tracing, transaction-pool validation, and maintenance execution. The factory uses the alloy-evm database-context extension so the internal Tempo context can execute overAnchoredZoneDb<DB>whileZoneEvmcontinues to expose and return the original caller-providedDB.this keeps Reth’s state ownership and commit path unchanged. Reads pass through the adapter during execution, while successful state transitions are validated and sanitized before Reth commits them through the original database. Inner database failures preserve their original error type; anchor, L1-read, and mirrored-write failures are propagated as typed custom EVM errors.
the adapter and native
TempoStateprecompile share one execution-local anchor controller. It tracks whether execution is still at the parent Tempo anchor or has advanced to its direct child, rejects reads at an inconsistent anchor, rejects parent-anchor reads before advancement, and rejects duplicate or non-contiguous advancement. Noncommitting payload simulations restore the controller snapshot so they cannot leak anchor state into included transaction execution.until TIP-20 policy storage moves fully into TIP-403, the adapter retains compatibility with the current packed layout. It reads the local packed word first, replaces only the L1-owned
transferPolicyIdfield for execution, and restores the local field before canonical transitions are committed. TIP-403 account or storage mutations fail closed.Precompile Execution
execution.rsnow has one execution path for Zone-native and upstream Tempo precompiles. Every wrapper receives the same EVM configuration, storage-action recorder, and non-creditable-slot set. Whether a storage read is local or L1-backed is entirely a database concern rather than a choice between local and L1-aware precompile providers.CallRulescontains only Zone-specific admission behavior:Continueor ABI-encoded revert dataTIP-20, TIP-403, and
TipFeeManagernow execute the upstream Tempo implementations directly over ordinaryEvmPrecompileStorageProviderstorage. The remaining Zone-specific behavior is limited to privacy checks, bridge mint/burn authorization, read-only TIP-403 selectors, fixed gas, and delegate-call policy.this removes
ZonePrecompileStorageProvider, the separate local/L1-backed execution orchestration, and the custom L1-awareZoneFeeManager. Tempo’s ordinary protocol fee manager now observes the same anchored policy state through the EVM database adapter.Follow-up work
PolicyProvider#701