From f9bd5a78b4124db619b16d472b9b661f1a445a34 Mon Sep 17 00:00:00 2001 From: Mikhail Rogachev Date: Wed, 24 Dec 2025 15:20:27 +0100 Subject: [PATCH 01/28] Add ArbCensoredTransactionsManager precompile (idle) --- contracts-local/src/precompiles | 2 +- go-ethereum | 2 +- precompiles/ArbCensoredTransactionsManager.go | 29 ++++++++ .../ArbCensoredTransactionsManager_test.go | 4 ++ precompiles/precompile.go | 6 ++ precompiles/precompile_test.go | 2 +- system_tests/eth_config_test.go | 71 ++++++++++--------- 7 files changed, 78 insertions(+), 38 deletions(-) create mode 100644 precompiles/ArbCensoredTransactionsManager.go create mode 100644 precompiles/ArbCensoredTransactionsManager_test.go diff --git a/contracts-local/src/precompiles b/contracts-local/src/precompiles index acb12a8bcc..3033065dd2 160000 --- a/contracts-local/src/precompiles +++ b/contracts-local/src/precompiles @@ -1 +1 @@ -Subproject commit acb12a8bcc5db8eea36a5ad641b6687a7be0e7ed +Subproject commit 3033065dd270577b2abcd4360bdfd472c6b041fe diff --git a/go-ethereum b/go-ethereum index 9db3547817..061391431a 160000 --- a/go-ethereum +++ b/go-ethereum @@ -1 +1 @@ -Subproject commit 9db354781776766033914bdcec72f23f0f1e5b38 +Subproject commit 061391431ad423f2ed35c2625449e59b1077513b diff --git a/precompiles/ArbCensoredTransactionsManager.go b/precompiles/ArbCensoredTransactionsManager.go new file mode 100644 index 0000000000..daec5ed631 --- /dev/null +++ b/precompiles/ArbCensoredTransactionsManager.go @@ -0,0 +1,29 @@ +// Copyright 2025, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md + +package precompiles + +import ( + "errors" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" +) + +// ArbCensoredTransactionsManager precompile enables ability to censor transactions by authorized callers. +// Authorized callers are added/removed through ArbOwner precompile. +type ArbCensoredTransactionsManager struct { + Address addr // 0x74 +} + +func (con ArbCensoredTransactionsManager) AddCensoredTransaction(c *Context, evm *vm.EVM, txHash common.Hash) error { + return errors.New("ArbCensoredTransactionsManager precompile is not yet implemented") +} + +func (con ArbCensoredTransactionsManager) DeleteCensoredTransaction(c *Context, evm *vm.EVM, txHash common.Hash) error { + return errors.New("ArbCensoredTransactionsManager precompile is not yet implemented") +} + +func (con ArbCensoredTransactionsManager) IsTransactionCensored(c *Context, evm *vm.EVM, txHash common.Hash) (bool, error) { + return false, errors.New("ArbCensoredTransactionsManager precompile is not yet implemented") +} diff --git a/precompiles/ArbCensoredTransactionsManager_test.go b/precompiles/ArbCensoredTransactionsManager_test.go new file mode 100644 index 0000000000..e288fa1286 --- /dev/null +++ b/precompiles/ArbCensoredTransactionsManager_test.go @@ -0,0 +1,4 @@ +// Copyright 2025, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md + +package precompiles diff --git a/precompiles/precompile.go b/precompiles/precompile.go index b41ffa7d9b..9acb2e6e08 100644 --- a/precompiles/precompile.go +++ b/precompiles/precompile.go @@ -662,6 +662,12 @@ func Precompiles() map[addr]ArbosPrecompile { ArbNativeTokenManager.methodsByName["MintNativeToken"].arbosVersion = params.ArbosVersion_41 ArbNativeTokenManager.methodsByName["BurnNativeToken"].arbosVersion = params.ArbosVersion_41 + ArbCensoredTransactionsManager := insert(MakePrecompile(precompilesgen.ArbCensoredTransactionsManagerMetaData, &ArbCensoredTransactionsManager{Address: types.ArbCensoredTransactionsManagerAddress})) + ArbCensoredTransactionsManager.arbosVersion = params.ArbosVersion_60 + ArbCensoredTransactionsManager.methodsByName["AddCensoredTransaction"].arbosVersion = params.ArbosVersion_60 + ArbCensoredTransactionsManager.methodsByName["DeleteCensoredTransaction"].arbosVersion = params.ArbosVersion_60 + ArbCensoredTransactionsManager.methodsByName["IsTransactionCensored"].arbosVersion = params.ArbosVersion_60 + // this should be executed after all precompiles have been inserted for _, contract := range contracts { precompile := contract.Precompile() diff --git a/precompiles/precompile_test.go b/precompiles/precompile_test.go index 4aa3956cbe..d522991f29 100644 --- a/precompiles/precompile_test.go +++ b/precompiles/precompile_test.go @@ -192,7 +192,7 @@ func TestPrecompilesPerArbosVersion(t *testing.T) { params.ArbosVersion_40: 3, params.ArbosVersion_41: 10, params.ArbosVersion_50: 9, - params.ArbosVersion_60: 3, + params.ArbosVersion_60: 9, } precompiles := Precompiles() diff --git a/system_tests/eth_config_test.go b/system_tests/eth_config_test.go index f4a8560519..82a60eebfd 100644 --- a/system_tests/eth_config_test.go +++ b/system_tests/eth_config_test.go @@ -51,41 +51,42 @@ func TestEthConfig(t *testing.T) { ChainId: (*hexutil.Big)(hexutil.MustDecodeBig("0x64aba")), ForkId: (hexutil.Bytes)(hexutil.MustDecode("0x9aa9b1b0")), Precompiles: map[string]common.Address{ - "ArbAddressTable": common.HexToAddress("0x0000000000000000000000000000000000000066"), - "ArbAggregator": common.HexToAddress("0x000000000000000000000000000000000000006d"), - "ArbBLS": common.HexToAddress("0x0000000000000000000000000000000000000067"), - "ArbDebug": common.HexToAddress("0x00000000000000000000000000000000000000ff"), - "ArbFunctionTable": common.HexToAddress("0x0000000000000000000000000000000000000068"), - "ArbGasInfo": common.HexToAddress("0x000000000000000000000000000000000000006c"), - "ArbInfo": common.HexToAddress("0x0000000000000000000000000000000000000065"), - "ArbNativeTokenManager": common.HexToAddress("0x0000000000000000000000000000000000000073"), - "ArbOwner": common.HexToAddress("0x0000000000000000000000000000000000000070"), - "ArbOwnerPublic": common.HexToAddress("0x000000000000000000000000000000000000006b"), - "ArbRetryableTx": common.HexToAddress("0x000000000000000000000000000000000000006e"), - "ArbStatistics": common.HexToAddress("0x000000000000000000000000000000000000006f"), - "ArbSys": common.HexToAddress("0x0000000000000000000000000000000000000064"), - "ArbWasm": common.HexToAddress("0x0000000000000000000000000000000000000071"), - "ArbWasmCache": common.HexToAddress("0x0000000000000000000000000000000000000072"), - "ArbosActs": common.HexToAddress("0x00000000000000000000000000000000000a4b05"), - "ArbosTest": common.HexToAddress("0x0000000000000000000000000000000000000069"), - "BLAKE2F": common.HexToAddress("0x0000000000000000000000000000000000000009"), - "BLS12_G1ADD": common.HexToAddress("0x000000000000000000000000000000000000000b"), - "BLS12_G1MSM": common.HexToAddress("0x000000000000000000000000000000000000000c"), - "BLS12_G2ADD": common.HexToAddress("0x000000000000000000000000000000000000000d"), - "BLS12_G2MSM": common.HexToAddress("0x000000000000000000000000000000000000000e"), - "BLS12_MAP_FP2_TO_G2": common.HexToAddress("0x0000000000000000000000000000000000000011"), - "BLS12_MAP_FP_TO_G1": common.HexToAddress("0x0000000000000000000000000000000000000010"), - "BLS12_PAIRING_CHECK": common.HexToAddress("0x000000000000000000000000000000000000000f"), - "BN254_ADD": common.HexToAddress("0x0000000000000000000000000000000000000006"), - "BN254_MUL": common.HexToAddress("0x0000000000000000000000000000000000000007"), - "BN254_PAIRING": common.HexToAddress("0x0000000000000000000000000000000000000008"), - "ECREC": common.HexToAddress("0x0000000000000000000000000000000000000001"), - "ID": common.HexToAddress("0x0000000000000000000000000000000000000004"), - "KZG_POINT_EVALUATION": common.HexToAddress("0x000000000000000000000000000000000000000a"), - "MODEXP": common.HexToAddress("0x0000000000000000000000000000000000000005"), - "P256VERIFY": common.HexToAddress("0x0000000000000000000000000000000000000100"), - "RIPEMD160": common.HexToAddress("0x0000000000000000000000000000000000000003"), - "SHA256": common.HexToAddress("0x0000000000000000000000000000000000000002"), + "ArbAddressTable": common.HexToAddress("0x0000000000000000000000000000000000000066"), + "ArbAggregator": common.HexToAddress("0x000000000000000000000000000000000000006d"), + "ArbBLS": common.HexToAddress("0x0000000000000000000000000000000000000067"), + "ArbDebug": common.HexToAddress("0x00000000000000000000000000000000000000ff"), + "ArbFunctionTable": common.HexToAddress("0x0000000000000000000000000000000000000068"), + "ArbGasInfo": common.HexToAddress("0x000000000000000000000000000000000000006c"), + "ArbInfo": common.HexToAddress("0x0000000000000000000000000000000000000065"), + "ArbNativeTokenManager": common.HexToAddress("0x0000000000000000000000000000000000000073"), + "ArbCensoredTransactionsManager": common.HexToAddress("0x0000000000000000000000000000000000000074"), + "ArbOwner": common.HexToAddress("0x0000000000000000000000000000000000000070"), + "ArbOwnerPublic": common.HexToAddress("0x000000000000000000000000000000000000006b"), + "ArbRetryableTx": common.HexToAddress("0x000000000000000000000000000000000000006e"), + "ArbStatistics": common.HexToAddress("0x000000000000000000000000000000000000006f"), + "ArbSys": common.HexToAddress("0x0000000000000000000000000000000000000064"), + "ArbWasm": common.HexToAddress("0x0000000000000000000000000000000000000071"), + "ArbWasmCache": common.HexToAddress("0x0000000000000000000000000000000000000072"), + "ArbosActs": common.HexToAddress("0x00000000000000000000000000000000000a4b05"), + "ArbosTest": common.HexToAddress("0x0000000000000000000000000000000000000069"), + "BLAKE2F": common.HexToAddress("0x0000000000000000000000000000000000000009"), + "BLS12_G1ADD": common.HexToAddress("0x000000000000000000000000000000000000000b"), + "BLS12_G1MSM": common.HexToAddress("0x000000000000000000000000000000000000000c"), + "BLS12_G2ADD": common.HexToAddress("0x000000000000000000000000000000000000000d"), + "BLS12_G2MSM": common.HexToAddress("0x000000000000000000000000000000000000000e"), + "BLS12_MAP_FP2_TO_G2": common.HexToAddress("0x0000000000000000000000000000000000000011"), + "BLS12_MAP_FP_TO_G1": common.HexToAddress("0x0000000000000000000000000000000000000010"), + "BLS12_PAIRING_CHECK": common.HexToAddress("0x000000000000000000000000000000000000000f"), + "BN254_ADD": common.HexToAddress("0x0000000000000000000000000000000000000006"), + "BN254_MUL": common.HexToAddress("0x0000000000000000000000000000000000000007"), + "BN254_PAIRING": common.HexToAddress("0x0000000000000000000000000000000000000008"), + "ECREC": common.HexToAddress("0x0000000000000000000000000000000000000001"), + "ID": common.HexToAddress("0x0000000000000000000000000000000000000004"), + "KZG_POINT_EVALUATION": common.HexToAddress("0x000000000000000000000000000000000000000a"), + "MODEXP": common.HexToAddress("0x0000000000000000000000000000000000000005"), + "P256VERIFY": common.HexToAddress("0x0000000000000000000000000000000000000100"), + "RIPEMD160": common.HexToAddress("0x0000000000000000000000000000000000000003"), + "SHA256": common.HexToAddress("0x0000000000000000000000000000000000000002"), }, SystemContracts: map[string]common.Address{ "HISTORY_STORAGE_ADDRESS": common.HexToAddress("0x0000f90827f1c53a10cb7a02335b175320002935"), From 89aed0f715865fdad7bc324c92531413e5d48ca0 Mon Sep 17 00:00:00 2001 From: Mikhail Rogachev Date: Fri, 26 Dec 2025 15:39:07 +0100 Subject: [PATCH 02/28] Limit access to ArbCensoredTransactionsManager with transaction censors --- arbos/arbosState/arbosstate.go | 35 +++++++++++++------ changelog/mrogachev-nit-4245.md | 3 ++ precompiles/ArbCensoredTransactionsManager.go | 28 +++++++++++++++ .../ArbCensoredTransactionsManager_test.go | 4 --- precompiles/ArbOwner.go | 34 ++++++++++++++++++ precompiles/precompile.go | 5 +++ precompiles/precompile_test.go | 2 +- 7 files changed, 95 insertions(+), 16 deletions(-) create mode 100644 changelog/mrogachev-nit-4245.md delete mode 100644 precompiles/ArbCensoredTransactionsManager_test.go diff --git a/arbos/arbosState/arbosstate.go b/arbos/arbosState/arbosstate.go index fbe5697551..d3879a9849 100644 --- a/arbos/arbosState/arbosstate.go +++ b/arbos/arbosState/arbosstate.go @@ -53,6 +53,7 @@ type ArbosState struct { addressTable *addressTable.AddressTable chainOwners *addressSet.AddressSet nativeTokenOwners *addressSet.AddressSet + transactionCensors *addressSet.AddressSet sendMerkle *merkleAccumulator.MerkleAccumulator programs *programs.Programs features *features.Features @@ -90,6 +91,7 @@ func OpenArbosState(stateDB vm.StateDB, burner burn.Burner) (*ArbosState, error) addressTable: addressTable.Open(backingStorage.OpenCachedSubStorage(addressTableSubspace)), chainOwners: addressSet.OpenAddressSet(backingStorage.OpenCachedSubStorage(chainOwnerSubspace)), nativeTokenOwners: addressSet.OpenAddressSet(backingStorage.OpenCachedSubStorage(nativeTokenOwnerSubspace)), + transactionCensors: addressSet.OpenAddressSet(backingStorage.OpenCachedSubStorage(transactionCensorSubspace)), sendMerkle: merkleAccumulator.OpenMerkleAccumulator(backingStorage.OpenCachedSubStorage(sendMerkleSubspace)), programs: programs.Open(arbosVersion, backingStorage.OpenSubStorage(programsSubspace)), features: features.Open(backingStorage.OpenSubStorage(featuresSubspace)), @@ -176,17 +178,18 @@ const ( type SubspaceID []byte var ( - l1PricingSubspace SubspaceID = []byte{0} - l2PricingSubspace SubspaceID = []byte{1} - retryablesSubspace SubspaceID = []byte{2} - addressTableSubspace SubspaceID = []byte{3} - chainOwnerSubspace SubspaceID = []byte{4} - sendMerkleSubspace SubspaceID = []byte{5} - blockhashesSubspace SubspaceID = []byte{6} - chainConfigSubspace SubspaceID = []byte{7} - programsSubspace SubspaceID = []byte{8} - featuresSubspace SubspaceID = []byte{9} - nativeTokenOwnerSubspace SubspaceID = []byte{10} + l1PricingSubspace SubspaceID = []byte{0} + l2PricingSubspace SubspaceID = []byte{1} + retryablesSubspace SubspaceID = []byte{2} + addressTableSubspace SubspaceID = []byte{3} + chainOwnerSubspace SubspaceID = []byte{4} + sendMerkleSubspace SubspaceID = []byte{5} + blockhashesSubspace SubspaceID = []byte{6} + chainConfigSubspace SubspaceID = []byte{7} + programsSubspace SubspaceID = []byte{8} + featuresSubspace SubspaceID = []byte{9} + nativeTokenOwnerSubspace SubspaceID = []byte{10} + transactionCensorSubspace SubspaceID = []byte{11} ) var PrecompileMinArbOSVersions = make(map[common.Address]uint64) @@ -308,6 +311,12 @@ func InitializeArbosState(stateDB vm.StateDB, burner burn.Burner, chainConfig *p return nil, err } + transactionCensorsStorage := sto.OpenCachedSubStorage(transactionCensorSubspace) + err = addressSet.Initialize(transactionCensorsStorage) + if err != nil { + return nil, err + } + aState, err := OpenArbosState(stateDB, burner) if err != nil { return nil, err @@ -562,6 +571,10 @@ func (state *ArbosState) NativeTokenOwners() *addressSet.AddressSet { return state.nativeTokenOwners } +func (state *ArbosState) TransactionCensors() *addressSet.AddressSet { + return state.transactionCensors +} + func (state *ArbosState) SendMerkleAccumulator() *merkleAccumulator.MerkleAccumulator { if state.sendMerkle == nil { state.sendMerkle = merkleAccumulator.OpenMerkleAccumulator(state.backingStorage.OpenCachedSubStorage(sendMerkleSubspace)) diff --git a/changelog/mrogachev-nit-4245.md b/changelog/mrogachev-nit-4245.md new file mode 100644 index 0000000000..9f5f0f4e80 --- /dev/null +++ b/changelog/mrogachev-nit-4245.md @@ -0,0 +1,3 @@ +### Added +- Add new precompile ArbCensoredTransactionsManager to manage censored transactions +- Add transaction censors to ArbOs and ArbOwner to limit access to ArbCensoredTransactionsManager diff --git a/precompiles/ArbCensoredTransactionsManager.go b/precompiles/ArbCensoredTransactionsManager.go index daec5ed631..5519fc799b 100644 --- a/precompiles/ArbCensoredTransactionsManager.go +++ b/precompiles/ArbCensoredTransactionsManager.go @@ -16,14 +16,42 @@ type ArbCensoredTransactionsManager struct { Address addr // 0x74 } +// Adds a transaction hash to the censored transactions list func (con ArbCensoredTransactionsManager) AddCensoredTransaction(c *Context, evm *vm.EVM, txHash common.Hash) error { + hasAccess, err := con.hasAccess(c) + if err != nil { + return err + } + if !hasAccess { + return c.BurnOut() + } return errors.New("ArbCensoredTransactionsManager precompile is not yet implemented") } +// Deletes a transaction hash from the censored transactions list func (con ArbCensoredTransactionsManager) DeleteCensoredTransaction(c *Context, evm *vm.EVM, txHash common.Hash) error { + hasAccess, err := con.hasAccess(c) + if err != nil { + return err + } + if !hasAccess { + return c.BurnOut() + } return errors.New("ArbCensoredTransactionsManager precompile is not yet implemented") } +// Checks if a transaction hash is in the censored transactions list func (con ArbCensoredTransactionsManager) IsTransactionCensored(c *Context, evm *vm.EVM, txHash common.Hash) (bool, error) { + hasAccess, err := con.hasAccess(c) + if err != nil { + return false, err + } + if !hasAccess { + return false, c.BurnOut() + } return false, errors.New("ArbCensoredTransactionsManager precompile is not yet implemented") } + +func (con ArbCensoredTransactionsManager) hasAccess(c *Context) (bool, error) { + return c.State.TransactionCensors().IsMember(c.caller) +} diff --git a/precompiles/ArbCensoredTransactionsManager_test.go b/precompiles/ArbCensoredTransactionsManager_test.go deleted file mode 100644 index e288fa1286..0000000000 --- a/precompiles/ArbCensoredTransactionsManager_test.go +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright 2025, Offchain Labs, Inc. -// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md - -package precompiles diff --git a/precompiles/ArbOwner.go b/precompiles/ArbOwner.go index b88afc3ec5..737671927b 100644 --- a/precompiles/ArbOwner.go +++ b/precompiles/ArbOwner.go @@ -122,6 +122,40 @@ func (con ArbOwner) GetAllNativeTokenOwners(c ctx, evm mech) ([]common.Address, return c.State.NativeTokenOwners().AllMembers(65536) } +// AddTransactionCensor adds account as a transaction censor (authorized to use ArbCensoredTransactionsManager) +func (con ArbOwner) AddTransactionCensor(c ctx, _ mech, censor addr) error { + member, err := con.IsTransactionCensor(c, nil, censor) + if err != nil { + return err + } + if member { + return errors.New("tried to add existing transaction censor") + } + return c.State.TransactionCensors().Add(censor) +} + +// RemoveTransactionCensor removes account from the list of transaction censors +func (con ArbOwner) RemoveTransactionCensor(c ctx, _ mech, censor addr) error { + member, err := con.IsTransactionCensor(c, nil, censor) + if err != nil { + return err + } + if !member { + return errors.New("tried to remove non existing transaction censor") + } + return c.State.TransactionCensors().Remove(censor, c.State.ArbOSVersion()) +} + +// IsTransactionCensor checks if the account is a transaction censor +func (con ArbOwner) IsTransactionCensor(c ctx, _ mech, censor addr) (bool, error) { + return c.State.TransactionCensors().IsMember(censor) +} + +// GetAllTransactionCensors retrieves the list of transaction censors +func (con ArbOwner) GetAllTransactionCensors(c ctx, evm mech) ([]common.Address, error) { + return c.State.TransactionCensors().AllMembers(65536) +} + // SetL1BaseFeeEstimateInertia sets how slowly ArbOS updates its estimate of the L1 basefee func (con ArbOwner) SetL1BaseFeeEstimateInertia(c ctx, evm mech, inertia uint64) error { return c.State.L1PricingState().SetInertia(inertia) diff --git a/precompiles/precompile.go b/precompiles/precompile.go index 9acb2e6e08..110dd3d387 100644 --- a/precompiles/precompile.go +++ b/precompiles/precompile.go @@ -655,6 +655,11 @@ func Precompiles() map[addr]ArbosPrecompile { ArbOwner.methodsByName["SetParentGasFloorPerToken"].arbosVersion = params.ArbosVersion_50 ArbOwner.methodsByName["SetMaxBlockGasLimit"].arbosVersion = params.ArbosVersion_50 + ArbOwner.methodsByName["AddTransactionCensor"].arbosVersion = params.ArbosVersion_60 + ArbOwner.methodsByName["RemoveTransactionCensor"].arbosVersion = params.ArbosVersion_60 + ArbOwner.methodsByName["IsTransactionCensor"].arbosVersion = params.ArbosVersion_60 + ArbOwner.methodsByName["GetAllTransactionCensors"].arbosVersion = params.ArbosVersion_60 + ArbOwnerPublic.methodsByName["GetNativeTokenManagementFrom"].arbosVersion = params.ArbosVersion_50 ArbNativeTokenManager := insert(MakePrecompile(precompilesgen.ArbNativeTokenManagerMetaData, &ArbNativeTokenManager{Address: types.ArbNativeTokenManagerAddress})) diff --git a/precompiles/precompile_test.go b/precompiles/precompile_test.go index d522991f29..eb203d8561 100644 --- a/precompiles/precompile_test.go +++ b/precompiles/precompile_test.go @@ -192,7 +192,7 @@ func TestPrecompilesPerArbosVersion(t *testing.T) { params.ArbosVersion_40: 3, params.ArbosVersion_41: 10, params.ArbosVersion_50: 9, - params.ArbosVersion_60: 9, + params.ArbosVersion_60: 10, } precompiles := Precompiles() From 987a1438f205a48f6455b063ef1fcd557ffb1b75 Mon Sep 17 00:00:00 2001 From: Mikhail Rogachev Date: Fri, 26 Dec 2025 16:58:55 +0100 Subject: [PATCH 03/28] Separated kv-storage for censored transactions --- arbos/censored_transactions/state.go | 38 +++++++++++++++++++ arbos/storage/storage.go | 18 +++++++-- nitro-testnode | 2 +- precompiles/ArbCensoredTransactionsManager.go | 15 +++++--- 4 files changed, 63 insertions(+), 10 deletions(-) create mode 100644 arbos/censored_transactions/state.go diff --git a/arbos/censored_transactions/state.go b/arbos/censored_transactions/state.go new file mode 100644 index 0000000000..7ac9d55b01 --- /dev/null +++ b/arbos/censored_transactions/state.go @@ -0,0 +1,38 @@ +// Copyright 2025, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md + +package censored_transactions + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + + "github.com/offchainlabs/nitro/arbos/burn" + "github.com/offchainlabs/nitro/arbos/storage" +) + +type CensoredTransactionsState struct { + store *storage.Storage +} + +func Open(statedb vm.StateDB, burner burn.Burner) *CensoredTransactionsState { + return &CensoredTransactionsState{ + store: storage.CensoredTransactionsStorage(statedb, burner), + } +} + +func (s *CensoredTransactionsState) Add(txHash common.Hash) error { + return s.store.SetUint64(txHash, 1) +} + +func (s *CensoredTransactionsState) Delete(txHash common.Hash) error { + return s.store.Clear(txHash) +} + +func (s *CensoredTransactionsState) IsCensored(txHash common.Hash) (bool, error) { + v, err := s.store.GetUint64(txHash) + if err != nil { + return false, err + } + return v != 0, nil +} diff --git a/arbos/storage/storage.go b/arbos/storage/storage.go index 050bd990ae..ecbbf56094 100644 --- a/arbos/storage/storage.go +++ b/arbos/storage/storage.go @@ -16,6 +16,7 @@ import ( "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/tracing" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" @@ -67,10 +68,9 @@ const storageKeyCacheSize = 1024 var storageHashCache = lru.NewCache[string, []byte](storageKeyCacheSize) var cacheFullLogged atomic.Bool -// NewGeth uses a Geth database to create an evm key-value store -func NewGeth(statedb vm.StateDB, burner burn.Burner) *Storage { - account := common.HexToAddress("0xA4B05FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") - statedb.SetNonce(account, 1, tracing.NonceChangeUnspecified) // setting the nonce ensures Geth won't treat ArbOS as empty +// KVStorage uses a Geth database to create an evm key-value store for an arbitrary account. +func KVStorage(statedb vm.StateDB, burner burn.Burner, account common.Address) *Storage { + statedb.SetNonce(account, 1, tracing.NonceChangeUnspecified) // ensures Geth won't treat the account as empty return &Storage{ account: account, db: statedb, @@ -80,6 +80,16 @@ func NewGeth(statedb vm.StateDB, burner burn.Burner) *Storage { } } +// NewGeth uses a Geth database to create an evm key-value store backed by the ArbOS state account. +func NewGeth(statedb vm.StateDB, burner burn.Burner) *Storage { + return KVStorage(statedb, burner, types.ArbosStateAddress) +} + +// CensoredTransactionsStorage creates an evm key-value store backed by the dedicated censored tx state account. +func CensoredTransactionsStorage(statedb vm.StateDB, burner burn.Burner) *Storage { + return KVStorage(statedb, burner, types.CensoredTransactionsStateAddress) +} + // NewMemoryBacked uses Geth's memory-backed database to create an evm key-value store. // Only used for testing. func NewMemoryBacked(burner burn.Burner) *Storage { diff --git a/nitro-testnode b/nitro-testnode index dfcc6b2e0f..c755cbb40b 160000 --- a/nitro-testnode +++ b/nitro-testnode @@ -1 +1 @@ -Subproject commit dfcc6b2e0f2abc1c889f2123c0a8ce0a6166a306 +Subproject commit c755cbb40b68ef7aaa587719927a7b74332dd2c1 diff --git a/precompiles/ArbCensoredTransactionsManager.go b/precompiles/ArbCensoredTransactionsManager.go index 5519fc799b..c21ada21e1 100644 --- a/precompiles/ArbCensoredTransactionsManager.go +++ b/precompiles/ArbCensoredTransactionsManager.go @@ -4,10 +4,10 @@ package precompiles import ( - "errors" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" + + "github.com/offchainlabs/nitro/arbos/censored_transactions" ) // ArbCensoredTransactionsManager precompile enables ability to censor transactions by authorized callers. @@ -25,7 +25,9 @@ func (con ArbCensoredTransactionsManager) AddCensoredTransaction(c *Context, evm if !hasAccess { return c.BurnOut() } - return errors.New("ArbCensoredTransactionsManager precompile is not yet implemented") + + censoredState := censored_transactions.Open(evm.StateDB, c) + return censoredState.Add(txHash) } // Deletes a transaction hash from the censored transactions list @@ -37,7 +39,9 @@ func (con ArbCensoredTransactionsManager) DeleteCensoredTransaction(c *Context, if !hasAccess { return c.BurnOut() } - return errors.New("ArbCensoredTransactionsManager precompile is not yet implemented") + + censoredState := censored_transactions.Open(evm.StateDB, c) + return censoredState.Delete(txHash) } // Checks if a transaction hash is in the censored transactions list @@ -49,7 +53,8 @@ func (con ArbCensoredTransactionsManager) IsTransactionCensored(c *Context, evm if !hasAccess { return false, c.BurnOut() } - return false, errors.New("ArbCensoredTransactionsManager precompile is not yet implemented") + censoredState := censored_transactions.Open(evm.StateDB, c) + return censoredState.IsCensored(txHash) } func (con ArbCensoredTransactionsManager) hasAccess(c *Context) (bool, error) { From bbce75cf4c14e4171b7f38776ef274b5607323f3 Mon Sep 17 00:00:00 2001 From: Mikhail Rogachev Date: Fri, 26 Dec 2025 17:45:36 +0100 Subject: [PATCH 04/28] Rename: censored -> filtered transactions --- .../state.go | 16 +++++----- arbos/storage/storage.go | 6 ++-- ...r.go => ArbFilteredTransactionsManager.go} | 32 +++++++++---------- precompiles/ArbOwner.go | 2 +- precompiles/precompile.go | 10 +++--- system_tests/eth_config_test.go | 2 +- 6 files changed, 34 insertions(+), 34 deletions(-) rename arbos/{censored_transactions => filteredTransactions}/state.go (57%) rename precompiles/{ArbCensoredTransactionsManager.go => ArbFilteredTransactionsManager.go} (52%) diff --git a/arbos/censored_transactions/state.go b/arbos/filteredTransactions/state.go similarity index 57% rename from arbos/censored_transactions/state.go rename to arbos/filteredTransactions/state.go index 7ac9d55b01..fa7c206d9e 100644 --- a/arbos/censored_transactions/state.go +++ b/arbos/filteredTransactions/state.go @@ -1,7 +1,7 @@ // Copyright 2025, Offchain Labs, Inc. // For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md -package censored_transactions +package filteredTransactions import ( "github.com/ethereum/go-ethereum/common" @@ -11,25 +11,25 @@ import ( "github.com/offchainlabs/nitro/arbos/storage" ) -type CensoredTransactionsState struct { +type FilteredTransactionsState struct { store *storage.Storage } -func Open(statedb vm.StateDB, burner burn.Burner) *CensoredTransactionsState { - return &CensoredTransactionsState{ - store: storage.CensoredTransactionsStorage(statedb, burner), +func Open(statedb vm.StateDB, burner burn.Burner) *FilteredTransactionsState { + return &FilteredTransactionsState{ + store: storage.FilteredTransactionsStorage(statedb, burner), } } -func (s *CensoredTransactionsState) Add(txHash common.Hash) error { +func (s *FilteredTransactionsState) Add(txHash common.Hash) error { return s.store.SetUint64(txHash, 1) } -func (s *CensoredTransactionsState) Delete(txHash common.Hash) error { +func (s *FilteredTransactionsState) Delete(txHash common.Hash) error { return s.store.Clear(txHash) } -func (s *CensoredTransactionsState) IsCensored(txHash common.Hash) (bool, error) { +func (s *FilteredTransactionsState) IsFiltered(txHash common.Hash) (bool, error) { v, err := s.store.GetUint64(txHash) if err != nil { return false, err diff --git a/arbos/storage/storage.go b/arbos/storage/storage.go index ecbbf56094..36ba85577a 100644 --- a/arbos/storage/storage.go +++ b/arbos/storage/storage.go @@ -85,9 +85,9 @@ func NewGeth(statedb vm.StateDB, burner burn.Burner) *Storage { return KVStorage(statedb, burner, types.ArbosStateAddress) } -// CensoredTransactionsStorage creates an evm key-value store backed by the dedicated censored tx state account. -func CensoredTransactionsStorage(statedb vm.StateDB, burner burn.Burner) *Storage { - return KVStorage(statedb, burner, types.CensoredTransactionsStateAddress) +// FilteredTransactionsStorage creates an evm key-value store backed by the dedicated filtered tx state account. +func FilteredTransactionsStorage(statedb vm.StateDB, burner burn.Burner) *Storage { + return KVStorage(statedb, burner, types.FilteredTransactionsStateAddress) } // NewMemoryBacked uses Geth's memory-backed database to create an evm key-value store. diff --git a/precompiles/ArbCensoredTransactionsManager.go b/precompiles/ArbFilteredTransactionsManager.go similarity index 52% rename from precompiles/ArbCensoredTransactionsManager.go rename to precompiles/ArbFilteredTransactionsManager.go index c21ada21e1..bcb57fef4b 100644 --- a/precompiles/ArbCensoredTransactionsManager.go +++ b/precompiles/ArbFilteredTransactionsManager.go @@ -7,17 +7,17 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/vm" - "github.com/offchainlabs/nitro/arbos/censored_transactions" + "github.com/offchainlabs/nitro/arbos/filteredTransactions" ) -// ArbCensoredTransactionsManager precompile enables ability to censor transactions by authorized callers. +// ArbFilteredTransactionsManager precompile enables ability to censor transactions by authorized callers. // Authorized callers are added/removed through ArbOwner precompile. -type ArbCensoredTransactionsManager struct { +type ArbFilteredTransactionsManager struct { Address addr // 0x74 } -// Adds a transaction hash to the censored transactions list -func (con ArbCensoredTransactionsManager) AddCensoredTransaction(c *Context, evm *vm.EVM, txHash common.Hash) error { +// Adds a transaction hash to the filtered transactions list +func (con ArbFilteredTransactionsManager) AddFilteredTransaction(c *Context, evm *vm.EVM, txHash common.Hash) error { hasAccess, err := con.hasAccess(c) if err != nil { return err @@ -26,12 +26,12 @@ func (con ArbCensoredTransactionsManager) AddCensoredTransaction(c *Context, evm return c.BurnOut() } - censoredState := censored_transactions.Open(evm.StateDB, c) - return censoredState.Add(txHash) + filteredState := filteredTransactions.Open(evm.StateDB, c) + return filteredState.Add(txHash) } -// Deletes a transaction hash from the censored transactions list -func (con ArbCensoredTransactionsManager) DeleteCensoredTransaction(c *Context, evm *vm.EVM, txHash common.Hash) error { +// Deletes a transaction hash from the filtered transactions list +func (con ArbFilteredTransactionsManager) DeleteFilteredTransaction(c *Context, evm *vm.EVM, txHash common.Hash) error { hasAccess, err := con.hasAccess(c) if err != nil { return err @@ -40,12 +40,12 @@ func (con ArbCensoredTransactionsManager) DeleteCensoredTransaction(c *Context, return c.BurnOut() } - censoredState := censored_transactions.Open(evm.StateDB, c) - return censoredState.Delete(txHash) + filteredState := filteredTransactions.Open(evm.StateDB, c) + return filteredState.Delete(txHash) } -// Checks if a transaction hash is in the censored transactions list -func (con ArbCensoredTransactionsManager) IsTransactionCensored(c *Context, evm *vm.EVM, txHash common.Hash) (bool, error) { +// Checks if a transaction hash is in the filtered transactions list +func (con ArbFilteredTransactionsManager) IsTransactionFiltered(c *Context, evm *vm.EVM, txHash common.Hash) (bool, error) { hasAccess, err := con.hasAccess(c) if err != nil { return false, err @@ -53,10 +53,10 @@ func (con ArbCensoredTransactionsManager) IsTransactionCensored(c *Context, evm if !hasAccess { return false, c.BurnOut() } - censoredState := censored_transactions.Open(evm.StateDB, c) - return censoredState.IsCensored(txHash) + filteredState := filteredTransactions.Open(evm.StateDB, c) + return filteredState.IsFiltered(txHash) } -func (con ArbCensoredTransactionsManager) hasAccess(c *Context) (bool, error) { +func (con ArbFilteredTransactionsManager) hasAccess(c *Context) (bool, error) { return c.State.TransactionCensors().IsMember(c.caller) } diff --git a/precompiles/ArbOwner.go b/precompiles/ArbOwner.go index 737671927b..7a38db2cb4 100644 --- a/precompiles/ArbOwner.go +++ b/precompiles/ArbOwner.go @@ -122,7 +122,7 @@ func (con ArbOwner) GetAllNativeTokenOwners(c ctx, evm mech) ([]common.Address, return c.State.NativeTokenOwners().AllMembers(65536) } -// AddTransactionCensor adds account as a transaction censor (authorized to use ArbCensoredTransactionsManager) +// AddTransactionCensor adds account as a transaction censor (authorized to use ArbFilteredTransactionsManager) func (con ArbOwner) AddTransactionCensor(c ctx, _ mech, censor addr) error { member, err := con.IsTransactionCensor(c, nil, censor) if err != nil { diff --git a/precompiles/precompile.go b/precompiles/precompile.go index 110dd3d387..a727f83282 100644 --- a/precompiles/precompile.go +++ b/precompiles/precompile.go @@ -667,11 +667,11 @@ func Precompiles() map[addr]ArbosPrecompile { ArbNativeTokenManager.methodsByName["MintNativeToken"].arbosVersion = params.ArbosVersion_41 ArbNativeTokenManager.methodsByName["BurnNativeToken"].arbosVersion = params.ArbosVersion_41 - ArbCensoredTransactionsManager := insert(MakePrecompile(precompilesgen.ArbCensoredTransactionsManagerMetaData, &ArbCensoredTransactionsManager{Address: types.ArbCensoredTransactionsManagerAddress})) - ArbCensoredTransactionsManager.arbosVersion = params.ArbosVersion_60 - ArbCensoredTransactionsManager.methodsByName["AddCensoredTransaction"].arbosVersion = params.ArbosVersion_60 - ArbCensoredTransactionsManager.methodsByName["DeleteCensoredTransaction"].arbosVersion = params.ArbosVersion_60 - ArbCensoredTransactionsManager.methodsByName["IsTransactionCensored"].arbosVersion = params.ArbosVersion_60 + ArbFilteredTransactionsManager := insert(MakePrecompile(precompilesgen.ArbFilteredTransactionsManagerMetaData, &ArbFilteredTransactionsManager{Address: types.ArbFilteredTransactionsManagerAddress})) + ArbFilteredTransactionsManager.arbosVersion = params.ArbosVersion_60 + ArbFilteredTransactionsManager.methodsByName["AddFilteredTransaction"].arbosVersion = params.ArbosVersion_60 + ArbFilteredTransactionsManager.methodsByName["DeleteFilteredTransaction"].arbosVersion = params.ArbosVersion_60 + ArbFilteredTransactionsManager.methodsByName["IsTransactionFiltered"].arbosVersion = params.ArbosVersion_60 // this should be executed after all precompiles have been inserted for _, contract := range contracts { diff --git a/system_tests/eth_config_test.go b/system_tests/eth_config_test.go index 82a60eebfd..1c55190631 100644 --- a/system_tests/eth_config_test.go +++ b/system_tests/eth_config_test.go @@ -59,7 +59,7 @@ func TestEthConfig(t *testing.T) { "ArbGasInfo": common.HexToAddress("0x000000000000000000000000000000000000006c"), "ArbInfo": common.HexToAddress("0x0000000000000000000000000000000000000065"), "ArbNativeTokenManager": common.HexToAddress("0x0000000000000000000000000000000000000073"), - "ArbCensoredTransactionsManager": common.HexToAddress("0x0000000000000000000000000000000000000074"), + "ArbFilteredTransactionsManager": common.HexToAddress("0x0000000000000000000000000000000000000074"), "ArbOwner": common.HexToAddress("0x0000000000000000000000000000000000000070"), "ArbOwnerPublic": common.HexToAddress("0x000000000000000000000000000000000000006b"), "ArbRetryableTx": common.HexToAddress("0x000000000000000000000000000000000000006e"), From 28feb693f4621e4e9625824563326fc64101c485 Mon Sep 17 00:00:00 2001 From: Mikhail Rogachev Date: Fri, 26 Dec 2025 18:24:33 +0100 Subject: [PATCH 05/28] chore: minor fixes --- arbos/filteredTransactions/state.go | 8 +++++--- changelog/mrogachev-nit-4245.md | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/arbos/filteredTransactions/state.go b/arbos/filteredTransactions/state.go index fa7c206d9e..f4ad1dd9fd 100644 --- a/arbos/filteredTransactions/state.go +++ b/arbos/filteredTransactions/state.go @@ -11,6 +11,8 @@ import ( "github.com/offchainlabs/nitro/arbos/storage" ) +var presentHash = common.BytesToHash([]byte{1}) + type FilteredTransactionsState struct { store *storage.Storage } @@ -22,7 +24,7 @@ func Open(statedb vm.StateDB, burner burn.Burner) *FilteredTransactionsState { } func (s *FilteredTransactionsState) Add(txHash common.Hash) error { - return s.store.SetUint64(txHash, 1) + return s.store.Set(txHash, presentHash) } func (s *FilteredTransactionsState) Delete(txHash common.Hash) error { @@ -30,9 +32,9 @@ func (s *FilteredTransactionsState) Delete(txHash common.Hash) error { } func (s *FilteredTransactionsState) IsFiltered(txHash common.Hash) (bool, error) { - v, err := s.store.GetUint64(txHash) + value, err := s.store.Get(txHash) if err != nil { return false, err } - return v != 0, nil + return value == presentHash, nil } diff --git a/changelog/mrogachev-nit-4245.md b/changelog/mrogachev-nit-4245.md index 9f5f0f4e80..ac4fba3b88 100644 --- a/changelog/mrogachev-nit-4245.md +++ b/changelog/mrogachev-nit-4245.md @@ -1,3 +1,3 @@ ### Added -- Add new precompile ArbCensoredTransactionsManager to manage censored transactions -- Add transaction censors to ArbOs and ArbOwner to limit access to ArbCensoredTransactionsManager +- Add new precompile ArbFilteredTransactionsManager to manage filtered transactions +- Add transaction censors to ArbOwner to limit access to ArbFilteredTransactionsManager From 1ad10841ceda9a91339225b389556aad7b006873 Mon Sep 17 00:00:00 2001 From: Mikhail Rogachev Date: Mon, 29 Dec 2025 13:06:20 +0100 Subject: [PATCH 06/28] Add a system test for filtered transaction manager --- system_tests/filtered_transactions_test.go | 107 +++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 system_tests/filtered_transactions_test.go diff --git a/system_tests/filtered_transactions_test.go b/system_tests/filtered_transactions_test.go new file mode 100644 index 0000000000..0fa96c0907 --- /dev/null +++ b/system_tests/filtered_transactions_test.go @@ -0,0 +1,107 @@ +// Copyright 2025, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md + +package arbtest + +import ( + "context" + "math/big" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" + + "github.com/offchainlabs/nitro/solgen/go/precompilesgen" +) + +func TestManageTransactionCensors(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx). + DefaultConfig(t, false). + WithArbOSVersion(params.ArbosVersion_60) + + cleanup := builder.Build(t) + defer cleanup() + + ownerTxOpts := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + + builder.L2Info.GenerateAccount("User") + builder.L2.TransferBalance(t, "Owner", "User", big.NewInt(1e16), builder.L2Info) + userTxOpts := builder.L2Info.GetDefaultTransactOpts("User", ctx) + + ownerCallOpts := &bind.CallOpts{Context: ctx, From: ownerTxOpts.From} + userCallOpts := &bind.CallOpts{Context: ctx, From: userTxOpts.From} + + txHash := common.BytesToHash([]byte{1, 2, 3, 4, 5}) + + arbOwner, err := precompilesgen.NewArbOwner(types.ArbOwnerAddress, builder.L2.Client) + require.NoError(t, err) + + arbFilteredTxs, err := precompilesgen.NewArbFilteredTransactionsManager( + types.ArbFilteredTransactionsManagerAddress, + builder.L2.Client, + ) + require.NoError(t, err) + + // Initially neither owner nor user can access the filtered tx manager + _, err = arbFilteredTxs.IsTransactionFiltered(ownerCallOpts, txHash) + require.Error(t, err) + + _, err = arbFilteredTxs.IsTransactionFiltered(userCallOpts, txHash) + require.Error(t, err) + + // Owner grants user transaction censor role + tx, err := arbOwner.AddTransactionCensor(&ownerTxOpts, userTxOpts.From) + require.NoError(t, err) + require.NotNil(t, tx) + + isCensor, err := arbOwner.IsTransactionCensor(ownerCallOpts, userTxOpts.From) + require.NoError(t, err) + require.True(t, isCensor) + + // Owner is still not a censor, so owner still cannot call the manager + _, err = arbFilteredTxs.IsTransactionFiltered(ownerCallOpts, txHash) + require.Error(t, err) + + // User can call the manager and the tx is initially not filtered + filtered, err := arbFilteredTxs.IsTransactionFiltered(userCallOpts, txHash) + require.NoError(t, err) + require.False(t, filtered) + + // User filters the tx + tx, err = arbFilteredTxs.AddFilteredTransaction(&userTxOpts, txHash) + require.NoError(t, err) + require.NotNil(t, tx) + + filtered, err = arbFilteredTxs.IsTransactionFiltered(userCallOpts, txHash) + require.NoError(t, err) + require.True(t, filtered) + + // User unfilters the tx + tx, err = arbFilteredTxs.DeleteFilteredTransaction(&userTxOpts, txHash) + require.NoError(t, err) + require.NotNil(t, tx) + + filtered, err = arbFilteredTxs.IsTransactionFiltered(userCallOpts, txHash) + require.NoError(t, err) + require.False(t, filtered) + + // Owner revokes the role + tx, err = arbOwner.RemoveTransactionCensor(&ownerTxOpts, userTxOpts.From) + require.NoError(t, err) + require.NotNil(t, tx) + + isCensor, err = arbOwner.IsTransactionCensor(ownerCallOpts, userTxOpts.From) + require.NoError(t, err) + require.False(t, isCensor) + + // User is no longer authorised + _, err = arbFilteredTxs.IsTransactionFiltered(userCallOpts, txHash) + require.Error(t, err) +} From 0a6b8bba7e549c2c38e32865c961c4be62bb1ae6 Mon Sep 17 00:00:00 2001 From: Mikhail Rogachev Date: Mon, 29 Dec 2025 18:05:33 +0100 Subject: [PATCH 07/28] Make ArbFilteredTransactionsManager access similar to ArbNativeTokenManager --- precompiles/ArbFilteredTransactionsManager.go | 23 +++++-------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/precompiles/ArbFilteredTransactionsManager.go b/precompiles/ArbFilteredTransactionsManager.go index bcb57fef4b..038f8dabe7 100644 --- a/precompiles/ArbFilteredTransactionsManager.go +++ b/precompiles/ArbFilteredTransactionsManager.go @@ -18,11 +18,7 @@ type ArbFilteredTransactionsManager struct { // Adds a transaction hash to the filtered transactions list func (con ArbFilteredTransactionsManager) AddFilteredTransaction(c *Context, evm *vm.EVM, txHash common.Hash) error { - hasAccess, err := con.hasAccess(c) - if err != nil { - return err - } - if !hasAccess { + if !con.hasAccess(c) { return c.BurnOut() } @@ -32,11 +28,7 @@ func (con ArbFilteredTransactionsManager) AddFilteredTransaction(c *Context, evm // Deletes a transaction hash from the filtered transactions list func (con ArbFilteredTransactionsManager) DeleteFilteredTransaction(c *Context, evm *vm.EVM, txHash common.Hash) error { - hasAccess, err := con.hasAccess(c) - if err != nil { - return err - } - if !hasAccess { + if !con.hasAccess(c) { return c.BurnOut() } @@ -46,17 +38,14 @@ func (con ArbFilteredTransactionsManager) DeleteFilteredTransaction(c *Context, // Checks if a transaction hash is in the filtered transactions list func (con ArbFilteredTransactionsManager) IsTransactionFiltered(c *Context, evm *vm.EVM, txHash common.Hash) (bool, error) { - hasAccess, err := con.hasAccess(c) - if err != nil { - return false, err - } - if !hasAccess { + if !con.hasAccess(c) { return false, c.BurnOut() } filteredState := filteredTransactions.Open(evm.StateDB, c) return filteredState.IsFiltered(txHash) } -func (con ArbFilteredTransactionsManager) hasAccess(c *Context) (bool, error) { - return c.State.TransactionCensors().IsMember(c.caller) +func (con ArbFilteredTransactionsManager) hasAccess(c *Context) bool { + manager, err := c.State.TransactionCensors().IsMember(c.caller) + return manager && err == nil } From 18a4b467d970e4da39e6b7340107dbb8c8e5e1b2 Mon Sep 17 00:00:00 2001 From: Mikhail Rogachev Date: Mon, 29 Dec 2025 18:57:23 +0100 Subject: [PATCH 08/28] Add CensorPrecompile wrapper to make transaction filtring free --- precompiles/ArbFilteredTransactionsManager.go | 16 ----- precompiles/precompile.go | 6 +- precompiles/wrapper.go | 58 +++++++++++++++++++ 3 files changed, 63 insertions(+), 17 deletions(-) diff --git a/precompiles/ArbFilteredTransactionsManager.go b/precompiles/ArbFilteredTransactionsManager.go index 038f8dabe7..2992b1d27c 100644 --- a/precompiles/ArbFilteredTransactionsManager.go +++ b/precompiles/ArbFilteredTransactionsManager.go @@ -18,34 +18,18 @@ type ArbFilteredTransactionsManager struct { // Adds a transaction hash to the filtered transactions list func (con ArbFilteredTransactionsManager) AddFilteredTransaction(c *Context, evm *vm.EVM, txHash common.Hash) error { - if !con.hasAccess(c) { - return c.BurnOut() - } - filteredState := filteredTransactions.Open(evm.StateDB, c) return filteredState.Add(txHash) } // Deletes a transaction hash from the filtered transactions list func (con ArbFilteredTransactionsManager) DeleteFilteredTransaction(c *Context, evm *vm.EVM, txHash common.Hash) error { - if !con.hasAccess(c) { - return c.BurnOut() - } - filteredState := filteredTransactions.Open(evm.StateDB, c) return filteredState.Delete(txHash) } // Checks if a transaction hash is in the filtered transactions list func (con ArbFilteredTransactionsManager) IsTransactionFiltered(c *Context, evm *vm.EVM, txHash common.Hash) (bool, error) { - if !con.hasAccess(c) { - return false, c.BurnOut() - } filteredState := filteredTransactions.Open(evm.StateDB, c) return filteredState.IsFiltered(txHash) } - -func (con ArbFilteredTransactionsManager) hasAccess(c *Context) bool { - manager, err := c.State.TransactionCensors().IsMember(c.caller) - return manager && err == nil -} diff --git a/precompiles/precompile.go b/precompiles/precompile.go index a727f83282..68d9f6438c 100644 --- a/precompiles/precompile.go +++ b/precompiles/precompile.go @@ -667,12 +667,16 @@ func Precompiles() map[addr]ArbosPrecompile { ArbNativeTokenManager.methodsByName["MintNativeToken"].arbosVersion = params.ArbosVersion_41 ArbNativeTokenManager.methodsByName["BurnNativeToken"].arbosVersion = params.ArbosVersion_41 - ArbFilteredTransactionsManager := insert(MakePrecompile(precompilesgen.ArbFilteredTransactionsManagerMetaData, &ArbFilteredTransactionsManager{Address: types.ArbFilteredTransactionsManagerAddress})) + ArbFilteredTransactionsManagerImpl := &ArbFilteredTransactionsManager{Address: types.ArbFilteredTransactionsManagerAddress} + + _, ArbFilteredTransactionsManager := MakePrecompile(precompilesgen.ArbFilteredTransactionsManagerMetaData, &ArbFilteredTransactionsManager{Address: types.ArbFilteredTransactionsManagerAddress}) ArbFilteredTransactionsManager.arbosVersion = params.ArbosVersion_60 ArbFilteredTransactionsManager.methodsByName["AddFilteredTransaction"].arbosVersion = params.ArbosVersion_60 ArbFilteredTransactionsManager.methodsByName["DeleteFilteredTransaction"].arbosVersion = params.ArbosVersion_60 ArbFilteredTransactionsManager.methodsByName["IsTransactionFiltered"].arbosVersion = params.ArbosVersion_60 + insert(censorOnly(ArbFilteredTransactionsManagerImpl.Address, ArbFilteredTransactionsManager)) + // this should be executed after all precompiles have been inserted for _, contract := range contracts { precompile := contract.Precompile() diff --git a/precompiles/wrapper.go b/precompiles/wrapper.go index 17ef00cd0f..e0a4136542 100644 --- a/precompiles/wrapper.go +++ b/precompiles/wrapper.go @@ -132,3 +132,61 @@ func (wrapper *OwnerPrecompile) Precompile() *Precompile { func (wrapper *OwnerPrecompile) Name() string { return wrapper.precompile.Name() } + +// CensorPrecompile is a precompile wrapper for those only transaction censors may use. +type CensorPrecompile struct { + precompile ArbosPrecompile +} + +func censorOnly(address addr, impl ArbosPrecompile) (addr, ArbosPrecompile) { + return address, &CensorPrecompile{precompile: impl} +} + +func (wrapper *CensorPrecompile) Address() common.Address { + return wrapper.precompile.Address() +} + +func (wrapper *CensorPrecompile) Call( + input []byte, + actingAsAddress common.Address, + caller common.Address, + value *big.Int, + readOnly bool, + gasSupplied uint64, + evm *vm.EVM, +) ([]byte, uint64, multigas.MultiGas, error) { + con := wrapper.precompile + + burner := &Context{ + gasSupplied: gasSupplied, + gasUsed: multigas.ZeroGas(), + tracingInfo: util.NewTracingInfo(evm, caller, wrapper.precompile.Address(), util.TracingDuringEVM), + } + state, err := arbosState.OpenArbosState(evm.StateDB, burner) + if err != nil { + return nil, burner.GasLeft(), burner.gasUsed, err + } + + censors := state.TransactionCensors() + isCensor, err := censors.IsMember(caller) + if err != nil { + return nil, burner.GasLeft(), burner.gasUsed, err + } + if !isCensor { + return nil, burner.GasLeft(), burner.gasUsed, errors.New("unauthorized caller to access-controlled method") + } + + output, _, _, err := con.Call(input, actingAsAddress, caller, value, readOnly, gasSupplied, evm) + if err != nil { + return output, gasSupplied, multigas.ZeroGas(), err + } + return output, gasSupplied, multigas.ZeroGas(), nil +} + +func (wrapper *CensorPrecompile) Precompile() *Precompile { + return wrapper.precompile.Precompile() +} + +func (wrapper *CensorPrecompile) Name() string { + return wrapper.precompile.Name() +} From 102cbf93fb60e9f16dc7cbba3f1662a1b4ff00f5 Mon Sep 17 00:00:00 2001 From: Mikhail Rogachev Date: Tue, 30 Dec 2025 11:52:18 +0100 Subject: [PATCH 09/28] Add events for filtered transaction --- precompiles/ArbFilteredTransactionsManager.go | 18 +++++++++-- system_tests/filtered_transactions_test.go | 32 +++++++++++++++++-- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/precompiles/ArbFilteredTransactionsManager.go b/precompiles/ArbFilteredTransactionsManager.go index 2992b1d27c..fe56a979bf 100644 --- a/precompiles/ArbFilteredTransactionsManager.go +++ b/precompiles/ArbFilteredTransactionsManager.go @@ -14,18 +14,32 @@ import ( // Authorized callers are added/removed through ArbOwner precompile. type ArbFilteredTransactionsManager struct { Address addr // 0x74 + + FilteredTransactionAdded func(ctx, mech, common.Hash) error + FilteredTransactionAddedGasCost func(common.Hash) (uint64, error) + + FilteredTransactionDeleted func(ctx, mech, common.Hash) error + FilteredTransactionDeletedGasCost func(common.Hash) (uint64, error) } // Adds a transaction hash to the filtered transactions list func (con ArbFilteredTransactionsManager) AddFilteredTransaction(c *Context, evm *vm.EVM, txHash common.Hash) error { filteredState := filteredTransactions.Open(evm.StateDB, c) - return filteredState.Add(txHash) + if err := filteredState.Add(txHash); err != nil { + return err + } + + return con.FilteredTransactionAdded(c, evm, txHash) } // Deletes a transaction hash from the filtered transactions list func (con ArbFilteredTransactionsManager) DeleteFilteredTransaction(c *Context, evm *vm.EVM, txHash common.Hash) error { filteredState := filteredTransactions.Open(evm.StateDB, c) - return filteredState.Delete(txHash) + if err := filteredState.Delete(txHash); err != nil { + return err + } + + return con.FilteredTransactionDeleted(c, evm, txHash) } // Checks if a transaction hash is in the filtered transactions list diff --git a/system_tests/filtered_transactions_test.go b/system_tests/filtered_transactions_test.go index 0fa96c0907..77b8385810 100644 --- a/system_tests/filtered_transactions_test.go +++ b/system_tests/filtered_transactions_test.go @@ -77,7 +77,21 @@ func TestManageTransactionCensors(t *testing.T) { // User filters the tx tx, err = arbFilteredTxs.AddFilteredTransaction(&userTxOpts, txHash) require.NoError(t, err) - require.NotNil(t, tx) + receipt, err := bind.WaitMined(ctx, builder.L2.Client, tx) + require.NoError(t, err) + require.Equal(t, types.ReceiptStatusSuccessful, receipt.Status) + + foundAdded := false + for _, lg := range receipt.Logs { + ev, parseErr := arbFilteredTxs.ParseFilteredTransactionAdded(*lg) + if parseErr != nil { + continue + } + require.Equal(t, txHash, common.BytesToHash(ev.TxHash[:])) + foundAdded = true + break + } + require.True(t, foundAdded) filtered, err = arbFilteredTxs.IsTransactionFiltered(userCallOpts, txHash) require.NoError(t, err) @@ -86,7 +100,21 @@ func TestManageTransactionCensors(t *testing.T) { // User unfilters the tx tx, err = arbFilteredTxs.DeleteFilteredTransaction(&userTxOpts, txHash) require.NoError(t, err) - require.NotNil(t, tx) + receipt, err = bind.WaitMined(ctx, builder.L2.Client, tx) + require.NoError(t, err) + require.Equal(t, types.ReceiptStatusSuccessful, receipt.Status) + + foundDeleted := false + for _, lg := range receipt.Logs { + ev, parseErr := arbFilteredTxs.ParseFilteredTransactionDeleted(*lg) + if parseErr != nil { + continue + } + require.Equal(t, txHash, common.BytesToHash(ev.TxHash[:])) + foundDeleted = true + break + } + require.True(t, foundDeleted) filtered, err = arbFilteredTxs.IsTransactionFiltered(userCallOpts, txHash) require.NoError(t, err) From cc5818a50eb0c3c9de7215da81a0325bbbe69780 Mon Sep 17 00:00:00 2001 From: Mikhail Rogachev Date: Wed, 31 Dec 2025 11:12:31 +0100 Subject: [PATCH 10/28] Add test for free calls for FilteredTransactionsManager --- system_tests/filtered_transactions_test.go | 72 ++++++++++++++++++++-- 1 file changed, 68 insertions(+), 4 deletions(-) diff --git a/system_tests/filtered_transactions_test.go b/system_tests/filtered_transactions_test.go index 77b8385810..00f621a443 100644 --- a/system_tests/filtered_transactions_test.go +++ b/system_tests/filtered_transactions_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/arbitrum/multigas" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/params" @@ -43,6 +44,11 @@ func TestManageTransactionCensors(t *testing.T) { arbOwner, err := precompilesgen.NewArbOwner(types.ArbOwnerAddress, builder.L2.Client) require.NoError(t, err) + filteredTransactionsManagerABI, err := precompilesgen.ArbFilteredTransactionsManagerMetaData.GetAbi() + Require(t, err) + addedTopic := filteredTransactionsManagerABI.Events["FilteredTransactionAdded"].ID + deletedTopic := filteredTransactionsManagerABI.Events["FilteredTransactionDeleted"].ID + arbFilteredTxs, err := precompilesgen.NewArbFilteredTransactionsManager( types.ArbFilteredTransactionsManagerAddress, builder.L2.Client, @@ -77,12 +83,14 @@ func TestManageTransactionCensors(t *testing.T) { // User filters the tx tx, err = arbFilteredTxs.AddFilteredTransaction(&userTxOpts, txHash) require.NoError(t, err) - receipt, err := bind.WaitMined(ctx, builder.L2.Client, tx) + receipt, err := builder.L2.EnsureTxSucceeded(tx) require.NoError(t, err) - require.Equal(t, types.ReceiptStatusSuccessful, receipt.Status) foundAdded := false for _, lg := range receipt.Logs { + if lg.Topics[0] != addedTopic { + continue + } ev, parseErr := arbFilteredTxs.ParseFilteredTransactionAdded(*lg) if parseErr != nil { continue @@ -100,12 +108,14 @@ func TestManageTransactionCensors(t *testing.T) { // User unfilters the tx tx, err = arbFilteredTxs.DeleteFilteredTransaction(&userTxOpts, txHash) require.NoError(t, err) - receipt, err = bind.WaitMined(ctx, builder.L2.Client, tx) + receipt, err = builder.L2.EnsureTxSucceeded(tx) require.NoError(t, err) - require.Equal(t, types.ReceiptStatusSuccessful, receipt.Status) foundDeleted := false for _, lg := range receipt.Logs { + if lg.Topics[0] != deletedTopic { + continue + } ev, parseErr := arbFilteredTxs.ParseFilteredTransactionDeleted(*lg) if parseErr != nil { continue @@ -133,3 +143,57 @@ func TestManageTransactionCensors(t *testing.T) { _, err = arbFilteredTxs.IsTransactionFiltered(userCallOpts, txHash) require.Error(t, err) } + +func TestFilteredTransactionsManagerFreeOps(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx). + DefaultConfig(t, false). + WithArbOSVersion(params.ArbosVersion_60) + + cleanup := builder.Build(t) + defer cleanup() + + ownerTxOpts := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + + censorName := "Censor" + builder.L2Info.GenerateAccount(censorName) + + builder.L2.TransferBalance(t, "Owner", censorName, big.NewInt(1e16), builder.L2Info) + censorTxOpts := builder.L2Info.GetDefaultTransactOpts(censorName, ctx) + censorTxOpts.GasLimit = 32000000 + + txHash := common.BytesToHash([]byte{1, 2, 3, 4, 5}) + + arbOwner, err := precompilesgen.NewArbOwner(types.ArbOwnerAddress, builder.L2.Client) + require.NoError(t, err) + + arbFilteredTxs, err := precompilesgen.NewArbFilteredTransactionsManager( + types.ArbFilteredTransactionsManagerAddress, + builder.L2.Client, + ) + require.NoError(t, err) + + tx, err := arbOwner.AddTransactionCensor(&ownerTxOpts, censorTxOpts.From) + require.NoError(t, err) + require.NotNil(t, tx) + + // Censor filters the tx + tx, err = arbFilteredTxs.AddFilteredTransaction(&censorTxOpts, txHash) + require.NoError(t, err) + receipt, err := builder.L2.EnsureTxSucceeded(tx) + require.NoError(t, err) + + // AddFilteredTransaction use storage set, but it should be free for censors + require.Equal(t, uint64(0), receipt.MultiGasUsed.Get(multigas.ResourceKindStorageAccess)) + + // Censor unfilters the tx + tx, err = arbFilteredTxs.DeleteFilteredTransaction(&censorTxOpts, txHash) + require.NoError(t, err) + receipt, err = builder.L2.EnsureTxSucceeded(tx) + require.NoError(t, err) + + // DeleteFilteredTransaction use storage clear, but it should be free for censors + require.Equal(t, uint64(0), receipt.MultiGasUsed.Get(multigas.ResourceKindStorageAccess)) +} From b0085c971d7b0d74eb658bb5e980127ac3d2bc68 Mon Sep 17 00:00:00 2001 From: Mikhail Rogachev Date: Mon, 5 Jan 2026 16:39:13 +0100 Subject: [PATCH 11/28] Add limited GetTransactionCensorshipFromTime --- arbos/arbosState/arbosstate.go | 103 ++++++++++++++++++--------------- precompiles/ArbOwner.go | 93 ++++++++++++++++++++++++----- precompiles/ArbOwnerPublic.go | 6 ++ precompiles/precompile.go | 2 + precompiles/precompile_test.go | 2 +- 5 files changed, 143 insertions(+), 63 deletions(-) diff --git a/arbos/arbosState/arbosstate.go b/arbos/arbosState/arbosstate.go index d3879a9849..8c32341c0f 100644 --- a/arbos/arbosState/arbosstate.go +++ b/arbos/arbosState/arbosstate.go @@ -43,29 +43,30 @@ import ( // persisted beyond the end of the test.) type ArbosState struct { - arbosVersion uint64 // version of the ArbOS storage format and semantics - upgradeVersion storage.StorageBackedUint64 // version we're planning to upgrade to, or 0 if not planning to upgrade - upgradeTimestamp storage.StorageBackedUint64 // when to do the planned upgrade - networkFeeAccount storage.StorageBackedAddress - l1PricingState *l1pricing.L1PricingState - l2PricingState *l2pricing.L2PricingState - retryableState *retryables.RetryableState - addressTable *addressTable.AddressTable - chainOwners *addressSet.AddressSet - nativeTokenOwners *addressSet.AddressSet - transactionCensors *addressSet.AddressSet - sendMerkle *merkleAccumulator.MerkleAccumulator - programs *programs.Programs - features *features.Features - blockhashes *blockhash.Blockhashes - chainId storage.StorageBackedBigInt - chainConfig storage.StorageBackedBytes - genesisBlockNum storage.StorageBackedUint64 - infraFeeAccount storage.StorageBackedAddress - brotliCompressionLevel storage.StorageBackedUint64 // brotli compression level used for pricing - nativeTokenEnabledTime storage.StorageBackedUint64 - backingStorage *storage.Storage - Burner burn.Burner + arbosVersion uint64 // version of the ArbOS storage format and semantics + upgradeVersion storage.StorageBackedUint64 // version we're planning to upgrade to, or 0 if not planning to upgrade + upgradeTimestamp storage.StorageBackedUint64 // when to do the planned upgrade + networkFeeAccount storage.StorageBackedAddress + l1PricingState *l1pricing.L1PricingState + l2PricingState *l2pricing.L2PricingState + retryableState *retryables.RetryableState + addressTable *addressTable.AddressTable + chainOwners *addressSet.AddressSet + nativeTokenOwners *addressSet.AddressSet + transactionCensors *addressSet.AddressSet + sendMerkle *merkleAccumulator.MerkleAccumulator + programs *programs.Programs + features *features.Features + blockhashes *blockhash.Blockhashes + chainId storage.StorageBackedBigInt + chainConfig storage.StorageBackedBytes + genesisBlockNum storage.StorageBackedUint64 + infraFeeAccount storage.StorageBackedAddress + brotliCompressionLevel storage.StorageBackedUint64 // brotli compression level used for pricing + nativeTokenEnabledTime storage.StorageBackedUint64 + transactionFilteringEnabledTime storage.StorageBackedUint64 + backingStorage *storage.Storage + Burner burn.Burner } var ErrUninitializedArbOS = errors.New("ArbOS uninitialized") @@ -81,29 +82,30 @@ func OpenArbosState(stateDB vm.StateDB, burner burn.Burner) (*ArbosState, error) return nil, ErrUninitializedArbOS } return &ArbosState{ - arbosVersion: arbosVersion, - upgradeVersion: backingStorage.OpenStorageBackedUint64(uint64(upgradeVersionOffset)), - upgradeTimestamp: backingStorage.OpenStorageBackedUint64(uint64(upgradeTimestampOffset)), - networkFeeAccount: backingStorage.OpenStorageBackedAddress(uint64(networkFeeAccountOffset)), - l1PricingState: l1pricing.OpenL1PricingState(backingStorage.OpenCachedSubStorage(l1PricingSubspace), arbosVersion), - l2PricingState: l2pricing.OpenL2PricingState(backingStorage.OpenCachedSubStorage(l2PricingSubspace), arbosVersion), - retryableState: retryables.OpenRetryableState(backingStorage.OpenCachedSubStorage(retryablesSubspace), stateDB), - addressTable: addressTable.Open(backingStorage.OpenCachedSubStorage(addressTableSubspace)), - chainOwners: addressSet.OpenAddressSet(backingStorage.OpenCachedSubStorage(chainOwnerSubspace)), - nativeTokenOwners: addressSet.OpenAddressSet(backingStorage.OpenCachedSubStorage(nativeTokenOwnerSubspace)), - transactionCensors: addressSet.OpenAddressSet(backingStorage.OpenCachedSubStorage(transactionCensorSubspace)), - sendMerkle: merkleAccumulator.OpenMerkleAccumulator(backingStorage.OpenCachedSubStorage(sendMerkleSubspace)), - programs: programs.Open(arbosVersion, backingStorage.OpenSubStorage(programsSubspace)), - features: features.Open(backingStorage.OpenSubStorage(featuresSubspace)), - blockhashes: blockhash.OpenBlockhashes(backingStorage.OpenCachedSubStorage(blockhashesSubspace)), - chainId: backingStorage.OpenStorageBackedBigInt(uint64(chainIdOffset)), - chainConfig: backingStorage.OpenStorageBackedBytes(chainConfigSubspace), - genesisBlockNum: backingStorage.OpenStorageBackedUint64(uint64(genesisBlockNumOffset)), - infraFeeAccount: backingStorage.OpenStorageBackedAddress(uint64(infraFeeAccountOffset)), - brotliCompressionLevel: backingStorage.OpenStorageBackedUint64(uint64(brotliCompressionLevelOffset)), - nativeTokenEnabledTime: backingStorage.OpenStorageBackedUint64(uint64(nativeTokenEnabledFromTimeOffset)), - backingStorage: backingStorage, - Burner: burner, + arbosVersion: arbosVersion, + upgradeVersion: backingStorage.OpenStorageBackedUint64(uint64(upgradeVersionOffset)), + upgradeTimestamp: backingStorage.OpenStorageBackedUint64(uint64(upgradeTimestampOffset)), + networkFeeAccount: backingStorage.OpenStorageBackedAddress(uint64(networkFeeAccountOffset)), + l1PricingState: l1pricing.OpenL1PricingState(backingStorage.OpenCachedSubStorage(l1PricingSubspace), arbosVersion), + l2PricingState: l2pricing.OpenL2PricingState(backingStorage.OpenCachedSubStorage(l2PricingSubspace), arbosVersion), + retryableState: retryables.OpenRetryableState(backingStorage.OpenCachedSubStorage(retryablesSubspace), stateDB), + addressTable: addressTable.Open(backingStorage.OpenCachedSubStorage(addressTableSubspace)), + chainOwners: addressSet.OpenAddressSet(backingStorage.OpenCachedSubStorage(chainOwnerSubspace)), + nativeTokenOwners: addressSet.OpenAddressSet(backingStorage.OpenCachedSubStorage(nativeTokenOwnerSubspace)), + transactionCensors: addressSet.OpenAddressSet(backingStorage.OpenCachedSubStorage(transactionCensorSubspace)), + sendMerkle: merkleAccumulator.OpenMerkleAccumulator(backingStorage.OpenCachedSubStorage(sendMerkleSubspace)), + programs: programs.Open(arbosVersion, backingStorage.OpenSubStorage(programsSubspace)), + features: features.Open(backingStorage.OpenSubStorage(featuresSubspace)), + blockhashes: blockhash.OpenBlockhashes(backingStorage.OpenCachedSubStorage(blockhashesSubspace)), + chainId: backingStorage.OpenStorageBackedBigInt(uint64(chainIdOffset)), + chainConfig: backingStorage.OpenStorageBackedBytes(chainConfigSubspace), + genesisBlockNum: backingStorage.OpenStorageBackedUint64(uint64(genesisBlockNumOffset)), + infraFeeAccount: backingStorage.OpenStorageBackedAddress(uint64(infraFeeAccountOffset)), + brotliCompressionLevel: backingStorage.OpenStorageBackedUint64(uint64(brotliCompressionLevelOffset)), + nativeTokenEnabledTime: backingStorage.OpenStorageBackedUint64(uint64(nativeTokenEnabledFromTimeOffset)), + transactionFilteringEnabledTime: backingStorage.OpenStorageBackedUint64(uint64(transactionFilteringEnabledFromTimeOffset)), + backingStorage: backingStorage, + Burner: burner, }, nil } @@ -173,6 +175,7 @@ const ( infraFeeAccountOffset brotliCompressionLevelOffset nativeTokenEnabledFromTimeOffset + transactionFilteringEnabledFromTimeOffset ) type SubspaceID []byte @@ -571,6 +574,14 @@ func (state *ArbosState) NativeTokenOwners() *addressSet.AddressSet { return state.nativeTokenOwners } +func (state *ArbosState) TransactionFilteringFromTime() (uint64, error) { + return state.transactionFilteringEnabledTime.Get() +} + +func (state *ArbosState) SetTransactionFilteringFromTime(val uint64) error { + return state.transactionFilteringEnabledTime.Set(val) +} + func (state *ArbosState) TransactionCensors() *addressSet.AddressSet { return state.transactionCensors } diff --git a/precompiles/ArbOwner.go b/precompiles/ArbOwner.go index 7a38db2cb4..36807d7425 100644 --- a/precompiles/ArbOwner.go +++ b/precompiles/ArbOwner.go @@ -30,11 +30,14 @@ type ArbOwner struct { } const NativeTokenEnableDelay = 7 * 24 * 60 * 60 +const TransactionFilteringEnableDelay = 7 * 24 * 60 * 60 var ( - ErrOutOfBounds = errors.New("value out of bounds") - ErrNativeTokenDelay = errors.New("native token feature must be enabled at least 7 days in the future") - ErrNativeTokenBackward = errors.New("native token feature cannot be updated to a time earlier than the current time at which it is scheduled to be enabled") + ErrOutOfBounds = errors.New("value out of bounds") + ErrNativeTokenDelay = errors.New("native token feature must be enabled at least 7 days in the future") + ErrNativeTokenBackward = errors.New("native token feature cannot be updated to a time earlier than the current time at which it is scheduled to be enabled") + ErrTransactionFilteringDelay = errors.New("transaction filtering feature must be enabled at least 7 days in the future") + ErrTransactionFilteringBackward = errors.New("transaction filtering feature cannot be updated to a time earlier than the current time at which it is scheduled to be enabled") ) // AddChainOwner adds account as a chain owner @@ -61,6 +64,38 @@ func (con ArbOwner) GetAllChainOwners(c ctx, evm mech) ([]common.Address, error) return c.State.ChainOwners().AllMembers(65536) } +// validateFeatureFromTimeUpdate enforces the exact scheduling rules used by +// SetNativeTokenManagementFrom (and other similar "FromTime" gates). +// +// Assumptions: +// - timestamp != 0 (0 is handled by the caller as "disable"). +// - delay is in seconds. +// - now is evm.Context.Time. +func validateFeatureFromTimeUpdate( + stored uint64, + now uint64, + timestamp uint64, + delay uint64, + errDelay error, + errBackward error, +) error { + // If the feature is disabled, then the time must be at least 7 days in the + // future. + // If the feature is scheduled to be enabled more than 7 days in the future, + // and the new time is also in the future, then it must be at least 7 days + // in the future. + if (stored == 0 && timestamp < now+delay) || + (stored > now+delay && timestamp < now+delay) { + return errDelay + } + // If the feature is scheduled to be enabled earlier than the minimum delay, + // then the new time to enable it must be only further in the future. + if stored > now && stored <= now+delay && timestamp < stored { + return errBackward + } + return nil +} + // SetNativeTokenManagementFrom sets a time in epoch seconds when the native token // management becomes enabled. Setting it to 0 disables the feature. // If the feature is disabled, then the time must be at least 7 days in the @@ -74,23 +109,49 @@ func (con ArbOwner) SetNativeTokenManagementFrom(c ctx, evm mech, timestamp uint return err } now := evm.Context.Time - // If the feature is disabled, then the time must be at least 7 days in the - // future. - // If the feature is scheduled to be enabled more than 7 days in the future, - // and the new time is also in the future, then it must be at least 7 days - // in the future. - if (stored == 0 && timestamp < now+NativeTokenEnableDelay) || - (stored > now+NativeTokenEnableDelay && timestamp < now+NativeTokenEnableDelay) { - return ErrNativeTokenDelay - } - // If the feature is scheduled to be enabled earlier than the minimum delay, - // then the new time to enable it must be only further in the future. - if stored > now && stored <= now+NativeTokenEnableDelay && timestamp < stored { - return ErrNativeTokenBackward + + if err := validateFeatureFromTimeUpdate( + stored, + now, + timestamp, + NativeTokenEnableDelay, + ErrNativeTokenDelay, + ErrNativeTokenBackward, + ); err != nil { + return err } + return c.State.SetNativeTokenManagementFromTime(timestamp) } +// SetTransactionFilteringFrom sets a time in epoch seconds when the transaction filterering +// feature becomes enabled. Setting it to 0 disables the feature. +// If the feature is disabled, then the time must be at least 7 days in the +// future. +func (con ArbOwner) SetTransactionFilteringFrom(c ctx, evm mech, timestamp uint64) error { + if timestamp == 0 { + return c.State.SetTransactionFilteringFromTime(0) + } + stored, err := c.State.TransactionFilteringFromTime() + if err != nil { + return err + } + now := evm.Context.Time + + if err := validateFeatureFromTimeUpdate( + stored, + now, + timestamp, + TransactionFilteringEnableDelay, + ErrTransactionFilteringDelay, + ErrTransactionFilteringBackward, + ); err != nil { + return err + } + + return c.State.SetTransactionFilteringFromTime(timestamp) +} + // AddNativeTokenOwner adds account as a native token owner func (con ArbOwner) AddNativeTokenOwner(c ctx, evm mech, newOwner addr) error { enabledTime, err := c.State.NativeTokenManagementFromTime() diff --git a/precompiles/ArbOwnerPublic.go b/precompiles/ArbOwnerPublic.go index c885fae25b..bde4589928 100644 --- a/precompiles/ArbOwnerPublic.go +++ b/precompiles/ArbOwnerPublic.go @@ -52,6 +52,12 @@ func (con ArbOwnerPublic) GetNativeTokenManagementFrom(c ctx, evm mech) (uint64, return c.State.NativeTokenManagementFromTime() } +// TransactionFilteringFrom returns the time in epoch seconds when the +// transaction filtering feature becomes enabled +func (con ArbOwnerPublic) GetTransactionFilteringFrom(c ctx, evm mech) (uint64, error) { + return c.State.TransactionFilteringFromTime() +} + // GetNetworkFeeAccount gets the network fee collector func (con ArbOwnerPublic) GetNetworkFeeAccount(c ctx, evm mech) (addr, error) { return c.State.NetworkFeeAccount() diff --git a/precompiles/precompile.go b/precompiles/precompile.go index 68d9f6438c..d2624bf557 100644 --- a/precompiles/precompile.go +++ b/precompiles/precompile.go @@ -561,6 +561,7 @@ func Precompiles() map[addr]ArbosPrecompile { ArbOwnerPublic.methodsByName["IsNativeTokenOwner"].arbosVersion = params.ArbosVersion_41 ArbOwnerPublic.methodsByName["GetAllNativeTokenOwners"].arbosVersion = params.ArbosVersion_41 ArbOwnerPublic.methodsByName["GetParentGasFloorPerToken"].arbosVersion = params.ArbosVersion_50 + ArbOwnerPublic.methodsByName["GetTransactionFilteringFrom"].arbosVersion = params.ArbosVersion_60 ArbWasmImpl := &ArbWasm{Address: types.ArbWasmAddress} ArbWasm := insert(MakePrecompile(precompilesgen.ArbWasmMetaData, ArbWasmImpl)) @@ -659,6 +660,7 @@ func Precompiles() map[addr]ArbosPrecompile { ArbOwner.methodsByName["RemoveTransactionCensor"].arbosVersion = params.ArbosVersion_60 ArbOwner.methodsByName["IsTransactionCensor"].arbosVersion = params.ArbosVersion_60 ArbOwner.methodsByName["GetAllTransactionCensors"].arbosVersion = params.ArbosVersion_60 + ArbOwner.methodsByName["SetTransactionFilteringFrom"].arbosVersion = params.ArbosVersion_60 ArbOwnerPublic.methodsByName["GetNativeTokenManagementFrom"].arbosVersion = params.ArbosVersion_50 diff --git a/precompiles/precompile_test.go b/precompiles/precompile_test.go index eb203d8561..b382a9ab43 100644 --- a/precompiles/precompile_test.go +++ b/precompiles/precompile_test.go @@ -192,7 +192,7 @@ func TestPrecompilesPerArbosVersion(t *testing.T) { params.ArbosVersion_40: 3, params.ArbosVersion_41: 10, params.ArbosVersion_50: 9, - params.ArbosVersion_60: 10, + params.ArbosVersion_60: 12, } precompiles := Precompiles() From fb009fe098f94476961b93e5b8fc16083b4b7499 Mon Sep 17 00:00:00 2001 From: Mikhail Rogachev Date: Wed, 7 Jan 2026 16:54:07 +0100 Subject: [PATCH 12/28] Finalise limited GetTransactionCensorshipFromTime --- changelog/mrogachev-nit-4245.md | 1 + precompiles/ArbOwner.go | 9 +++- system_tests/filtered_transactions_test.go | 49 +++++++++++++++++++--- 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/changelog/mrogachev-nit-4245.md b/changelog/mrogachev-nit-4245.md index ac4fba3b88..de3314c5c1 100644 --- a/changelog/mrogachev-nit-4245.md +++ b/changelog/mrogachev-nit-4245.md @@ -1,3 +1,4 @@ ### Added - Add new precompile ArbFilteredTransactionsManager to manage filtered transactions - Add transaction censors to ArbOwner to limit access to ArbFilteredTransactionsManager +- Limit ArbOwners ability to create transaction censorers with TransactionFilteringFromTime diff --git a/precompiles/ArbOwner.go b/precompiles/ArbOwner.go index 36807d7425..6c52952000 100644 --- a/precompiles/ArbOwner.go +++ b/precompiles/ArbOwner.go @@ -184,7 +184,14 @@ func (con ArbOwner) GetAllNativeTokenOwners(c ctx, evm mech) ([]common.Address, } // AddTransactionCensor adds account as a transaction censor (authorized to use ArbFilteredTransactionsManager) -func (con ArbOwner) AddTransactionCensor(c ctx, _ mech, censor addr) error { +func (con ArbOwner) AddTransactionCensor(c ctx, evm mech, censor addr) error { + enabledTime, err := c.State.TransactionFilteringFromTime() + if err != nil { + return err + } + if enabledTime == 0 || enabledTime > evm.Context.Time { + return errors.New("transaction filtering feature is not enabled yet") + } member, err := con.IsTransactionCensor(c, nil, censor) if err != nil { return err diff --git a/system_tests/filtered_transactions_test.go b/system_tests/filtered_transactions_test.go index 00f621a443..9728afb596 100644 --- a/system_tests/filtered_transactions_test.go +++ b/system_tests/filtered_transactions_test.go @@ -16,6 +16,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/params" + "github.com/offchainlabs/nitro/precompiles" "github.com/offchainlabs/nitro/solgen/go/precompilesgen" ) @@ -24,7 +25,7 @@ func TestManageTransactionCensors(t *testing.T) { defer cancel() builder := NewNodeBuilder(ctx). - DefaultConfig(t, false). + DefaultConfig(t, true). WithArbOSVersion(params.ArbosVersion_60) cleanup := builder.Build(t) @@ -33,6 +34,7 @@ func TestManageTransactionCensors(t *testing.T) { ownerTxOpts := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) builder.L2Info.GenerateAccount("User") + builder.L2Info.GenerateAccount("User2") // For time warp builder.L2.TransferBalance(t, "Owner", "User", big.NewInt(1e16), builder.L2Info) userTxOpts := builder.L2Info.GetDefaultTransactOpts("User", ctx) @@ -62,10 +64,27 @@ func TestManageTransactionCensors(t *testing.T) { _, err = arbFilteredTxs.IsTransactionFiltered(userCallOpts, txHash) require.Error(t, err) + // Adding a censor should be disabled by default by ArbCensoredTransactionManagerFromTime + _, err = arbOwner.AddTransactionCensor(&ownerTxOpts, userTxOpts.From) + require.Error(t, err) + + // Enable transaction filtering feature 7 days in the future and warp time forward + hdr, err := builder.L2.Client.HeaderByNumber(ctx, nil) + require.NoError(t, err) + enableAt := hdr.Time + precompiles.TransactionFilteringEnableDelay + + tx, err := arbOwner.SetTransactionFilteringFrom(&ownerTxOpts, enableAt) + require.NoError(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + require.NoError(t, err) + + warpL1Time(t, builder, ctx, hdr.Time, precompiles.TransactionFilteringEnableDelay+1) + // Owner grants user transaction censor role - tx, err := arbOwner.AddTransactionCensor(&ownerTxOpts, userTxOpts.From) + tx, err = arbOwner.AddTransactionCensor(&ownerTxOpts, userTxOpts.From) + require.NoError(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) require.NoError(t, err) - require.NotNil(t, tx) isCensor, err := arbOwner.IsTransactionCensor(ownerCallOpts, userTxOpts.From) require.NoError(t, err) @@ -142,6 +161,12 @@ func TestManageTransactionCensors(t *testing.T) { // User is no longer authorised _, err = arbFilteredTxs.IsTransactionFiltered(userCallOpts, txHash) require.Error(t, err) + + // Disable transaction filtering feature again + tx, err = arbOwner.SetTransactionFilteringFrom(&ownerTxOpts, 0) + require.NoError(t, err) + receipt, err = builder.L2.EnsureTxSucceeded(tx) + require.NoError(t, err) } func TestFilteredTransactionsManagerFreeOps(t *testing.T) { @@ -149,7 +174,7 @@ func TestFilteredTransactionsManagerFreeOps(t *testing.T) { defer cancel() builder := NewNodeBuilder(ctx). - DefaultConfig(t, false). + DefaultConfig(t, true). WithArbOSVersion(params.ArbosVersion_60) cleanup := builder.Build(t) @@ -159,6 +184,7 @@ func TestFilteredTransactionsManagerFreeOps(t *testing.T) { censorName := "Censor" builder.L2Info.GenerateAccount(censorName) + builder.L2Info.GenerateAccount("User2") // For time warp builder.L2.TransferBalance(t, "Owner", censorName, big.NewInt(1e16), builder.L2Info) censorTxOpts := builder.L2Info.GetDefaultTransactOpts(censorName, ctx) @@ -175,7 +201,20 @@ func TestFilteredTransactionsManagerFreeOps(t *testing.T) { ) require.NoError(t, err) - tx, err := arbOwner.AddTransactionCensor(&ownerTxOpts, censorTxOpts.From) + // Enable transaction filtering feature 7 days in the future and warp time forward + hdr, err := builder.L2.Client.HeaderByNumber(ctx, nil) + require.NoError(t, err) + enableAt := hdr.Time + precompiles.TransactionFilteringEnableDelay + + tx, err := arbOwner.SetTransactionFilteringFrom(&ownerTxOpts, enableAt) + require.NoError(t, err) + _, err = builder.L2.EnsureTxSucceeded(tx) + require.NoError(t, err) + + warpL1Time(t, builder, ctx, hdr.Time, precompiles.TransactionFilteringEnableDelay+1) + + // Owner grants censor transaction censor role + tx, err = arbOwner.AddTransactionCensor(&ownerTxOpts, censorTxOpts.From) require.NoError(t, err) require.NotNil(t, tx) From fa1ca8a79a4d642472f69bbedd0330203456f310 Mon Sep 17 00:00:00 2001 From: Mikhail Rogachev Date: Thu, 8 Jan 2026 12:05:15 +0100 Subject: [PATCH 13/28] Add TransactionFilteringEnabled to ArbOS state init --- arbos/arbosState/arbosstate.go | 10 ++++++++++ system_tests/filtered_transactions_test.go | 21 +++++++-------------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/arbos/arbosState/arbosstate.go b/arbos/arbosState/arbosstate.go index 8c32341c0f..40e4b705a4 100644 --- a/arbos/arbosState/arbosstate.go +++ b/arbos/arbosState/arbosstate.go @@ -235,6 +235,16 @@ func InitializeArbosState(stateDB vm.StateDB, burner burn.Burner, chainConfig *p return nil, err } + transactionFilteringEnabledFromTime := uint64(0) + if genesisArbOSInit != nil && genesisArbOSInit.TransactionFilteringEnabled { + // Same logic as for native token management above + transactionFilteringEnabledFromTime = uint64(1) + } + err = sto.SetUint64ByUint64(uint64(transactionFilteringEnabledFromTimeOffset), transactionFilteringEnabledFromTime) + if err != nil { + return nil, err + } + err = sto.SetUint64ByUint64(uint64(versionOffset), 1) // initialize to version 1; upgrade at end of this func if needed if err != nil { return nil, err diff --git a/system_tests/filtered_transactions_test.go b/system_tests/filtered_transactions_test.go index 9728afb596..ab932c2fa0 100644 --- a/system_tests/filtered_transactions_test.go +++ b/system_tests/filtered_transactions_test.go @@ -173,9 +173,14 @@ func TestFilteredTransactionsManagerFreeOps(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + arbOSInit := ¶ms.ArbOSInit{ + TransactionFilteringEnabled: true, + } + builder := NewNodeBuilder(ctx). DefaultConfig(t, true). - WithArbOSVersion(params.ArbosVersion_60) + WithArbOSVersion(params.ArbosVersion_60). + WithArbOSInit(arbOSInit) cleanup := builder.Build(t) defer cleanup() @@ -201,20 +206,8 @@ func TestFilteredTransactionsManagerFreeOps(t *testing.T) { ) require.NoError(t, err) - // Enable transaction filtering feature 7 days in the future and warp time forward - hdr, err := builder.L2.Client.HeaderByNumber(ctx, nil) - require.NoError(t, err) - enableAt := hdr.Time + precompiles.TransactionFilteringEnableDelay - - tx, err := arbOwner.SetTransactionFilteringFrom(&ownerTxOpts, enableAt) - require.NoError(t, err) - _, err = builder.L2.EnsureTxSucceeded(tx) - require.NoError(t, err) - - warpL1Time(t, builder, ctx, hdr.Time, precompiles.TransactionFilteringEnableDelay+1) - // Owner grants censor transaction censor role - tx, err = arbOwner.AddTransactionCensor(&ownerTxOpts, censorTxOpts.From) + tx, err := arbOwner.AddTransactionCensor(&ownerTxOpts, censorTxOpts.From) require.NoError(t, err) require.NotNil(t, tx) From 64d57250b423aad3b8b2aec26bbbee6a3e805bda Mon Sep 17 00:00:00 2001 From: Mikhail Rogachev Date: Thu, 8 Jan 2026 13:09:21 +0100 Subject: [PATCH 14/28] Improve tests and polish comments --- precompiles/ArbOwner.go | 13 ++++--------- system_tests/filtered_transactions_test.go | 11 ++++++++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/precompiles/ArbOwner.go b/precompiles/ArbOwner.go index 6c52952000..a89a371050 100644 --- a/precompiles/ArbOwner.go +++ b/precompiles/ArbOwner.go @@ -29,8 +29,8 @@ type ArbOwner struct { OwnerActsGasCost func(bytes4, addr, []byte) (uint64, error) } -const NativeTokenEnableDelay = 7 * 24 * 60 * 60 -const TransactionFilteringEnableDelay = 7 * 24 * 60 * 60 +const NativeTokenEnableDelay = 7 * 24 * 60 * 60 // one week +const TransactionFilteringEnableDelay = 7 * 24 * 60 * 60 // one week var ( ErrOutOfBounds = errors.New("value out of bounds") @@ -64,13 +64,8 @@ func (con ArbOwner) GetAllChainOwners(c ctx, evm mech) ([]common.Address, error) return c.State.ChainOwners().AllMembers(65536) } -// validateFeatureFromTimeUpdate enforces the exact scheduling rules used by -// SetNativeTokenManagementFrom (and other similar "FromTime" gates). -// -// Assumptions: -// - timestamp != 0 (0 is handled by the caller as "disable"). -// - delay is in seconds. -// - now is evm.Context.Time. +// validateFeatureFromTimeUpdate enforces the scheduling rules used by +// SetNativeTokenManagementFrom and SetTransactionFilteringFrom func validateFeatureFromTimeUpdate( stored uint64, now uint64, diff --git a/system_tests/filtered_transactions_test.go b/system_tests/filtered_transactions_test.go index ab932c2fa0..0f92b4ee43 100644 --- a/system_tests/filtered_transactions_test.go +++ b/system_tests/filtered_transactions_test.go @@ -68,11 +68,15 @@ func TestManageTransactionCensors(t *testing.T) { _, err = arbOwner.AddTransactionCensor(&ownerTxOpts, userTxOpts.From) require.Error(t, err) - // Enable transaction filtering feature 7 days in the future and warp time forward + // Make sure transaction filtering can not be enabled before one week delay hdr, err := builder.L2.Client.HeaderByNumber(ctx, nil) require.NoError(t, err) - enableAt := hdr.Time + precompiles.TransactionFilteringEnableDelay + tryEnableAt := hdr.Time + (5 * 24 * 60 * 60) // 5 days in the future + _, err = arbOwner.SetTransactionFilteringFrom(&ownerTxOpts, tryEnableAt) + require.Error(t, err) + // Enable transaction filtering feature 7 days in the future and warp time forward + enableAt := hdr.Time + precompiles.TransactionFilteringEnableDelay tx, err := arbOwner.SetTransactionFilteringFrom(&ownerTxOpts, enableAt) require.NoError(t, err) _, err = builder.L2.EnsureTxSucceeded(tx) @@ -105,6 +109,7 @@ func TestManageTransactionCensors(t *testing.T) { receipt, err := builder.L2.EnsureTxSucceeded(tx) require.NoError(t, err) + // Check that the FilteredTransactionAdded event was emitted foundAdded := false for _, lg := range receipt.Logs { if lg.Topics[0] != addedTopic { @@ -130,6 +135,7 @@ func TestManageTransactionCensors(t *testing.T) { receipt, err = builder.L2.EnsureTxSucceeded(tx) require.NoError(t, err) + // Check that the FilteredTransactionDeleted event was emitted foundDeleted := false for _, lg := range receipt.Logs { if lg.Topics[0] != deletedTopic { @@ -189,7 +195,6 @@ func TestFilteredTransactionsManagerFreeOps(t *testing.T) { censorName := "Censor" builder.L2Info.GenerateAccount(censorName) - builder.L2Info.GenerateAccount("User2") // For time warp builder.L2.TransferBalance(t, "Owner", censorName, big.NewInt(1e16), builder.L2Info) censorTxOpts := builder.L2Info.GetDefaultTransactOpts(censorName, ctx) From d5690a1e2c718739b99f8b548c831e9a0bec5a73 Mon Sep 17 00:00:00 2001 From: Mikhail Rogachev Date: Fri, 9 Jan 2026 13:53:26 +0100 Subject: [PATCH 15/28] Review fixes and polishing --- arbos/arbosState/arbosstate.go | 36 ++++++------- changelog/mrogachev-nit-4245.md | 4 +- nitro-testnode | 2 +- precompiles/ArbFilteredTransactionsManager.go | 2 +- precompiles/ArbOwner.go | 53 +++++++------------ precompiles/precompile.go | 10 ++-- precompiles/wrapper.go | 24 +++++---- system_tests/filtered_transactions_test.go | 48 ++++++++--------- 8 files changed, 83 insertions(+), 96 deletions(-) diff --git a/arbos/arbosState/arbosstate.go b/arbos/arbosState/arbosstate.go index 40e4b705a4..4a579945c7 100644 --- a/arbos/arbosState/arbosstate.go +++ b/arbos/arbosState/arbosstate.go @@ -53,7 +53,7 @@ type ArbosState struct { addressTable *addressTable.AddressTable chainOwners *addressSet.AddressSet nativeTokenOwners *addressSet.AddressSet - transactionCensors *addressSet.AddressSet + transactionFilterers *addressSet.AddressSet sendMerkle *merkleAccumulator.MerkleAccumulator programs *programs.Programs features *features.Features @@ -92,7 +92,7 @@ func OpenArbosState(stateDB vm.StateDB, burner burn.Burner) (*ArbosState, error) addressTable: addressTable.Open(backingStorage.OpenCachedSubStorage(addressTableSubspace)), chainOwners: addressSet.OpenAddressSet(backingStorage.OpenCachedSubStorage(chainOwnerSubspace)), nativeTokenOwners: addressSet.OpenAddressSet(backingStorage.OpenCachedSubStorage(nativeTokenOwnerSubspace)), - transactionCensors: addressSet.OpenAddressSet(backingStorage.OpenCachedSubStorage(transactionCensorSubspace)), + transactionFilterers: addressSet.OpenAddressSet(backingStorage.OpenCachedSubStorage(transactionFiltererSubspace)), sendMerkle: merkleAccumulator.OpenMerkleAccumulator(backingStorage.OpenCachedSubStorage(sendMerkleSubspace)), programs: programs.Open(arbosVersion, backingStorage.OpenSubStorage(programsSubspace)), features: features.Open(backingStorage.OpenSubStorage(featuresSubspace)), @@ -181,18 +181,18 @@ const ( type SubspaceID []byte var ( - l1PricingSubspace SubspaceID = []byte{0} - l2PricingSubspace SubspaceID = []byte{1} - retryablesSubspace SubspaceID = []byte{2} - addressTableSubspace SubspaceID = []byte{3} - chainOwnerSubspace SubspaceID = []byte{4} - sendMerkleSubspace SubspaceID = []byte{5} - blockhashesSubspace SubspaceID = []byte{6} - chainConfigSubspace SubspaceID = []byte{7} - programsSubspace SubspaceID = []byte{8} - featuresSubspace SubspaceID = []byte{9} - nativeTokenOwnerSubspace SubspaceID = []byte{10} - transactionCensorSubspace SubspaceID = []byte{11} + l1PricingSubspace SubspaceID = []byte{0} + l2PricingSubspace SubspaceID = []byte{1} + retryablesSubspace SubspaceID = []byte{2} + addressTableSubspace SubspaceID = []byte{3} + chainOwnerSubspace SubspaceID = []byte{4} + sendMerkleSubspace SubspaceID = []byte{5} + blockhashesSubspace SubspaceID = []byte{6} + chainConfigSubspace SubspaceID = []byte{7} + programsSubspace SubspaceID = []byte{8} + featuresSubspace SubspaceID = []byte{9} + nativeTokenOwnerSubspace SubspaceID = []byte{10} + transactionFiltererSubspace SubspaceID = []byte{11} ) var PrecompileMinArbOSVersions = make(map[common.Address]uint64) @@ -324,8 +324,8 @@ func InitializeArbosState(stateDB vm.StateDB, burner burn.Burner, chainConfig *p return nil, err } - transactionCensorsStorage := sto.OpenCachedSubStorage(transactionCensorSubspace) - err = addressSet.Initialize(transactionCensorsStorage) + transactionFiltererStorage := sto.OpenCachedSubStorage(transactionFiltererSubspace) + err = addressSet.Initialize(transactionFiltererStorage) if err != nil { return nil, err } @@ -592,8 +592,8 @@ func (state *ArbosState) SetTransactionFilteringFromTime(val uint64) error { return state.transactionFilteringEnabledTime.Set(val) } -func (state *ArbosState) TransactionCensors() *addressSet.AddressSet { - return state.transactionCensors +func (state *ArbosState) TransactionFilterers() *addressSet.AddressSet { + return state.transactionFilterers } func (state *ArbosState) SendMerkleAccumulator() *merkleAccumulator.MerkleAccumulator { diff --git a/changelog/mrogachev-nit-4245.md b/changelog/mrogachev-nit-4245.md index de3314c5c1..2231d26416 100644 --- a/changelog/mrogachev-nit-4245.md +++ b/changelog/mrogachev-nit-4245.md @@ -1,4 +1,4 @@ ### Added - Add new precompile ArbFilteredTransactionsManager to manage filtered transactions -- Add transaction censors to ArbOwner to limit access to ArbFilteredTransactionsManager -- Limit ArbOwners ability to create transaction censorers with TransactionFilteringFromTime +- Add transaction filterers to ArbOwner to limit access to ArbFilteredTransactionsManager +- Limit ArbOwners ability to create transaction filterers with TransactionFilteringFromTime diff --git a/nitro-testnode b/nitro-testnode index c755cbb40b..661d1a90e2 160000 --- a/nitro-testnode +++ b/nitro-testnode @@ -1 +1 @@ -Subproject commit c755cbb40b68ef7aaa587719927a7b74332dd2c1 +Subproject commit 661d1a90e2101e8471712b552a14ff6d1f857a62 diff --git a/precompiles/ArbFilteredTransactionsManager.go b/precompiles/ArbFilteredTransactionsManager.go index fe56a979bf..b10e566c32 100644 --- a/precompiles/ArbFilteredTransactionsManager.go +++ b/precompiles/ArbFilteredTransactionsManager.go @@ -10,7 +10,7 @@ import ( "github.com/offchainlabs/nitro/arbos/filteredTransactions" ) -// ArbFilteredTransactionsManager precompile enables ability to censor transactions by authorized callers. +// ArbFilteredTransactionsManager precompile enables ability to filter transactions by authorized callers. // Authorized callers are added/removed through ArbOwner precompile. type ArbFilteredTransactionsManager struct { Address addr // 0x74 diff --git a/precompiles/ArbOwner.go b/precompiles/ArbOwner.go index a89a371050..11484899ed 100644 --- a/precompiles/ArbOwner.go +++ b/precompiles/ArbOwner.go @@ -33,11 +33,9 @@ const NativeTokenEnableDelay = 7 * 24 * 60 * 60 // one week const TransactionFilteringEnableDelay = 7 * 24 * 60 * 60 // one week var ( - ErrOutOfBounds = errors.New("value out of bounds") - ErrNativeTokenDelay = errors.New("native token feature must be enabled at least 7 days in the future") - ErrNativeTokenBackward = errors.New("native token feature cannot be updated to a time earlier than the current time at which it is scheduled to be enabled") - ErrTransactionFilteringDelay = errors.New("transaction filtering feature must be enabled at least 7 days in the future") - ErrTransactionFilteringBackward = errors.New("transaction filtering feature cannot be updated to a time earlier than the current time at which it is scheduled to be enabled") + ErrOutOfBounds = errors.New("value out of bounds") + ErrDelay = errors.New("feature must be enabled at least 7 days in the future") + ErrBackward = errors.New("feature cannot be updated to a time earlier than the current scheduled enable time") ) // AddChainOwner adds account as a chain owner @@ -71,8 +69,6 @@ func validateFeatureFromTimeUpdate( now uint64, timestamp uint64, delay uint64, - errDelay error, - errBackward error, ) error { // If the feature is disabled, then the time must be at least 7 days in the // future. @@ -81,12 +77,12 @@ func validateFeatureFromTimeUpdate( // in the future. if (stored == 0 && timestamp < now+delay) || (stored > now+delay && timestamp < now+delay) { - return errDelay + return ErrDelay } // If the feature is scheduled to be enabled earlier than the minimum delay, // then the new time to enable it must be only further in the future. if stored > now && stored <= now+delay && timestamp < stored { - return errBackward + return ErrBackward } return nil } @@ -110,8 +106,6 @@ func (con ArbOwner) SetNativeTokenManagementFrom(c ctx, evm mech, timestamp uint now, timestamp, NativeTokenEnableDelay, - ErrNativeTokenDelay, - ErrNativeTokenBackward, ); err != nil { return err } @@ -138,8 +132,6 @@ func (con ArbOwner) SetTransactionFilteringFrom(c ctx, evm mech, timestamp uint6 now, timestamp, TransactionFilteringEnableDelay, - ErrTransactionFilteringDelay, - ErrTransactionFilteringBackward, ); err != nil { return err } @@ -178,8 +170,8 @@ func (con ArbOwner) GetAllNativeTokenOwners(c ctx, evm mech) ([]common.Address, return c.State.NativeTokenOwners().AllMembers(65536) } -// AddTransactionCensor adds account as a transaction censor (authorized to use ArbFilteredTransactionsManager) -func (con ArbOwner) AddTransactionCensor(c ctx, evm mech, censor addr) error { +// AddTransactionFilterer adds account as a transaction filterer (authorized to use ArbFilteredTransactionsManager) +func (con ArbOwner) AddTransactionFilterer(c ctx, evm mech, filterer addr) error { enabledTime, err := c.State.TransactionFilteringFromTime() if err != nil { return err @@ -187,36 +179,29 @@ func (con ArbOwner) AddTransactionCensor(c ctx, evm mech, censor addr) error { if enabledTime == 0 || enabledTime > evm.Context.Time { return errors.New("transaction filtering feature is not enabled yet") } - member, err := con.IsTransactionCensor(c, nil, censor) - if err != nil { - return err - } - if member { - return errors.New("tried to add existing transaction censor") - } - return c.State.TransactionCensors().Add(censor) + return c.State.TransactionFilterers().Add(filterer) } -// RemoveTransactionCensor removes account from the list of transaction censors -func (con ArbOwner) RemoveTransactionCensor(c ctx, _ mech, censor addr) error { - member, err := con.IsTransactionCensor(c, nil, censor) +// RemoveTransactionFilterer removes account from the list of transaction filterers +func (con ArbOwner) RemoveTransactionFilterer(c ctx, _ mech, filterer addr) error { + member, err := con.IsTransactionFilterer(c, nil, filterer) if err != nil { return err } if !member { - return errors.New("tried to remove non existing transaction censor") + return errors.New("tried to remove non existing transaction filterer") } - return c.State.TransactionCensors().Remove(censor, c.State.ArbOSVersion()) + return c.State.TransactionFilterers().Remove(filterer, c.State.ArbOSVersion()) } -// IsTransactionCensor checks if the account is a transaction censor -func (con ArbOwner) IsTransactionCensor(c ctx, _ mech, censor addr) (bool, error) { - return c.State.TransactionCensors().IsMember(censor) +// IsTransactionFilterer checks if the account is a transaction filterer +func (con ArbOwner) IsTransactionFilterer(c ctx, _ mech, filterer addr) (bool, error) { + return c.State.TransactionFilterers().IsMember(filterer) } -// GetAllTransactionCensors retrieves the list of transaction censors -func (con ArbOwner) GetAllTransactionCensors(c ctx, evm mech) ([]common.Address, error) { - return c.State.TransactionCensors().AllMembers(65536) +// GetAllTransactionFilterers retrieves the list of transaction filterers +func (con ArbOwner) GetAllTransactionFilterers(c ctx, evm mech) ([]common.Address, error) { + return c.State.TransactionFilterers().AllMembers(65536) } // SetL1BaseFeeEstimateInertia sets how slowly ArbOS updates its estimate of the L1 basefee diff --git a/precompiles/precompile.go b/precompiles/precompile.go index d2624bf557..3e906299c6 100644 --- a/precompiles/precompile.go +++ b/precompiles/precompile.go @@ -656,10 +656,10 @@ func Precompiles() map[addr]ArbosPrecompile { ArbOwner.methodsByName["SetParentGasFloorPerToken"].arbosVersion = params.ArbosVersion_50 ArbOwner.methodsByName["SetMaxBlockGasLimit"].arbosVersion = params.ArbosVersion_50 - ArbOwner.methodsByName["AddTransactionCensor"].arbosVersion = params.ArbosVersion_60 - ArbOwner.methodsByName["RemoveTransactionCensor"].arbosVersion = params.ArbosVersion_60 - ArbOwner.methodsByName["IsTransactionCensor"].arbosVersion = params.ArbosVersion_60 - ArbOwner.methodsByName["GetAllTransactionCensors"].arbosVersion = params.ArbosVersion_60 + ArbOwner.methodsByName["AddTransactionFilterer"].arbosVersion = params.ArbosVersion_60 + ArbOwner.methodsByName["RemoveTransactionFilterer"].arbosVersion = params.ArbosVersion_60 + ArbOwner.methodsByName["IsTransactionFilterer"].arbosVersion = params.ArbosVersion_60 + ArbOwner.methodsByName["GetAllTransactionFilterers"].arbosVersion = params.ArbosVersion_60 ArbOwner.methodsByName["SetTransactionFilteringFrom"].arbosVersion = params.ArbosVersion_60 ArbOwnerPublic.methodsByName["GetNativeTokenManagementFrom"].arbosVersion = params.ArbosVersion_50 @@ -677,7 +677,7 @@ func Precompiles() map[addr]ArbosPrecompile { ArbFilteredTransactionsManager.methodsByName["DeleteFilteredTransaction"].arbosVersion = params.ArbosVersion_60 ArbFilteredTransactionsManager.methodsByName["IsTransactionFiltered"].arbosVersion = params.ArbosVersion_60 - insert(censorOnly(ArbFilteredTransactionsManagerImpl.Address, ArbFilteredTransactionsManager)) + insert(filtererOnly(ArbFilteredTransactionsManagerImpl.Address, ArbFilteredTransactionsManager)) // this should be executed after all precompiles have been inserted for _, contract := range contracts { diff --git a/precompiles/wrapper.go b/precompiles/wrapper.go index e0a4136542..11b25117e3 100644 --- a/precompiles/wrapper.go +++ b/precompiles/wrapper.go @@ -133,20 +133,22 @@ func (wrapper *OwnerPrecompile) Name() string { return wrapper.precompile.Name() } -// CensorPrecompile is a precompile wrapper for those only transaction censors may use. -type CensorPrecompile struct { +// TransactionFilterPrecompile is a precompile wrapper for those only transaction filterers may use. +type TransactionFilterPrecompile struct { precompile ArbosPrecompile } -func censorOnly(address addr, impl ArbosPrecompile) (addr, ArbosPrecompile) { - return address, &CensorPrecompile{precompile: impl} +func filtererOnly(address addr, impl ArbosPrecompile) (addr, ArbosPrecompile) { + return address, &TransactionFilterPrecompile{precompile: impl} } -func (wrapper *CensorPrecompile) Address() common.Address { +func (wrapper *TransactionFilterPrecompile) Address() common.Address { return wrapper.precompile.Address() } -func (wrapper *CensorPrecompile) Call( +// Call is overridden to enforce the transaction-filterer permission and to keep the +// underlying state reads/writes free for authorised callers (no StorageAccess multigas). +func (wrapper *TransactionFilterPrecompile) Call( input []byte, actingAsAddress common.Address, caller common.Address, @@ -167,12 +169,12 @@ func (wrapper *CensorPrecompile) Call( return nil, burner.GasLeft(), burner.gasUsed, err } - censors := state.TransactionCensors() - isCensor, err := censors.IsMember(caller) + filterers := state.TransactionFilterers() + isFilterer, err := filterers.IsMember(caller) if err != nil { return nil, burner.GasLeft(), burner.gasUsed, err } - if !isCensor { + if !isFilterer { return nil, burner.GasLeft(), burner.gasUsed, errors.New("unauthorized caller to access-controlled method") } @@ -183,10 +185,10 @@ func (wrapper *CensorPrecompile) Call( return output, gasSupplied, multigas.ZeroGas(), nil } -func (wrapper *CensorPrecompile) Precompile() *Precompile { +func (wrapper *TransactionFilterPrecompile) Precompile() *Precompile { return wrapper.precompile.Precompile() } -func (wrapper *CensorPrecompile) Name() string { +func (wrapper *TransactionFilterPrecompile) Name() string { return wrapper.precompile.Name() } diff --git a/system_tests/filtered_transactions_test.go b/system_tests/filtered_transactions_test.go index 0f92b4ee43..c7956b3d8d 100644 --- a/system_tests/filtered_transactions_test.go +++ b/system_tests/filtered_transactions_test.go @@ -20,7 +20,7 @@ import ( "github.com/offchainlabs/nitro/solgen/go/precompilesgen" ) -func TestManageTransactionCensors(t *testing.T) { +func TestManageTransactionFilterers(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -64,8 +64,8 @@ func TestManageTransactionCensors(t *testing.T) { _, err = arbFilteredTxs.IsTransactionFiltered(userCallOpts, txHash) require.Error(t, err) - // Adding a censor should be disabled by default by ArbCensoredTransactionManagerFromTime - _, err = arbOwner.AddTransactionCensor(&ownerTxOpts, userTxOpts.From) + // Adding a filterer should be disabled by default by ArbFiltereredTransactionManagerFromTime + _, err = arbOwner.AddTransactionFilterer(&ownerTxOpts, userTxOpts.From) require.Error(t, err) // Make sure transaction filtering can not be enabled before one week delay @@ -84,17 +84,17 @@ func TestManageTransactionCensors(t *testing.T) { warpL1Time(t, builder, ctx, hdr.Time, precompiles.TransactionFilteringEnableDelay+1) - // Owner grants user transaction censor role - tx, err = arbOwner.AddTransactionCensor(&ownerTxOpts, userTxOpts.From) + // Owner grants user transaction filterer role + tx, err = arbOwner.AddTransactionFilterer(&ownerTxOpts, userTxOpts.From) require.NoError(t, err) _, err = builder.L2.EnsureTxSucceeded(tx) require.NoError(t, err) - isCensor, err := arbOwner.IsTransactionCensor(ownerCallOpts, userTxOpts.From) + isFilterer, err := arbOwner.IsTransactionFilterer(ownerCallOpts, userTxOpts.From) require.NoError(t, err) - require.True(t, isCensor) + require.True(t, isFilterer) - // Owner is still not a censor, so owner still cannot call the manager + // Owner is still not a filterer, so owner still cannot call the manager _, err = arbFilteredTxs.IsTransactionFiltered(ownerCallOpts, txHash) require.Error(t, err) @@ -156,13 +156,13 @@ func TestManageTransactionCensors(t *testing.T) { require.False(t, filtered) // Owner revokes the role - tx, err = arbOwner.RemoveTransactionCensor(&ownerTxOpts, userTxOpts.From) + tx, err = arbOwner.RemoveTransactionFilterer(&ownerTxOpts, userTxOpts.From) require.NoError(t, err) require.NotNil(t, tx) - isCensor, err = arbOwner.IsTransactionCensor(ownerCallOpts, userTxOpts.From) + isFilterer, err = arbOwner.IsTransactionFilterer(ownerCallOpts, userTxOpts.From) require.NoError(t, err) - require.False(t, isCensor) + require.False(t, isFilterer) // User is no longer authorised _, err = arbFilteredTxs.IsTransactionFiltered(userCallOpts, txHash) @@ -193,12 +193,12 @@ func TestFilteredTransactionsManagerFreeOps(t *testing.T) { ownerTxOpts := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) - censorName := "Censor" - builder.L2Info.GenerateAccount(censorName) + filtererName := "Filterer" + builder.L2Info.GenerateAccount(filtererName) - builder.L2.TransferBalance(t, "Owner", censorName, big.NewInt(1e16), builder.L2Info) - censorTxOpts := builder.L2Info.GetDefaultTransactOpts(censorName, ctx) - censorTxOpts.GasLimit = 32000000 + builder.L2.TransferBalance(t, "Owner", filtererName, big.NewInt(1e16), builder.L2Info) + filtererTxOpts := builder.L2Info.GetDefaultTransactOpts(filtererName, ctx) + filtererTxOpts.GasLimit = 32000000 txHash := common.BytesToHash([]byte{1, 2, 3, 4, 5}) @@ -211,26 +211,26 @@ func TestFilteredTransactionsManagerFreeOps(t *testing.T) { ) require.NoError(t, err) - // Owner grants censor transaction censor role - tx, err := arbOwner.AddTransactionCensor(&ownerTxOpts, censorTxOpts.From) + // Owner grants filterer transaction filterer role + tx, err := arbOwner.AddTransactionFilterer(&ownerTxOpts, filtererTxOpts.From) require.NoError(t, err) require.NotNil(t, tx) - // Censor filters the tx - tx, err = arbFilteredTxs.AddFilteredTransaction(&censorTxOpts, txHash) + // Filterer filters the tx + tx, err = arbFilteredTxs.AddFilteredTransaction(&filtererTxOpts, txHash) require.NoError(t, err) receipt, err := builder.L2.EnsureTxSucceeded(tx) require.NoError(t, err) - // AddFilteredTransaction use storage set, but it should be free for censors + // AddFilteredTransaction use storage set, but it should be free for filterers require.Equal(t, uint64(0), receipt.MultiGasUsed.Get(multigas.ResourceKindStorageAccess)) - // Censor unfilters the tx - tx, err = arbFilteredTxs.DeleteFilteredTransaction(&censorTxOpts, txHash) + // Filterer unfilters the tx + tx, err = arbFilteredTxs.DeleteFilteredTransaction(&filtererTxOpts, txHash) require.NoError(t, err) receipt, err = builder.L2.EnsureTxSucceeded(tx) require.NoError(t, err) - // DeleteFilteredTransaction use storage clear, but it should be free for censors + // DeleteFilteredTransaction use storage clear, but it should be free for filterers require.Equal(t, uint64(0), receipt.MultiGasUsed.Get(multigas.ResourceKindStorageAccess)) } From 55dea76623adb3ebc13da660f39c3a2f267c1f36 Mon Sep 17 00:00:00 2001 From: Mikhail Rogachev Date: Mon, 12 Jan 2026 14:25:30 +0100 Subject: [PATCH 16/28] Make `IsTransactionFiltered` public --- precompiles/ArbFilteredTransactionsManager.go | 13 ++++++++++ precompiles/wrapper.go | 26 +++++++++++++------ system_tests/filtered_transactions_test.go | 23 +++++++++------- 3 files changed, 45 insertions(+), 17 deletions(-) diff --git a/precompiles/ArbFilteredTransactionsManager.go b/precompiles/ArbFilteredTransactionsManager.go index b10e566c32..ddb1b59f27 100644 --- a/precompiles/ArbFilteredTransactionsManager.go +++ b/precompiles/ArbFilteredTransactionsManager.go @@ -24,6 +24,10 @@ type ArbFilteredTransactionsManager struct { // Adds a transaction hash to the filtered transactions list func (con ArbFilteredTransactionsManager) AddFilteredTransaction(c *Context, evm *vm.EVM, txHash common.Hash) error { + if !con.hasAccess(c) { + return c.BurnOut() + } + filteredState := filteredTransactions.Open(evm.StateDB, c) if err := filteredState.Add(txHash); err != nil { return err @@ -34,6 +38,10 @@ func (con ArbFilteredTransactionsManager) AddFilteredTransaction(c *Context, evm // Deletes a transaction hash from the filtered transactions list func (con ArbFilteredTransactionsManager) DeleteFilteredTransaction(c *Context, evm *vm.EVM, txHash common.Hash) error { + if !con.hasAccess(c) { + return c.BurnOut() + } + filteredState := filteredTransactions.Open(evm.StateDB, c) if err := filteredState.Delete(txHash); err != nil { return err @@ -47,3 +55,8 @@ func (con ArbFilteredTransactionsManager) IsTransactionFiltered(c *Context, evm filteredState := filteredTransactions.Open(evm.StateDB, c) return filteredState.IsFiltered(txHash) } + +func (con ArbFilteredTransactionsManager) hasAccess(c *Context) bool { + manager, err := c.State.TransactionFilterers().IsMember(c.caller) + return manager && err == nil +} diff --git a/precompiles/wrapper.go b/precompiles/wrapper.go index 11b25117e3..dfaa8d945f 100644 --- a/precompiles/wrapper.go +++ b/precompiles/wrapper.go @@ -133,7 +133,8 @@ func (wrapper *OwnerPrecompile) Name() string { return wrapper.precompile.Name() } -// TransactionFilterPrecompile is a precompile wrapper for those only transaction filterers may use. +// TransactionFilterPrecompile wraps ArbFilteredTransactionsManager to preserve free storage access for filterers. +// Access control is NOT enforced here. type TransactionFilterPrecompile struct { precompile ArbosPrecompile } @@ -146,8 +147,7 @@ func (wrapper *TransactionFilterPrecompile) Address() common.Address { return wrapper.precompile.Address() } -// Call is overridden to enforce the transaction-filterer permission and to keep the -// underlying state reads/writes free for authorised callers (no StorageAccess multigas). +// Call decides gas charging based on caller role, but always forwards the call. func (wrapper *TransactionFilterPrecompile) Call( input []byte, actingAsAddress common.Address, @@ -174,15 +174,25 @@ func (wrapper *TransactionFilterPrecompile) Call( if err != nil { return nil, burner.GasLeft(), burner.gasUsed, err } - if !isFilterer { - return nil, burner.GasLeft(), burner.gasUsed, errors.New("unauthorized caller to access-controlled method") - } - output, _, _, err := con.Call(input, actingAsAddress, caller, value, readOnly, gasSupplied, evm) + output, _, _, err := con.Call( + input, + actingAsAddress, + caller, + value, + readOnly, + gasSupplied, + evm, + ) if err != nil { return output, gasSupplied, multigas.ZeroGas(), err } - return output, gasSupplied, multigas.ZeroGas(), nil + + // Gas charging decision + if isFilterer { + return output, gasSupplied, multigas.ZeroGas(), nil + } + return output, burner.GasLeft(), burner.gasUsed, nil } func (wrapper *TransactionFilterPrecompile) Precompile() *Precompile { diff --git a/system_tests/filtered_transactions_test.go b/system_tests/filtered_transactions_test.go index c7956b3d8d..516ed9ba99 100644 --- a/system_tests/filtered_transactions_test.go +++ b/system_tests/filtered_transactions_test.go @@ -57,13 +57,6 @@ func TestManageTransactionFilterers(t *testing.T) { ) require.NoError(t, err) - // Initially neither owner nor user can access the filtered tx manager - _, err = arbFilteredTxs.IsTransactionFiltered(ownerCallOpts, txHash) - require.Error(t, err) - - _, err = arbFilteredTxs.IsTransactionFiltered(userCallOpts, txHash) - require.Error(t, err) - // Adding a filterer should be disabled by default by ArbFiltereredTransactionManagerFromTime _, err = arbOwner.AddTransactionFilterer(&ownerTxOpts, userTxOpts.From) require.Error(t, err) @@ -84,6 +77,18 @@ func TestManageTransactionFilterers(t *testing.T) { warpL1Time(t, builder, ctx, hdr.Time, precompiles.TransactionFilteringEnableDelay+1) + // Initially neither owner nor user can modify filtered transactions, + // but both can read (get) filtered status + _, err = arbFilteredTxs.IsTransactionFiltered(ownerCallOpts, txHash) + require.NoError(t, err) + _, err = arbFilteredTxs.AddFilteredTransaction(&ownerTxOpts, txHash) + require.Error(t, err) + + _, err = arbFilteredTxs.IsTransactionFiltered(userCallOpts, txHash) + require.NoError(t, err) + _, err = arbFilteredTxs.AddFilteredTransaction(&userTxOpts, txHash) + require.Error(t, err) + // Owner grants user transaction filterer role tx, err = arbOwner.AddTransactionFilterer(&ownerTxOpts, userTxOpts.From) require.NoError(t, err) @@ -95,7 +100,7 @@ func TestManageTransactionFilterers(t *testing.T) { require.True(t, isFilterer) // Owner is still not a filterer, so owner still cannot call the manager - _, err = arbFilteredTxs.IsTransactionFiltered(ownerCallOpts, txHash) + _, err = arbFilteredTxs.AddFilteredTransaction(&ownerTxOpts, txHash) require.Error(t, err) // User can call the manager and the tx is initially not filtered @@ -165,7 +170,7 @@ func TestManageTransactionFilterers(t *testing.T) { require.False(t, isFilterer) // User is no longer authorised - _, err = arbFilteredTxs.IsTransactionFiltered(userCallOpts, txHash) + _, err = arbFilteredTxs.DeleteFilteredTransaction(&userTxOpts, txHash) require.Error(t, err) // Disable transaction filtering feature again From b82304e8e4fcce96392b00011a620b7cd57240df Mon Sep 17 00:00:00 2001 From: Mikhail Rogachev Date: Mon, 12 Jan 2026 15:55:28 +0100 Subject: [PATCH 17/28] Add events and public methods for transaction filterers --- nitro-testnode | 2 +- precompiles/ArbOwner.go | 21 ++++++++++++++++++--- precompiles/ArbOwnerPublic.go | 10 ++++++++++ precompiles/precompile.go | 5 ++++- precompiles/precompile_test.go | 2 +- 5 files changed, 34 insertions(+), 6 deletions(-) diff --git a/nitro-testnode b/nitro-testnode index 661d1a90e2..c7f35226a8 160000 --- a/nitro-testnode +++ b/nitro-testnode @@ -1 +1 @@ -Subproject commit 661d1a90e2101e8471712b552a14ff6d1f857a62 +Subproject commit c7f35226a883ed9833e7c16ef7fc5b7e29c2ebc8 diff --git a/precompiles/ArbOwner.go b/precompiles/ArbOwner.go index 11484899ed..124a885cc5 100644 --- a/precompiles/ArbOwner.go +++ b/precompiles/ArbOwner.go @@ -24,9 +24,16 @@ import ( // which ensures only a chain owner can access these methods. For methods that // are safe for non-owners to call, see ArbOwnerOld type ArbOwner struct { - Address addr // 0x70 + Address addr // 0x70 + OwnerActs func(ctx, mech, bytes4, addr, []byte) error OwnerActsGasCost func(bytes4, addr, []byte) (uint64, error) + + TransactionFiltererAdded func(ctx, mech, common.Address) error + TransactionFiltererAddedGasCost func(common.Address) (uint64, error) + + TransactionFiltererRemoved func(ctx, mech, common.Address) error + TransactionFiltererRemovedGasCost func(common.Address) (uint64, error) } const NativeTokenEnableDelay = 7 * 24 * 60 * 60 // one week @@ -179,7 +186,11 @@ func (con ArbOwner) AddTransactionFilterer(c ctx, evm mech, filterer addr) error if enabledTime == 0 || enabledTime > evm.Context.Time { return errors.New("transaction filtering feature is not enabled yet") } - return c.State.TransactionFilterers().Add(filterer) + + if err := c.State.TransactionFilterers().Add(filterer); err != nil { + return err + } + return con.TransactionFiltererAdded(c, evm, filterer) } // RemoveTransactionFilterer removes account from the list of transaction filterers @@ -191,7 +202,11 @@ func (con ArbOwner) RemoveTransactionFilterer(c ctx, _ mech, filterer addr) erro if !member { return errors.New("tried to remove non existing transaction filterer") } - return c.State.TransactionFilterers().Remove(filterer, c.State.ArbOSVersion()) + + if err := c.State.TransactionFilterers().Remove(filterer, c.State.ArbOSVersion()); err != nil { + return err + } + return con.TransactionFiltererRemoved(c, nil, filterer) } // IsTransactionFilterer checks if the account is a transaction filterer diff --git a/precompiles/ArbOwnerPublic.go b/precompiles/ArbOwnerPublic.go index bde4589928..ca85b1e712 100644 --- a/precompiles/ArbOwnerPublic.go +++ b/precompiles/ArbOwnerPublic.go @@ -58,6 +58,16 @@ func (con ArbOwnerPublic) GetTransactionFilteringFrom(c ctx, evm mech) (uint64, return c.State.TransactionFilteringFromTime() } +// IsTransactionFilterer checks if the account is a transaction filterer +func (con ArbOwnerPublic) IsTransactionFilterer(c ctx, evm mech, filterer addr) (bool, error) { + return c.State.TransactionFilterers().IsMember(filterer) +} + +// GetAllTransactionFilterers retrieves the list of transaction filterers +func (con ArbOwnerPublic) GetAllTransactionFilterers(c ctx, evm mech) ([]common.Address, error) { + return c.State.TransactionFilterers().AllMembers(65536) +} + // GetNetworkFeeAccount gets the network fee collector func (con ArbOwnerPublic) GetNetworkFeeAccount(c ctx, evm mech) (addr, error) { return c.State.NetworkFeeAccount() diff --git a/precompiles/precompile.go b/precompiles/precompile.go index 3e906299c6..8025cd5ce6 100644 --- a/precompiles/precompile.go +++ b/precompiles/precompile.go @@ -561,7 +561,6 @@ func Precompiles() map[addr]ArbosPrecompile { ArbOwnerPublic.methodsByName["IsNativeTokenOwner"].arbosVersion = params.ArbosVersion_41 ArbOwnerPublic.methodsByName["GetAllNativeTokenOwners"].arbosVersion = params.ArbosVersion_41 ArbOwnerPublic.methodsByName["GetParentGasFloorPerToken"].arbosVersion = params.ArbosVersion_50 - ArbOwnerPublic.methodsByName["GetTransactionFilteringFrom"].arbosVersion = params.ArbosVersion_60 ArbWasmImpl := &ArbWasm{Address: types.ArbWasmAddress} ArbWasm := insert(MakePrecompile(precompilesgen.ArbWasmMetaData, ArbWasmImpl)) @@ -664,6 +663,10 @@ func Precompiles() map[addr]ArbosPrecompile { ArbOwnerPublic.methodsByName["GetNativeTokenManagementFrom"].arbosVersion = params.ArbosVersion_50 + ArbOwnerPublic.methodsByName["GetTransactionFilteringFrom"].arbosVersion = params.ArbosVersion_60 + ArbOwnerPublic.methodsByName["IsTransactionFilterer"].arbosVersion = params.ArbosVersion_60 + ArbOwnerPublic.methodsByName["GetAllTransactionFilterers"].arbosVersion = params.ArbosVersion_60 + ArbNativeTokenManager := insert(MakePrecompile(precompilesgen.ArbNativeTokenManagerMetaData, &ArbNativeTokenManager{Address: types.ArbNativeTokenManagerAddress})) ArbNativeTokenManager.arbosVersion = params.ArbosVersion_41 ArbNativeTokenManager.methodsByName["MintNativeToken"].arbosVersion = params.ArbosVersion_41 diff --git a/precompiles/precompile_test.go b/precompiles/precompile_test.go index b382a9ab43..1a77f60ada 100644 --- a/precompiles/precompile_test.go +++ b/precompiles/precompile_test.go @@ -192,7 +192,7 @@ func TestPrecompilesPerArbosVersion(t *testing.T) { params.ArbosVersion_40: 3, params.ArbosVersion_41: 10, params.ArbosVersion_50: 9, - params.ArbosVersion_60: 12, + params.ArbosVersion_60: 15, } precompiles := Precompiles() From 9d83f9c05e6ef540299562b000c895fe55d5a1dd Mon Sep 17 00:00:00 2001 From: Mikhail Rogachev Date: Mon, 12 Jan 2026 16:40:31 +0100 Subject: [PATCH 18/28] tmp: add stubs to compile upstream version --- precompiles/ArbOwner.go | 11 ++++++++--- precompiles/ArbOwnerPublic.go | 7 +++++++ precompiles/precompile.go | 2 ++ precompiles/precompile_test.go | 2 +- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/precompiles/ArbOwner.go b/precompiles/ArbOwner.go index 124a885cc5..e46285f149 100644 --- a/precompiles/ArbOwner.go +++ b/precompiles/ArbOwner.go @@ -194,8 +194,8 @@ func (con ArbOwner) AddTransactionFilterer(c ctx, evm mech, filterer addr) error } // RemoveTransactionFilterer removes account from the list of transaction filterers -func (con ArbOwner) RemoveTransactionFilterer(c ctx, _ mech, filterer addr) error { - member, err := con.IsTransactionFilterer(c, nil, filterer) +func (con ArbOwner) RemoveTransactionFilterer(c ctx, evm mech, filterer addr) error { + member, err := con.IsTransactionFilterer(c, evm, filterer) if err != nil { return err } @@ -206,7 +206,7 @@ func (con ArbOwner) RemoveTransactionFilterer(c ctx, _ mech, filterer addr) erro if err := c.State.TransactionFilterers().Remove(filterer, c.State.ArbOSVersion()); err != nil { return err } - return con.TransactionFiltererRemoved(c, nil, filterer) + return con.TransactionFiltererRemoved(c, evm, filterer) } // IsTransactionFilterer checks if the account is a transaction filterer @@ -640,3 +640,8 @@ func (con ArbOwner) SetMultiGasPricingConstraints( } return nil } + +func (con ArbOwner) SetMaxStylusContractFragments(c ctx, evm mech, maxFragments uint16) error { + // NOTE: waits https://github.com/OffchainLabs/nitro/pull/4193 + return errors.New("SetMaxStylusContractFragments is not yet implemented") +} diff --git a/precompiles/ArbOwnerPublic.go b/precompiles/ArbOwnerPublic.go index ca85b1e712..9995997498 100644 --- a/precompiles/ArbOwnerPublic.go +++ b/precompiles/ArbOwnerPublic.go @@ -4,6 +4,8 @@ package precompiles import ( + "errors" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/params" ) @@ -109,3 +111,8 @@ func (con ArbOwnerPublic) IsCalldataPriceIncreaseEnabled(c ctx, _ mech) (bool, e func (con ArbOwnerPublic) GetParentGasFloorPerToken(c ctx, evm mech) (uint64, error) { return c.State.L1PricingState().ParentGasFloorPerToken() } + +func (con ArbOwnerPublic) GetMaxStylusContractFragments(c ctx, evm mech) (uint16, error) { + // NOTE: waits https://github.com/OffchainLabs/nitro/pull/4193 + return 0, errors.New("GetMaxStylusContractFragments is not yet implemented") +} diff --git a/precompiles/precompile.go b/precompiles/precompile.go index 8025cd5ce6..eae08eb3e6 100644 --- a/precompiles/precompile.go +++ b/precompiles/precompile.go @@ -561,6 +561,7 @@ func Precompiles() map[addr]ArbosPrecompile { ArbOwnerPublic.methodsByName["IsNativeTokenOwner"].arbosVersion = params.ArbosVersion_41 ArbOwnerPublic.methodsByName["GetAllNativeTokenOwners"].arbosVersion = params.ArbosVersion_41 ArbOwnerPublic.methodsByName["GetParentGasFloorPerToken"].arbosVersion = params.ArbosVersion_50 + ArbOwnerPublic.methodsByName["GetMaxStylusContractFragments"].arbosVersion = params.ArbosVersion_60 ArbWasmImpl := &ArbWasm{Address: types.ArbWasmAddress} ArbWasm := insert(MakePrecompile(precompilesgen.ArbWasmMetaData, ArbWasmImpl)) @@ -622,6 +623,7 @@ func Precompiles() map[addr]ArbosPrecompile { ArbOwner.methodsByName["SetGasPricingConstraints"].arbosVersion = params.ArbosVersion_50 ArbOwner.methodsByName["SetGasBacklog"].arbosVersion = params.ArbosVersion_50 ArbOwner.methodsByName["SetMultiGasPricingConstraints"].arbosVersion = params.ArbosVersion_60 + ArbOwner.methodsByName["SetMaxStylusContractFragments"].arbosVersion = params.ArbosVersion_60 stylusMethods := []string{ "SetInkPrice", "SetWasmMaxStackDepth", "SetWasmFreePages", "SetWasmPageGas", "SetWasmPageLimit", "SetWasmMinInitGas", "SetWasmInitCostScalar", diff --git a/precompiles/precompile_test.go b/precompiles/precompile_test.go index 1a77f60ada..15fcb4b39c 100644 --- a/precompiles/precompile_test.go +++ b/precompiles/precompile_test.go @@ -192,7 +192,7 @@ func TestPrecompilesPerArbosVersion(t *testing.T) { params.ArbosVersion_40: 3, params.ArbosVersion_41: 10, params.ArbosVersion_50: 9, - params.ArbosVersion_60: 15, + params.ArbosVersion_60: 17, } precompiles := Precompiles() From bcb35a93f55809fffe2d95b538c23ad1d02676c9 Mon Sep 17 00:00:00 2001 From: Mikhail Rogachev Date: Mon, 12 Jan 2026 17:43:17 +0100 Subject: [PATCH 19/28] Add test for filterer add/remove event --- system_tests/filtered_transactions_test.go | 54 ++++++++++++++++++---- 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/system_tests/filtered_transactions_test.go b/system_tests/filtered_transactions_test.go index 516ed9ba99..ce0bf9c356 100644 --- a/system_tests/filtered_transactions_test.go +++ b/system_tests/filtered_transactions_test.go @@ -46,10 +46,15 @@ func TestManageTransactionFilterers(t *testing.T) { arbOwner, err := precompilesgen.NewArbOwner(types.ArbOwnerAddress, builder.L2.Client) require.NoError(t, err) + arbOwnerABI, err := precompilesgen.ArbOwnerMetaData.GetAbi() + Require(t, err) + filtererAddedTopic := arbOwnerABI.Events["TransactionFiltererAdded"].ID + filtererDeletedTopic := arbOwnerABI.Events["TransactionFiltererRemoved"].ID + filteredTransactionsManagerABI, err := precompilesgen.ArbFilteredTransactionsManagerMetaData.GetAbi() Require(t, err) - addedTopic := filteredTransactionsManagerABI.Events["FilteredTransactionAdded"].ID - deletedTopic := filteredTransactionsManagerABI.Events["FilteredTransactionDeleted"].ID + txAddedTopic := filteredTransactionsManagerABI.Events["FilteredTransactionAdded"].ID + txDeletedTopic := filteredTransactionsManagerABI.Events["FilteredTransactionDeleted"].ID arbFilteredTxs, err := precompilesgen.NewArbFilteredTransactionsManager( types.ArbFilteredTransactionsManagerAddress, @@ -92,9 +97,25 @@ func TestManageTransactionFilterers(t *testing.T) { // Owner grants user transaction filterer role tx, err = arbOwner.AddTransactionFilterer(&ownerTxOpts, userTxOpts.From) require.NoError(t, err) - _, err = builder.L2.EnsureTxSucceeded(tx) + receipt, err := builder.L2.EnsureTxSucceeded(tx) require.NoError(t, err) + // Check that the TransactionFiltererAdded event was emitted + foundAdded := false + for _, lg := range receipt.Logs { + if lg.Topics[0] != filtererAddedTopic { + continue + } + ev, parseErr := arbOwner.ParseTransactionFiltererAdded(*lg) + if parseErr != nil { + continue + } + require.Equal(t, userTxOpts.From, ev.Filterer) + foundAdded = true + break + } + require.True(t, foundAdded) + isFilterer, err := arbOwner.IsTransactionFilterer(ownerCallOpts, userTxOpts.From) require.NoError(t, err) require.True(t, isFilterer) @@ -111,13 +132,13 @@ func TestManageTransactionFilterers(t *testing.T) { // User filters the tx tx, err = arbFilteredTxs.AddFilteredTransaction(&userTxOpts, txHash) require.NoError(t, err) - receipt, err := builder.L2.EnsureTxSucceeded(tx) + receipt, err = builder.L2.EnsureTxSucceeded(tx) require.NoError(t, err) // Check that the FilteredTransactionAdded event was emitted - foundAdded := false + foundAdded = false for _, lg := range receipt.Logs { - if lg.Topics[0] != addedTopic { + if lg.Topics[0] != txAddedTopic { continue } ev, parseErr := arbFilteredTxs.ParseFilteredTransactionAdded(*lg) @@ -143,7 +164,7 @@ func TestManageTransactionFilterers(t *testing.T) { // Check that the FilteredTransactionDeleted event was emitted foundDeleted := false for _, lg := range receipt.Logs { - if lg.Topics[0] != deletedTopic { + if lg.Topics[0] != txDeletedTopic { continue } ev, parseErr := arbFilteredTxs.ParseFilteredTransactionDeleted(*lg) @@ -163,7 +184,24 @@ func TestManageTransactionFilterers(t *testing.T) { // Owner revokes the role tx, err = arbOwner.RemoveTransactionFilterer(&ownerTxOpts, userTxOpts.From) require.NoError(t, err) - require.NotNil(t, tx) + receipt, err = builder.L2.EnsureTxSucceeded(tx) + require.NoError(t, err) + + // Check that the TransactionFiltererRemoved event was emitted + foundDeleted = false + for _, lg := range receipt.Logs { + if lg.Topics[0] != filtererDeletedTopic { + continue + } + ev, parseErr := arbOwner.ParseTransactionFiltererRemoved(*lg) + if parseErr != nil { + continue + } + require.Equal(t, userTxOpts.From, ev.Filterer) + foundDeleted = true + break + } + require.True(t, foundDeleted) isFilterer, err = arbOwner.IsTransactionFilterer(ownerCallOpts, userTxOpts.From) require.NoError(t, err) From 2058741974e509e222043d50985e6eca5b9dca71 Mon Sep 17 00:00:00 2001 From: Mikhail Rogachev Date: Tue, 13 Jan 2026 20:45:39 +0100 Subject: [PATCH 20/28] Review fixes and minor polishing --- .../ArbFilteredTransactionsManager_test.go | 91 +++++++++++++++++++ precompiles/ArbOwner.go | 5 +- precompiles/ArbOwnerPublic.go | 3 +- precompiles/wrapper.go | 11 +-- system_tests/filtered_transactions_test.go | 46 ++++------ 5 files changed, 117 insertions(+), 39 deletions(-) create mode 100644 precompiles/ArbFilteredTransactionsManager_test.go diff --git a/precompiles/ArbFilteredTransactionsManager_test.go b/precompiles/ArbFilteredTransactionsManager_test.go new file mode 100644 index 0000000000..4692c793f2 --- /dev/null +++ b/precompiles/ArbFilteredTransactionsManager_test.go @@ -0,0 +1,91 @@ +// Copyright 2026, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md + +package precompiles + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/arbitrum/multigas" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/offchainlabs/nitro/arbos/arbosState" + "github.com/offchainlabs/nitro/arbos/burn" + "github.com/offchainlabs/nitro/arbos/filteredTransactions" + "github.com/offchainlabs/nitro/arbos/util" + "github.com/offchainlabs/nitro/util/testhelpers" +) + +func setupFilteredTransactionsHandles( + t *testing.T, +) ( + *vm.EVM, + *arbosState.ArbosState, + *Context, + *ArbFilteredTransactionsManager, +) { + t.Helper() + + evm := newMockEVMForTesting() + caller := common.BytesToAddress(crypto.Keccak256([]byte("caller"))[:20]) + + tracer := util.NewTracingInfo(evm, testhelpers.RandomAddress(), types.ArbosAddress, util.TracingDuringEVM) + _, err := arbosState.OpenArbosState(evm.StateDB, burn.NewSystemBurner(tracer, false)) + require.NoError(t, err) + + callCtx := testContext(caller, evm) + callCtx.gasSupplied = 100000 + callCtx.gasUsed = multigas.ZeroGas() + + state, err := arbosState.OpenArbosState(evm.StateDB, callCtx) + require.NoError(t, err) + + con := &ArbFilteredTransactionsManager{ + FilteredTransactionAdded: func(ctx ctx, evm mech, txHash common.Hash) error { return nil }, + FilteredTransactionDeleted: func(ctx ctx, evm mech, txHash common.Hash) error { return nil }, + } + + return evm, state, callCtx, con +} + +func TestFilteredTransactionsManagerBurnOutForNonFilterer(t *testing.T) { + t.Parallel() + + evm, _, callCtx, con := setupFilteredTransactionsHandles(t) + + txHash := common.BytesToHash([]byte{1, 2, 3, 4, 5}) + + err := con.AddFilteredTransaction(callCtx, evm, txHash) + require.ErrorIs(t, err, vm.ErrOutOfGas) +} + +func TestFilteredTransactionsManagerAddDeleteForFilterer(t *testing.T) { + t.Parallel() + + evm, state, callCtx, con := setupFilteredTransactionsHandles(t) + + txHash := common.BytesToHash([]byte{5, 4, 3, 2, 1}) + + err := state.TransactionFilterers().Add(callCtx.caller) + require.NoError(t, err) + + err = con.AddFilteredTransaction(callCtx, evm, txHash) + require.NoError(t, err) + + filteredState := filteredTransactions.Open(evm.StateDB, callCtx) + isFiltered, err := filteredState.IsFiltered(txHash) + require.NoError(t, err) + require.True(t, isFiltered) + + err = con.DeleteFilteredTransaction(callCtx, evm, txHash) + require.NoError(t, err) + + isFiltered, err = filteredState.IsFiltered(txHash) + require.NoError(t, err) + require.False(t, isFiltered) +} diff --git a/precompiles/ArbOwner.go b/precompiles/ArbOwner.go index e46285f149..18ccd090e6 100644 --- a/precompiles/ArbOwner.go +++ b/precompiles/ArbOwner.go @@ -210,7 +210,7 @@ func (con ArbOwner) RemoveTransactionFilterer(c ctx, evm mech, filterer addr) er } // IsTransactionFilterer checks if the account is a transaction filterer -func (con ArbOwner) IsTransactionFilterer(c ctx, _ mech, filterer addr) (bool, error) { +func (con ArbOwner) IsTransactionFilterer(c ctx, evm mech, filterer addr) (bool, error) { return c.State.TransactionFilterers().IsMember(filterer) } @@ -642,6 +642,5 @@ func (con ArbOwner) SetMultiGasPricingConstraints( } func (con ArbOwner) SetMaxStylusContractFragments(c ctx, evm mech, maxFragments uint16) error { - // NOTE: waits https://github.com/OffchainLabs/nitro/pull/4193 - return errors.New("SetMaxStylusContractFragments is not yet implemented") + return errors.New("SetMaxStylusContractFragments is not implemented yet") } diff --git a/precompiles/ArbOwnerPublic.go b/precompiles/ArbOwnerPublic.go index 9995997498..606fbd11d5 100644 --- a/precompiles/ArbOwnerPublic.go +++ b/precompiles/ArbOwnerPublic.go @@ -113,6 +113,5 @@ func (con ArbOwnerPublic) GetParentGasFloorPerToken(c ctx, evm mech) (uint64, er } func (con ArbOwnerPublic) GetMaxStylusContractFragments(c ctx, evm mech) (uint16, error) { - // NOTE: waits https://github.com/OffchainLabs/nitro/pull/4193 - return 0, errors.New("GetMaxStylusContractFragments is not yet implemented") + return 0, errors.New("GetMaxStylusContractFragments is not implemented yet") } diff --git a/precompiles/wrapper.go b/precompiles/wrapper.go index dfaa8d945f..8992c3dc6d 100644 --- a/precompiles/wrapper.go +++ b/precompiles/wrapper.go @@ -134,7 +134,7 @@ func (wrapper *OwnerPrecompile) Name() string { } // TransactionFilterPrecompile wraps ArbFilteredTransactionsManager to preserve free storage access for filterers. -// Access control is NOT enforced here. +// Call forwards the call and decides whether storage access is free based on caller role. type TransactionFilterPrecompile struct { precompile ArbosPrecompile } @@ -184,15 +184,10 @@ func (wrapper *TransactionFilterPrecompile) Call( gasSupplied, evm, ) - if err != nil { - return output, gasSupplied, multigas.ZeroGas(), err - } - - // Gas charging decision if isFilterer { - return output, gasSupplied, multigas.ZeroGas(), nil + return output, gasSupplied, multigas.ZeroGas(), err } - return output, burner.GasLeft(), burner.gasUsed, nil + return output, burner.GasLeft(), burner.gasUsed, err } func (wrapper *TransactionFilterPrecompile) Precompile() *Precompile { diff --git a/system_tests/filtered_transactions_test.go b/system_tests/filtered_transactions_test.go index ce0bf9c356..29c634f4b0 100644 --- a/system_tests/filtered_transactions_test.go +++ b/system_tests/filtered_transactions_test.go @@ -84,8 +84,9 @@ func TestManageTransactionFilterers(t *testing.T) { // Initially neither owner nor user can modify filtered transactions, // but both can read (get) filtered status - _, err = arbFilteredTxs.IsTransactionFiltered(ownerCallOpts, txHash) + isFiltered, err := arbFilteredTxs.IsTransactionFiltered(ownerCallOpts, txHash) require.NoError(t, err) + require.False(t, isFiltered) _, err = arbFilteredTxs.AddFilteredTransaction(&ownerTxOpts, txHash) require.Error(t, err) @@ -106,10 +107,8 @@ func TestManageTransactionFilterers(t *testing.T) { if lg.Topics[0] != filtererAddedTopic { continue } - ev, parseErr := arbOwner.ParseTransactionFiltererAdded(*lg) - if parseErr != nil { - continue - } + ev, err := arbOwner.ParseTransactionFiltererAdded(*lg) + require.NoError(t, err) require.Equal(t, userTxOpts.From, ev.Filterer) foundAdded = true break @@ -141,10 +140,8 @@ func TestManageTransactionFilterers(t *testing.T) { if lg.Topics[0] != txAddedTopic { continue } - ev, parseErr := arbFilteredTxs.ParseFilteredTransactionAdded(*lg) - if parseErr != nil { - continue - } + ev, err := arbFilteredTxs.ParseFilteredTransactionAdded(*lg) + require.NoError(t, err) require.Equal(t, txHash, common.BytesToHash(ev.TxHash[:])) foundAdded = true break @@ -167,10 +164,8 @@ func TestManageTransactionFilterers(t *testing.T) { if lg.Topics[0] != txDeletedTopic { continue } - ev, parseErr := arbFilteredTxs.ParseFilteredTransactionDeleted(*lg) - if parseErr != nil { - continue - } + ev, err := arbFilteredTxs.ParseFilteredTransactionDeleted(*lg) + require.NoError(t, err) require.Equal(t, txHash, common.BytesToHash(ev.TxHash[:])) foundDeleted = true break @@ -193,10 +188,8 @@ func TestManageTransactionFilterers(t *testing.T) { if lg.Topics[0] != filtererDeletedTopic { continue } - ev, parseErr := arbOwner.ParseTransactionFiltererRemoved(*lg) - if parseErr != nil { - continue - } + ev, err := arbOwner.ParseTransactionFiltererRemoved(*lg) + require.NoError(t, err) require.Equal(t, userTxOpts.From, ev.Filterer) foundDeleted = true break @@ -235,11 +228,12 @@ func TestFilteredTransactionsManagerFreeOps(t *testing.T) { defer cleanup() ownerTxOpts := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + ownerTxOpts.GasLimit = 32000000 filtererName := "Filterer" builder.L2Info.GenerateAccount(filtererName) - builder.L2.TransferBalance(t, "Owner", filtererName, big.NewInt(1e16), builder.L2Info) + filtererTxOpts := builder.L2Info.GetDefaultTransactOpts(filtererName, ctx) filtererTxOpts.GasLimit = 32000000 @@ -254,26 +248,26 @@ func TestFilteredTransactionsManagerFreeOps(t *testing.T) { ) require.NoError(t, err) - // Owner grants filterer transaction filterer role + // Non-filterer call should fail + _, err = arbFilteredTxs.AddFilteredTransaction(&ownerTxOpts, txHash) + require.Error(t, err) + + // Owner grants filterer role tx, err := arbOwner.AddTransactionFilterer(&ownerTxOpts, filtererTxOpts.From) require.NoError(t, err) - require.NotNil(t, tx) + _, err = builder.L2.EnsureTxSucceeded(tx) + require.NoError(t, err) - // Filterer filters the tx + // Filterer acts (should be free for StorageAccess) tx, err = arbFilteredTxs.AddFilteredTransaction(&filtererTxOpts, txHash) require.NoError(t, err) receipt, err := builder.L2.EnsureTxSucceeded(tx) require.NoError(t, err) - - // AddFilteredTransaction use storage set, but it should be free for filterers require.Equal(t, uint64(0), receipt.MultiGasUsed.Get(multigas.ResourceKindStorageAccess)) - // Filterer unfilters the tx tx, err = arbFilteredTxs.DeleteFilteredTransaction(&filtererTxOpts, txHash) require.NoError(t, err) receipt, err = builder.L2.EnsureTxSucceeded(tx) require.NoError(t, err) - - // DeleteFilteredTransaction use storage clear, but it should be free for filterers require.Equal(t, uint64(0), receipt.MultiGasUsed.Get(multigas.ResourceKindStorageAccess)) } From 614caa6ec90e5399c9ef79b3e458e9a492560fa2 Mon Sep 17 00:00:00 2001 From: Mikhail Rogachev Date: Wed, 14 Jan 2026 14:01:51 +0100 Subject: [PATCH 21/28] chore: remove unused arbos state --- precompiles/ArbFilteredTransactionsManager_test.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/precompiles/ArbFilteredTransactionsManager_test.go b/precompiles/ArbFilteredTransactionsManager_test.go index 4692c793f2..7a662f3a80 100644 --- a/precompiles/ArbFilteredTransactionsManager_test.go +++ b/precompiles/ArbFilteredTransactionsManager_test.go @@ -10,15 +10,11 @@ import ( "github.com/ethereum/go-ethereum/arbitrum/multigas" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/offchainlabs/nitro/arbos/arbosState" - "github.com/offchainlabs/nitro/arbos/burn" "github.com/offchainlabs/nitro/arbos/filteredTransactions" - "github.com/offchainlabs/nitro/arbos/util" - "github.com/offchainlabs/nitro/util/testhelpers" ) func setupFilteredTransactionsHandles( @@ -34,10 +30,6 @@ func setupFilteredTransactionsHandles( evm := newMockEVMForTesting() caller := common.BytesToAddress(crypto.Keccak256([]byte("caller"))[:20]) - tracer := util.NewTracingInfo(evm, testhelpers.RandomAddress(), types.ArbosAddress, util.TracingDuringEVM) - _, err := arbosState.OpenArbosState(evm.StateDB, burn.NewSystemBurner(tracer, false)) - require.NoError(t, err) - callCtx := testContext(caller, evm) callCtx.gasSupplied = 100000 callCtx.gasUsed = multigas.ZeroGas() From da82012b237539da759352c27bc5a05411ba4586 Mon Sep 17 00:00:00 2001 From: Mikhail Rogachev Date: Thu, 15 Jan 2026 13:20:08 +0100 Subject: [PATCH 22/28] Review fixes --- arbos/arbosState/arbosstate.go | 17 +++-- go-ethereum | 2 +- precompiles/ArbOwner.go | 86 +++++++--------------- system_tests/filtered_transactions_test.go | 4 +- 4 files changed, 38 insertions(+), 71 deletions(-) diff --git a/arbos/arbosState/arbosstate.go b/arbos/arbosState/arbosstate.go index 4a579945c7..805cb691a3 100644 --- a/arbos/arbosState/arbosstate.go +++ b/arbos/arbosState/arbosstate.go @@ -324,12 +324,6 @@ func InitializeArbosState(stateDB vm.StateDB, burner burn.Burner, chainConfig *p return nil, err } - transactionFiltererStorage := sto.OpenCachedSubStorage(transactionFiltererSubspace) - err = addressSet.Initialize(transactionFiltererStorage) - if err != nil { - return nil, err - } - aState, err := OpenArbosState(stateDB, burner) if err != nil { return nil, err @@ -468,7 +462,8 @@ func (state *ArbosState) UpgradeArbosVersion( // these versions are left to Orbit chains for custom upgrades. case params.ArbosVersion_60: - // no change state needed + ensure(addressSet.Initialize(state.backingStorage.OpenSubStorage(transactionFiltererSubspace))) + default: return fmt.Errorf( "the chain is upgrading to unsupported ArbOS version %v, %w", @@ -654,3 +649,11 @@ func (state *ArbosState) SetChainConfig(serializedChainConfig []byte) error { func (state *ArbosState) GenesisBlockNum() (uint64, error) { return state.genesisBlockNum.Get() } + +func (state *ArbosState) NativeTokenEnabledTimeHandle() storage.StorageBackedUint64 { + return state.nativeTokenEnabledTime +} + +func (state *ArbosState) TransactionFilteringEnabledTimeHandle() storage.StorageBackedUint64 { + return state.transactionFilteringEnabledTime +} diff --git a/go-ethereum b/go-ethereum index 061391431a..adc40b17ea 160000 --- a/go-ethereum +++ b/go-ethereum @@ -1 +1 @@ -Subproject commit 061391431ad423f2ed35c2625449e59b1077513b +Subproject commit adc40b17ea28b803965309afc7fb833b142e197e diff --git a/precompiles/ArbOwner.go b/precompiles/ArbOwner.go index 18ccd090e6..ebe0d6dad7 100644 --- a/precompiles/ArbOwner.go +++ b/precompiles/ArbOwner.go @@ -16,6 +16,7 @@ import ( "github.com/offchainlabs/nitro/arbos/l1pricing" "github.com/offchainlabs/nitro/arbos/l2pricing" "github.com/offchainlabs/nitro/arbos/programs" + "github.com/offchainlabs/nitro/arbos/storage" "github.com/offchainlabs/nitro/util/arbmath" ) @@ -36,8 +37,7 @@ type ArbOwner struct { TransactionFiltererRemovedGasCost func(common.Address) (uint64, error) } -const NativeTokenEnableDelay = 7 * 24 * 60 * 60 // one week -const TransactionFilteringEnableDelay = 7 * 24 * 60 * 60 // one week +const FeatureEnableDelay = 7 * 24 * 60 * 60 // one week var ( ErrOutOfBounds = errors.New("value out of bounds") @@ -69,81 +69,45 @@ func (con ArbOwner) GetAllChainOwners(c ctx, evm mech) ([]common.Address, error) return c.State.ChainOwners().AllMembers(65536) } -// validateFeatureFromTimeUpdate enforces the scheduling rules used by -// SetNativeTokenManagementFrom and SetTransactionFilteringFrom -func validateFeatureFromTimeUpdate( - stored uint64, - now uint64, - timestamp uint64, - delay uint64, -) error { - // If the feature is disabled, then the time must be at least 7 days in the +// setFeatureFromTime sets a time in epoch seconds when a feature becomes enabled. +// Setting it to 0 disables the feature. +// If the feature is disabled, then the time must be at least FeatureEnableDelay days in the future. +func setFeatureFromTime(field storage.StorageBackedUint64, now, timestamp uint64) error { + if timestamp == 0 { + return field.Set(0) + } + stored, err := field.Get() + if err != nil { + return err + } + + // If the feature is disabled, then the time must be at least FeatureEnableDelay days in the // future. // If the feature is scheduled to be enabled more than 7 days in the future, // and the new time is also in the future, then it must be at least 7 days // in the future. - if (stored == 0 && timestamp < now+delay) || - (stored > now+delay && timestamp < now+delay) { + if (stored == 0 && timestamp < now+FeatureEnableDelay) || + (stored > now+FeatureEnableDelay && timestamp < now+FeatureEnableDelay) { return ErrDelay } + // If the feature is scheduled to be enabled earlier than the minimum delay, // then the new time to enable it must be only further in the future. - if stored > now && stored <= now+delay && timestamp < stored { + if stored > now && stored <= now+FeatureEnableDelay && timestamp < stored { return ErrBackward } - return nil + + return field.Set(timestamp) } -// SetNativeTokenManagementFrom sets a time in epoch seconds when the native token -// management becomes enabled. Setting it to 0 disables the feature. -// If the feature is disabled, then the time must be at least 7 days in the -// future. +// SetNativeTokenManagementFrom sets native token management enabled-from time. func (con ArbOwner) SetNativeTokenManagementFrom(c ctx, evm mech, timestamp uint64) error { - if timestamp == 0 { - return c.State.SetNativeTokenManagementFromTime(0) - } - stored, err := c.State.NativeTokenManagementFromTime() - if err != nil { - return err - } - now := evm.Context.Time - - if err := validateFeatureFromTimeUpdate( - stored, - now, - timestamp, - NativeTokenEnableDelay, - ); err != nil { - return err - } - - return c.State.SetNativeTokenManagementFromTime(timestamp) + return setFeatureFromTime(c.State.NativeTokenEnabledTimeHandle(), evm.Context.Time, timestamp) } -// SetTransactionFilteringFrom sets a time in epoch seconds when the transaction filterering -// feature becomes enabled. Setting it to 0 disables the feature. -// If the feature is disabled, then the time must be at least 7 days in the -// future. +// SetTransactionFilteringFrom sets transaction filtering enabled-from time. func (con ArbOwner) SetTransactionFilteringFrom(c ctx, evm mech, timestamp uint64) error { - if timestamp == 0 { - return c.State.SetTransactionFilteringFromTime(0) - } - stored, err := c.State.TransactionFilteringFromTime() - if err != nil { - return err - } - now := evm.Context.Time - - if err := validateFeatureFromTimeUpdate( - stored, - now, - timestamp, - TransactionFilteringEnableDelay, - ); err != nil { - return err - } - - return c.State.SetTransactionFilteringFromTime(timestamp) + return setFeatureFromTime(c.State.TransactionFilteringEnabledTimeHandle(), evm.Context.Time, timestamp) } // AddNativeTokenOwner adds account as a native token owner diff --git a/system_tests/filtered_transactions_test.go b/system_tests/filtered_transactions_test.go index 29c634f4b0..1c1bf5dbea 100644 --- a/system_tests/filtered_transactions_test.go +++ b/system_tests/filtered_transactions_test.go @@ -74,13 +74,13 @@ func TestManageTransactionFilterers(t *testing.T) { require.Error(t, err) // Enable transaction filtering feature 7 days in the future and warp time forward - enableAt := hdr.Time + precompiles.TransactionFilteringEnableDelay + enableAt := hdr.Time + precompiles.FeatureEnableDelay tx, err := arbOwner.SetTransactionFilteringFrom(&ownerTxOpts, enableAt) require.NoError(t, err) _, err = builder.L2.EnsureTxSucceeded(tx) require.NoError(t, err) - warpL1Time(t, builder, ctx, hdr.Time, precompiles.TransactionFilteringEnableDelay+1) + warpL1Time(t, builder, ctx, hdr.Time, precompiles.FeatureEnableDelay+1) // Initially neither owner nor user can modify filtered transactions, // but both can read (get) filtered status From d6c9e07f6ff5a085f0c8660b8d1f4de7571990ed Mon Sep 17 00:00:00 2001 From: Mikhail Rogachev Date: Fri, 16 Jan 2026 13:03:08 +0100 Subject: [PATCH 23/28] Use ArbosVersion_TransactionFiltering for precomplie --- arbos/arbosState/arbosstate.go | 2 +- contracts-local/src/precompiles | 2 +- go-ethereum | 2 +- precompiles/precompile.go | 24 +++++++++++----------- system_tests/filtered_transactions_test.go | 4 ++-- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/arbos/arbosState/arbosstate.go b/arbos/arbosState/arbosstate.go index 805cb691a3..16e3f5b635 100644 --- a/arbos/arbosState/arbosstate.go +++ b/arbos/arbosState/arbosstate.go @@ -461,7 +461,7 @@ func (state *ArbosState) UpgradeArbosVersion( case 52, 53, 54, 55, 56, 57, 58, 59: // these versions are left to Orbit chains for custom upgrades. - case params.ArbosVersion_60: + case params.ArbosVersion_TransactionFiltering: ensure(addressSet.Initialize(state.backingStorage.OpenSubStorage(transactionFiltererSubspace))) default: diff --git a/contracts-local/src/precompiles b/contracts-local/src/precompiles index 3033065dd2..0e455541b5 160000 --- a/contracts-local/src/precompiles +++ b/contracts-local/src/precompiles @@ -1 +1 @@ -Subproject commit 3033065dd270577b2abcd4360bdfd472c6b041fe +Subproject commit 0e455541b5dc9203506d995b153575f18cf71cdd diff --git a/go-ethereum b/go-ethereum index adc40b17ea..9a29c21047 160000 --- a/go-ethereum +++ b/go-ethereum @@ -1 +1 @@ -Subproject commit adc40b17ea28b803965309afc7fb833b142e197e +Subproject commit 9a29c21047881573e809d0e60cc3fb88d810de6a diff --git a/precompiles/precompile.go b/precompiles/precompile.go index eae08eb3e6..16f004a9df 100644 --- a/precompiles/precompile.go +++ b/precompiles/precompile.go @@ -657,17 +657,17 @@ func Precompiles() map[addr]ArbosPrecompile { ArbOwner.methodsByName["SetParentGasFloorPerToken"].arbosVersion = params.ArbosVersion_50 ArbOwner.methodsByName["SetMaxBlockGasLimit"].arbosVersion = params.ArbosVersion_50 - ArbOwner.methodsByName["AddTransactionFilterer"].arbosVersion = params.ArbosVersion_60 - ArbOwner.methodsByName["RemoveTransactionFilterer"].arbosVersion = params.ArbosVersion_60 - ArbOwner.methodsByName["IsTransactionFilterer"].arbosVersion = params.ArbosVersion_60 - ArbOwner.methodsByName["GetAllTransactionFilterers"].arbosVersion = params.ArbosVersion_60 - ArbOwner.methodsByName["SetTransactionFilteringFrom"].arbosVersion = params.ArbosVersion_60 + ArbOwner.methodsByName["AddTransactionFilterer"].arbosVersion = params.ArbosVersion_TransactionFiltering + ArbOwner.methodsByName["RemoveTransactionFilterer"].arbosVersion = params.ArbosVersion_TransactionFiltering + ArbOwner.methodsByName["IsTransactionFilterer"].arbosVersion = params.ArbosVersion_TransactionFiltering + ArbOwner.methodsByName["GetAllTransactionFilterers"].arbosVersion = params.ArbosVersion_TransactionFiltering + ArbOwner.methodsByName["SetTransactionFilteringFrom"].arbosVersion = params.ArbosVersion_TransactionFiltering ArbOwnerPublic.methodsByName["GetNativeTokenManagementFrom"].arbosVersion = params.ArbosVersion_50 - ArbOwnerPublic.methodsByName["GetTransactionFilteringFrom"].arbosVersion = params.ArbosVersion_60 - ArbOwnerPublic.methodsByName["IsTransactionFilterer"].arbosVersion = params.ArbosVersion_60 - ArbOwnerPublic.methodsByName["GetAllTransactionFilterers"].arbosVersion = params.ArbosVersion_60 + ArbOwnerPublic.methodsByName["GetTransactionFilteringFrom"].arbosVersion = params.ArbosVersion_TransactionFiltering + ArbOwnerPublic.methodsByName["IsTransactionFilterer"].arbosVersion = params.ArbosVersion_TransactionFiltering + ArbOwnerPublic.methodsByName["GetAllTransactionFilterers"].arbosVersion = params.ArbosVersion_TransactionFiltering ArbNativeTokenManager := insert(MakePrecompile(precompilesgen.ArbNativeTokenManagerMetaData, &ArbNativeTokenManager{Address: types.ArbNativeTokenManagerAddress})) ArbNativeTokenManager.arbosVersion = params.ArbosVersion_41 @@ -677,10 +677,10 @@ func Precompiles() map[addr]ArbosPrecompile { ArbFilteredTransactionsManagerImpl := &ArbFilteredTransactionsManager{Address: types.ArbFilteredTransactionsManagerAddress} _, ArbFilteredTransactionsManager := MakePrecompile(precompilesgen.ArbFilteredTransactionsManagerMetaData, &ArbFilteredTransactionsManager{Address: types.ArbFilteredTransactionsManagerAddress}) - ArbFilteredTransactionsManager.arbosVersion = params.ArbosVersion_60 - ArbFilteredTransactionsManager.methodsByName["AddFilteredTransaction"].arbosVersion = params.ArbosVersion_60 - ArbFilteredTransactionsManager.methodsByName["DeleteFilteredTransaction"].arbosVersion = params.ArbosVersion_60 - ArbFilteredTransactionsManager.methodsByName["IsTransactionFiltered"].arbosVersion = params.ArbosVersion_60 + ArbFilteredTransactionsManager.arbosVersion = params.ArbosVersion_TransactionFiltering + ArbFilteredTransactionsManager.methodsByName["AddFilteredTransaction"].arbosVersion = params.ArbosVersion_TransactionFiltering + ArbFilteredTransactionsManager.methodsByName["DeleteFilteredTransaction"].arbosVersion = params.ArbosVersion_TransactionFiltering + ArbFilteredTransactionsManager.methodsByName["IsTransactionFiltered"].arbosVersion = params.ArbosVersion_TransactionFiltering insert(filtererOnly(ArbFilteredTransactionsManagerImpl.Address, ArbFilteredTransactionsManager)) diff --git a/system_tests/filtered_transactions_test.go b/system_tests/filtered_transactions_test.go index 1c1bf5dbea..4ae7bf3bfe 100644 --- a/system_tests/filtered_transactions_test.go +++ b/system_tests/filtered_transactions_test.go @@ -26,7 +26,7 @@ func TestManageTransactionFilterers(t *testing.T) { builder := NewNodeBuilder(ctx). DefaultConfig(t, true). - WithArbOSVersion(params.ArbosVersion_60) + WithArbOSVersion(params.ArbosVersion_TransactionFiltering) cleanup := builder.Build(t) defer cleanup() @@ -221,7 +221,7 @@ func TestFilteredTransactionsManagerFreeOps(t *testing.T) { builder := NewNodeBuilder(ctx). DefaultConfig(t, true). - WithArbOSVersion(params.ArbosVersion_60). + WithArbOSVersion(params.ArbosVersion_TransactionFiltering). WithArbOSInit(arbOSInit) cleanup := builder.Build(t) From 7919304d49bf6bf9b1594d17ca99c8a42ffe55e3 Mon Sep 17 00:00:00 2001 From: Mikhail Rogachev Date: Fri, 16 Jan 2026 17:55:28 +0100 Subject: [PATCH 24/28] Rename TransactionFilterPrecompile to FreeAccessPrecompile --- precompiles/wrapper.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/precompiles/wrapper.go b/precompiles/wrapper.go index 8992c3dc6d..fb1d8d5413 100644 --- a/precompiles/wrapper.go +++ b/precompiles/wrapper.go @@ -133,22 +133,22 @@ func (wrapper *OwnerPrecompile) Name() string { return wrapper.precompile.Name() } -// TransactionFilterPrecompile wraps ArbFilteredTransactionsManager to preserve free storage access for filterers. +// FreeAccessPrecompile wraps ArbFilteredTransactionsManager to preserve free storage access for filterers. // Call forwards the call and decides whether storage access is free based on caller role. -type TransactionFilterPrecompile struct { +type FreeAccessPrecompile struct { precompile ArbosPrecompile } func filtererOnly(address addr, impl ArbosPrecompile) (addr, ArbosPrecompile) { - return address, &TransactionFilterPrecompile{precompile: impl} + return address, &FreeAccessPrecompile{precompile: impl} } -func (wrapper *TransactionFilterPrecompile) Address() common.Address { +func (wrapper *FreeAccessPrecompile) Address() common.Address { return wrapper.precompile.Address() } // Call decides gas charging based on caller role, but always forwards the call. -func (wrapper *TransactionFilterPrecompile) Call( +func (wrapper *FreeAccessPrecompile) Call( input []byte, actingAsAddress common.Address, caller common.Address, @@ -190,10 +190,10 @@ func (wrapper *TransactionFilterPrecompile) Call( return output, burner.GasLeft(), burner.gasUsed, err } -func (wrapper *TransactionFilterPrecompile) Precompile() *Precompile { +func (wrapper *FreeAccessPrecompile) Precompile() *Precompile { return wrapper.precompile.Precompile() } -func (wrapper *TransactionFilterPrecompile) Name() string { +func (wrapper *FreeAccessPrecompile) Name() string { return wrapper.precompile.Name() } From 7cd9b175c2dade907aece4a7acd6e11a87a0b1e0 Mon Sep 17 00:00:00 2001 From: Mikhail Rogachev Date: Mon, 19 Jan 2026 15:58:43 +0100 Subject: [PATCH 25/28] update go-ethereum pin --- go-ethereum | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go-ethereum b/go-ethereum index 9a29c21047..0890754e46 160000 --- a/go-ethereum +++ b/go-ethereum @@ -1 +1 @@ -Subproject commit 9a29c21047881573e809d0e60cc3fb88d810de6a +Subproject commit 0890754e46fb10c8c9e9d2395778639dd4f2d775 From 7f4a52cbbcc681029c15330ddc477a5358f1e1dd Mon Sep 17 00:00:00 2001 From: Mikhail Rogachev Date: Tue, 20 Jan 2026 11:12:35 +0100 Subject: [PATCH 26/28] Review fixes --- changelog/mrogachev-nit-4245.md | 2 +- precompiles/ArbAggregator.go | 2 +- precompiles/ArbOwner.go | 7 ++++--- precompiles/ArbOwnerPublic.go | 6 +++--- precompiles/ArbWasmCache.go | 2 +- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/changelog/mrogachev-nit-4245.md b/changelog/mrogachev-nit-4245.md index 2231d26416..863c7a33f3 100644 --- a/changelog/mrogachev-nit-4245.md +++ b/changelog/mrogachev-nit-4245.md @@ -1,4 +1,4 @@ ### Added - Add new precompile ArbFilteredTransactionsManager to manage filtered transactions - Add transaction filterers to ArbOwner to limit access to ArbFilteredTransactionsManager -- Limit ArbOwners ability to create transaction filterers with TransactionFilteringFromTime +- Limit ArbOwners' ability to create transaction filterers with TransactionFilteringFromTime diff --git a/precompiles/ArbAggregator.go b/precompiles/ArbAggregator.go index 3b8a934496..47c0cddfb9 100644 --- a/precompiles/ArbAggregator.go +++ b/precompiles/ArbAggregator.go @@ -35,7 +35,7 @@ func (con ArbAggregator) GetDefaultAggregator(c ctx, evm mech) (addr, error) { // GetBatchPosters gets the addresses of all current batch posters func (con ArbAggregator) GetBatchPosters(c ctx, evm mech) ([]addr, error) { - return c.State.L1PricingState().BatchPosterTable().AllPosters(65536) + return c.State.L1PricingState().BatchPosterTable().AllPosters(maxGetAllMembers) } // Adds additional batch poster address diff --git a/precompiles/ArbOwner.go b/precompiles/ArbOwner.go index a9a60badde..1bca582c23 100644 --- a/precompiles/ArbOwner.go +++ b/precompiles/ArbOwner.go @@ -37,6 +37,7 @@ type ArbOwner struct { TransactionFiltererRemovedGasCost func(common.Address) (uint64, error) } +const maxGetAllMembers = 65536 const FeatureEnableDelay = 7 * 24 * 60 * 60 // one week var ( @@ -66,7 +67,7 @@ func (con ArbOwner) IsChainOwner(c ctx, evm mech, addr addr) (bool, error) { // GetAllChainOwners retrieves the list of chain owners func (con ArbOwner) GetAllChainOwners(c ctx, evm mech) ([]common.Address, error) { - return c.State.ChainOwners().AllMembers(65536) + return c.State.ChainOwners().AllMembers(maxGetAllMembers) } // setFeatureFromTime sets a time in epoch seconds when a feature becomes enabled. @@ -138,7 +139,7 @@ func (con ArbOwner) IsNativeTokenOwner(c ctx, evm mech, addr addr) (bool, error) // GetAllNativeTokenOwners retrieves the list of native token owners func (con ArbOwner) GetAllNativeTokenOwners(c ctx, evm mech) ([]common.Address, error) { - return c.State.NativeTokenOwners().AllMembers(65536) + return c.State.NativeTokenOwners().AllMembers(maxGetAllMembers) } // AddTransactionFilterer adds account as a transaction filterer (authorized to use ArbFilteredTransactionsManager) @@ -180,7 +181,7 @@ func (con ArbOwner) IsTransactionFilterer(c ctx, evm mech, filterer addr) (bool, // GetAllTransactionFilterers retrieves the list of transaction filterers func (con ArbOwner) GetAllTransactionFilterers(c ctx, evm mech) ([]common.Address, error) { - return c.State.TransactionFilterers().AllMembers(65536) + return c.State.TransactionFilterers().AllMembers(maxGetAllMembers) } // SetL1BaseFeeEstimateInertia sets how slowly ArbOS updates its estimate of the L1 basefee diff --git a/precompiles/ArbOwnerPublic.go b/precompiles/ArbOwnerPublic.go index 05effc2886..048f17ac1d 100644 --- a/precompiles/ArbOwnerPublic.go +++ b/precompiles/ArbOwnerPublic.go @@ -21,7 +21,7 @@ type ArbOwnerPublic struct { // GetAllChainOwners retrieves the list of chain owners func (con ArbOwnerPublic) GetAllChainOwners(c ctx, evm mech) ([]common.Address, error) { - return c.State.ChainOwners().AllMembers(65536) + return c.State.ChainOwners().AllMembers(maxGetAllMembers) } // RectifyChainOwner checks if the account is a chain owner @@ -45,7 +45,7 @@ func (con ArbOwnerPublic) IsNativeTokenOwner(c ctx, evm mech, addr addr) (bool, // GetAllNativeTokenOwners retrieves the list of native token owners func (con ArbOwnerPublic) GetAllNativeTokenOwners(c ctx, evm mech) ([]common.Address, error) { - return c.State.NativeTokenOwners().AllMembers(65536) + return c.State.NativeTokenOwners().AllMembers(maxGetAllMembers) } // GetNativeTokenMangementFrom returns the time in epoch seconds when the @@ -67,7 +67,7 @@ func (con ArbOwnerPublic) IsTransactionFilterer(c ctx, evm mech, filterer addr) // GetAllTransactionFilterers retrieves the list of transaction filterers func (con ArbOwnerPublic) GetAllTransactionFilterers(c ctx, evm mech) ([]common.Address, error) { - return c.State.TransactionFilterers().AllMembers(65536) + return c.State.TransactionFilterers().AllMembers(maxGetAllMembers) } // GetNetworkFeeAccount gets the network fee collector diff --git a/precompiles/ArbWasmCache.go b/precompiles/ArbWasmCache.go index 135a19bc2f..ca6d04fa1e 100644 --- a/precompiles/ArbWasmCache.go +++ b/precompiles/ArbWasmCache.go @@ -19,7 +19,7 @@ func (con ArbWasmCache) IsCacheManager(c ctx, _ mech, addr addr) (bool, error) { // Retrieve all authorized address managers. func (con ArbWasmCache) AllCacheManagers(c ctx, _ mech) ([]addr, error) { - return c.State.Programs().CacheManagers().AllMembers(65536) + return c.State.Programs().CacheManagers().AllMembers(maxGetAllMembers) } // Deprecated: replaced with CacheProgram. From e8b552118c4059610eb32b5763bd67a6a8ffc7af Mon Sep 17 00:00:00 2001 From: Mikhail Rogachev Date: Tue, 20 Jan 2026 11:17:01 +0100 Subject: [PATCH 27/28] Add minor comment --- arbos/arbosState/arbosstate.go | 1 + 1 file changed, 1 insertion(+) diff --git a/arbos/arbosState/arbosstate.go b/arbos/arbosState/arbosstate.go index 425fd5be17..f93e456451 100644 --- a/arbos/arbosState/arbosstate.go +++ b/arbos/arbosState/arbosstate.go @@ -462,6 +462,7 @@ func (state *ArbosState) UpgradeArbosVersion( // these versions are left to Orbit chains for custom upgrades. case params.ArbosVersion_TransactionFiltering: + // Once the final ArbOS version is locked in, this can be moved to that numeric version. ensure(addressSet.Initialize(state.backingStorage.OpenSubStorage(transactionFiltererSubspace))) default: From 140c32e2550dc40ef440538d4270f5267bd8ab46 Mon Sep 17 00:00:00 2001 From: Mikhail Rogachev Date: Tue, 20 Jan 2026 12:36:21 +0100 Subject: [PATCH 28/28] Update test-node pin --- nitro-testnode | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nitro-testnode b/nitro-testnode index c7f35226a8..60545cc51a 160000 --- a/nitro-testnode +++ b/nitro-testnode @@ -1 +1 @@ -Subproject commit c7f35226a883ed9833e7c16ef7fc5b7e29c2ebc8 +Subproject commit 60545cc51adca81db46d7e0491d1886cc9d4e827