Skip to content

Commit

Permalink
op-chain-ops: fix Go forge script broadcast handling (ethereum-optimi…
Browse files Browse the repository at this point in the history
  • Loading branch information
protolambda authored Sep 10, 2024
1 parent de31478 commit 9d73864
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 58 deletions.
110 changes: 85 additions & 25 deletions op-chain-ops/script/prank.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ package script
import (
"bytes"
"errors"
"fmt"
"math/big"

"github.com/holiman/uint256"

"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"
)

// Prank represents an active prank task for the next sub-call.
Expand Down Expand Up @@ -159,35 +162,92 @@ const (
CallerModeRecurrentPrank
)

// Broadcast captures a transaction that was selected to be broadcasted
type BroadcastType string

const (
BroadcastCall BroadcastType = "call"
BroadcastCreate BroadcastType = "create"
// BroadcastCreate2 is to be broadcast via the Create2Deployer,
// and not really documented much anywhere.
BroadcastCreate2 BroadcastType = "create2"
)

func (bt BroadcastType) String() string {
return string(bt)
}

func (bt BroadcastType) MarshalText() ([]byte, error) {
return []byte(bt.String()), nil
}

func (bt *BroadcastType) UnmarshalText(data []byte) error {
v := BroadcastType(data)
switch v {
case BroadcastCall, BroadcastCreate, BroadcastCreate2:
*bt = v
return nil
default:
return fmt.Errorf("unrecognized broadcast type bytes: %x", data)
}
}

// Broadcast captures a transaction that was selected to be broadcast
// via vm.broadcast(). Actually submitting the transaction is left up
// to other tools.
type Broadcast struct {
From common.Address
To common.Address
Calldata []byte
Value *big.Int
From common.Address `json:"from"`
To common.Address `json:"to"` // set to expected contract address, if this is a deployment
Input hexutil.Bytes `json:"input"` // set to contract-creation code, if this is a deployment
Value *hexutil.U256 `json:"value"`
Salt common.Hash `json:"salt"` // set if this is a Create2 broadcast
Type BroadcastType `json:"type"`
}

// NewBroadcastFromCtx creates a Broadcast from a VM context. This method
// is preferred to manually creating the struct since it correctly handles
// data that must be copied prior to being returned to prevent accidental
// mutation.
func NewBroadcastFromCtx(ctx *vm.ScopeContext) Broadcast {
// Consistently return nil for zero values in order
// for tests to have a deterministic value to compare
// against.
value := ctx.CallValue().ToBig()
if value.Cmp(common.Big0) == 0 {
value = nil
}

// Need to clone CallInput() below since it's used within
// the VM itself elsewhere.
return Broadcast{
From: ctx.Caller(),
To: ctx.Address(),
Calldata: bytes.Clone(ctx.CallInput()),
Value: value,
// NewBroadcast creates a Broadcast from a parent callframe, and the completed child callframe.
// This method is preferred to manually creating the struct since it correctly handles
// data that must be copied prior to being returned to prevent accidental mutation.
func NewBroadcast(parent, current *CallFrame) Broadcast {
ctx := current.Ctx

value := ctx.CallValue()
if value == nil {
value = uint256.NewInt(0)
}

// Code is tracked separate from calldata input,
// even though they are the same thing for a regular contract creation
input := ctx.CallInput()
if ctx.Contract.IsDeployment {
input = ctx.Contract.Code
}

bcast := Broadcast{
From: ctx.Caller(),
To: ctx.Address(),
// Need to clone the input below since memory is reused in the VM
Input: bytes.Clone(input),
Value: (*hexutil.U256)(value.Clone()),
}

switch parent.LastOp {
case vm.CREATE:
bcast.Type = BroadcastCreate
case vm.CREATE2:
bcast.Salt = parent.LastCreate2Salt
initHash := crypto.Keccak256Hash(bcast.Input)
expectedAddr := crypto.CreateAddress2(bcast.From, bcast.Salt, initHash[:])
// Sanity-check the create2 salt is correct by checking the address computation.
if expectedAddr != bcast.To {
panic(fmt.Errorf("script bug: create2 broadcast has "+
"unexpected address: %s, expected %s. Sender: %s, Salt: %s, Inithash: %s",
bcast.To, expectedAddr, bcast.From, bcast.Salt, initHash))
}
bcast.Type = BroadcastCreate2
case vm.CALL:
bcast.Type = BroadcastCall
default:
panic(fmt.Errorf("unexpected broadcast operation %s", parent.LastOp))
}

return bcast
}
36 changes: 24 additions & 12 deletions op-chain-ops/script/script.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package script
import (
"bytes"
"encoding/binary"
"encoding/hex"
"encoding/json"
"fmt"
"math/big"
Expand Down Expand Up @@ -40,6 +39,9 @@ type CallFrame struct {
LastOp vm.OpCode
LastPC uint64

// To reconstruct a create2 later, e.g. on broadcast
LastCreate2Salt [32]byte

// 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.
Expand Down Expand Up @@ -391,17 +393,24 @@ func (h *Host) unwindCallstack(depth int) {
if len(h.callStack) > 1 {
parentCallFrame := h.callStack[len(h.callStack)-2]
if parentCallFrame.Prank != nil {
if parentCallFrame.Prank.Broadcast && parentCallFrame.LastOp != vm.STATICCALL {
currentFrame := h.callStack[len(h.callStack)-1]
bcast := NewBroadcastFromCtx(currentFrame.Ctx)
h.hooks.OnBroadcast(bcast)
h.log.Debug(
"called broadcast hook",
"from", bcast.From,
"to", bcast.To,
"calldata", hex.EncodeToString(bcast.Calldata),
"value", bcast.Value,
)
if parentCallFrame.Prank.Broadcast {
if parentCallFrame.LastOp == vm.DELEGATECALL {
h.log.Warn("Cannot broadcast a delegate-call. Ignoring broadcast hook.")
} else if parentCallFrame.LastOp == vm.STATICCALL {
h.log.Trace("Broadcast is active, ignoring static-call.")
} else {
currentCallFrame := h.callStack[len(h.callStack)-1]
bcast := NewBroadcast(parentCallFrame, currentCallFrame)
h.log.Debug(
"calling broadcast hook",
"from", bcast.From,
"to", bcast.To,
"input", bcast.Input,
"value", bcast.Value,
"type", bcast.Type,
)
h.hooks.OnBroadcast(bcast)
}
}

// While going back to the parent, restore the tx.origin.
Expand Down Expand Up @@ -448,6 +457,9 @@ func (h *Host) onOpcode(pc uint64, op byte, gas, cost uint64, scope tracing.OpCo
}
cf.LastOp = vm.OpCode(op)
cf.LastPC = pc
if cf.LastOp == vm.CREATE2 {
cf.LastCreate2Salt = scopeCtx.Stack.Back(3).Bytes32()
}
}

// onStorageChange is a trace-hook to capture state changes
Expand Down
72 changes: 56 additions & 16 deletions op-chain-ops/script/script_test.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package script

import (
"bytes"
"encoding/json"
"fmt"
"math/big"
"strings"
"testing"

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"

"github.com/holiman/uint256"
"github.com/stretchr/testify/require"

"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/crypto"
"github.com/ethereum/go-ethereum/log"

"github.com/ethereum-optimism/optimism/op-chain-ops/foundry"
Expand Down Expand Up @@ -54,27 +58,58 @@ func TestScriptBroadcast(t *testing.T) {
return data
}

fooBar, err := af.ReadArtifact("ScriptExample.s.sol", "FooBar")
require.NoError(t, err)

expectedInitCode := bytes.Clone(fooBar.Bytecode.Object)
// Add the contract init argument we use in the script
expectedInitCode = append(expectedInitCode, leftPad32(big.NewInt(1234).Bytes())...)
salt := uint256.NewInt(42).Bytes32()

senderAddr := common.HexToAddress("0x5b73C5498c1E3b4dbA84de0F1833c4a029d90519")
expBroadcasts := []Broadcast{
{
From: senderAddr,
To: senderAddr,
Calldata: mustEncodeCalldata("call1", "single_call1"),
From: senderAddr,
To: senderAddr,
Input: mustEncodeCalldata("call1", "single_call1"),
Value: (*hexutil.U256)(uint256.NewInt(0)),
Type: BroadcastCall,
},
{
From: senderAddr,
To: senderAddr,
Calldata: mustEncodeCalldata("call1", "startstop_call1"),
From: common.HexToAddress("0x0000000000000000000000000000000000C0FFEE"),
To: senderAddr,
Input: mustEncodeCalldata("call1", "startstop_call1"),
Value: (*hexutil.U256)(uint256.NewInt(0)),
Type: BroadcastCall,
},
{
From: senderAddr,
To: senderAddr,
Calldata: mustEncodeCalldata("call2", "startstop_call2"),
From: common.HexToAddress("0x0000000000000000000000000000000000C0FFEE"),
To: senderAddr,
Input: mustEncodeCalldata("call2", "startstop_call2"),
Value: (*hexutil.U256)(uint256.NewInt(0)),
Type: BroadcastCall,
},
{
From: senderAddr,
To: senderAddr,
Calldata: mustEncodeCalldata("nested1", "nested"),
From: common.HexToAddress("0x1234"),
To: senderAddr,
Input: mustEncodeCalldata("nested1", "nested"),
Value: (*hexutil.U256)(uint256.NewInt(0)),
Type: BroadcastCall,
},
{
From: common.HexToAddress("0x123456"),
To: crypto.CreateAddress(common.HexToAddress("0x123456"), 0),
Input: expectedInitCode,
Value: (*hexutil.U256)(uint256.NewInt(0)),
Type: BroadcastCreate,
},
{
From: common.HexToAddress("0xcafe"),
To: crypto.CreateAddress2(common.HexToAddress("0xcafe"), salt, crypto.Keccak256(expectedInitCode)),
Input: expectedInitCode,
Value: (*hexutil.U256)(uint256.NewInt(0)),
Type: BroadcastCreate2,
Salt: salt,
},
}

Expand All @@ -92,5 +127,10 @@ func TestScriptBroadcast(t *testing.T) {
input := bytes4("runBroadcast()")
returnData, _, err := h.Call(scriptContext.Sender, addr, input[:], DefaultFoundryGasLimit, uint256.NewInt(0))
require.NoError(t, err, "call failed: %x", string(returnData))
require.EqualValues(t, expBroadcasts, broadcasts)

expected, err := json.MarshalIndent(expBroadcasts, " ", " ")
require.NoError(t, err)
got, err := json.MarshalIndent(broadcasts, " ", " ")
require.NoError(t, err)
require.Equal(t, string(expected), string(got))
}
25 changes: 23 additions & 2 deletions op-chain-ops/script/testdata/scripts/ScriptExample.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ interface Vm {
function startPrank(address msgSender) external;
function stopPrank() external;
function broadcast() external;
function broadcast(address msgSender) external;
function startBroadcast(address msgSender) external;
function startBroadcast() external;
function stopBroadcast() external;
}
Expand Down Expand Up @@ -104,17 +106,28 @@ contract ScriptExample {
this.call2("single_call2");

console.log("testing start/stop");
vm.startBroadcast();
vm.startBroadcast(address(uint160(0xc0ffee)));
this.call1("startstop_call1");
this.call2("startstop_call2");
this.callPure("startstop_pure");
vm.stopBroadcast();
this.call1("startstop_call3");

console.log("testing nested");
vm.startBroadcast();
vm.startBroadcast(address(uint160(0x1234)));
this.nested1("nested");
vm.stopBroadcast();

console.log("contract deployment");
vm.broadcast(address(uint160(0x123456)));
FooBar x = new FooBar(1234);
require(x.foo() == 1234);

console.log("create 2");
vm.broadcast(address(uint160(0xcafe)));
FooBar y = new FooBar{salt: bytes32(uint256(42))}(1234);
require(y.foo() == 1234);
console.log("done!");
}

/// @notice example external function, to force a CALL, and test vm.startPrank with.
Expand Down Expand Up @@ -147,3 +160,11 @@ contract ScriptExample {
console.log(_v);
}
}

contract FooBar {
uint256 public foo;

constructor(uint256 v) {
foo = v;
}
}

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

0 comments on commit 9d73864

Please sign in to comment.