Time Travel and Teleport
vlayer allows seamless aggregation of data from different blocks and chains. We refer to these capabilities as Time Travel and Teleport. How is it done?
Note: Teleportation is currently possible only from L1 chains to L2 optimistic chains. We plan to support teleportation from L2 to L1 in the future.
Verification
At the beginning of the guest::main we verify whether the data for each execution location is coherent. However, we have not yet checked whether data from multiple execution locations align with each other. Specifically, we need to ensure that:
- The blocks we claim to be on the same chain are actually there (allowing time travel between blocks on the same chain).
- The blocks associated with a given chain truly belong to that chain (enabling teleportation to the specified chain).
The points above are verified by the Verifier::verify function. The Verifier struct is used both during the host preflight and guest execution. Because of that it is parametrized by Recording Clients (in host) and Reading Clients (in guest).
The verify function performs above verifications by:
I. Time Travel Verification
Is possible thanks to Chain Proofs. Verification steps are as follows:
- Retrieve Blocks: Extract the list of blocks to be verified and group them by chain.
- Iterate Over Chains: For each chain run time travel
verifyfunction on its blocks. - Skip Single-Block Cases: If only one block exists, no verification is needed.
- Request Chain Proof: Fetch cryptographic proof of chain integrity.
- Verifies Chain Proof: Run the chain proof
verifyfunction on the obtained Chain Proof to check its validity. - Validate Blocks: Compare each block’s hash with the hash obtained by block number from the validated Chain Proof.
II. Teleport Verification
- Identify Destination Chains: Extract execution locations from
CachedEvmEnv, filtering for chains different from the starting one. - Skip Local Testnets: If the source chain is a local testnet, teleport verification is skipped.
- Validate Chain Anchors: Ensure the destination chain is properly anchored to the source chain using
assert_anchor(). - Fetch Latest Confirmed L2 Block: Use the
AnchorStateRegistryandsequencer_clientto get the latest confirmed block on the destination chain. - Verify Latest Confirmed Block Hash Consistency: Compare the latest confirmed block’s hashes.
- Verify Latest Teleport Location Is Confirmed: Using function
ensure_latest_teleport_location_is_confirmedwe check that latest destination block number is not greater than latest confirmed block number.
Verifier Safety & Testability
To prevent unauthorized custom verifier implementations, we use Sealed trait pattern. This ensures that IVerifier trait cannot be implemented outside the file it was defined - except when the testing feature is enabled.
This design is crucial because verifiers are composable. When testing a Verifier that is composed from other verifiers, we need to mock them with fake implementations. This flexibility is achieved by allowing special implementations under the testing feature.
Macros Overview
The following macros work together to enforce sealing and enable test mocking:
sealed_trait!- Creates a private module (seal) containing a traitSealed. By requiring verifier traits to extendseal::Sealed, only types that also implement Sealed (and hence are defined within controlled environment) can implement the verifier traits.verifier_trait!- Defines the actual verifier trait (e.g.,IVerifier) with a verify method. The trait extendsseal::Sealed.impl_verifier_for_fn!- Allows functions to be used as verifiers by implementing the verifier trait for them. This is only enabled in testing (or when thetestingfeature is turned on).impl_sealed_for_fn!- Implements theSealedtrait for functions with the appropriate signature.sealed_with_test_mock!- This is a convenience macro that ties everything together. It:- Calls
sealed_trait!to create theSealedtrait - Calls
impl_sealed_for_fn!to allow function pointers to be sealed - Defines verifier trait using
verifier_trait! - Implements the verifier trait for function pointers with
impl_verifier_for_fn!
- Calls
Inspector
Both Time Travel and Teleport features are made possible by the Inspector struct, a custom implementation of the Inspector trait from REVM. Its purpose is to handle travel calls that alter the execution context by switching the blockchain network or block number.
How does it work? When ExecutionLocation is updated, Inspector:
- Creates a separate EVM with new
ExecutionLocationcontext (usingtransaction_callbackfunction passed as argument). - Executes the subcall on a separate inner EVM with updated location.
#![allow(unused)] fn main() { pub struct Inspector<'a> { start_chain_id: ChainId, pub location: Option<ExecutionLocation>, transaction_callback: Box<TransactionCallback<'a>>, metadata: Vec<Metadata>, } }
Key Responsibilities of the Inspector
1. Tracks Execution Context (Chain & Block Info)
It maintains the ExecutionLocation which consists of chain_id and block_number
2. Handles Travel Calls
There are two special functions that modify execution context:
set_block(block_number): Updates the block number while keeping the same chain.set_chain(chain_id, block_number): Changes both the blockchain network and block number.
3. Intercepts Contract Calls
Intercepts every contract call and determines how to handle it:
- Precompiled Contracts: If the call targets a precompiled contract, it logs the call and records relevant metadata.
- Travel Call Contract: If the call is directed to the designated travel call contract (identified by
CONTRACT_ADDR), theInspectorparses the input arguments and triggers a travel call by invoking eitherset_blockorset_chain. - Standard Calls: If no travel call is detected, the
Inspectorallows the call to proceed normally. However, if a travel call has already set a new context, it is processed using the providedtransaction_callbackand applies the updated execution context in theon_callfunction.
4. Monitors & Logs Precompiled Contracts
If the call is made to a precompiled contract it logs the call and records metadata.
Precompiles used by vlayer are listed here.
ExecutionResult to CallOutcome conversion
ExecutionResult and CallOutcome are revm structs used in the Inspector code. They are necessary to make travel calls work.
ExecutionResultis an enum representing the complete outcome of a transaction. It has three variants—Success,Revert, andHalt—and includes transaction information such as gas usage, gas refunds, logs, and output data.CallOutcomeis a struct representing the result of a single call within the EVM interpreter. It encapsulates anInterpreterResult(which contains output data and gas usage) along with amemory_offset(the range in memory where the output data is located).
Most fields stored in ExecutionResult have equivalents in CallOutcome. The only exceptions arelogs and gas_refunded fields from ExecutionResult::Success, which do not exist in CallOutcome. Conversely, CallOutcome includes memory_offset, which has no direct counterpart in ExecutionResult.
When Inspector::call is executed, it must return a CallOutcome. However, the transaction_callback run inside Inspector::call executes the full EVM and returns an ExecutionResult. Hence, the conversion between the two is needed.
This conversion is performed using the execution_result_to_call_outcome function within Inspector::on_call. During this process logs and gas_refunded fields from ExecutionResult::Success are discarded, as they are not required in CallOutcome. memory_offset is obtained from CallInputs, which is also passed to execution_result_to_call_outcome as an argument.
Executor
Executor struct handles running EVM transactions. Inspector is created by the Executor struct and used while building EVM.
#![allow(unused)] fn main() { pub struct Executor<'envs, D: RevmDB> { envs: &'envs CachedEvmEnv<D>, } }
call
The Executor provides a public call method that runs the internal execution (internal_call).
internal_call
The private internal_call method performs the core execution of an EVM transaction, including support for recursive internal calls (when one smart contract calls another). In this implementation, the envs are shared across recursive calls, meaning that any modification performed by one call is visible to others.
But updates to the database state (contained in the ProofDb structure, being a part of env) are safe because the state is modified only by inserting new entries. New keys are added to the accounts, contracts, and block_hash_numbers collections, while existing entries remain unchanged.
Error handling
Due to the design of revm's Inspector trait, the Inspector::call (run inside EVM build in Executor::internal_call) method must return an Option<CallOutcome> rather than a Result. This limitation means that errors occurring during intercepted calls cannot be directly propagated via the return type.
To work around this constraint, our Inspector implementation uses panics to signal errors. The panic is then caught in the Executor::call method using panic::catch_unwind. This mechanism allows us to convert panics into proper error results, ensuring that errors are not lost, even though the Inspector::call function itself cannot return an error.
On-chain Verification
When the proving process begins, a specific block is selected as the settlement block—the block we commit to. Then, a call to the Prover contract is executed within zkEVM environment. The guest proof is valid providing the block and contract assumptions used during its generation are accurate.
These assumptions are encapsulated in a dedicated struct used within the guest code:
struct CallAssumptions {
address proverContractAddress;
bytes4 functionSelector;
uint256 settleBlockNumber;
bytes32 settleBlockHash;
}
The struct is created inside the guest::main function. Since the guest itself cannot independently prove the validity of these assumptions, they must be verified externally.
To achieve this, CallAssumptions is included in the GuestOutput and subsequently verified on-chain using the Verifier contract, specifically through the _verifyExecutionEnv. This verification ensures that the proof aligns with a valid blockchain state.
Validation Steps in _verifyExecutionEnv
The _verifyExecutionEnv function checks the following:
- Prover Contract Validation: Ensures that the proof comes from the correct
proverContractAddress. - Function Selector Validation: Verifies that the function being executed matches the expected function selector.
- Block Number Validation: Ensures that the proof is based on a past block (not from the future) and that the block falls within the last 256 blocks—the maximum number of historical blocks accessible during EVM execution.
- Block Hash Validation: Confirms that the
settleBlockHashmatches the actual on-chain block hash at thesettleBlockNumber.