diff --git a/arbos/arbosState/arbosstate.go b/arbos/arbosState/arbosstate.go index fbe5697551..805cb691a3 100644 --- a/arbos/arbosState/arbosstate.go +++ b/arbos/arbosState/arbosstate.go @@ -43,28 +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 - 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 + transactionFilterers *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") @@ -80,28 +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)), - 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)), + 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)), + 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 } @@ -171,22 +175,24 @@ const ( infraFeeAccountOffset brotliCompressionLevelOffset nativeTokenEnabledFromTimeOffset + transactionFilteringEnabledFromTimeOffset ) 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} + transactionFiltererSubspace SubspaceID = []byte{11} ) var PrecompileMinArbOSVersions = make(map[common.Address]uint64) @@ -229,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 @@ -446,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", @@ -562,6 +579,18 @@ 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) TransactionFilterers() *addressSet.AddressSet { + return state.transactionFilterers +} + func (state *ArbosState) SendMerkleAccumulator() *merkleAccumulator.MerkleAccumulator { if state.sendMerkle == nil { state.sendMerkle = merkleAccumulator.OpenMerkleAccumulator(state.backingStorage.OpenCachedSubStorage(sendMerkleSubspace)) @@ -620,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/arbos/filteredTransactions/state.go b/arbos/filteredTransactions/state.go new file mode 100644 index 0000000000..f4ad1dd9fd --- /dev/null +++ b/arbos/filteredTransactions/state.go @@ -0,0 +1,40 @@ +// Copyright 2025, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md + +package filteredTransactions + +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" +) + +var presentHash = common.BytesToHash([]byte{1}) + +type FilteredTransactionsState struct { + store *storage.Storage +} + +func Open(statedb vm.StateDB, burner burn.Burner) *FilteredTransactionsState { + return &FilteredTransactionsState{ + store: storage.FilteredTransactionsStorage(statedb, burner), + } +} + +func (s *FilteredTransactionsState) Add(txHash common.Hash) error { + return s.store.Set(txHash, presentHash) +} + +func (s *FilteredTransactionsState) Delete(txHash common.Hash) error { + return s.store.Clear(txHash) +} + +func (s *FilteredTransactionsState) IsFiltered(txHash common.Hash) (bool, error) { + value, err := s.store.Get(txHash) + if err != nil { + return false, err + } + return value == presentHash, nil +} diff --git a/arbos/storage/storage.go b/arbos/storage/storage.go index 050bd990ae..36ba85577a 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) +} + +// 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. // Only used for testing. func NewMemoryBacked(burner burn.Burner) *Storage { diff --git a/changelog/mrogachev-nit-4245.md b/changelog/mrogachev-nit-4245.md new file mode 100644 index 0000000000..2231d26416 --- /dev/null +++ b/changelog/mrogachev-nit-4245.md @@ -0,0 +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 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..adc40b17ea 160000 --- a/go-ethereum +++ b/go-ethereum @@ -1 +1 @@ -Subproject commit 9db354781776766033914bdcec72f23f0f1e5b38 +Subproject commit adc40b17ea28b803965309afc7fb833b142e197e diff --git a/nitro-testnode b/nitro-testnode index dfcc6b2e0f..c7f35226a8 160000 --- a/nitro-testnode +++ b/nitro-testnode @@ -1 +1 @@ -Subproject commit dfcc6b2e0f2abc1c889f2123c0a8ce0a6166a306 +Subproject commit c7f35226a883ed9833e7c16ef7fc5b7e29c2ebc8 diff --git a/precompiles/ArbFilteredTransactionsManager.go b/precompiles/ArbFilteredTransactionsManager.go new file mode 100644 index 0000000000..ddb1b59f27 --- /dev/null +++ b/precompiles/ArbFilteredTransactionsManager.go @@ -0,0 +1,62 @@ +// Copyright 2025, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md + +package precompiles + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + + "github.com/offchainlabs/nitro/arbos/filteredTransactions" +) + +// ArbFilteredTransactionsManager precompile enables ability to filter transactions by authorized callers. +// 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 { + if !con.hasAccess(c) { + return c.BurnOut() + } + + filteredState := filteredTransactions.Open(evm.StateDB, c) + 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 { + if !con.hasAccess(c) { + return c.BurnOut() + } + + filteredState := filteredTransactions.Open(evm.StateDB, c) + 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 +func (con ArbFilteredTransactionsManager) IsTransactionFiltered(c *Context, evm *vm.EVM, txHash common.Hash) (bool, error) { + 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/ArbFilteredTransactionsManager_test.go b/precompiles/ArbFilteredTransactionsManager_test.go new file mode 100644 index 0000000000..7a662f3a80 --- /dev/null +++ b/precompiles/ArbFilteredTransactionsManager_test.go @@ -0,0 +1,83 @@ +// 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/vm" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/offchainlabs/nitro/arbos/arbosState" + "github.com/offchainlabs/nitro/arbos/filteredTransactions" +) + +func setupFilteredTransactionsHandles( + t *testing.T, +) ( + *vm.EVM, + *arbosState.ArbosState, + *Context, + *ArbFilteredTransactionsManager, +) { + t.Helper() + + evm := newMockEVMForTesting() + caller := common.BytesToAddress(crypto.Keccak256([]byte("caller"))[:20]) + + 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 b88afc3ec5..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" ) @@ -24,17 +25,24 @@ 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 +const FeatureEnableDelay = 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") + 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 @@ -61,34 +69,45 @@ func (con ArbOwner) GetAllChainOwners(c ctx, evm mech) ([]common.Address, error) return c.State.ChainOwners().AllMembers(65536) } -// 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. -func (con ArbOwner) SetNativeTokenManagementFrom(c ctx, evm mech, timestamp uint64) error { +// 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 c.State.SetNativeTokenManagementFromTime(0) + return field.Set(0) } - stored, err := c.State.NativeTokenManagementFromTime() + stored, err := field.Get() if err != nil { return err } - now := evm.Context.Time - // If the feature is disabled, then the time must be at least 7 days in the + + // 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+NativeTokenEnableDelay) || - (stored > now+NativeTokenEnableDelay && timestamp < now+NativeTokenEnableDelay) { - return ErrNativeTokenDelay + 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+NativeTokenEnableDelay && timestamp < stored { - return ErrNativeTokenBackward + if stored > now && stored <= now+FeatureEnableDelay && timestamp < stored { + return ErrBackward } - return c.State.SetNativeTokenManagementFromTime(timestamp) + + return field.Set(timestamp) +} + +// SetNativeTokenManagementFrom sets native token management enabled-from time. +func (con ArbOwner) SetNativeTokenManagementFrom(c ctx, evm mech, timestamp uint64) error { + return setFeatureFromTime(c.State.NativeTokenEnabledTimeHandle(), evm.Context.Time, timestamp) +} + +// SetTransactionFilteringFrom sets transaction filtering enabled-from time. +func (con ArbOwner) SetTransactionFilteringFrom(c ctx, evm mech, timestamp uint64) error { + return setFeatureFromTime(c.State.TransactionFilteringEnabledTimeHandle(), evm.Context.Time, timestamp) } // AddNativeTokenOwner adds account as a native token owner @@ -122,6 +141,48 @@ func (con ArbOwner) GetAllNativeTokenOwners(c ctx, evm mech) ([]common.Address, return c.State.NativeTokenOwners().AllMembers(65536) } +// 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 + } + if enabledTime == 0 || enabledTime > evm.Context.Time { + return errors.New("transaction filtering feature is not enabled yet") + } + + 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 +func (con ArbOwner) RemoveTransactionFilterer(c ctx, evm mech, filterer addr) error { + member, err := con.IsTransactionFilterer(c, evm, filterer) + if err != nil { + return err + } + if !member { + return errors.New("tried to remove non existing transaction filterer") + } + + if err := c.State.TransactionFilterers().Remove(filterer, c.State.ArbOSVersion()); err != nil { + return err + } + return con.TransactionFiltererRemoved(c, evm, filterer) +} + +// IsTransactionFilterer checks if the account is a transaction filterer +func (con ArbOwner) IsTransactionFilterer(c ctx, evm mech, filterer addr) (bool, error) { + return c.State.TransactionFilterers().IsMember(filterer) +} + +// 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 func (con ArbOwner) SetL1BaseFeeEstimateInertia(c ctx, evm mech, inertia uint64) error { return c.State.L1PricingState().SetInertia(inertia) @@ -543,3 +604,7 @@ func (con ArbOwner) SetMultiGasPricingConstraints( } return nil } + +func (con ArbOwner) SetMaxStylusContractFragments(c ctx, evm mech, maxFragments uint16) error { + return errors.New("SetMaxStylusContractFragments is not implemented yet") +} diff --git a/precompiles/ArbOwnerPublic.go b/precompiles/ArbOwnerPublic.go index c885fae25b..606fbd11d5 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" ) @@ -52,6 +54,22 @@ 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() +} + +// 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() @@ -93,3 +111,7 @@ 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) { + return 0, errors.New("GetMaxStylusContractFragments is not implemented yet") +} diff --git a/precompiles/precompile.go b/precompiles/precompile.go index b41ffa7d9b..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", @@ -655,13 +657,33 @@ 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 + 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 ArbNativeTokenManager.methodsByName["BurnNativeToken"].arbosVersion = params.ArbosVersion_41 + 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(filtererOnly(ArbFilteredTransactionsManagerImpl.Address, ArbFilteredTransactionsManager)) + // 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..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: 3, + params.ArbosVersion_60: 17, } precompiles := Precompiles() diff --git a/precompiles/wrapper.go b/precompiles/wrapper.go index 17ef00cd0f..8992c3dc6d 100644 --- a/precompiles/wrapper.go +++ b/precompiles/wrapper.go @@ -132,3 +132,68 @@ func (wrapper *OwnerPrecompile) Precompile() *Precompile { func (wrapper *OwnerPrecompile) Name() string { return wrapper.precompile.Name() } + +// TransactionFilterPrecompile 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 { + precompile ArbosPrecompile +} + +func filtererOnly(address addr, impl ArbosPrecompile) (addr, ArbosPrecompile) { + return address, &TransactionFilterPrecompile{precompile: impl} +} + +func (wrapper *TransactionFilterPrecompile) Address() common.Address { + return wrapper.precompile.Address() +} + +// Call decides gas charging based on caller role, but always forwards the call. +func (wrapper *TransactionFilterPrecompile) 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 + } + + filterers := state.TransactionFilterers() + isFilterer, err := filterers.IsMember(caller) + if err != nil { + return nil, burner.GasLeft(), burner.gasUsed, err + } + + output, _, _, err := con.Call( + input, + actingAsAddress, + caller, + value, + readOnly, + gasSupplied, + evm, + ) + if isFilterer { + return output, gasSupplied, multigas.ZeroGas(), err + } + return output, burner.GasLeft(), burner.gasUsed, err +} + +func (wrapper *TransactionFilterPrecompile) Precompile() *Precompile { + return wrapper.precompile.Precompile() +} + +func (wrapper *TransactionFilterPrecompile) Name() string { + return wrapper.precompile.Name() +} diff --git a/system_tests/eth_config_test.go b/system_tests/eth_config_test.go index f4a8560519..1c55190631 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"), + "ArbFilteredTransactionsManager": 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"), diff --git a/system_tests/filtered_transactions_test.go b/system_tests/filtered_transactions_test.go new file mode 100644 index 0000000000..1c1bf5dbea --- /dev/null +++ b/system_tests/filtered_transactions_test.go @@ -0,0 +1,273 @@ +// 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/arbitrum/multigas" + "github.com/ethereum/go-ethereum/common" + "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" +) + +func TestManageTransactionFilterers(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx). + DefaultConfig(t, true). + WithArbOSVersion(params.ArbosVersion_60) + + cleanup := builder.Build(t) + defer cleanup() + + 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) + + 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) + + 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) + txAddedTopic := filteredTransactionsManagerABI.Events["FilteredTransactionAdded"].ID + txDeletedTopic := filteredTransactionsManagerABI.Events["FilteredTransactionDeleted"].ID + + arbFilteredTxs, err := precompilesgen.NewArbFilteredTransactionsManager( + types.ArbFilteredTransactionsManagerAddress, + builder.L2.Client, + ) + require.NoError(t, err) + + // 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 + hdr, err := builder.L2.Client.HeaderByNumber(ctx, nil) + require.NoError(t, err) + 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.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.FeatureEnableDelay+1) + + // Initially neither owner nor user can modify filtered transactions, + // but both can read (get) filtered status + isFiltered, err := arbFilteredTxs.IsTransactionFiltered(ownerCallOpts, txHash) + require.NoError(t, err) + require.False(t, isFiltered) + _, 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) + 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, err := arbOwner.ParseTransactionFiltererAdded(*lg) + require.NoError(t, err) + 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) + + // Owner is still not a filterer, so owner still cannot call the manager + _, err = arbFilteredTxs.AddFilteredTransaction(&ownerTxOpts, 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) + 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] != txAddedTopic { + continue + } + ev, err := arbFilteredTxs.ParseFilteredTransactionAdded(*lg) + require.NoError(t, err) + 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) + require.True(t, filtered) + + // User unfilters the tx + tx, err = arbFilteredTxs.DeleteFilteredTransaction(&userTxOpts, txHash) + require.NoError(t, err) + 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] != txDeletedTopic { + continue + } + ev, err := arbFilteredTxs.ParseFilteredTransactionDeleted(*lg) + require.NoError(t, err) + 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) + require.False(t, filtered) + + // Owner revokes the role + tx, err = arbOwner.RemoveTransactionFilterer(&ownerTxOpts, userTxOpts.From) + require.NoError(t, err) + 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, err := arbOwner.ParseTransactionFiltererRemoved(*lg) + require.NoError(t, err) + 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) + require.False(t, isFilterer) + + // User is no longer authorised + _, err = arbFilteredTxs.DeleteFilteredTransaction(&userTxOpts, 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) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + arbOSInit := ¶ms.ArbOSInit{ + TransactionFilteringEnabled: true, + } + + builder := NewNodeBuilder(ctx). + DefaultConfig(t, true). + WithArbOSVersion(params.ArbosVersion_60). + WithArbOSInit(arbOSInit) + + cleanup := builder.Build(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 + + 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) + + // 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) + _, err = builder.L2.EnsureTxSucceeded(tx) + require.NoError(t, err) + + // 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) + require.Equal(t, uint64(0), receipt.MultiGasUsed.Get(multigas.ResourceKindStorageAccess)) + + tx, err = arbFilteredTxs.DeleteFilteredTransaction(&filtererTxOpts, txHash) + require.NoError(t, err) + receipt, err = builder.L2.EnsureTxSucceeded(tx) + require.NoError(t, err) + require.Equal(t, uint64(0), receipt.MultiGasUsed.Get(multigas.ResourceKindStorageAccess)) +}