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 beggining 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
verify
function 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
verify
function 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
AnchorStateRegistry
andsequencer_client
to 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_confirmed
we 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 thetesting
feature is turned on).impl_sealed_for_fn!
- Implements theSealed
trait 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 theSealed
trait - 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
ExecutionLocation
context (usingtransaction_callback
function 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
), theInspector
parses the input arguments and triggers a travel call by invoking eitherset_block
orset_chain
. - Standard Calls: If no travel call is detected, the
Inspector
allows the call to proceed normally. However, if a travel call has already set a new context, it is processed using the providedtransaction_callback
and applies the updated execution context in theon_call
function.
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.
ExecutionResult
is 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.CallOutcome
is 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
settleBlockHash
matches the actual on-chain block hash at thesettleBlockNumber
.