-
Notifications
You must be signed in to change notification settings - Fork 0
feat: L2 Forked tests #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: sc-feat/l2-forked-test
Are you sure you want to change the base?
Changes from 6 commits
81de013
31e65cf
0a96b50
3900bc2
802504d
0d3e03d
c47ae15
cad3050
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
# Forked Testing Framework for L2 Networks: Design Doc | ||
|
||
## Purpose | ||
|
||
This document outlines the proposed design for an integrated L2 Forked Test suite and defines the scope of this integration. | ||
|
||
## Summary | ||
|
||
This proposal describes a testing framework for L2 networks that lets us run tests against realistic contract states. The framework applies Network Upgrade Transactions (NUTs) to forked networks, creating a consistent environment for testing Predeploy contracts. By implementing NUTs in Solidity or combining Go with Solidity, we can improve test coverage, build confidence in our upgrades, and identify issues earlier. | ||
|
||
## Problem Statement + Context | ||
|
||
As Predeploy contracts development advances, we currently lack an efficient method to run test suites against the state of arbitrary networks. We need to develop a configurable testing framework that accurately replicates the state of Predeploy contracts within selected networks, initialized from a specified block number (initial state). | ||
|
||
This framework should be able to take a network's initial state and apply the required Network Upgrade Transactions (NUTs) to reach the latest version of the contracts for running the test suite. This approach would help increase confidence that the upgrade process doesn't introduce unexpected bugs, allowing contributors to catch errors earlier in the release process. While this implementation won't perfectly mirror the environment after NUTs are applied to the real network, it represents a significant improvement to our testing capabilities. | ||
|
||
The solution must provide a deterministic state to the test suite, enabling the execution of targeted test cases against this final state. This state results from applying a set of NUTs to the initial forked state. Therefore, we need to implement a structured way to describe these NUTs and a method to apply them. | ||
|
||
It's also important that this solution remains loosely coupled with the test setup itself, so that running tests against different chains starting with different initial states requires minimal changes to the Setup test file. | ||
|
||
## Proposed Solution | ||
|
||
Currently, L1 forked state is abstracted by ForkLive, which handles reading the L1 contract's code and upgrading when needed. Ideally, we'd like a similar approach for L2 fork tests, where a single contract is responsible for setting up L2 Predeploys in the final state needed for our test suite. We propose a two-part approach: one part abstracts the NUTs that occur in each upgrade, while the other part executes them sequentially. For L2, we have the advantage of knowing the addresses in advance, so unlike ForkLive, storing addresses isn't necessary. | ||
|
||
### Implementation Components | ||
|
||
1. **NUT Definitions**: Specifications of the Network Upgrade Tests to be executed prior to test suite initialization | ||
|
||
```solidity | ||
interface NUTExecutor { | ||
function execute(bytes calldata _calldata) external; | ||
} | ||
|
||
contract XForkExecutor is NUTExecutor { | ||
function execute(bytes calldata _calldata) external { | ||
/// 1. Deploy a new `L1BlockImpl` contract. | ||
/// 2. Upgrade only the `L1Block` contract to the new implementation by | ||
/// calling `L2ProxyAdmin.upgrade(address(L1BlockProxy), address(L1BlockImpl))`. | ||
/// 3. Call `L1Block.setXFork()` to pull the values from L2 contracts. | ||
/// 4. Upgrades the remainder of the L2 contracts via `L2ProxyAdmin.upgrade()`. | ||
} | ||
} | ||
|
||
``` | ||
|
||
1. **Execution Script**: A specialized module that manages the sequential execution of defined NUTs | ||
|
||
```solidity | ||
contract Upgrader { | ||
function upgrade(NUTExecutor[] memory _executors) external { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The idea here is that you can apply multiple upgrades, similar to how Forklive calls |
||
// Apply NUTExecutors based on the initial state | ||
} | ||
} | ||
|
||
``` | ||
|
||
This approach facilitates the specification of NUT sets to be executed from a defined initial state. The implementation would be encapsulated within a contract similar to ForkLive and serve as an alternative to the L2Genesis script, to be invoked during the Setup phase. | ||
|
||
### Integration with Client Upgrade Process | ||
|
||
Currently, we use Go scripts to prepare NUTs for each upgrade, which are then passed back to the pipeline. These NUTs are defined in dedicated Go files that build them individually. Ideally, we should share these transaction definitions between the client and our test suite to eliminate code duplication and ensure that our test upgrades closely mirror production environments. | ||
|
||
Once we have contracts that handle entire sets of NUTs, the transactions in the Go scripts can be replaced by two standard transactions: | ||
|
||
1. Deploying the appropriate NUTExecutor contract for that particular upgrade | ||
2. Calling the execute function on that contract | ||
Comment on lines
+63
to
+66
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where would the go scripts read the NUTExecutor bytecode from, the artifacts? |
||
|
||
This approach offers flexibility and power as the exact same transactions could be executed both in production upgrades and our Solidity test suite (provided we use the same arguments). Additionally, it would enable the test suite to easily fuzz transactions as needed. | ||
|
||
**Advantages:** | ||
|
||
- Provides high-fidelity representation of Predeploy states by simulating complete upgrade paths from an initial network state | ||
- Offers flexible configuration options for various testing scenarios | ||
|
||
However, current limitations prevent us from implementing a truly shared approach, as we require NUT scripts to execute within the context of privileged accounts such as ProxyAdmin or the Depositor Account. To address this, we propose the implementation of the following solution: | ||
|
||
#### L2 `ProxyAdmin` Upgrade | ||
|
||
It's possible to upgrade the L2 `ProxyAdmin` contract to allow limited delegated calls, enabling NUT scripts to execute within the `ProxyAdmin` context and perform contract upgrades. While this change would allow us to deploy new implementations and upgrading the proxies, configuration settings in these new implementations can be managed by verifying `tx.origin` rather than `msg.sender` as the Depositor Account. We consider this approach semantically correct as it still verifies that the Depositor Account is the originator of the transaction. | ||
0xiamflux marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
```solidity | ||
contract L2ProxyAdmin is ProxyAdmin { | ||
|
||
/// @dev allow Constants.DEPOSITOR_ACCOUNT to perform delegated calls | ||
function _checkOwner() internal view override { | ||
require(owner() == _msgSender() || Constants.DEPOSITOR_ACCOUNT == _msgSender(), "Ownable: caller is not the owner"); | ||
} | ||
|
||
function performDelegateCall(address _target) external payable onlyOwner { | ||
(bool success,) = _target.delegatecall(abi.encodeCall(INUTExecutor.execute, ())); | ||
require(success, "ProxyAdmin: delegatecall to target failed"); | ||
} | ||
} | ||
``` | ||
|
||
```solidity | ||
contract GasPriceOracle is ISemver { | ||
/// ... rest of the code | ||
|
||
/// @notice Set chain to be Isthmus chain (callable by depositor account) | ||
function setIsthmus() external { | ||
require( | ||
tx.origin == Constants.DEPOSITOR_ACCOUNT, | ||
"GasPriceOracle: only the depositor account can set isIsthmus flag" | ||
); | ||
require(isFjord, "GasPriceOracle: Isthmus can only be activated after Fjord"); | ||
require(isIsthmus == false, "GasPriceOracle: Isthmus already active"); | ||
isIsthmus = true; | ||
} | ||
} | ||
``` | ||
|
||
> Designating the Depositor Account as the owner of the L2ProxyAdmin and checking for `tx.origin` on configuration changes on new implementations would allow for the execution of all NUTs by impersonating only a single EOA | ||
|
||
## Example: Upgrading a Predeploy to a new Implementation | ||
|
||
In this example, the `NUTExecutor.execute` function's responsibility is to deploy a new implementation contract and initiate the upgrade by invoking the `upgradeTo` method on the Proxy. | ||
|
||
```mermaid | ||
sequenceDiagram | ||
Depositor ->>+L2ProxyAdmin: performDelegateCall | ||
L2ProxyAdmin ->>+NUTExecutor: delegateCall(execute()) | ||
Note over NUTExecutor: NUT execution begins | ||
NUTExecutor->>NUTExecutor: Deploy new Implementation | ||
NUTExecutor->>+Proxy: upgradeTo(newImplementation) | ||
Proxy-->>-NUTExecutor: success | ||
Note over NUTExecutor: NUT execution completes | ||
NUTExecutor-->>-L2ProxyAdmin: return result | ||
``` | ||
|
||
## Impact on Developer Experience | ||
|
||
Moving away from Go in favor of Solidity to define the NUTs would imply a change in the upgrade release process. Initially, this transition would require updating the Go scripts and creating Solidity NUT definitions. However, the result would be a more cohesive and integrated process for upgrading L2 Predeploys. This approach would also increase confidence in our test suite by ensuring it reflects a more realistic state of the network. | ||
|
||
## Alternative Approaches Explored | ||
|
||
#### Pectra Hardfork and EIP-7702 | ||
|
||
The Ethereum Pectra Hardfork introduces EIP-7702, which enables EOAs to delegate control to smart contracts capable of executing code directly from the address. This would allow us to integrate the NUT Executor approach into L2 upgrades, as privileged accounts could execute NUT scripts via delegated calls. | ||
|
||
This alternative was quickly discarded because it is impossible to obtain the required signatures for authorization lists, which would have been necessary to attach the delegated contracts to the Depositor Account. | ||
|
||
### NUT parser middleware | ||
|
||
Network upgrade transactions are defined in Go scripts following the naming convention <fork_name>\_upgrade_transactions.go (e.g., fjord_upgrade_transactions.go). These scripts are used by the client to send the necessary transactions. | ||
We could create middleware Go scripts that consume these transactions and return them in a format which can be consumed by Solidity for execution from Foundry tests. The L2ForkLive would call these Go scripts via FFI and parse the response for it to execute each one of the calls. | ||
|
||
**Advantages:** | ||
|
||
- No changes needed to existing Go scripts | ||
- Maintains consistency between NUTs used in testing and actual upgrades | ||
|
||
**Disadvantages:** | ||
|
||
- Requires parsing of complex data returned from Go scripts | ||
- Introduces middleware that's tightly coupled with the L2ForkLive Foundry script | ||
|
||
## Risks & Uncertainties | ||
|
||
- Are there any issues by having differences between implementation contract addresses and the expected derived address from the `Predeploys.predeployToCodeNamespace` function? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how is
_calldata
defined?Do we expect that it might need to vary for different networks, or do we maybe not need it?