Commit 9d738648 authored by protolambda's avatar protolambda Committed by GitHub

op-chain-ops: fix Go forge script broadcast handling (#11832)

parent de31478b
......@@ -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.
......@@ -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
}
......@@ -3,7 +3,6 @@ package script
import (
"bytes"
"encoding/binary"
"encoding/hex"
"encoding/json"
"fmt"
"math/big"
......@@ -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.
......@@ -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.
......@@ -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
......
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"
......@@ -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,
},
}
......@@ -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))
}
......@@ -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;
}
......@@ -104,7 +106,7 @@ 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");
......@@ -112,9 +114,20 @@ contract ScriptExample {
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.
......@@ -147,3 +160,11 @@ contract ScriptExample {
console.log(_v);
}
}
contract FooBar {
uint256 public foo;
constructor(uint256 v) {
foo = v;
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
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