Skip to content
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

L1 Contracts Developer Vision and UX #177

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open

Conversation

mds1
Copy link
Contributor

@mds1 mds1 commented Dec 17, 2024

This design doc describes the vision we are working towards with the OPCM Upgrades and superchain-ops projects that the EVM Safety team is working on. We have spoken about these flows and ideas in various conversations and docs, but want to get the overall end goal we are working towards in a centralized place to ensure we are all aligned

Some details still need to be figured out, and suggestions on those areas is welcome, but the high level flow and UX is the key aspect here.

@mds1 mds1 changed the title L1 Contracts Developer Visition L1 Contracts Developer Vision and UX Dec 17, 2024
@mds1 mds1 requested a review from mslipper December 17, 2024 18:18

There will also be some other UX and safety improvements for writing these solidity files.

- Addresses for a chain can be fetched in Solidity via `addresses.getAddress("ContractName", l2ChainId)`.
Copy link
Contributor

@skeletor-spaceman skeletor-spaceman Dec 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, would it make it better if we used a struct(s) rather than hardcoded strings?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is using the already implemented Addresses.sol from Forge Proposal Simulator. I'll tag the team here to add color.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests give some color to how Addresses will be fetched in the superchain-ops repo: https://github.com/ethereum-optimism/superchain-ops/blob/main/test/AddressRegistry.t.sol#L28-L42

Screenshot 2024-12-20 at 2 13 51 PM

First the string that identifies the contract is passed, then the L2 ChainId is passed.

If your feature may not go into the next release, create a child contract such as `contract SystemConfigInterop is SystemConfig`.
In this contract you can add your feature, and once we know it's ready for release it will be merged
into the parent contract it inherits from. Tests for `SystemConfigInterop` would live in `SystemConfigInterop.t.sol`
(TODO flesh out more details on how this should work with OPCM, e.g. do we need an `OPCM*` for each feature, like
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to decided on a path forward for interop asap. With the upcoming onsite, we will want to use OPCM to deploy the system. I lean towards an OPCMInterop sort of design. Designing a diff on top of base OPCM that executes after base OPCM seems more complex to think about.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fwiw, op-deployer currently allows users to specify a useInterop flag when deploying.

If a user chooses to deploy a chain with interop enabled, op-deployer deploys our OPContractsManagerInterop contract.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does seem like a really key part of the design. A key part of it will be making it easy to toggle specific parts of upgrades on and off. We will often work on multiple things at once (or change the order things ship) and don't want that to always involve a whole bunch of rebasing/rewriting OPCM child contracts. I'd suggest looking for a way to have upgrade "tasks" that are the smallest unit we might reasonably ship. We make sensible trade offs between flexibility in what we ship and complexity of reasoning about interactions between tasks etc, but importantly we wouldn't just have a Isthmus upgrade task, we'd have separate tasks for each feature in Isthmus.

Then we'd need a way to bundle different tasks together in supported configurations. The OPCM* pattern with child contracts could work well for that. I'm hesitant to just have booleans for it as that tends to lead to a combinatorial explosion of possible configurations. We want the flexibility to change the combination we ship, but to reduce testing cost we want a small set of configurations that are actually "in the development pipeline" so that we can dedicate testing to those.

aspect is how deploy and upgrade scripts are written and tested during development, as this is a key
change.

Originally, `Deploy.s.sol` was the script that run as part of test `setUp`, so we can run tests
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just linking out to this github issue as I don't see it mentioned here: ethereum-optimism/optimism#13331

The same solidity code that is called by op-deployer should be called by a test setup script to populate the unit tests. There should be no ffi out to an external process to get JSON to populate the state for solidity unit tests.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah +1 on this. The tests should compose the Solidity scripts that OP Deployer uses under the hood. If we want to make defining test chains in Solidity more declarative, we can build that into the Solidity itself.

IMO it's a smell every time we FFI out to something from Solidity.


## Execution of Onchain Transactions

With new implementation contracts deployed, we can prepare playbooks, or tasks, in superchain-ops.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move away from gnosis safe JSON being the input to the superchain-ops tasks? We don't use the UI anyways, its not safe enough to use because it doesn't allow for the various security checks that we do (local simulation + assertions, observe the various hashes and compare to ledger).

Looking at the work that the protocol team has to do to generate the JSON for the superchain-ops task (ethereum-optimism/optimism#13412) it is WAY to complex. We need something that is dead simple. It sounds like we will just write a forge script now? And have access to a "superchain-ops standard library" of sorts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, writing forge scripts is the plan. From disussions with @mds1, we want to get as close as possible to having everything just be a template.
Even upgrades should mostly reuse code from an upgrade library, although it will vary somewhat depending on what new config values are introduced.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is for everything to be a template, so no new solidity code needs to be written for any proposals. You just create a new toml config file for the proposal you want to run, and then run a forge script passing the toml config file path.

If a new type of proposal is created that cannot be templatized, then it can be written in plain Solidity like so:

Screenshot 2024-12-20 at 2 10 47 PM

This new way of doing things will not have any more JSON files with calldata in them. Instead, everything will be as human readable as possible.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This toml config file https://github.com/solidity-labs-io/superchain-ops/pull/10/files#diff-83b78f6d8fd1a6be20fd6fcebebf9b523e0beb9d40f539a0b330eff579d945b4 shows some of how we may templatize changing the gas limit in future proposals. This PR is very much still a WIP.

Once we have finished setting up the scaffolding, we will fully flush out templates and docs for a smooth task devex.

account for your changes in order to test them. This is great, as it's now trivial to keep deploy
scripts up to date, and we essentially get this for free.

OPCM will also contain an `upgrade` method as described in the [OPCM Upgrades design doc](l1-upgrades.md).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to be super clear, the upgrade method is used to upgrade from the previous version to the same version that is deployed via the deploy method

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct.

While the length will likely be proportional to the length of the full document,
the summary should be as succinct as possible. -->

The EVM Safety team is working on two projects—OPCM (OP Contracts Managr) Upgrades and superchain-ops
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The EVM Safety team is working on two projects—OPCM (OP Contracts Managr) Upgrades and superchain-ops
The EVM Safety team is working on two projects—OPCM (OP Contracts Manager) Upgrades and superchain-ops

As a rule of thumb, including code snippets (except for defining an external API)
is likely too low level. -->

The proposed UX upon for L1 smart contract development is described below.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The proposed UX upon for L1 smart contract development is described below.
The proposed UX for L1 smart contract development is described below.

If your feature may not go into the next release, create a child contract such as `contract SystemConfigInterop is SystemConfig`.
In this contract you can add your feature, and once we know it's ready for release it will be merged
into the parent contract it inherits from. Tests for `SystemConfigInterop` would live in `SystemConfigInterop.t.sol`
(TODO flesh out more details on how this should work with OPCM, e.g. do we need an `OPCM*` for each feature, like
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fwiw, op-deployer currently allows users to specify a useInterop flag when deploying.

If a user chooses to deploy a chain with interop enabled, op-deployer deploys our OPContractsManagerInterop contract.

An alternate approach to inheritance is to put code directly into `SystemConfig`, but behind a
feature flag. This is the approach client code uses so they can ship releases without also shipping
features that are not production ready. However, in safety-critical systems dead code is typically
not allowed, so we likely will not allowed this approach for contracts. However, we mention it here
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
not allowed, so we likely will not allowed this approach for contracts. However, we mention it here
not allowed, so we likely will not allow this approach for contracts. However, we mention it here


The current [release and versioning process](https://github.com/ethereum-optimism/optimism/blob/develop/packages/contracts-bedrock/meta/VERSIONING.md)
is unintuitive and requires a lot of manual work. It is not possible to have the source code for the
entirety of a release at a single commit, and therefore very difficult to test contract releases.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking that I broadly understand the order of operations here for Isthmus Cycle 34:

  1. Identify the release candidate of op-contracts/v2.0.0 that we're happy with e.g. op-contracts/v2.0.0-rc.1
  2. Deploy implementation contracts via op-deployer bootstrap for release candidate op-contracts/v2.0.0-rc.1 (this gives us an OPCM deployment address)
  3. Get governance approval and then call the OPCM upgrade function. Upon invoking upgrade, the release candidate suffix is programmatically stripped and the deployments version now becomes op-contracts/v2.0.0. Ref: Contract Versioning - L1 Upgrades

The DeployImplementations script will write the version into the OPCM, along with an rc tag. The rc tag will be programmatically removed when upgrade() is called by the Upgrade Controller multisig, which signifies governance approval.

Is this accurate? If so, I think it's worth adding something similar into this doc.

An alternate approach that we might support that avoids the need for bootstrap commands is using
CREATE2 in the `DeploySuperchain.s.sol` and `DeployImplementations.s.sol` scripts. This way,
deploying contracts is as easy as re-running those scripts—if bytecode for a contract has not changed,
the resulting create2 address is the same, so we can skip deploying that unchanged contract.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By doing this, are we loosing the ability to map all implementation contracts to a single commit? I like the side affect of having all implementations on the same commit. But I see positives in the create2 approach too.

command in op-deployer, and run that new command to deploy the new contracts.

An alternate approach that we might support that avoids the need for bootstrap commands is using
CREATE2 in the `DeploySuperchain.s.sol` and `DeployImplementations.s.sol` scripts. This way,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this idea - there'd be just one bootstrap command to deploy the implementations in this case. We should take care to minimize the amount of configuration required to deploy the implementations so that deploying individual contracts is easy.

Copy link
Contributor

@ajsutton ajsutton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This matches my understanding of the discussions we've been having well. The golden rule for me is "keep develop releasable" - that's the basis of continuous integration and it's really really powerful. We often think we can take a shortcut because we are "100% sure" this change will be in the next release and then we discover something or there's a change of plans and it costs us more to get back to a releasable state than what we saved. And the more you do it the right way, the more tooling/patterns/knowledge we have and the easier it gets.

Comment on lines +51 to +53
A team starts working on a feature. If, and only if, they are 100% sure this feature is going
into the next release, feature work can happen on contracts directly. Otherwise the feature must be
developed using the inheritance pattern described in [`smart-contract-feature-development.md`](../smart-contract-feature-development.md).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would express this as requiring the tip of develop to always be releasable. You can never be 100% sure that a change will go into the next release unless it is merged as a single PR because you never know when an emergency release may be required or something else happens that changes the plan. So basically stuff that's small enough to ship to production with a single PR can just be done, everything else needs some form of feature toggle (the inheritance pattern).

Comment on lines +97 to +100
responsible for running the tests against the `upgrade` method for all mainnet and sepolia chains
that the Security Council + Optimism Foundation (the 2/2) hold keys for. This enforces that
developers are also writing the upgrade logic at the same time, to provide guarantees that the full
test suite passes for new deploys and upgrades for all existing chains.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What exactly are "the tests"? I'd expect most of our e2e tests and possibly even contract tests to fail when used with existing chain data like this.

It would make a lot of sense though to have dedicated "compatibility tests" which can assert a bunch of different invariants. We can then upgrade all chains and check those invariants again. Invariants might be "can't withdraw using an in-progress dispute game" like what the superchain-registry checks (possibly exactly those) but more powerful would be if the invariants can capture data before the upgrade and assert after. Then we can assert invariants like "balance of OptimismPortal is unchanged by the upgrade".

If your feature may not go into the next release, create a child contract such as `contract SystemConfigInterop is SystemConfig`.
In this contract you can add your feature, and once we know it's ready for release it will be merged
into the parent contract it inherits from. Tests for `SystemConfigInterop` would live in `SystemConfigInterop.t.sol`
(TODO flesh out more details on how this should work with OPCM, e.g. do we need an `OPCM*` for each feature, like
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does seem like a really key part of the design. A key part of it will be making it easy to toggle specific parts of upgrades on and off. We will often work on multiple things at once (or change the order things ship) and don't want that to always involve a whole bunch of rebasing/rewriting OPCM child contracts. I'd suggest looking for a way to have upgrade "tasks" that are the smallest unit we might reasonably ship. We make sensible trade offs between flexibility in what we ship and complexity of reasoning about interactions between tasks etc, but importantly we wouldn't just have a Isthmus upgrade task, we'd have separate tasks for each feature in Isthmus.

Then we'd need a way to bundle different tasks together in supported configurations. The OPCM* pattern with child contracts could work well for that. I'm hesitant to just have booleans for it as that tends to lead to a combinatorial explosion of possible configurations. We want the flexibility to change the combination we ship, but to reduce testing cost we want a small set of configurations that are actually "in the development pipeline" so that we can dedicate testing to those.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants