Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelog/bragaigor-nit-4744.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
### Configuration
- Add `--execution.disable-arbowner-ethcall` flag to disable ArbOwner precompile calls outside on-chain execution
11 changes: 11 additions & 0 deletions execution/gethexec/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"github.com/offchainlabs/nitro/consensus/consensusrpcclient"
"github.com/offchainlabs/nitro/execution"
executionrpcserver "github.com/offchainlabs/nitro/execution/rpcserver"
"github.com/offchainlabs/nitro/gethhook"
"github.com/offchainlabs/nitro/solgen/go/precompilesgen"
"github.com/offchainlabs/nitro/timeboost"
"github.com/offchainlabs/nitro/util"
Expand Down Expand Up @@ -140,6 +141,7 @@ type Config struct {
ExposeMultiGas bool `koanf:"expose-multi-gas"`
RPCServer rpcserver.Config `koanf:"rpc-server"`
ConsensusRPCClient rpcclient.ClientConfig `koanf:"consensus-rpc-client" reload:"hot"`
DisableArbOwnerEthCall bool `koanf:"disable-arbowner-ethcall"`

forwardingTarget string
}
Expand Down Expand Up @@ -191,6 +193,7 @@ func ConfigAddOptions(prefix string, f *pflag.FlagSet) {
f.Uint64(prefix+".block-metadata-api-cache-size", ConfigDefault.BlockMetadataApiCacheSize, "size (in bytes) of lru cache storing the blockMetadata to service arb_getRawBlockMetadata")
f.Uint64(prefix+".block-metadata-api-blocks-limit", ConfigDefault.BlockMetadataApiBlocksLimit, "maximum number of blocks allowed to be queried for blockMetadata per arb_getRawBlockMetadata query. Enabled by default, set 0 to disable the limit")
f.Bool(prefix+".expose-multi-gas", false, "experimental: expose multi-dimensional gas in transaction receipts")
f.Bool(prefix+".disable-arbowner-ethcall", ConfigDefault.DisableArbOwnerEthCall, "disable ArbOwner precompile calls outside on-chain execution (ethcall, gas estimation)")
LiveTracingConfigAddOptions(prefix+".vmtrace", f)
rpcserver.ConfigAddOptions(prefix+".rpc-server", "execution", f)
rpcclient.RPCClientAddOptions(prefix+".consensus-rpc-client", f, &ConfigDefault.ConsensusRPCClient)
Expand Down Expand Up @@ -230,6 +233,7 @@ var ConfigDefault = Config{
BlockMetadataApiBlocksLimit: 100,
VmTrace: DefaultLiveTracingConfig,
ExposeMultiGas: false,
DisableArbOwnerEthCall: false,

RPCServer: rpcserver.DefaultConfig,
ConsensusRPCClient: rpcclient.ClientConfig{
Expand Down Expand Up @@ -442,6 +446,13 @@ func (n *ExecutionNode) MarkFeedStart(to arbutil.MessageIndex) containers.Promis

func (n *ExecutionNode) Initialize(ctx context.Context) error {
config := n.configFetcher.Get()
if config.DisableArbOwnerEthCall {
ownerPC := gethhook.GetOwnerPrecompile()
if ownerPC == nil {
return fmt.Errorf("cannot enable disable-arbowner-ethcall: ArbOwner precompile not found")
}
ownerPC.SetDisableEthCall(true)
}
err := n.ExecEngine.Initialize(config.Caching.StylusLRUCacheCapacity, &config.StylusTarget)
if err != nil {
return fmt.Errorf("error initializing execution engine: %w", err)
Expand Down
17 changes: 16 additions & 1 deletion gethhook/geth-hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,22 @@ import (
"github.com/ethereum/go-ethereum/arbitrum/multigas"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/log"

"github.com/offchainlabs/nitro/arbos"
"github.com/offchainlabs/nitro/precompiles"
)

// arbOwnerPrecompile holds the *OwnerPrecompile retrieved from the precompile map during init().
// ExecutionNode.Initialize() configures it later when the node config is available.
var arbOwnerPrecompile *precompiles.OwnerPrecompile

func GetOwnerPrecompile() *precompiles.OwnerPrecompile {
return arbOwnerPrecompile
}

Comment on lines +23 to +28
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The *OwnerPrecompile reference in gethhook is an unexported package-level variable, which we'd normally avoid. Here's why it's necessary:

Precompile instances are created during gethhook.init() — a void function that runs automatically at package load time. Node config (DisableOffchainArbOwner) only becomes available much later in ExecutionNode.Initialize(). Since init() can't return values and has no caller to hand the instance to, the pointer must be held somewhere between creation and configuration. The alternatives we evaluated:

  • storing it in chain config (wrong: this is a node-level concern, not consensus state)
  • a freestanding global atomic bool (worse: config detached from the object it belongs to)
  • or looking it up from the VM precompile maps (fragile: requires unwrapping across multiple layers and picking an arbitrary ArbOS version map)

all the above were all strictly worse. The unexported var with an exported getter is the smallest surface: set exactly once in init(), immutable after that, and the actual config flag (disableOffchain) lives on the OwnerPrecompile struct where it belongs.

In more detail:

  1. gethhook/geth-hook.go init() — runs at package load time (triggered in gethexec/blockchain.go importing gethhook). This is where precompiles.Precompiles() is called, which creates the *OwnerPrecompile instance. That instance gets wrapped in ArbosPrecompileWrapper and stored in the VM's global precompile maps (vm.PrecompiledContractsBeforeArbOS30, etc.). After init() returns, nobody holds a direct reference to the *OwnerPrecompile — it's buried inside the wrapper inside the VM maps.
  2. gethexec.CreateExecutionNode() — runs much later, when the node is actually being set up. This is where configFetcher (which has the DisableOffchainArbOwner flag) first becomes available.
  3. ExecutionNode.Initialize() — called after CreateExecutionNode. This is where we want to call ownerPC.SetDisableOffchain(config.DisableOffchainArbOwner). But we need the *OwnerPrecompile pointer to do that.

The gap: the instance is created in step 1, the config arrives in step 3, and there's no object that naturally carries the pointer from 1 to 3. Hence the global

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice solve!

type ArbosPrecompileWrapper struct {
inner precompiles.ArbosPrecompile
}
Expand Down Expand Up @@ -59,7 +68,13 @@ func init() {

// process arbos precompiles
precompileErrors := make(map[[4]byte]abi.Error)
for addr, precompile := range precompiles.Precompiles() {
arbosPrecompiles := precompiles.Precompiles()
if ownerPC, ok := arbosPrecompiles[types.ArbOwnerAddress].(*precompiles.OwnerPrecompile); ok {
arbOwnerPrecompile = ownerPC
} else {
log.Error("ArbOwner precompile is not an *OwnerPrecompile, disable-arbowner-ethcall flag will not work")
Comment thread
diegoximenes marked this conversation as resolved.
Outdated
}
for addr, precompile := range arbosPrecompiles {
for _, errABI := range precompile.Precompile().GetErrorABIs() {
precompileErrors[[4]byte(errABI.ID.Bytes())] = errABI
}
Expand Down
17 changes: 15 additions & 2 deletions precompiles/wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"

"github.com/offchainlabs/nitro/arbos"
"github.com/offchainlabs/nitro/arbos/arbosState"
"github.com/offchainlabs/nitro/arbos/util"
)
Expand Down Expand Up @@ -61,8 +62,9 @@ func (wrapper *DebugPrecompile) Name() string {

// OwnerPrecompile is a precompile wrapper for those only chain owners may use
type OwnerPrecompile struct {
precompile ArbosPrecompile
emitSuccess func(mech, bytes4, addr, []byte) error
precompile ArbosPrecompile
emitSuccess func(mech, bytes4, addr, []byte) error
disableEthCall bool
}

func ownerOnly(address addr, impl ArbosPrecompile, emit func(mech, bytes4, addr, []byte) error) (addr, ArbosPrecompile) {
Expand All @@ -72,6 +74,10 @@ func ownerOnly(address addr, impl ArbosPrecompile, emit func(mech, bytes4, addr,
}
}

func (wrapper *OwnerPrecompile) SetDisableEthCall(disable bool) {
wrapper.disableEthCall = disable
}

func (wrapper *OwnerPrecompile) Address() common.Address {
return wrapper.precompile.Address()
}
Expand All @@ -85,6 +91,13 @@ func (wrapper *OwnerPrecompile) Call(
gasSupplied uint64,
evm *vm.EVM,
) ([]byte, uint64, multigas.MultiGas, error) {
if wrapper.disableEthCall {
txProcessor, ok := evm.ProcessingHook.(*arbos.TxProcessor)
if !ok || !txProcessor.RunContext().IsExecutedOnChain() {
Comment thread
diegoximenes marked this conversation as resolved.
Outdated
Comment thread
diegoximenes marked this conversation as resolved.
Outdated
return nil, gasSupplied, multigas.ZeroGas(), errors.New("ArbOwner precompile is disabled outside on-chain execution")
}
}

con := wrapper.precompile

burner := &Context{
Expand Down
56 changes: 56 additions & 0 deletions system_tests/precompile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import (
"math/big"
"slices"
"sort"
"strings"
"testing"
"time"

"github.com/google/go-cmp/cmp"

"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
Expand All @@ -26,6 +28,7 @@ import (
"github.com/offchainlabs/nitro/arbos/burn"
"github.com/offchainlabs/nitro/arbos/l1pricing"
"github.com/offchainlabs/nitro/cmd/chaininfo"
"github.com/offchainlabs/nitro/gethhook"
"github.com/offchainlabs/nitro/precompiles"
"github.com/offchainlabs/nitro/solgen/go/localgen"
"github.com/offchainlabs/nitro/solgen/go/precompilesgen"
Expand Down Expand Up @@ -1357,3 +1360,56 @@ func TestArbDebugOverwriteContractCode(t *testing.T) {
t.Fatal("expected code B to be", testCodeB, "got", code)
}
}

func TestDisableArbOwnerEthCall(t *testing.T) {
Comment thread
diegoximenes marked this conversation as resolved.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

builder := NewNodeBuilder(ctx).DefaultConfig(t, false).DontParalellise()
builder.execConfig.DisableArbOwnerEthCall = true
cleanup := builder.Build(t)
// The DisableArbOwnerEthCall flag is set on the global OwnerPrecompile singleton,
// so it persists across tests running in the same process. We must:
// 1. Not run in parallel (DontParalellise above) to avoid affecting concurrent tests
// 2. Reset the flag on cleanup to avoid affecting subsequent tests
defer func() {
gethhook.GetOwnerPrecompile().SetDisableEthCall(false)
cleanup()
}()

arbOwnerABI, err := precompilesgen.ArbOwnerMetaData.GetAbi()
Require(t, err)
calldata, err := arbOwnerABI.Pack("getAllChainOwners")
Require(t, err)
arbOwnerAddr := types.ArbOwnerAddress

expectedErrMsg := "ArbOwner precompile is disabled outside on-chain execution"

// eth_call should fail
_, err = builder.L2.Client.CallContract(ctx, ethereum.CallMsg{
To: &arbOwnerAddr,
Data: calldata,
}, nil)
if err == nil || !strings.Contains(err.Error(), expectedErrMsg) {
Fatal(t, "eth_call to ArbOwner expected error containing", expectedErrMsg, "got", err)
}

// eth_estimateGas should fail
_, err = builder.L2.Client.EstimateGas(ctx, ethereum.CallMsg{
To: &arbOwnerAddr,
Data: calldata,
})
if err == nil || !strings.Contains(err.Error(), expectedErrMsg) {
Fatal(t, "eth_estimateGas to ArbOwner expected error containing", expectedErrMsg, "got", err)
}

// On-chain transaction should still succeed
auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx)
auth.GasLimit = 32_000_000
arbOwner, err := precompilesgen.NewArbOwner(arbOwnerAddr, builder.L2.Client)
Require(t, err)
tx, err := arbOwner.AddChainOwner(&auth, common.HexToAddress("0xdeadbeef"))
Require(t, err)
_, err = builder.L2.EnsureTxSucceeded(tx)
Require(t, err)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want a final cleanup here that sets the static to false (default), so that whatever order tests are running other tests will see the same value

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's probably line 1376 where I included gethhook.GetOwnerPrecompile().SetDisableEthCall(false) inside defer, or you meant something else?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tracking this

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missed that. Thanks!

}
Loading