Commit 85d8c86e authored by protolambda's avatar protolambda Committed by GitHub

op-chain-ops: forge script fixes and improvements (#11577)

parent 36279371
......@@ -234,7 +234,7 @@ require (
rsc.io/tmplfunc v0.0.3 // indirect
)
replace github.com/ethereum/go-ethereum v1.14.8 => github.com/ethereum-optimism/op-geth v1.101408.0-rc.4
replace github.com/ethereum/go-ethereum v1.14.8 => github.com/ethereum-optimism/op-geth v1.101408.0-rc.4.0.20240822213944-6c8de76e0720
// replace github.com/ethereum/go-ethereum => ../op-geth
......
......@@ -176,8 +176,8 @@ github.com/elastic/gosigar v0.14.2 h1:Dg80n8cr90OZ7x+bAax/QjoW/XqTI11RmA79ZwIm9/
github.com/elastic/gosigar v0.14.2/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs=
github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.3 h1:RWHKLhCrQThMfch+QJ1Z8veEq5ZO3DfIhZ7xgRP9WTc=
github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.3/go.mod h1:QziizLAiF0KqyLdNJYD7O5cpDlaFMNZzlxYNcWsJUxs=
github.com/ethereum-optimism/op-geth v1.101408.0-rc.4 h1:OhiSpP+IOoKe+9chfHYjQFFwGruLT9Uh52+LFk4y6ms=
github.com/ethereum-optimism/op-geth v1.101408.0-rc.4/go.mod h1:Mk8AhvlqFbjI9oW2ymThSSoqc6kiEH0/tCmHGMEu6ac=
github.com/ethereum-optimism/op-geth v1.101408.0-rc.4.0.20240822213944-6c8de76e0720 h1:PlMldvODGzwEBLRpK/mVUdrVa9LEN1cC0j5nKk5q7Jg=
github.com/ethereum-optimism/op-geth v1.101408.0-rc.4.0.20240822213944-6c8de76e0720/go.mod h1:Mk8AhvlqFbjI9oW2ymThSSoqc6kiEH0/tCmHGMEu6ac=
github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20240821192748-42bd03ba8313 h1:SVSFg8ccdRBJxOdRS1pK8oIHvMufiPAQz1gkQsEPnZc=
github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20240821192748-42bd03ba8313/go.mod h1:XaVXL9jg8BcyOeugECgIUGa9Y3DjYJj71RHmb5qon6M=
github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA=
......
......@@ -153,7 +153,8 @@ func hydrateBindingsField(
// Decodes the result of the backend into values to return as function, including error/revert handling.
outDecodeFn := func(result []byte, resultErr error) []reflect.Value {
if resultErr != nil {
if errors.Is(resultErr, vm.ErrExecutionReverted) {
// Empty return-data might happen on a regular description-less revert. No need to unpack in that case.
if len(result) > 0 && errors.Is(resultErr, vm.ErrExecutionReverted) {
msg, err := abi.UnpackRevert(result)
if err != nil {
return returnErr(fmt.Errorf("failed to unpack result args: %w", err))
......
package script
import (
"bytes"
"math/big"
"github.com/holiman/uint256"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
)
......@@ -64,7 +66,7 @@ func (c *CheatCodesPrecompile) Load(account common.Address, slot [32]byte) [32]b
// Etch implements https://book.getfoundry.sh/cheatcodes/etch
func (c *CheatCodesPrecompile) Etch(who common.Address, code []byte) {
c.h.state.SetCode(who, code)
c.h.state.SetCode(who, bytes.Clone(code)) // important to clone; geth EVM will reuse the calldata memory.
}
// Deal implements https://book.getfoundry.sh/cheatcodes/deal
......@@ -138,6 +140,17 @@ func (c *CheatCodesPrecompile) GetNonce(addr common.Address) uint64 {
return c.h.state.GetNonce(addr)
}
func (c *CheatCodesPrecompile) ResetNonce(addr common.Address) {
// Undocumented cheatcode of forge, but used a lot.
// Resets nonce to 0 if EOA, or 1 if contract.
// In scripts often set code to empty first when using it, it then becomes 0.
if c.h.state.GetCodeHash(addr) == types.EmptyCodeHash {
c.h.state.SetNonce(addr, 0)
} else {
c.h.state.SetNonce(addr, 1)
}
}
// MockCall_b96213e4 implements https://book.getfoundry.sh/cheatcodes/mock-call
func (c *CheatCodesPrecompile) MockCall_b96213e4(where common.Address, data []byte, retdata []byte) error {
panic("mockCall not supported")
......@@ -189,7 +202,7 @@ func (c *CheatCodesPrecompile) StartBroadcast_7fb5297f() error {
// StartBroadcast_7fec2a8d implements https://book.getfoundry.sh/cheatcodes/start-broadcast
func (c *CheatCodesPrecompile) StartBroadcast_7fec2a8d(who common.Address) error {
c.h.log.Info("starting repeat-broadcast", "who", who)
return c.h.Prank(nil, nil, true, true)
return c.h.Prank(&who, nil, true, true)
}
// StopBroadcast implements https://book.getfoundry.sh/cheatcodes/stop-broadcast
......
package script
import (
"bytes"
"encoding/json"
"errors"
"fmt"
......@@ -36,7 +37,7 @@ func (c *CheatCodesPrecompile) ProjectRoot() string {
func (c *CheatCodesPrecompile) getArtifact(input string) (*foundry.Artifact, error) {
// fetching by relative file path, or using a contract version, is not supported
parts := strings.SplitN(input, ":", 1)
parts := strings.SplitN(input, ":", 2)
name := parts[0] + ".sol"
contract := parts[0]
if len(parts) == 2 {
......@@ -52,7 +53,7 @@ func (c *CheatCodesPrecompile) GetCode(input string) ([]byte, error) {
if err != nil {
return nil, err
}
return artifact.Bytecode.Object, nil
return bytes.Clone(artifact.Bytecode.Object), nil
}
// GetDeployedCode implements https://book.getfoundry.sh/cheatcodes/get-deployed-code
......@@ -61,7 +62,7 @@ func (c *CheatCodesPrecompile) GetDeployedCode(input string) ([]byte, error) {
if err != nil {
return nil, err
}
return artifact.DeployedBytecode.Object, nil
return bytes.Clone(artifact.DeployedBytecode.Object), nil
}
// Sleep implements https://book.getfoundry.sh/cheatcodes/sleep
......
......@@ -2,11 +2,6 @@ package script
import (
"errors"
"fmt"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum-optimism/optimism/op-chain-ops/foundry"
)
func (c *CheatCodesPrecompile) LoadAllocs(pathToAllocsJson string) error {
......@@ -17,23 +12,10 @@ func (c *CheatCodesPrecompile) LoadAllocs(pathToAllocsJson string) error {
func (c *CheatCodesPrecompile) DumpState(pathToStateJson string) error {
c.h.log.Info("dumping state", "target", pathToStateJson)
// We have to commit the existing state to the trie,
// for all the state-changes to be captured by the trie iterator.
root, err := c.h.state.Commit(c.h.env.Context.BlockNumber.Uint64(), true)
allocs, err := c.h.StateDump()
if err != nil {
return fmt.Errorf("failed to commit state: %w", err)
return err
}
// We need a state object around the state DB
st, err := state.New(root, c.h.stateDB, nil)
if err != nil {
return fmt.Errorf("failed to create state object for state-dumping: %w", err)
}
// After Commit we cannot reuse the old State, so we update the host to use the new one
c.h.state = st
c.h.env.StateDB = st
var allocs foundry.ForgeAllocs
allocs.FromState(st)
// This may be written somewhere in the future (or run some callback to collect the state dump)
_ = allocs
c.h.log.Info("state-dumping is not supported, but have state",
......
......@@ -37,7 +37,7 @@ func (c *CheatCodesPrecompile) Skip() error {
// Label implements https://book.getfoundry.sh/cheatcodes/label
func (c *CheatCodesPrecompile) Label(addr common.Address, label string) {
c.h.labels[addr] = label
c.h.Label(addr, label)
}
// GetLabel implements https://book.getfoundry.sh/cheatcodes/get-label
......
......@@ -17,6 +17,8 @@ var (
// ConsoleAddr is known as CONSOLE, "console.log" in ascii.
// Utils like console.sol and console2.sol work by executing a staticcall to this address.
ConsoleAddr = common.HexToAddress("0x000000000000000000636F6e736F6c652e6c6f67")
// ScriptDeployer is used for temporary scripts address(uint160(uint256(keccak256("op-stack script deployer"))))
ScriptDeployer = common.HexToAddress("0x76Ce131128F3616871f8CDA86d18fAB44E4d0D8B")
)
const (
......@@ -25,25 +27,25 @@ const (
)
type Context struct {
chainID *big.Int
sender common.Address
origin common.Address
feeRecipient common.Address
gasLimit uint64
blockNum uint64
timestamp uint64
prevRandao common.Hash
blobHashes []common.Hash
ChainID *big.Int
Sender common.Address
Origin common.Address
FeeRecipient common.Address
GasLimit uint64
BlockNum uint64
Timestamp uint64
PrevRandao common.Hash
BlobHashes []common.Hash
}
var DefaultContext = Context{
chainID: big.NewInt(1337),
sender: DefaultSenderAddr,
origin: DefaultSenderAddr,
feeRecipient: common.Address{},
gasLimit: DefaultFoundryGasLimit,
blockNum: 0,
timestamp: 0,
prevRandao: common.Hash{},
blobHashes: []common.Hash{},
ChainID: big.NewInt(1337),
Sender: DefaultSenderAddr,
Origin: DefaultSenderAddr,
FeeRecipient: common.Address{},
GasLimit: DefaultFoundryGasLimit,
BlockNum: 0,
Timestamp: 0,
PrevRandao: common.Hash{},
BlobHashes: []common.Hash{},
}
package script
import (
"errors"
"math/big"
"github.com/holiman/uint256"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/vm"
)
// Prank represents an active prank task for the next sub-call.
// This is embedded into a call-frame, to then influence the sub-call through a caller-override.
type Prank struct {
// Sender overrides msg.sender
Sender *common.Address
// Origin overrides tx.origin (set to actual origin if not part of the prank)
Origin *common.Address
// PrevOrigin is the tx.origin to restore after the prank
PrevOrigin common.Address
// Repeat is true if the prank persists after returning from a sub-call
Repeat bool
// A Prank may be a broadcast also.
Broadcast bool
}
// prankRef implements the vm.ContractRef interface, to mock a caller.
type prankRef struct {
prank common.Address
ref vm.ContractRef
}
var _ vm.ContractRef = (*prankRef)(nil)
func (p *prankRef) Address() common.Address {
return p.prank
}
// Value returns the value send into this contract context.
// The delegate call tracer implicitly relies on this being implemented on ContractRef
func (p *prankRef) Value() *uint256.Int {
return p.ref.(interface{ Value() *uint256.Int }).Value()
}
func (h *Host) handleCaller(caller vm.ContractRef) vm.ContractRef {
// apply prank, if top call-frame had set up a prank
if len(h.callStack) > 0 {
parentCallFrame := h.callStack[len(h.callStack)-1]
if parentCallFrame.Prank != nil && caller.Address() != VMAddr { // pranks do not apply to the cheatcode precompile
if parentCallFrame.Prank.Sender != nil {
return &prankRef{
prank: *parentCallFrame.Prank.Sender,
ref: caller,
}
}
if parentCallFrame.Prank.Origin != nil {
h.env.TxContext.Origin = *parentCallFrame.Prank.Origin
}
}
}
return caller
}
// Prank applies a prank to the current call-frame.
// Any sub-call will apply the prank to their frame context.
func (h *Host) Prank(msgSender *common.Address, txOrigin *common.Address, repeat bool, broadcast bool) error {
if len(h.callStack) == 0 {
h.log.Warn("no call stack")
return nil // cannot prank while not in a call.
}
cf := &h.callStack[len(h.callStack)-1]
if cf.Prank != nil {
if cf.Prank.Broadcast && !broadcast {
return errors.New("you have an active broadcast; broadcasting and pranks are not compatible")
}
if !cf.Prank.Broadcast && broadcast {
return errors.New("you have an active prank; broadcasting and pranks are not compatible")
}
}
h.log.Warn("prank", "sender", msgSender)
cf.Prank = &Prank{
Sender: msgSender,
Origin: txOrigin,
PrevOrigin: h.env.TxContext.Origin,
Repeat: repeat,
Broadcast: broadcast,
}
return nil
}
// StopPrank disables the current prank. Any sub-call will not be pranked.
func (h *Host) StopPrank(broadcast bool) error {
if len(h.callStack) == 0 {
return nil
}
cf := &h.callStack[len(h.callStack)-1]
if cf.Prank == nil {
if broadcast {
return errors.New("no broadcast in progress to stop")
}
return nil
}
if cf.Prank.Broadcast && !broadcast {
// stopPrank on active broadcast is silent and no-op
return nil
}
if !cf.Prank.Broadcast && broadcast {
return errors.New("no broadcast in progress to stop")
}
cf.Prank = nil
return nil
}
// CallerMode returns the type of the top-most callframe,
// i.e. if we are in regular operation, a prank, or a broadcast (special kind of prank).
func (h *Host) CallerMode() CallerMode {
if len(h.callStack) == 0 {
return CallerModeNone
}
cf := &h.callStack[len(h.callStack)-1]
if cf.Prank != nil {
if cf.Prank.Broadcast {
if cf.Prank.Repeat {
return CallerModeRecurrentBroadcast
}
return CallerModeBroadcast
}
if cf.Prank.Repeat {
return CallerModeRecurrentPrank
}
return CallerModePrank
}
return CallerModeNone
}
// CallerMode matches the CallerMode forge cheatcode enum.
type CallerMode uint8
func (cm CallerMode) Big() *big.Int {
return big.NewInt(int64(cm))
}
const (
CallerModeNone CallerMode = iota
CallerModeBroadcast
CallerModeRecurrentBroadcast
CallerModePrank
CallerModeRecurrentPrank
)
......@@ -10,6 +10,7 @@ import (
"strings"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
)
......@@ -55,22 +56,37 @@ func rightPad32(data []byte) []byte {
type Precompile[E any] struct {
Precompile E
fieldsOnly bool
// abiMethods is effectively the jump-table for 4-byte ABI calls to the precompile.
abiMethods map[[4]byte]*precompileFunc
}
var _ vm.PrecompiledContract = (*Precompile[struct{}])(nil)
type PrecompileOption[E any] func(p *Precompile[E])
func WithFieldsOnly[E any](p *Precompile[E]) {
p.fieldsOnly = 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.
// Fields with a tag `evm:"-"` will be ignored, or can override their ABI name to x with this tag: `evm:"x"`.
// Field names and method names are adjusted to start with a lowercase character in the ABI signature.
// Method names may end with a `_X` where X must be the 4byte selector (this is sanity-checked),
// to support multiple variants of the same method with different ABI input parameters.
// Methods may return an error, which will result in a revert, rather than become an ABI encoded arg, if not nil.
// All precompile methods have 0 gas cost.
func NewPrecompile[E any](e E) (*Precompile[E], error) {
out := &Precompile[E]{Precompile: e, abiMethods: make(map[[4]byte]*precompileFunc)}
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,
}
for _, opt := range opts {
opt(out)
}
elemVal := reflect.ValueOf(e)
// setup methods (and if pointer, the indirect methods also)
if err := out.setupMethods(&elemVal); err != nil {
......@@ -85,6 +101,9 @@ func NewPrecompile[E any](e E) (*Precompile[E], error) {
// setupMethods iterates through all exposed methods of val, and sets them all up as ABI methods.
func (p *Precompile[E]) setupMethods(val *reflect.Value) error {
if p.fieldsOnly {
return nil
}
typ := val.Type()
methodCount := val.NumMethod()
for i := 0; i < methodCount; i++ {
......@@ -431,6 +450,10 @@ func (p *Precompile[E]) setupStructField(fieldDef *reflect.StructField, fieldVal
if lo := strings.ToLower(abiFunctionName[:1]); lo != abiFunctionName[:1] {
abiFunctionName = lo + abiFunctionName[1:]
}
// The tag can override the field name
if v, ok := fieldDef.Tag.Lookup("evm"); ok {
abiFunctionName = v
}
// The ABI signature of public fields in solidity is simply a getter function of the same name.
// The return type is not part of the ABI signature. So we just append "()" to turn it into a function.
methodSig := abiFunctionName + "()"
......@@ -455,7 +478,14 @@ func (p *Precompile[E]) setupStructField(fieldDef *reflect.StructField, fieldVal
if len(input) != 0 { // 4 byte selector is already trimmed
return nil, fmt.Errorf("unexpected input: %x", input)
}
outData, err := outArgs.PackValues([]any{fieldVal.Interface()})
v := fieldVal.Interface()
if abiVal, ok := v.(interface{ ToABI() []byte }); ok {
return abiVal.ToABI(), nil
}
if bigInt, ok := v.(*hexutil.Big); ok { // We can change this to use convertType later, if we need more generic type handling.
v = (*big.Int)(bigInt)
}
outData, err := outArgs.PackValues([]any{v})
if err != nil {
return nil, fmt.Errorf("method %s failed to pack return data: %w", methodSig, err)
}
......
This diff is collapsed.
......@@ -19,7 +19,7 @@ func TestScript(t *testing.T) {
af := foundry.OpenArtifactsDir("./testdata/test-artifacts")
scriptContext := DefaultContext
h := NewHost(logger, af, scriptContext)
h := NewHost(logger, af, nil, scriptContext)
addr, err := h.LoadContract("ScriptExample.s.sol", "ScriptExample")
require.NoError(t, err)
......@@ -27,7 +27,7 @@ func TestScript(t *testing.T) {
h.SetEnvVar("EXAMPLE_BOOL", "true")
input := bytes4("run()")
returnData, _, err := h.Call(scriptContext.sender, addr, input[:], DefaultFoundryGasLimit, uint256.NewInt(0))
returnData, _, err := h.Call(scriptContext.Sender, addr, input[:], DefaultFoundryGasLimit, uint256.NewInt(0))
require.NoError(t, err, "call failed: %x", string(returnData))
require.NotNil(t, captLog.FindLog(
testlog.NewAttributesFilter("p0", "sender nonce"),
......
package script
import (
"fmt"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
)
func checkABI(abiData *abi.ABI, methodSignature string) bool {
for _, m := range abiData.Methods {
if m.Sig == methodSignature {
return true
}
}
return false
}
// WithScript deploys a script contract, at a create-address based on the ScriptDeployer.
// The returned cleanup function wipes the script account again (but not the storage).
func WithScript[B any](h *Host, name string, contract string) (b *B, cleanup func(), err error) {
// load contract artifact
artifact, err := h.af.ReadArtifact(name, contract)
if err != nil {
return nil, nil, fmt.Errorf("could not load script artifact: %w", err)
}
deployer := ScriptDeployer
deployNonce := h.state.GetNonce(deployer)
// compute address of script contract to be deployed
addr := crypto.CreateAddress(deployer, deployNonce)
// init bindings (with ABI check)
bindings, err := MakeBindings[B](h.ScriptBackendFn(addr), func(abiDef string) bool {
return checkABI(&artifact.ABI, abiDef)
})
if err != nil {
return nil, nil, fmt.Errorf("failed to make bindings: %w", err)
}
// Scripts can be very large
h.EnforceMaxCodeSize(false)
defer h.EnforceMaxCodeSize(true)
// deploy the script contract
deployedAddr, err := h.Create(deployer, artifact.Bytecode.Object)
if err != nil {
return nil, nil, fmt.Errorf("failed to deploy script: %w", err)
}
if deployedAddr != addr {
return nil, nil, fmt.Errorf("deployed to unexpected address %s, expected %s", deployedAddr, addr)
}
h.RememberArtifact(addr, artifact, contract)
h.Label(addr, contract)
return bindings, func() {
h.Wipe(addr)
}, nil
}
// WithPrecompileAtAddress turns a struct into a precompile,
// and inserts it as override at the given address in the host.
// A cleanup function is returned, to remove the precompile override again.
func WithPrecompileAtAddress[E any](h *Host, addr common.Address, elem E, opts ...PrecompileOption[E]) (cleanup func(), err error) {
if h.HasPrecompileOverride(addr) {
return nil, fmt.Errorf("already have existing precompile override at %s", addr)
}
precompile, err := NewPrecompile[E](elem, opts...)
if err != nil {
return nil, fmt.Errorf("failed to construct precompile: %w", err)
}
h.SetPrecompile(addr, precompile)
h.Label(addr, fmt.Sprintf("%T", precompile.Precompile))
return func() {
h.SetPrecompile(addr, nil)
}, 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