-
Notifications
You must be signed in to change notification settings - Fork 48
Add L2 Contract Upgrades design doc #351
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
Changes from all commits
37ae3a1
54ed200
8547059
ab68e43
08696a5
4c8b6fd
642d046
d1c7927
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,223 @@ | ||
| ## Purpose | ||
|
|
||
| To enable deterministic, well tested, hard fork driven upgrades of L2 contracts, with both the implementation and upgrade path written in Solidity. | ||
|
|
||
| The customers for this work include: | ||
|
|
||
| - protocol devs seeking to modify L2 contracts | ||
| - chain operators looking for improvements | ||
| - anyone who has ever worried about unclear and inconsistent versioning of L2 contracts across OP chains | ||
|
|
||
| ## Summary | ||
|
|
||
| The proposed design maintains the current method of injecting Network Upgrade Transactions (NUTs) at a specific fork block height, while improving on the development and testing process in order to better enable safe, well-tested, multi-client upgrades of L2 contracts. | ||
|
|
||
| This is achieved by putting the transaction data into a JSON file, and then executing it at the specified fork block. | ||
|
|
||
| The design aims to create a solution which parallels the OPCM, but for L2. | ||
|
|
||
| ## Problem Statement + Context | ||
|
|
||
| The status quo for L2 contract development suffers from the following problems: | ||
|
|
||
| 1. **Separation of development and testing flows:** Contracts are implemented in Solidity, but upgraded with Go, making the upgrade path difficult to test. | ||
| 2. **Separation between upgrade and deployment flows:** Deployment of L2 contracts is done during L2 Genesis generation (a solidity script). Conversely upgrading happens in NUT files (go scripts). | ||
| 3. **Lack of feature flags and network specific configuration:** We don't have a mechanism to modify upgrades based on config or feature flags. | ||
| 4. **Release and Versioning issues:** We have an L1 contracts release process, but we don’t have a parallel process for L2 contracts. | ||
| 5. **Lack of verifiability:** We don’t have a way to ensure that the bytecode in the NUT file is actually generated by the compiled output of the source code on a given commit. | ||
| 6. **Fragmentation of L2 contract versions:** Different OP Chains are running different versions of the L2 contracts, and we don't know who is running what. | ||
| 7. **Stagnant L2 contracts:** We're afraid to make changes to L2 contracts, because we don't have a good way to roll them out. | ||
|
|
||
| ## Requirements | ||
|
|
||
| The design described here is intended to fulfill the following requirements: | ||
|
|
||
| - Upgrades must be 100% deterministic with a clear, immutable record of the exact transactions and bytecode executed. | ||
| - Execution must occur at an exact fork timestamp for consensus-coupled contracts like L1Block. | ||
| - Clear provenance of upgrade bytecode, ie. it is easy to verify that the upgrade bytecode comes from the solidity source code of a given commit. | ||
| - Upgrade paths should be testable end-to-end, ideally authored in Solidity and exercised in local and CI environments | ||
| - Support for feature flags | ||
| - Provide a mechanism for different implementations ([specs](https://specs.optimism.io/protocol/fjord/derivation.html#network-upgrade-automation-transactions), Go, Rust, etc.) to keep upgrade transactions in sync. | ||
| - Clarify how L2 contracts are versioned and released. | ||
|
|
||
| ### Non-requirements | ||
|
|
||
| The design does not attempt to fulfil these non-requirements: | ||
|
|
||
| - Improving or simplifying how L2 contract upgrades can be initiated via actions taken on L1. For example this project will not add a new method to an L1 contract which triggers a `TransactionDeposited` with specific data to cause an upgrade to occur to an L2 contract (ie. like [this](https://www.notion.so/A-mechanism-for-upgrading-L2-contracts-via-L1-transactions-2249adb08eb64cd193cc7cd30c146603?pvs=21) earlier proposal). | ||
| - The ability to inject network specific configuration data into an upgrade transaction. This has not been required in previous hardforks and the need is not foreseen in the near future. | ||
|
|
||
| It is worth noting that the requirements above are primarily provided for the L1 contracts by the OPCM and tooling associated with it. A nice-to-have is to reuse existing OPCM related code, patterns and tooling so that the experience of developing and upgrading L2 contracts aligns as closely as possible with that of L1 contracts. | ||
|
|
||
| <aside> | ||
| 📢 | ||
|
|
||
| The source of truth for these requirements in maintained in the [PR FAQ](https://www.notion.so/L2ContractManagement-PR-FAQ-281f153ee16280c09c74c19963c06b05?pvs=21) here. | ||
|
|
||
| </aside> | ||
|
|
||
| ### Milestones | ||
|
|
||
| The project of overhauling how L2 contracts are managed is a large one. In order to control scope, the deliverable for this work at hand will be to upgrade a subset of L2 contracts via hard fork to bring them in line with the source code in the monorepo. | ||
| The exact set of contracts is yet to be determined, but we will skip upgrading any edge-case contracts which require extra effort to upgrade. | ||
|
|
||
| **Future Milestone** | ||
| This design also does not attempt to address Standard Genesis generation, which is reserved for future milestones. | ||
|
|
||
| ## Proposed Solution | ||
|
|
||
| Network upgrade transactions will continue to be executed at a [specified fork block height](https://github.com/ethereum-optimism/optimism/blob/2fdd97053ba8fb2bc27dcb38dcdc42ade7c8c78d/op-node/rollup/derive/attributes.go#L148-L154), except that rather than executing a set of transactions defined in a stand alone go file, the transactions will be read from a JSON file. | ||
|
|
||
| The NUT file will be stored in the monorepo, and tracked by git. It will be updated by the `just snapshots` command. | ||
|
|
||
| ### What upgrade transactions will do | ||
|
|
||
| A given set of upgrade transactions will typically perform the following actions: | ||
|
|
||
| 1. deploy new `ConditionalDeployer` contract (This step will only be needed the first time this scheme is used) | ||
| 2. deploy new implementations via the `ConditionalDeployer` | ||
| 2. deploy a new `L2ContractsManager` | ||
| 3. update the `ProxyAdmin` (This step will only be needed the first time this scheme is used) | ||
| 4. execute a call to `ProxyAdmin.upgradePredeploys()` | ||
| 1. This function will `DELEGATECALL` the new `L2ContractsManager`'s `upgrade()` function. | ||
| 2. For all predeploys being upgrade, `L2ContractsManager.upgrade()` will make a call to that predeploy's `upgradeTo()` function | ||
|
|
||
| ### New ConditionalDeployer | ||
|
|
||
| A new `ConditionalDeployer` contract is needed to ensure that step 2 "deploy new implementations" can preserve the following properties of the L1 OPCM system: | ||
|
|
||
| 1. If a contract's bytecode is unchanged, then the implementation address will be unchanged. This | ||
| is achieved on L1 in the DeployImplemenations script's [use](https://github.com/ethereum-optimism/optimism/blob/60f0c8d0beb2ea22f0ebc11416a22978b182dbfa/packages/contracts-bedrock/scripts/deploy/DeployImplementations.s.sol#L260) of [`createDeterministic`](https://github.com/ethereum-optimism/optimism/blob/60f0c8d0beb2ea22f0ebc11416a22978b182dbfa/packages/contracts-bedrock/scripts/libraries/DeployUtils.sol#L145). | ||
| 2. A developer does not need to think about whether or not to include a given Predeploy in the | ||
| upgrade. The Predeploy will always be upgraded to the latest implementation address. If | ||
| the contract is unchanged, then its bytecode and implementation address will be unchanged, but | ||
| regardless the upgrade call will always be included in the `L2ContractsManager` so that it need not | ||
| be edited between upgrades. | ||
|
|
||
| The `ConditionalDeployer` contract will be very minimal contract which will receive the initcode and then: | ||
| 1. determine whether a create2 collision will occur | ||
| 2. if not: forward the initcode to the [determinstic-deployer](https://github.com/Arachnid/deterministic-deployment-proxy/blob/master/source/deterministic-deployment-proxy.yul#L13) contract. | ||
| 3. if yes: return | ||
|
|
||
| The `ConditionalDeployer` should not revert if called with properly formed inputs. | ||
|
|
||
|
|
||
| ### New L2 ProxyAdmin | ||
|
|
||
| The `ProxyAdmin` predeploy (at `0x42...18`) will be upgraded with a new implementation which: | ||
|
|
||
| 1. maintains the current interface for backwards compatibility | ||
| 2. removes code which is necessary to support the Chugsplash and Resolved Delegate proxies (Optional, can be cut from scope) | ||
maurelian marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 3. gets a new `upgradePredeploys()` function (as described above) | ||
| 1. This function is only callable by the Depositor account (`0xdeaddead…0001`) | ||
|
|
||
| ### The L2ContractsManager contract | ||
maurelian marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| The initial version of the `L2ContractsManager` developed for this work will be minimal, and its `upgrade()` function must not accept any arguments in order to maintain the determinism of the upgrade bundle. | ||
|
|
||
| In pseudocode it will do the following: | ||
|
|
||
| ```python | ||
| # Contains network specific config values | ||
| class L2Config: | ||
| l1CrossDomainMessengerConfig: L2CrossDomainMessengerConfig | ||
| l2StandardBridgeConfig: L2StandardBridgeConfig | ||
| l2ERC721BridgeConfig: L2ERC721BridgeConfig | ||
| # etc. | ||
|
|
||
| def gather_config() -> (L2Config): | ||
| # Calls to existing predeploys to ready any network specific config values | ||
| # into an L2Config object. | ||
|
|
||
| def upgrade_and_set_config: | ||
| # for each upgradeable L2 contract | ||
| # if that contract has initializer args, call `Proxy.upgradeToAndCall()` | ||
| # if it does not, call `Proxy.upgradeTo()` | ||
|
|
||
| def upgrade(): | ||
| l2Config: L2Config = gather_config() | ||
| upgrade_and_set_config(l2Config) | ||
| ``` | ||
|
|
||
| ### Gas limits for activation blocks | ||
|
|
||
| Upgrade transactions must be included in the L2 block and must fit within the block gas limit. Currently, there is only 1M gas guaranteed to be available for upgrade transactions. This limit comes from `_resourceConfig.systemTxMaxGas` being set to 1,000,000, which constrains the amount of gas available for system transactions including upgrades. While L2 block gas limits are typically set much higher in practice, the system only guarantees a minimum of `SystemConfig.minimumGasLimit()`, and the `systemTxMaxGas` constraint means upgrade transactions cannot exceed 1M gas. | ||
|
|
||
| The upgrade transactions described in this design will almost certainly exceed 1M gas, making the current limit insufficient. | ||
|
|
||
| **Solution: Additional Upgrade Gas** | ||
|
|
||
| Rather than increasing `systemTxMaxGas` (which would require L1 changes and still wouldn't be future-proof), we will implement a mechanism that guarantees sufficient gas for upgrade transactions regardless of the regular block gas limit. This will be achieved by introducing the concept of additional upgrade gas, which allows upgrade transactions to use gas beyond the normal `systemTxMaxGas` limit. | ||
|
|
||
| The implementation involves modifications to `op-node/rollup/derive/attributes.go` to handle upgrade gas allocation, similar to what can be seen in [this PR](https://github.com/ethereum-optimism/optimism/pull/14797). We should consider simply hardcoding the upgrade block gas amount very high (50M?), | ||
| and testing to ensure it is not exceeded by a future upgrade. | ||
|
|
||
| ### Generating deterministic JSON NUT bundles | ||
|
|
||
| The NUT bundle will be generated via a solidity script. Similar to the | ||
| current go-defined NUTs, this script must be deterministic, and cannot depend on execution in an EVM. The implementation addresses will therefore need to be pre-computed as seen in [the go](https://github.com/ethereum-optimism/optimism/blob/2fdd97053ba8fb2bc27dcb38dcdc42ade7c8c78d/op-node/rollup/derive/ecotone_upgrade_transactions.go#L24-L25), although this will more likely be done using the CREATE2 or CREATEX deployer. | ||
|
|
||
| A major benefit of this approach is that it enables one to easily verify that a bundle of NUTs corresponds to a given monorepo commit, by simply: | ||
|
|
||
| 1. checking out the commit | ||
| 2. building the contracts | ||
| 3. regenerating the bundle | ||
| 4. verifying that it matches | ||
|
|
||
| The NUT bundle will be stored in the monorepo, will be generated by a `just` command, and tracked by git. | ||
|
Member
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. It's important that the NUTs for a particular fork are generated on-demand by a |
||
|
|
||
| ### L2 Contracts in scope | ||
|
|
||
| **The goal is to upgrade all proxied L2 contracts.** | ||
|
|
||
| It is anticipated* that this will be doable without significant changes to any of the contract implementations. Moving variables from `immutable` (in bytecode) to storage, and setting them in an `initialize()` function is not considered significant. | ||
|
|
||
| If more significant work is required which has not been anticipated, affected contracts will be removed from the scope and upgraded in a future milestone. | ||
|
|
||
| * *It may be the case that the ongoing FeeVault upgrade work presents difficulty for upgrading FeeVauts, in which case they would be excluded.* | ||
|
|
||
| ### L2 contract versioning | ||
|
|
||
| **The goal is to release all L1 and L2 contracts under a single tag.** | ||
|
|
||
| This is already de facto the case because op-deployer generates genesis files with whatever L2 contracts are contained in the latest L1 contracts release. It is only because the L2 contract changes have not been rolled out to pre-existing chains that we haven’t officially included L2 contracts in releases. | ||
|
|
||
| Per the previous section, it is possible that not all L2 contracts are upgraded by this work, but even in that case, we should simply begin including all L2 contracts in each release, and aim to upgrade all L2 contracts in an upgrade as soon as possible. | ||
|
|
||
| ### Upgrade testing | ||
|
|
||
| Testing of the upgrade path is absolutely necessary. This will be achieved using fork based tests similar to the existing L1 fork testing. For L2 upgrade fork tests, we will: | ||
|
|
||
| 1. generate the upgrade bundle | ||
| 2. fork an L2 chain | ||
| 3. read and execute the upgrade bundle | ||
| 4. execute the tests | ||
|
|
||
| The above steps will be runnable with a `just` command which should be a simple wrapper a `forge` command (no shell or go script required). | ||
| It is not necessary run L2 upgrade tests in CI for all OP Chains, but it should be easily to manually run the test the upgrade tests against an arbitrary L2 chain. | ||
|
|
||
| ### Feature flags | ||
|
|
||
| **Features flagging THIS feature** | ||
|
|
||
| Features flags are not considered necessary for this feature, because it does not require significant changes to the L2 predeploys, and it will not be ‘activated’ until it is implemented in an upgrade. | ||
maurelian marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| **Supporting dev feature flags in upgrades** | ||
|
|
||
| The design proposed here must support feature flagging of L2 Predeploys, and should do so using the patterns described in [Smart Contract Feature Flagging & System Customizations](https://www.notion.so/Smart-Contract-Feature-Flagging-System-Customizations-28cf153ee16280e884c1f6fed632d5c8?pvs=21). | ||
| In order to achieve this, we can insert the `FeatureFlags` [contract](https://github.com/ethereum-optimism/optimism/blob/d09c836f818c73ae139f60b717654c4e53712743/packages/contracts-bedrock/test/setup/FeatureFlags.sol#L17) at some address using `vm.etch()`, and set the `devFeatureBitmap` using `vm.store()`. | ||
| The L2CM can then set a custom config values, or upgrade a contract to an experimental feature flagged implementation address based on the | ||
| values in the `devFeatureBitmap`. | ||
|
|
||
| This should work for all environments where feature flags must be supported ([foundry tests and alphanets](https://www.notion.so/Smart-Contract-Feature-Flagging-System-Customizations-28cf153ee16280e884c1f6fed632d5c8?pvs=21)). | ||
|
|
||
| ### Configurable features | ||
|
|
||
| Any configurable production features are expected to be configued in the `L1Block` contract. | ||
| Just like with dev features, the L2CM will activate any configurable values, or set config specific | ||
| implementations based on the data available in `L1Block`. | ||
|
|
||
| ## Risks & Uncertainties | ||
|
|
||
| - We do not have a good understanding of what code is running on the various L2s. This may lead to unexpected challenges. | ||
| - Other projects are occurring in parallel which will affect the predeploys. | ||
Uh oh!
There was an error while loading. Please reload this page.
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.
Is the intention to deploy the conditional deployer on every hard fork? I think it only needs to be done once, and if so, perhaps it's worth specifying it just like we do with ProxyAdmin. What do you think?
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.
good call out, addressed in this commit