Add flag to disable ArbOwner outside on-chain execution#4591
Add flag to disable ArbOwner outside on-chain execution#4591
Conversation
- When `DisableOffchainArbOwner` is set to `true` in chain config, `OwnerPrecompile.Call` panics - Defaults to `false` — no behavior change unless explicitly opted in Signed-off-by: Igor Braga <5835477+bragaigor@users.noreply.github.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #4591 +/- ##
==========================================
- Coverage 34.30% 29.12% -5.18%
==========================================
Files 498 498
Lines 59096 59115 +19
==========================================
- Hits 20270 17217 -3053
- Misses 35238 38768 +3530
+ Partials 3588 3130 -458 |
❌ 242 Tests Failed:
View the top 3 failed tests by shortest run time
📣 Thoughts on this report? Let Codecov know! | Powered by Codecov |
Add `--execution.disable-offchain-arbowner` flag to disable ArbOwner precompile calls outside on-chain execution Signed-off-by: Igor Braga <5835477+bragaigor@users.noreply.github.com>
| var arbOwnerPrecompile *precompiles.OwnerPrecompile | ||
|
|
||
| func GetOwnerPrecompile() *precompiles.OwnerPrecompile { | ||
| return arbOwnerPrecompile | ||
| } | ||
|
|
There was a problem hiding this comment.
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:
gethhook/geth-hook.goinit()— runs at package load time (triggered ingethexec/blockchain.goimporting gethhook). This is whereprecompiles.Precompiles()is called, which creates the*OwnerPrecompileinstance. That instance gets wrapped inArbosPrecompileWrapperand stored in the VM's global precompile maps (vm.PrecompiledContractsBeforeArbOS30, etc.). Afterinit()returns, nobody holds a direct reference to the*OwnerPrecompile— it's buried inside the wrapper inside the VM maps.gethexec.CreateExecutionNode()— runs much later, when the node is actually being set up. This is where configFetcher (which has theDisableOffchainArbOwnerflag) first becomes available.ExecutionNode.Initialize()— called afterCreateExecutionNode. This is where we want to callownerPC.SetDisableOffchain(config.DisableOffchainArbOwner). But we need the*OwnerPrecompilepointer 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
Signed-off-by: Igor Braga <5835477+bragaigor@users.noreply.github.com>
gethhook/geth-hook.go
Outdated
| // arbOwnerPrecompile holds the *OwnerPrecompile created during init(). | ||
| // This is needed because init() creates the instance (before node config exists) | ||
| // and ExecutionNode.Initialize() configures it later (when config is available). | ||
| // Since init() can't return values, this var bridges the gap between the two. |
There was a problem hiding this comment.
question for reviewer: we could update this comment to include more details, something like:
// arbOwnerPrecompile is an unexported package-level variable holding the
// *OwnerPrecompile created during init(). We'd normally avoid a package-level
// var, but the precompile lifecycle requires it:
//
// - init() calls precompiles.Precompiles(), which creates the *OwnerPrecompile.
// That instance is then wrapped in ArbosPrecompileWrapper and stored in the
// VM's global precompile maps (vm.PrecompiledContractsBeforeArbOS30, etc.).
// After init() returns, no one holds a direct reference to the inner
// *OwnerPrecompile — it's buried inside the wrapper inside the VM maps.
//
// - ExecutionNode.Initialize() runs much later, once node config (including
// DisableOffchainArbOwner) is available. It needs the *OwnerPrecompile
// pointer to call SetDisableOffchain.
//
// There is no object that naturally carries the pointer from init() to
// Initialize(), so this variable bridges the gap. Alternatives considered:
//
// - Storing it in chain config: wrong scope (node-level concern, not consensus).
// - A freestanding global atomic.Bool: worse (config detached from the object).
// - Looking it up from the VM precompile maps: fragile (requires unwrapping
// across multiple layers and picking an arbitrary ArbOS version map).
//
// This var is set exactly once during init() and never reassigned. The actual
// config flag (disableOffchain) lives on the OwnerPrecompile struct where it
// belongs.no strong opinion to which one to use
There was a problem hiding this comment.
short description is better than long description
precompiles/precompile.go
Outdated
| } | ||
|
|
||
| func Precompiles() map[addr]ArbosPrecompile { | ||
| func Precompiles() (map[addr]ArbosPrecompile, *OwnerPrecompile) { |
There was a problem hiding this comment.
No need to have this second return value.
The map returned by Precompiles can be used to retrieve the OwnerPrecompile, just use ArbOwnerImpl.Address as the key for accessing through the map
There was a problem hiding this comment.
That's a good catch thanks!
precompiles/wrapper.go
Outdated
| emitSuccess func(mech, bytes4, addr, []byte) error | ||
| precompile ArbosPrecompile | ||
| emitSuccess func(mech, bytes4, addr, []byte) error | ||
| disableOffchain atomic.Bool |
There was a problem hiding this comment.
Why it needs to be atomic?
There was a problem hiding this comment.
Nope, it was remanence of an initial solution I strayed fro. Using regular bool
| ExposeMultiGas bool `koanf:"expose-multi-gas"` | ||
| RPCServer rpcserver.Config `koanf:"rpc-server"` | ||
| ConsensusRPCClient rpcclient.ClientConfig `koanf:"consensus-rpc-client" reload:"hot"` | ||
| DisableOffchainArbOwner bool `koanf:"disable-offchain-arbowner"` |
There was a problem hiding this comment.
If you check MessageRunContext, you are going to see that NonMutating happens to be the negation of ExecutedOnChain.
So using non mutating here can be more appropriate than using offchain.
There was a problem hiding this comment.
Yeah and furthermore IsNonMutating() and !IsExecutedOnChain() are equivalent:
func (c *MessageRunContext) IsNonMutating() bool {
return c.runMode == messageGasEstimationMode || c.runMode == messageEthcallMode
}
...
func (c *MessageRunContext) IsExecutedOnChain() bool {
return c.IsCommitMode() || c.runMode == messageReplayMode || c.runMode == messageRecordingMode
}@joshuacolvin0 please advice if you had any other plans for DisableOffchainArbOwner as using IsNonMutating seems to be doing exactly what DisableOffchainArbOwner. If we went with IsNonMutating then the following check:
if wrapper.disableOffchain {
txProcessor, ok := evm.ProcessingHook.(*arbos.TxProcessor)
if !ok || !txProcessor.RunContext().IsExecutedOnChain() {
return nil, gasSupplied, multigas.ZeroGas(), errors.New("ArbOwner precompile is disabled outside on-chain
execution")
}
} would turn into something like:
txProcessor, ok := evm.ProcessingHook.(*arbos.TxProcessor)
if !ok || txProcessor.MsgIsNonMutating() {
return nil, gasSupplied, multigas.ZeroGas(), errors.New("ArbOwner precompile is disabled outside on-chain
execution")
}| ) ([]byte, uint64, multigas.MultiGas, error) { | ||
| if wrapper.disableOffchain.Load() { | ||
| txProcessor, ok := evm.ProcessingHook.(*arbos.TxProcessor) | ||
| if !ok || !txProcessor.RunContext().IsExecutedOnChain() { |
There was a problem hiding this comment.
The task mentions to use IsExecutedOnChain.
Check with who created the task if you can use evm.ProcessinHook.MsgIsNonMutating() instead, which today is the negation of IsExecutedOnChain.
This way you avoid casting.
If not possible then split this if in two.
One to return an error due to casting, and another to return the error that is being proposed here.
precompiles/wrapper.go
Outdated
| if wrapper.disableOffchain.Load() { | ||
| txProcessor, ok := evm.ProcessingHook.(*arbos.TxProcessor) | ||
| if !ok || !txProcessor.RunContext().IsExecutedOnChain() { | ||
| return nil, 0, multigas.ComputationGas(gasSupplied), errors.New("ArbOwner precompile is disabled outside on-chain execution") |
There was a problem hiding this comment.
Why returning 0 and multigas.ComputationGas(gasSupplied)?
I am not sure what is the expected behavior, related to those gas values, for eth_estimateGas if it returns an error.
There was a problem hiding this comment.
I think it's best to follow what the rest of Call does so modifying to return:
return nil, gasSupplied, multigas.ZeroGas(), errors.New("ArbOwner precompile is disabled outside on-chain execution")
precompiles/ArbOwner_test.go
Outdated
| } | ||
| } | ||
|
|
||
| func TestDisableOffchainArbOwner(t *testing.T) { |
There was a problem hiding this comment.
I didn't go over this test func yet, but a test in system_tests, sending eth_call and eth_estimateGas, and using the new Execution node config added by this PR, couldn't be used instead of this?
Signed-off-by: Igor Braga <5835477+bragaigor@users.noreply.github.com>
Signed-off-by: Igor Braga <5835477+bragaigor@users.noreply.github.com>
--execution.disable-offchain-arbownernode config flag (defaultfalse)OwnerPrecompile.Callpanics if the run context is notIsExecutedOnChain()(blocks ethcall and gas estimation modes)atomic.Boolfield on theOwnerPrecompilestruct, configured duringExecutionNode.Initialize()viagethhook.GetOwnerPrecompile()The
*OwnerPrecompilereference is held in an unexported var in thegethhookpackage, exposed viaGetOwnerPrecompile(). This is necessary because precompile instances are created duringgethhook.init()(a void function that runs at package load time), while node config only becomes available later inExecutionNode.Initialize(). Sinceinit()cannot return values, the pointer must be held somewhere to bridge that gap. An unexported var with a getter is the smallest surface area — set once, immutable after init, and the actual config (disableOffchain) lives on theOwnerPrecompilestruct where it belongs.closes NIT-4744