Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f9bd5a7
Add ArbCensoredTransactionsManager precompile (idle)
MishkaRogachev Dec 24, 2025
89aed0f
Limit access to ArbCensoredTransactionsManager with transaction censors
MishkaRogachev Dec 26, 2025
987a143
Separated kv-storage for censored transactions
MishkaRogachev Dec 26, 2025
bbce75c
Rename: censored -> filtered transactions
MishkaRogachev Dec 26, 2025
28feb69
chore: minor fixes
MishkaRogachev Dec 26, 2025
1ad1084
Add a system test for filtered transaction manager
MishkaRogachev Dec 29, 2025
0a6b8bb
Make ArbFilteredTransactionsManager access similar to ArbNativeTokenM…
MishkaRogachev Dec 29, 2025
18a4b46
Add CensorPrecompile wrapper to make transaction filtring free
MishkaRogachev Dec 29, 2025
102cbf9
Add events for filtered transaction
MishkaRogachev Dec 30, 2025
cc5818a
Add test for free calls for FilteredTransactionsManager
MishkaRogachev Dec 31, 2025
b0085c9
Add limited GetTransactionCensorshipFromTime
MishkaRogachev Jan 5, 2026
fb009fe
Finalise limited GetTransactionCensorshipFromTime
MishkaRogachev Jan 7, 2026
fa1ca8a
Add TransactionFilteringEnabled to ArbOS state init
MishkaRogachev Jan 8, 2026
64d5725
Improve tests and polish comments
MishkaRogachev Jan 8, 2026
d5690a1
Review fixes and polishing
MishkaRogachev Jan 9, 2026
55dea76
Make `IsTransactionFiltered` public
MishkaRogachev Jan 12, 2026
b82304e
Add events and public methods for transaction filterers
MishkaRogachev Jan 12, 2026
9d83f9c
tmp: add stubs to compile upstream version
MishkaRogachev Jan 12, 2026
bcb35a9
Add test for filterer add/remove event
MishkaRogachev Jan 12, 2026
2058741
Review fixes and minor polishing
MishkaRogachev Jan 13, 2026
614caa6
chore: remove unused arbos state
MishkaRogachev Jan 14, 2026
da82012
Review fixes
MishkaRogachev Jan 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 93 additions & 56 deletions arbos/arbosState/arbosstate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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
}
40 changes: 40 additions & 0 deletions arbos/filteredTransactions/state.go
Original file line number Diff line number Diff line change
@@ -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
}
18 changes: 14 additions & 4 deletions arbos/storage/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand All @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions changelog/mrogachev-nit-4245.md
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion contracts-local/src/precompiles
2 changes: 1 addition & 1 deletion go-ethereum
2 changes: 1 addition & 1 deletion nitro-testnode
Submodule nitro-testnode updated 1 files
+1 −1 blockscout
62 changes: 62 additions & 0 deletions precompiles/ArbFilteredTransactionsManager.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading