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 ...@@ -3,12 +3,15 @@ package script
import ( import (
"bytes" "bytes"
"errors" "errors"
"fmt"
"math/big" "math/big"
"github.com/holiman/uint256" "github.com/holiman/uint256"
"github.com/ethereum/go-ethereum/common" "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/core/vm"
"github.com/ethereum/go-ethereum/crypto"
) )
// Prank represents an active prank task for the next sub-call. // Prank represents an active prank task for the next sub-call.
...@@ -159,35 +162,92 @@ const ( ...@@ -159,35 +162,92 @@ const (
CallerModeRecurrentPrank 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 // via vm.broadcast(). Actually submitting the transaction is left up
// to other tools. // to other tools.
type Broadcast struct { type Broadcast struct {
From common.Address From common.Address `json:"from"`
To common.Address To common.Address `json:"to"` // set to expected contract address, if this is a deployment
Calldata []byte Input hexutil.Bytes `json:"input"` // set to contract-creation code, if this is a deployment
Value *big.Int 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 // NewBroadcast creates a Broadcast from a parent callframe, and the completed child callframe.
// is preferred to manually creating the struct since it correctly handles // 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 // data that must be copied prior to being returned to prevent accidental mutation.
// mutation. func NewBroadcast(parent, current *CallFrame) Broadcast {
func NewBroadcastFromCtx(ctx *vm.ScopeContext) Broadcast { ctx := current.Ctx
// Consistently return nil for zero values in order
// for tests to have a deterministic value to compare value := ctx.CallValue()
// against. if value == nil {
value := ctx.CallValue().ToBig() value = uint256.NewInt(0)
if value.Cmp(common.Big0) == 0 { }
value = nil
} // Code is tracked separate from calldata input,
// even though they are the same thing for a regular contract creation
// Need to clone CallInput() below since it's used within input := ctx.CallInput()
// the VM itself elsewhere. if ctx.Contract.IsDeployment {
return Broadcast{ input = ctx.Contract.Code
From: ctx.Caller(), }
To: ctx.Address(),
Calldata: bytes.Clone(ctx.CallInput()), bcast := Broadcast{
Value: value, 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 ...@@ -3,7 +3,6 @@ package script
import ( import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"math/big" "math/big"
...@@ -40,6 +39,9 @@ type CallFrame struct { ...@@ -40,6 +39,9 @@ type CallFrame struct {
LastOp vm.OpCode LastOp vm.OpCode
LastPC uint64 LastPC uint64
// To reconstruct a create2 later, e.g. on broadcast
LastCreate2Salt [32]byte
// Reverts often happen in generated code. // Reverts often happen in generated code.
// We want to fallback to logging the source-map position of // We want to fallback to logging the source-map position of
// the non-generated code, i.e. the origin of the last successful jump. // the non-generated code, i.e. the origin of the last successful jump.
...@@ -391,17 +393,24 @@ func (h *Host) unwindCallstack(depth int) { ...@@ -391,17 +393,24 @@ func (h *Host) unwindCallstack(depth int) {
if len(h.callStack) > 1 { if len(h.callStack) > 1 {
parentCallFrame := h.callStack[len(h.callStack)-2] parentCallFrame := h.callStack[len(h.callStack)-2]
if parentCallFrame.Prank != nil { if parentCallFrame.Prank != nil {
if parentCallFrame.Prank.Broadcast && parentCallFrame.LastOp != vm.STATICCALL { if parentCallFrame.Prank.Broadcast {
currentFrame := h.callStack[len(h.callStack)-1] if parentCallFrame.LastOp == vm.DELEGATECALL {
bcast := NewBroadcastFromCtx(currentFrame.Ctx) h.log.Warn("Cannot broadcast a delegate-call. Ignoring broadcast hook.")
h.hooks.OnBroadcast(bcast) } else if parentCallFrame.LastOp == vm.STATICCALL {
h.log.Debug( h.log.Trace("Broadcast is active, ignoring static-call.")
"called broadcast hook", } else {
"from", bcast.From, currentCallFrame := h.callStack[len(h.callStack)-1]
"to", bcast.To, bcast := NewBroadcast(parentCallFrame, currentCallFrame)
"calldata", hex.EncodeToString(bcast.Calldata), h.log.Debug(
"value", bcast.Value, "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. // 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 ...@@ -448,6 +457,9 @@ func (h *Host) onOpcode(pc uint64, op byte, gas, cost uint64, scope tracing.OpCo
} }
cf.LastOp = vm.OpCode(op) cf.LastOp = vm.OpCode(op)
cf.LastPC = pc cf.LastPC = pc
if cf.LastOp == vm.CREATE2 {
cf.LastCreate2Salt = scopeCtx.Stack.Back(3).Bytes32()
}
} }
// onStorageChange is a trace-hook to capture state changes // onStorageChange is a trace-hook to capture state changes
......
package script package script
import ( import (
"bytes"
"encoding/json"
"fmt" "fmt"
"math/big"
"strings" "strings"
"testing" "testing"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/holiman/uint256" "github.com/holiman/uint256"
"github.com/stretchr/testify/require" "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/go-ethereum/log"
"github.com/ethereum-optimism/optimism/op-chain-ops/foundry" "github.com/ethereum-optimism/optimism/op-chain-ops/foundry"
...@@ -54,27 +58,58 @@ func TestScriptBroadcast(t *testing.T) { ...@@ -54,27 +58,58 @@ func TestScriptBroadcast(t *testing.T) {
return data 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") senderAddr := common.HexToAddress("0x5b73C5498c1E3b4dbA84de0F1833c4a029d90519")
expBroadcasts := []Broadcast{ expBroadcasts := []Broadcast{
{ {
From: senderAddr, From: senderAddr,
To: senderAddr, To: senderAddr,
Calldata: mustEncodeCalldata("call1", "single_call1"), Input: mustEncodeCalldata("call1", "single_call1"),
Value: (*hexutil.U256)(uint256.NewInt(0)),
Type: BroadcastCall,
}, },
{ {
From: senderAddr, From: common.HexToAddress("0x0000000000000000000000000000000000C0FFEE"),
To: senderAddr, To: senderAddr,
Calldata: mustEncodeCalldata("call1", "startstop_call1"), Input: mustEncodeCalldata("call1", "startstop_call1"),
Value: (*hexutil.U256)(uint256.NewInt(0)),
Type: BroadcastCall,
}, },
{ {
From: senderAddr, From: common.HexToAddress("0x0000000000000000000000000000000000C0FFEE"),
To: senderAddr, To: senderAddr,
Calldata: mustEncodeCalldata("call2", "startstop_call2"), Input: mustEncodeCalldata("call2", "startstop_call2"),
Value: (*hexutil.U256)(uint256.NewInt(0)),
Type: BroadcastCall,
}, },
{ {
From: senderAddr, From: common.HexToAddress("0x1234"),
To: senderAddr, To: senderAddr,
Calldata: mustEncodeCalldata("nested1", "nested"), 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) { ...@@ -92,5 +127,10 @@ func TestScriptBroadcast(t *testing.T) {
input := bytes4("runBroadcast()") input := bytes4("runBroadcast()")
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.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 { ...@@ -9,6 +9,8 @@ interface Vm {
function startPrank(address msgSender) external; function startPrank(address msgSender) external;
function stopPrank() external; function stopPrank() external;
function broadcast() external; function broadcast() external;
function broadcast(address msgSender) external;
function startBroadcast(address msgSender) external;
function startBroadcast() external; function startBroadcast() external;
function stopBroadcast() external; function stopBroadcast() external;
} }
...@@ -104,7 +106,7 @@ contract ScriptExample { ...@@ -104,7 +106,7 @@ contract ScriptExample {
this.call2("single_call2"); this.call2("single_call2");
console.log("testing start/stop"); console.log("testing start/stop");
vm.startBroadcast(); vm.startBroadcast(address(uint160(0xc0ffee)));
this.call1("startstop_call1"); this.call1("startstop_call1");
this.call2("startstop_call2"); this.call2("startstop_call2");
this.callPure("startstop_pure"); this.callPure("startstop_pure");
...@@ -112,9 +114,20 @@ contract ScriptExample { ...@@ -112,9 +114,20 @@ contract ScriptExample {
this.call1("startstop_call3"); this.call1("startstop_call3");
console.log("testing nested"); console.log("testing nested");
vm.startBroadcast(); vm.startBroadcast(address(uint160(0x1234)));
this.nested1("nested"); this.nested1("nested");
vm.stopBroadcast(); 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. /// @notice example external function, to force a CALL, and test vm.startPrank with.
...@@ -147,3 +160,11 @@ contract ScriptExample { ...@@ -147,3 +160,11 @@ contract ScriptExample {
console.log(_v); 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