Commit 49a4e9fa authored by protolambda's avatar protolambda Committed by GitHub

op-chain-ops: prep / utils for interop genesis work (#11790)

parent afcc51a4
......@@ -55,18 +55,26 @@ type SuperchainOperatorRole uint64
const (
// SuperchainDeployerKey is the deployer of the superchain contracts.
SuperchainDeployerKey SuperchainOperatorRole = 0
// SuperchainProxyAdminOwner is the key that owns the superchain ProxyAdmin
SuperchainProxyAdminOwner SuperchainOperatorRole = 1
// SuperchainConfigGuardianKey is the Guardian of the SuperchainConfig.
SuperchainConfigGuardianKey SuperchainOperatorRole = 1
SuperchainConfigGuardianKey SuperchainOperatorRole = 2
// SuperchainProtocolVersionsOwner is the key that can make ProtocolVersions changes.
SuperchainProtocolVersionsOwner SuperchainOperatorRole = 3
// DependencySetManagerKey is the key used to manage the dependency set of a superchain.
DependencySetManagerKey SuperchainOperatorRole = 2
DependencySetManagerKey SuperchainOperatorRole = 4
)
func (role SuperchainOperatorRole) String() string {
switch role {
case SuperchainDeployerKey:
return "superchain-deployer"
case SuperchainProxyAdminOwner:
return "superchain-proxy-admin-owner"
case SuperchainConfigGuardianKey:
return "superchain-config-guardian"
case SuperchainProtocolVersionsOwner:
return "superchain-protocol-versions-owner"
case DependencySetManagerKey:
return "dependency-set-manager"
default:
......@@ -122,6 +130,8 @@ const (
L1FeeVaultRecipientRole ChainOperatorRole = 8
// SequencerFeeVaultRecipientRole is the key that receives form the SequencerFeeVault predeploy
SequencerFeeVaultRecipientRole ChainOperatorRole = 9
// SystemConfigOwner is the key that can make SystemConfig changes.
SystemConfigOwner ChainOperatorRole = 10
)
func (role ChainOperatorRole) String() string {
......@@ -146,6 +156,8 @@ func (role ChainOperatorRole) String() string {
return "l1-fee-vault-recipient"
case SequencerFeeVaultRecipientRole:
return "sequencer-fee-vault-recipient"
case SystemConfigOwner:
return "system-config-owner"
default:
return fmt.Sprintf("unknown-operator-%d", uint64(role))
}
......
......@@ -10,11 +10,15 @@ import (
"strings"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
)
var setterFnSig = "set(bytes4,address)"
var setterFnBytes4 = bytes4(setterFnSig)
// precompileFunc is a prepared function to perform a method call / field read with ABI decoding/encoding.
type precompileFunc struct {
goName string
......@@ -52,12 +56,20 @@ func rightPad32(data []byte) []byte {
return append(out, make([]byte, 32-(len(out)%32))...)
}
type settableField struct {
name string
value *reflect.Value
}
// Precompile is a wrapper around a Go object, making it a precompile.
type Precompile[E any] struct {
Precompile E
fieldsOnly bool
fieldSetter bool
settable map[[4]byte]*settableField
// abiMethods is effectively the jump-table for 4-byte ABI calls to the precompile.
abiMethods map[[4]byte]*precompileFunc
}
......@@ -70,6 +82,10 @@ func WithFieldsOnly[E any](p *Precompile[E]) {
p.fieldsOnly = true
}
func WithFieldSetter[E any](p *Precompile[E]) {
p.fieldSetter = true
}
// NewPrecompile wraps a Go object into a Precompile.
// All exported fields and methods will have a corresponding ABI interface.
// Fields with a tag `evm:"-"` will be ignored, or can override their ABI name to x with this tag: `evm:"x"`.
......@@ -80,9 +96,11 @@ func WithFieldsOnly[E any](p *Precompile[E]) {
// All precompile methods have 0 gas cost.
func NewPrecompile[E any](e E, opts ...PrecompileOption[E]) (*Precompile[E], error) {
out := &Precompile[E]{
Precompile: e,
abiMethods: make(map[[4]byte]*precompileFunc),
fieldsOnly: false,
Precompile: e,
abiMethods: make(map[[4]byte]*precompileFunc),
fieldsOnly: false,
fieldSetter: false,
settable: make(map[[4]byte]*settableField),
}
for _, opt := range opts {
opt(out)
......@@ -96,6 +114,8 @@ func NewPrecompile[E any](e E, opts ...PrecompileOption[E]) (*Precompile[E], err
if err := out.setupFields(&elemVal); err != nil {
return nil, fmt.Errorf("failed to setup fields of precompile: %w", err)
}
// create setter that can handle of the fields
out.setupFieldSetter()
return out, nil
}
......@@ -496,9 +516,42 @@ func (p *Precompile[E]) setupStructField(fieldDef *reflect.StructField, fieldVal
abiSignature: methodSig,
fn: fn,
}
// register field as settable
if p.fieldSetter && fieldDef.Type.AssignableTo(typeFor[common.Address]()) {
p.settable[byte4Sig] = &settableField{
name: fieldDef.Name,
value: fieldVal,
}
}
return nil
}
func (p *Precompile[E]) setupFieldSetter() {
if !p.fieldSetter {
return
}
p.abiMethods[setterFnBytes4] = &precompileFunc{
goName: "__fieldSetter___",
abiSignature: setterFnSig,
fn: func(input []byte) ([]byte, error) {
if len(input) != 32*2 {
return nil, fmt.Errorf("cannot set address field to %d bytes", len(input))
}
if [32 - 4]byte(input[4:32]) != ([32 - 4]byte{}) {
return nil, fmt.Errorf("unexpected selector content, input: %x", input[:])
}
selector := [4]byte(input[:4])
f, ok := p.settable[selector]
if !ok {
return nil, fmt.Errorf("unknown address field selector 0x%x", selector)
}
addr := common.Address(input[32*2-20 : 32*2])
f.value.Set(reflect.ValueOf(addr))
return nil, nil
},
}
}
// RequiredGas is part of the vm.PrecompiledContract interface, and all system precompiles use 0 gas.
func (p *Precompile[E]) RequiredGas(input []byte) uint64 {
return 0
......
......@@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
)
......@@ -119,3 +120,24 @@ func TestPrecompile(t *testing.T) {
require.NoError(t, err)
require.Equal(t, b32((42+100+7)*3), out)
}
type DeploymentExample struct {
FooBar common.Address
}
func TestDeploymentOutputPrecompile(t *testing.T) {
e := &DeploymentExample{}
p, err := NewPrecompile[*DeploymentExample](e, WithFieldSetter[*DeploymentExample])
require.NoError(t, err)
addr := common.Address{0: 0x42, 19: 0xaa}
fooBarSelector := bytes4("fooBar()")
var input []byte
input = append(input, setterFnBytes4[:]...)
input = append(input, rightPad32(fooBarSelector[:])...)
input = append(input, leftPad32(addr[:])...)
out, err := p.Run(input)
require.NoError(t, err)
require.Empty(t, out)
require.Equal(t, addr, e.FooBar)
}
......@@ -29,6 +29,9 @@ import (
"github.com/ethereum-optimism/optimism/op-chain-ops/srcmap"
)
// jumpHistory is the amount of successful jumps to track for debugging.
const jumpHistory = 5
// CallFrame encodes the scope context of the current call
type CallFrame struct {
Depth int
......@@ -39,7 +42,9 @@ type CallFrame struct {
// Reverts often happen in generated code.
// We want to fallback to logging the source-map position of
// the non-generated code, i.e. the origin of the last successful jump.
LastJumpPC uint64
// And beyond that, a short history of the latest jumps is useful for debugging.
// This is a list of program-counters at the time of the jump (i.e. before raching JUMPDEST).
LastJumps []uint64
Ctx *vm.ScopeContext
......@@ -79,6 +84,8 @@ type Host struct {
// src-maps are disabled if this is nil.
srcFS *foundry.SourceMapFS
srcMaps map[common.Address]*srcmap.SourceMap
onLabel []func(name string, addr common.Address)
}
// NewHost creates a Host that can load contracts from the given Artifacts FS,
......@@ -378,11 +385,10 @@ func (h *Host) onOpcode(pc uint64, op byte, gas, cost uint64, scope tracing.OpCo
// We do this here, instead of onEnter, to capture an initialized scope.
if len(h.callStack) == 0 || h.callStack[len(h.callStack)-1].Depth < depth {
h.callStack = append(h.callStack, CallFrame{
Depth: depth,
LastOp: vm.OpCode(op),
LastPC: pc,
LastJumpPC: pc,
Ctx: scopeCtx,
Depth: depth,
LastOp: vm.OpCode(op),
LastPC: pc,
Ctx: scopeCtx,
})
}
// Sanity check that top of the call-stack matches the scope context now
......@@ -391,7 +397,11 @@ func (h *Host) onOpcode(pc uint64, op byte, gas, cost uint64, scope tracing.OpCo
}
cf := &h.callStack[len(h.callStack)-1]
if vm.OpCode(op) == vm.JUMPDEST { // remember the last PC before successful jump
cf.LastJumpPC = cf.LastPC
cf.LastJumps = append(cf.LastJumps, cf.LastPC)
if len(cf.LastJumps) > jumpHistory {
copy(cf.LastJumps[:], cf.LastJumps[len(cf.LastJumps)-jumpHistory:])
cf.LastJumps = cf.LastJumps[:jumpHistory]
}
}
cf.LastOp = vm.OpCode(op)
cf.LastPC = pc
......@@ -527,10 +537,14 @@ func (h *Host) EnforceMaxCodeSize(v bool) {
func (h *Host) LogCallStack() {
for _, cf := range h.callStack {
callsite := ""
if srcMap, ok := h.srcMaps[cf.Ctx.Address()]; ok {
srcMap, ok := h.srcMaps[cf.Ctx.Address()]
if !ok && cf.Ctx.Contract.CodeAddr != nil { // if delegate-call, we might know the implementation code.
srcMap, ok = h.srcMaps[*cf.Ctx.Contract.CodeAddr]
}
if ok {
callsite = srcMap.FormattedInfo(cf.LastPC)
if callsite == "unknown:0:0" {
callsite = srcMap.FormattedInfo(cf.LastJumpPC)
if callsite == "unknown:0:0" && len(cf.LastJumps) > 0 {
callsite = srcMap.FormattedInfo(cf.LastJumps[len(cf.LastJumps)-1])
}
}
input := cf.Ctx.CallInput()
......@@ -538,9 +552,14 @@ func (h *Host) LogCallStack() {
if len(input) >= 4 {
byte4 = fmt.Sprintf("0x%x", input[:4])
}
h.log.Debug("callframe", "depth", cf.Depth, "input", hexutil.Bytes(input), "pc", cf.LastPC, "op", cf.LastOp)
h.log.Debug("callframe input", "depth", cf.Depth, "input", hexutil.Bytes(input), "pc", cf.LastPC, "op", cf.LastOp)
h.log.Warn("callframe", "depth", cf.Depth, "byte4", byte4,
"addr", cf.Ctx.Address(), "callsite", callsite, "label", h.labels[cf.Ctx.Address()])
if srcMap != nil {
for _, jmpPC := range cf.LastJumps {
h.log.Debug("recent jump", "depth", cf.Depth, "callsite", srcMap.FormattedInfo(jmpPC), "pc", jmpPC)
}
}
}
}
......@@ -548,4 +567,40 @@ func (h *Host) LogCallStack() {
func (h *Host) Label(addr common.Address, label string) {
h.log.Debug("labeling", "addr", addr, "label", label)
h.labels[addr] = label
for _, fn := range h.onLabel {
fn(label, addr)
}
}
// NewScriptAddress creates a new address for the ScriptDeployer account, and bumps the nonce.
func (h *Host) NewScriptAddress() common.Address {
deployer := ScriptDeployer
deployNonce := h.state.GetNonce(deployer)
// compute address of script contract to be deployed
addr := crypto.CreateAddress(deployer, deployNonce)
h.state.SetNonce(deployer, deployNonce+1)
return addr
}
func (h *Host) ChainID() *big.Int {
return new(big.Int).Set(h.chainCfg.ChainID)
}
func (h *Host) Artifacts() *foundry.ArtifactsFS {
return h.af
}
// RememberOnLabel links the contract source-code of srcFile upon a given label
func (h *Host) RememberOnLabel(label, srcFile, contract string) error {
artifact, err := h.af.ReadArtifact(srcFile, contract)
if err != nil {
return fmt.Errorf("failed to read artifact %s (contract %s) for label %q", srcFile, contract, label)
}
h.onLabel = append(h.onLabel, func(v string, addr common.Address) {
if label == v {
h.RememberArtifact(addr, artifact, contract)
}
})
return nil
}
......@@ -120,6 +120,10 @@ type SourceMap struct {
// This location is the source file-path, the line number, and column number.
// This may return an error, as the source-file is lazy-loaded to calculate the position data.
func (s *SourceMap) Info(pc uint64) (source string, line uint32, col uint32, err error) {
if pc >= uint64(len(s.Instr)) {
source = "invalid"
return
}
instr := s.Instr[pc]
if instr.F < 0 || instr == (InstrMapping{}) {
return "generated", 0, 0, nil
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment