From 2e6abe1e4da243d626c26e1e22b761057200571d Mon Sep 17 00:00:00 2001 From: ganeshvanahalli Date: Wed, 8 Apr 2026 14:55:46 +0530 Subject: [PATCH 1/9] Make message extractor able to pick up MEL config upgrade events --- arbnode/mel/extraction/abis.go | 5 + arbnode/mel/extraction/mel_config_lookup.go | 70 +++++++++++ .../extraction/message_extraction_function.go | 33 ++++++ .../message_extraction_function_test.go | 6 + arbnode/mel/extraction/types.go | 9 ++ arbnode/mel/messages.go | 6 + arbnode/mel/runner/initialize.go | 2 +- .../mel/runner/logs_and_headers_fetcher.go | 28 ++++- .../runner/logs_and_headers_fetcher_test.go | 2 +- arbnode/mel/runner/mel.go | 2 +- arbnode/mel/state.go | 16 ++- changelog/ganeshvanahalli-nit-4779.md | 2 + contracts | 2 +- system_tests/message_extraction_layer_test.go | 110 +++++++++++++++++- 14 files changed, 283 insertions(+), 10 deletions(-) create mode 100644 arbnode/mel/extraction/mel_config_lookup.go create mode 100644 changelog/ganeshvanahalli-nit-4779.md diff --git a/arbnode/mel/extraction/abis.go b/arbnode/mel/extraction/abis.go index 65aacf8d2d3..c4de783a22e 100644 --- a/arbnode/mel/extraction/abis.go +++ b/arbnode/mel/extraction/abis.go @@ -5,6 +5,7 @@ package melextraction import ( "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" "github.com/offchainlabs/nitro/solgen/go/bridgegen" ) @@ -12,6 +13,7 @@ import ( var BatchDeliveredID common.Hash var InboxMessageDeliveredID common.Hash var InboxMessageFromOriginID common.Hash +var MELConfigEventID common.Hash var SeqInboxABI *abi.ABI var IBridgeABI *abi.ABI var iInboxABI *abi.ABI @@ -45,4 +47,7 @@ func init() { panic(err) } iInboxABI = parsedIInboxABI + + // MELConfigEvent(address inbox, address sequencerInbox, uint256 melVersionActivationBlock) + MELConfigEventID = crypto.Keccak256Hash([]byte("MELConfigEvent(address,address,uint256)")) } diff --git a/arbnode/mel/extraction/mel_config_lookup.go b/arbnode/mel/extraction/mel_config_lookup.go new file mode 100644 index 00000000000..3faba7e634e --- /dev/null +++ b/arbnode/mel/extraction/mel_config_lookup.go @@ -0,0 +1,70 @@ +// Copyright 2026, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md +package melextraction + +import ( + "context" + "math/big" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + + "github.com/offchainlabs/nitro/arbnode/mel" +) + +// melConfigEventABI is a manually defined ABI for the MELConfigEvent. +// This can be replaced with rollupgen.RollupAdminLogicMetaData.GetAbi() +// once the Go bindings are regenerated from the updated Solidity contracts. +var melConfigEventABI *abi.ABI + +func init() { + const melConfigEventABIJSON = `[{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"inbox","type":"address"},{"indexed":false,"internalType":"address","name":"sequencerInbox","type":"address"},{"indexed":false,"internalType":"uint256","name":"melVersionActivationBlock","type":"uint256"}],"name":"MELConfigEvent","type":"event"}]` + parsed, err := abi.JSON(strings.NewReader(melConfigEventABIJSON)) + if err != nil { + panic(err) + } + melConfigEventABI = &parsed +} + +// melConfigEventFields holds the decoded fields from a MELConfigEvent log. +type melConfigEventFields struct { + Inbox common.Address + SequencerInbox common.Address + MelVersionActivationBlock *big.Int +} + +// ParseMELConfigFromBlock scans the logs of the given parent chain block for +// a MELConfigEvent. The log prefetcher already filters by rollup address, +// so this function only needs to match the event topic. +// Returns nil if no config event is found in the block. +func ParseMELConfigFromBlock( + ctx context.Context, + parentChainHeader *types.Header, + logsFetcher LogsFetcher, + eventUnpacker EventUnpacker, +) (*mel.MELConfig, error) { + logs, err := logsFetcher.LogsForBlockHash(ctx, parentChainHeader.Hash()) + if err != nil { + return nil, err + } + for _, log := range logs { + if log == nil || len(log.Topics) == 0 || log.Topics[0] != MELConfigEventID { + continue + } + event := new(melConfigEventFields) + if err := eventUnpacker.UnpackLogTo(event, melConfigEventABI, "MELConfigEvent", *log); err != nil { + return nil, err + } + if !event.MelVersionActivationBlock.IsUint64() { + return nil, nil + } + return &mel.MELConfig{ + Inbox: event.Inbox, + SequencerInbox: event.SequencerInbox, + VersionActivationBlock: event.MelVersionActivationBlock.Uint64(), + }, nil + } + return nil, nil +} diff --git a/arbnode/mel/extraction/message_extraction_function.go b/arbnode/mel/extraction/message_extraction_function.go index 4c3064ec1a0..f471d6334b1 100644 --- a/arbnode/mel/extraction/message_extraction_function.go +++ b/arbnode/mel/extraction/message_extraction_function.go @@ -79,6 +79,7 @@ func ExtractMessages( messagesFromBatchSegments, arbstate.ParseSequencerMessage, arbostypes.ParseBatchPostingReportMessageFields, + ParseMELConfigFromBlock, ) } @@ -101,6 +102,7 @@ func extractMessagesImpl( extractBatchMessages batchMsgExtractionFunc, parseSequencerMessage sequencerMessageParserFunc, parseBatchPostingReport batchPostingReportParserFunc, + lookupMELConfig melConfigLookupFunc, ) (*mel.State, []*arbostypes.MessageWithMetadata, []*mel.DelayedInboxMessage, []*mel.BatchMetadata, error) { state := inputState.Clone() @@ -118,6 +120,26 @@ func extractMessagesImpl( state.ParentChainBlockHash = parentChainHeader.Hash() state.ParentChainBlockNumber = parentChainHeader.Number.Uint64() state.ParentChainPreviousBlockHash = parentChainHeader.ParentHash + + // Check for MEL config events in this block. + melConfig, err := lookupMELConfig( + ctx, + parentChainHeader, + logsFetcher, + eventUnpacker, + ) + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("failed to lookup MEL config event: %w", err) + } + if melConfig != nil { + if melConfig.VersionActivationBlock <= state.VersionActivationBlock { + return nil, nil, nil, nil, fmt.Errorf("current MEL state VersionActivationBlock: %d cannot be greater or equal to the next scheduled MEL config's VersionActivationBlock: %d", state.VersionActivationBlock, melConfig.VersionActivationBlock) + } + state.VersionActivationBlock = melConfig.VersionActivationBlock + state.PendingInbox = melConfig.Inbox + state.PendingSequencerInbox = melConfig.SequencerInbox + } + // Now, check for any logs emitted by the sequencer inbox by txs // included in the parent chain block. batches, batchTxs, err := lookupBatches( @@ -267,5 +289,16 @@ func extractMessagesImpl( return nil, nil, nil, nil, fmt.Errorf("batch AfterDelayedCount: %d and MEL state DelayedMessagesRead: %d mismatch", batch.AfterDelayedCount, state.DelayedMessagesRead) } } + // Apply pending config if the activation block has been reached. + if state.ParentChainBlockNumber == state.VersionActivationBlock { + state.DelayedMessagePostingTargetAddress = state.PendingInbox + state.BatchPostingTargetAddress = state.PendingSequencerInbox + state.Version += 1 + // Reset PendingInbox and PendingSequencerInbox because having them non-zero after a version upgrade is incorrect, + // we don't reset the VersionActivationBlock though, to keep in the state when the current version was activated + state.PendingInbox = common.Address{} + state.PendingSequencerInbox = common.Address{} + + } return state, messages, delayedMessages, batchMetas, nil } diff --git a/arbnode/mel/extraction/message_extraction_function_test.go b/arbnode/mel/extraction/message_extraction_function_test.go index 14dc1b7a47c..0024ef3891e 100644 --- a/arbnode/mel/extraction/message_extraction_function_test.go +++ b/arbnode/mel/extraction/message_extraction_function_test.go @@ -24,6 +24,11 @@ import ( "github.com/offchainlabs/nitro/daprovider" ) +// noopMELConfigLookup is a no-op MEL config lookup used in tests that don't need config event handling. +func noopMELConfigLookup(_ context.Context, _ *types.Header, _ LogsFetcher, _ EventUnpacker) (*mel.MELConfig, error) { + return nil, nil +} + func TestExtractMessages(t *testing.T) { ctx := context.Background() prevParentBlockHash := common.HexToHash("0x1234") @@ -203,6 +208,7 @@ func TestExtractMessages(t *testing.T) { tt.extractBatchMessages, tt.parseSequencerMsg, tt.parseReport, + noopMELConfigLookup, ) } diff --git a/arbnode/mel/extraction/types.go b/arbnode/mel/extraction/types.go index 201569dbdc5..670a11b9590 100644 --- a/arbnode/mel/extraction/types.go +++ b/arbnode/mel/extraction/types.go @@ -82,3 +82,12 @@ type batchMsgExtractionFunc func( type batchPostingReportParserFunc func( rd io.Reader, ) (*big.Int, common.Address, common.Hash, uint64, *big.Int, uint64, error) + +// Defines a function that can lookup a MEL config event from a parent chain block. +// See: ParseMELConfigFromBlock. +type melConfigLookupFunc func( + ctx context.Context, + parentChainHeader *types.Header, + logsFetcher LogsFetcher, + eventUnpacker EventUnpacker, +) (*mel.MELConfig, error) diff --git a/arbnode/mel/messages.go b/arbnode/mel/messages.go index 78ee74d7744..a0bf2da4d3d 100644 --- a/arbnode/mel/messages.go +++ b/arbnode/mel/messages.go @@ -84,3 +84,9 @@ type MessageSyncProgress struct { BatchProcessed uint64 MsgCount arbutil.MessageIndex } + +type MELConfig struct { + Inbox common.Address + SequencerInbox common.Address + VersionActivationBlock uint64 +} diff --git a/arbnode/mel/runner/initialize.go b/arbnode/mel/runner/initialize.go index 56385fb893b..7a13b045b63 100644 --- a/arbnode/mel/runner/initialize.go +++ b/arbnode/mel/runner/initialize.go @@ -28,7 +28,7 @@ func (m *MessageExtractor) initialize(ctx context.Context, current *fsm.CurrentS return m.config.RetryInterval, fmt.Errorf("failed to get start parent chain block: %d corresponding to head mel state from parent chain: %w", melState.ParentChainBlockNumber, err) } // Initialize logsPreFetcher - m.logsAndHeadersPreFetcher = newLogsAndHeadersFetcher(m.parentChainReader, m.config.BlocksToPrefetch) + m.logsAndHeadersPreFetcher = newLogsAndHeadersFetcher(m.parentChainReader, m.config.BlocksToPrefetch, m.addrs.Rollup) // We check if our head mel state's parentChainBlockHash matches the one on-chain, if it doesnt then we detected a reorg if melState.ParentChainBlockHash != startBlock.Hash() { log.Info("MEL detected L1 reorg at the start", "block", melState.ParentChainBlockNumber, "parentChainBlockHash", melState.ParentChainBlockHash, "onchainParentChainBlockHash", startBlock.Hash()) // Log level is Info because L1 reorgs are a common occurrence diff --git a/arbnode/mel/runner/logs_and_headers_fetcher.go b/arbnode/mel/runner/logs_and_headers_fetcher.go index 2816b1f75d5..c73b02d47bc 100644 --- a/arbnode/mel/runner/logs_and_headers_fetcher.go +++ b/arbnode/mel/runner/logs_and_headers_fetcher.go @@ -24,15 +24,17 @@ type logsAndHeadersFetcher struct { toBlock uint64 blocksToFetch uint64 chainHeight uint64 + rollupAddr common.Address headers []*types.Header logsByTxIndex map[common.Hash]map[uint][]*types.Log logsByBlockHash map[common.Hash][]*types.Log } -func newLogsAndHeadersFetcher(parentChainReader ParentChainReader, blocksToFetch uint64) *logsAndHeadersFetcher { +func newLogsAndHeadersFetcher(parentChainReader ParentChainReader, blocksToFetch uint64, rollupAddr common.Address) *logsAndHeadersFetcher { return &logsAndHeadersFetcher{ parentChainReader: parentChainReader, blocksToFetch: blocksToFetch, + rollupAddr: rollupAddr, logsByTxIndex: make(map[common.Hash]map[uint][]*types.Log), logsByBlockHash: make(map[common.Hash][]*types.Log), } @@ -81,6 +83,9 @@ func (f *logsAndHeadersFetcher) fetch(ctx context.Context, preState *mel.State) if fetchLogsErr == nil { fetchLogsErr = f.fetchDelayedMessageLogs(ctx, parentChainBlockNumber, toBlock, preState.DelayedMessagePostingTargetAddress) } + if fetchLogsErr == nil && f.rollupAddr != (common.Address{}) { + fetchLogsErr = f.fetchRollupLogs(ctx, parentChainBlockNumber, toBlock) + } wg.Done() }() wg.Wait() @@ -165,6 +170,27 @@ func (f *logsAndHeadersFetcher) fetchDelayedMessageLogs(ctx context.Context, fro return conditionalFetch(nil, [][]common.Hash{{melextraction.InboxMessageDeliveredID, melextraction.InboxMessageFromOriginID}}) } +func (f *logsAndHeadersFetcher) fetchRollupLogs(ctx context.Context, from, to uint64) error { + query := ethereum.FilterQuery{ + FromBlock: new(big.Int).SetUint64(from), + ToBlock: new(big.Int).SetUint64(to), + Addresses: []common.Address{f.rollupAddr}, + Topics: [][]common.Hash{{melextraction.MELConfigEventID}}, + } + logs, err := f.parentChainReader.FilterLogs(ctx, query) + if err != nil { + return err + } + for _, log := range logs { + f.logsByBlockHash[log.BlockHash] = append(f.logsByBlockHash[log.BlockHash], &log) + if _, ok := f.logsByTxIndex[log.BlockHash]; !ok { + f.logsByTxIndex[log.BlockHash] = make(map[uint][]*types.Log) + } + f.logsByTxIndex[log.BlockHash][log.TxIndex] = append(f.logsByTxIndex[log.BlockHash][log.TxIndex], &log) + } + return nil +} + func (f *logsAndHeadersFetcher) getHeaderByNumber(ctx context.Context, number uint64) (*types.Header, error) { if len(f.headers) == 0 || number < f.fromBlock || number > f.toBlock { // uninitialized or out of range queries should directly be forwarded to parentChainReader return f.parentChainReader.HeaderByNumber(ctx, new(big.Int).SetUint64(number)) diff --git a/arbnode/mel/runner/logs_and_headers_fetcher_test.go b/arbnode/mel/runner/logs_and_headers_fetcher_test.go index 9ce7a6df35d..ff3c7260c3b 100644 --- a/arbnode/mel/runner/logs_and_headers_fetcher_test.go +++ b/arbnode/mel/runner/logs_and_headers_fetcher_test.go @@ -87,7 +87,7 @@ func TestLogsFetcher(t *testing.T) { } parentChainReader := &mockParentChainReader{logs: append(batchTxLogs, delayedMsgTxLogs...)} - fetcher := newLogsAndHeadersFetcher(parentChainReader, 10) + fetcher := newLogsAndHeadersFetcher(parentChainReader, 10, common.Address{}) fetcher.chainHeight = 100 melState := &mel.State{ ParentChainBlockNumber: 1, diff --git a/arbnode/mel/runner/mel.go b/arbnode/mel/runner/mel.go index f2d760cbf40..70434d50dcc 100644 --- a/arbnode/mel/runner/mel.go +++ b/arbnode/mel/runner/mel.go @@ -453,7 +453,7 @@ func (m *MessageExtractor) GetSequencerMessageBytes(ctx context.Context, seqNum func (m *MessageExtractor) GetSequencerMessageBytesForParentBlock(ctx context.Context, seqNum uint64, parentChainBlock uint64) ([]byte, common.Hash, error) { // No need to specify a max headers to fetch, as we are using the logs fetcher only, so we can pass in a 0. - logsFetcher := newLogsAndHeadersFetcher(m.parentChainReader, 0) + logsFetcher := newLogsAndHeadersFetcher(m.parentChainReader, 0, m.addrs.Rollup) if err := logsFetcher.fetchSequencerBatchLogs(ctx, parentChainBlock, parentChainBlock); err != nil { return nil, common.Hash{}, err } diff --git a/arbnode/mel/state.go b/arbnode/mel/state.go index eb1790646c9..a52604696c0 100644 --- a/arbnode/mel/state.go +++ b/arbnode/mel/state.go @@ -31,7 +31,9 @@ func SplitPreimage(preimage []byte) (left, right common.Hash, err error) { // be deterministically constructed from any start state and parent chain blocks from // that point onwards. type State struct { - Version uint16 + Version uint16 + VersionActivationBlock uint64 + ParentChainId uint64 ParentChainBlockNumber uint64 BatchPostingTargetAddress common.Address @@ -46,6 +48,11 @@ type State struct { DelayedMessageInboxAcc common.Hash DelayedMessageOutboxAcc common.Hash + // Pending value changes to DelayedMessagePostingTargetAddress and + // BatchPostingTargetAddress from a MELConfig upgrade event + PendingInbox common.Address + PendingSequencerInbox common.Address + msgPreimagesDest daprovider.PreimagesMap delayedMsgPreimagesDest daprovider.PreimagesMap // delayedMsgPreimages is always populated during delayed message operations @@ -90,14 +97,19 @@ func (s *State) Clone() *State { parentChainPrevHash := common.Hash{} delayedInboxAcc := common.Hash{} delayedOutboxAcc := common.Hash{} + pendingInbox := common.Address{} + pendingSequencerInbox := common.Address{} copy(batchPostingTarget[:], s.BatchPostingTargetAddress[:]) copy(delayedMessageTarget[:], s.DelayedMessagePostingTargetAddress[:]) copy(parentChainHash[:], s.ParentChainBlockHash[:]) copy(parentChainPrevHash[:], s.ParentChainPreviousBlockHash[:]) copy(delayedInboxAcc[:], s.DelayedMessageInboxAcc[:]) copy(delayedOutboxAcc[:], s.DelayedMessageOutboxAcc[:]) + copy(pendingInbox[:], s.PendingInbox[:]) + copy(pendingSequencerInbox[:], s.PendingSequencerInbox[:]) return &State{ Version: s.Version, + VersionActivationBlock: s.VersionActivationBlock, ParentChainId: s.ParentChainId, ParentChainBlockNumber: s.ParentChainBlockNumber, BatchPostingTargetAddress: batchPostingTarget, @@ -110,6 +122,8 @@ func (s *State) Clone() *State { DelayedMessagesSeen: s.DelayedMessagesSeen, DelayedMessageInboxAcc: delayedInboxAcc, DelayedMessageOutboxAcc: delayedOutboxAcc, + PendingInbox: pendingInbox, + PendingSequencerInbox: pendingSequencerInbox, // LocalMsgAccumulator is intentionally not copied — each cloned state // starts a fresh hash chain for its own batch of accumulated messages. // diff --git a/changelog/ganeshvanahalli-nit-4779.md b/changelog/ganeshvanahalli-nit-4779.md new file mode 100644 index 00000000000..294ee7509d0 --- /dev/null +++ b/changelog/ganeshvanahalli-nit-4779.md @@ -0,0 +1,2 @@ +### Added + - Make message extractor able to pick up MEL config upgrade events \ No newline at end of file diff --git a/contracts b/contracts index 4341b132cfb..2905da0533d 160000 --- a/contracts +++ b/contracts @@ -1 +1 @@ -Subproject commit 4341b132cfbdcc980ead03765ca5224ff6cb5d97 +Subproject commit 2905da0533d83d0e1aad80944b81225d89fdf117 diff --git a/system_tests/message_extraction_layer_test.go b/system_tests/message_extraction_layer_test.go index 286829ab847..ce90195e330 100644 --- a/system_tests/message_extraction_layer_test.go +++ b/system_tests/message_extraction_layer_test.go @@ -4,9 +4,11 @@ import ( "bytes" "context" "math/big" + "strings" "testing" "time" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" @@ -26,6 +28,7 @@ import ( "github.com/offchainlabs/nitro/solgen/go/bridgegen" "github.com/offchainlabs/nitro/solgen/go/precompilesgen" "github.com/offchainlabs/nitro/solgen/go/rollupgen" + "github.com/offchainlabs/nitro/solgen/go/upgrade_executorgen" "github.com/offchainlabs/nitro/staker/bold" "github.com/offchainlabs/nitro/util/headerreader" "github.com/offchainlabs/nitro/util/testhelpers" @@ -762,6 +765,105 @@ func TestMessageExtractionLayer_UseArbDBForStoringDelayedMessages(t *testing.T) } } +func TestMessageExtractionLayer_MELConfigEvent(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + builder := NewNodeBuilder(ctx). + DefaultConfig(t, true). + WithDelayBuffer(0) + builder.nodeConfig.MessageExtraction.Enable = true + builder.nodeConfig.BatchPoster.MaxDelay = time.Hour + builder.nodeConfig.BatchPoster.PollInterval = time.Hour + cleanup := builder.Build(t) + defer cleanup() + + msgExtractor := builder.L2.ConsensusNode.MessageExtractor + + // Wait for MEL to catch up + select { + case <-msgExtractor.CaughtUp(): + case <-time.After(time.Minute): + t.Fatal("timed out waiting for MEL to catch up") + } + + preConfigState, err := msgExtractor.GetHeadState() + Require(t, err) + if preConfigState.Version != 0 { + t.Fatalf("Expected initial version 0, got %d", preConfigState.Version) + } + + // Call setMELConfig on the rollup contract via the UpgradeExecutor. + // The new inbox/sequencerInbox addresses are dummy addresses for this test — + // what we're testing is that MEL detects the event and applies the config change. + newInbox := common.HexToAddress("0x0000000000000000000000000000000000001111") + newSequencerInbox := common.HexToAddress("0x0000000000000000000000000000000000002222") + + // Determine an activation block in the future + currentL1Block, err := builder.L1.Client.BlockNumber(ctx) + Require(t, err) + activationBlock := currentL1Block + 20 + + // Pack the setMELConfig calldata using the generated rollup ABI + rollupABI, err := abi.JSON(strings.NewReader(rollupgen.RollupAdminLogicABI)) + Require(t, err) + calldata, err := rollupABI.Pack("setMELConfig", newInbox, newSequencerInbox, new(big.Int).SetUint64(activationBlock)) + Require(t, err) + + deployAuth := builder.L1Info.GetDefaultTransactOpts("RollupOwner", ctx) + upgradeExecutor, err := upgrade_executorgen.NewUpgradeExecutor(builder.addresses.UpgradeExecutor, builder.L1.Client) + Require(t, err) + tx, err := upgradeExecutor.ExecuteCall(&deployAuth, builder.addresses.Rollup, calldata) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, builder.L1.Client, tx) + Require(t, err) + + // Advance L1 past the activation block + // #nosec G115 + blocksToAdvance := int(activationBlock - currentL1Block + 5) + AdvanceL1(t, ctx, builder.L1.Client, builder.L1Info, blocksToAdvance) + + // Wait for MEL to process past the activation block + timeout := time.NewTimer(time.Minute) + defer timeout.Stop() + tick := time.NewTicker(200 * time.Millisecond) + defer tick.Stop() + for { + headState, err := msgExtractor.GetHeadState() + Require(t, err) + if headState.ParentChainBlockNumber >= activationBlock { + break + } + select { + case <-tick.C: + case <-timeout.C: + t.Fatal("timed out waiting for MEL to process past activation block") + } + } + + // Verify the config was applied + postConfigState, err := msgExtractor.GetHeadState() + Require(t, err) + if postConfigState.Version != 1 { + t.Fatalf("Expected version 1 after config activation, got %d", postConfigState.Version) + } + if postConfigState.VersionActivationBlock != activationBlock { + t.Fatalf("Expected VersionActivationBlock %d, got %d", activationBlock, postConfigState.VersionActivationBlock) + } + if postConfigState.BatchPostingTargetAddress != newSequencerInbox { + t.Fatalf("Expected BatchPostingTargetAddress %s, got %s", newSequencerInbox.Hex(), postConfigState.BatchPostingTargetAddress.Hex()) + } + if postConfigState.DelayedMessagePostingTargetAddress != newInbox { + t.Fatalf("Expected DelayedMessagePostingTargetAddress %s, got %s", newInbox.Hex(), postConfigState.DelayedMessagePostingTargetAddress.Hex()) + } + if postConfigState.PendingInbox != (common.Address{}) { + t.Fatalf("Expected PendingInbox to be zero after activation, got %s", postConfigState.PendingInbox.Hex()) + } + if postConfigState.PendingSequencerInbox != (common.Address{}) { + t.Fatalf("Expected PendingSequencerInbox to be zero after activation, got %s", postConfigState.PendingSequencerInbox.Hex()) + } +} + // TestMELMigrationFromLegacyNode verifies that a node previously running with // the legacy inbox reader/tracker can be seamlessly migrated to MEL. // @@ -885,8 +987,8 @@ func TestMELMigrationFromLegacyNode(t *testing.T) { } // Verify migration state - melExtractor := builder.L2.ConsensusNode.MessageExtractor - headState, err := melExtractor.GetHeadState() + msgExtractor := builder.L2.ConsensusNode.MessageExtractor + headState, err := msgExtractor.GetHeadState() Require(t, err) t.Logf("Post-migration MEL state: delayedSeen=%d, delayedRead=%d, batchCount=%d, msgCount=%d, parentChainBlock=%d", headState.DelayedMessagesSeen, headState.DelayedMessagesRead, headState.BatchCount, headState.MsgCount, headState.ParentChainBlockNumber) @@ -951,7 +1053,7 @@ func TestMELMigrationFromLegacyNode(t *testing.T) { forceBatchPost(t, ctx, builder) // Wait for MEL to process the new batch - postBatchState, err := melExtractor.GetHeadState() + postBatchState, err := msgExtractor.GetHeadState() Require(t, err) timeout := time.NewTimer(2 * time.Minute) defer timeout.Stop() @@ -960,7 +1062,7 @@ func TestMELMigrationFromLegacyNode(t *testing.T) { for postBatchState.BatchCount <= headState.BatchCount { select { case <-tick.C: - postBatchState, err = melExtractor.GetHeadState() + postBatchState, err = msgExtractor.GetHeadState() Require(t, err) case <-timeout.C: t.Fatalf("timed out waiting for MEL to process new batch. current batch count: %d, expected > %d", From 7b6029d05c592bb05c7960af55003ab338cffb11 Mon Sep 17 00:00:00 2001 From: ganeshvanahalli Date: Wed, 8 Apr 2026 15:34:38 +0530 Subject: [PATCH 2/9] cleanup melconfig event abi logic --- arbnode/mel/extraction/abis.go | 11 +++++--- arbnode/mel/extraction/mel_config_lookup.go | 30 +++------------------ 2 files changed, 11 insertions(+), 30 deletions(-) diff --git a/arbnode/mel/extraction/abis.go b/arbnode/mel/extraction/abis.go index c4de783a22e..c5d5127ccf1 100644 --- a/arbnode/mel/extraction/abis.go +++ b/arbnode/mel/extraction/abis.go @@ -5,9 +5,9 @@ package melextraction import ( "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" "github.com/offchainlabs/nitro/solgen/go/bridgegen" + "github.com/offchainlabs/nitro/solgen/go/rollupgen" ) var BatchDeliveredID common.Hash @@ -16,6 +16,7 @@ var InboxMessageFromOriginID common.Hash var MELConfigEventID common.Hash var SeqInboxABI *abi.ABI var IBridgeABI *abi.ABI +var RollupAdminABI *abi.ABI var iInboxABI *abi.ABI var iDelayedMessageProviderABI *abi.ABI @@ -48,6 +49,10 @@ func init() { } iInboxABI = parsedIInboxABI - // MELConfigEvent(address inbox, address sequencerInbox, uint256 melVersionActivationBlock) - MELConfigEventID = crypto.Keccak256Hash([]byte("MELConfigEvent(address,address,uint256)")) + parsedRollupAdminABI, err := rollupgen.RollupAdminLogicMetaData.GetAbi() + if err != nil { + panic(err) + } + RollupAdminABI = parsedRollupAdminABI + MELConfigEventID = parsedRollupAdminABI.Events["MELConfigEvent"].ID } diff --git a/arbnode/mel/extraction/mel_config_lookup.go b/arbnode/mel/extraction/mel_config_lookup.go index 3faba7e634e..d922c6b4034 100644 --- a/arbnode/mel/extraction/mel_config_lookup.go +++ b/arbnode/mel/extraction/mel_config_lookup.go @@ -4,37 +4,13 @@ package melextraction import ( "context" - "math/big" - "strings" - "github.com/ethereum/go-ethereum/accounts/abi" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/offchainlabs/nitro/arbnode/mel" + "github.com/offchainlabs/nitro/solgen/go/rollupgen" ) -// melConfigEventABI is a manually defined ABI for the MELConfigEvent. -// This can be replaced with rollupgen.RollupAdminLogicMetaData.GetAbi() -// once the Go bindings are regenerated from the updated Solidity contracts. -var melConfigEventABI *abi.ABI - -func init() { - const melConfigEventABIJSON = `[{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"inbox","type":"address"},{"indexed":false,"internalType":"address","name":"sequencerInbox","type":"address"},{"indexed":false,"internalType":"uint256","name":"melVersionActivationBlock","type":"uint256"}],"name":"MELConfigEvent","type":"event"}]` - parsed, err := abi.JSON(strings.NewReader(melConfigEventABIJSON)) - if err != nil { - panic(err) - } - melConfigEventABI = &parsed -} - -// melConfigEventFields holds the decoded fields from a MELConfigEvent log. -type melConfigEventFields struct { - Inbox common.Address - SequencerInbox common.Address - MelVersionActivationBlock *big.Int -} - // ParseMELConfigFromBlock scans the logs of the given parent chain block for // a MELConfigEvent. The log prefetcher already filters by rollup address, // so this function only needs to match the event topic. @@ -53,8 +29,8 @@ func ParseMELConfigFromBlock( if log == nil || len(log.Topics) == 0 || log.Topics[0] != MELConfigEventID { continue } - event := new(melConfigEventFields) - if err := eventUnpacker.UnpackLogTo(event, melConfigEventABI, "MELConfigEvent", *log); err != nil { + event := new(rollupgen.RollupAdminLogicMELConfigEvent) + if err := eventUnpacker.UnpackLogTo(event, RollupAdminABI, "MELConfigEvent", *log); err != nil { return nil, err } if !event.MelVersionActivationBlock.IsUint64() { From fabd482575580be2d3018164332857d9431d0f62 Mon Sep 17 00:00:00 2001 From: ganeshvanahalli Date: Thu, 9 Apr 2026 17:30:40 +0530 Subject: [PATCH 3/9] fix typos --- arbnode/mel/extraction/message_extraction_function.go | 4 ++-- arbnode/mel/runner/logs_and_headers_fetcher.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/arbnode/mel/extraction/message_extraction_function.go b/arbnode/mel/extraction/message_extraction_function.go index f471d6334b1..c2ff0810c0d 100644 --- a/arbnode/mel/extraction/message_extraction_function.go +++ b/arbnode/mel/extraction/message_extraction_function.go @@ -132,8 +132,8 @@ func extractMessagesImpl( return nil, nil, nil, nil, fmt.Errorf("failed to lookup MEL config event: %w", err) } if melConfig != nil { - if melConfig.VersionActivationBlock <= state.VersionActivationBlock { - return nil, nil, nil, nil, fmt.Errorf("current MEL state VersionActivationBlock: %d cannot be greater or equal to the next scheduled MEL config's VersionActivationBlock: %d", state.VersionActivationBlock, melConfig.VersionActivationBlock) + if state.VersionActivationBlock >= melConfig.VersionActivationBlock { + return nil, nil, nil, nil, fmt.Errorf("current MEL state VersionActivationBlock: %d cannot be greater than or equal to the next scheduled MEL config's VersionActivationBlock: %d", state.VersionActivationBlock, melConfig.VersionActivationBlock) } state.VersionActivationBlock = melConfig.VersionActivationBlock state.PendingInbox = melConfig.Inbox diff --git a/arbnode/mel/runner/logs_and_headers_fetcher.go b/arbnode/mel/runner/logs_and_headers_fetcher.go index c73b02d47bc..bae5be7071f 100644 --- a/arbnode/mel/runner/logs_and_headers_fetcher.go +++ b/arbnode/mel/runner/logs_and_headers_fetcher.go @@ -84,7 +84,7 @@ func (f *logsAndHeadersFetcher) fetch(ctx context.Context, preState *mel.State) fetchLogsErr = f.fetchDelayedMessageLogs(ctx, parentChainBlockNumber, toBlock, preState.DelayedMessagePostingTargetAddress) } if fetchLogsErr == nil && f.rollupAddr != (common.Address{}) { - fetchLogsErr = f.fetchRollupLogs(ctx, parentChainBlockNumber, toBlock) + fetchLogsErr = f.fetchMELConfigUpdateLogs(ctx, parentChainBlockNumber, toBlock) } wg.Done() }() @@ -170,7 +170,7 @@ func (f *logsAndHeadersFetcher) fetchDelayedMessageLogs(ctx context.Context, fro return conditionalFetch(nil, [][]common.Hash{{melextraction.InboxMessageDeliveredID, melextraction.InboxMessageFromOriginID}}) } -func (f *logsAndHeadersFetcher) fetchRollupLogs(ctx context.Context, from, to uint64) error { +func (f *logsAndHeadersFetcher) fetchMELConfigUpdateLogs(ctx context.Context, from, to uint64) error { query := ethereum.FilterQuery{ FromBlock: new(big.Int).SetUint64(from), ToBlock: new(big.Int).SetUint64(to), From db332f1421dcb06736102f6724cd6a23925e3d79 Mon Sep 17 00:00:00 2001 From: ganeshvanahalli Date: Thu, 9 Apr 2026 18:25:59 +0530 Subject: [PATCH 4/9] update submodule pin --- contracts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts b/contracts index 2905da0533d..546744c05e3 160000 --- a/contracts +++ b/contracts @@ -1 +1 @@ -Subproject commit 2905da0533d83d0e1aad80944b81225d89fdf117 +Subproject commit 546744c05e35c04d64aa5ef22f36dbb249642aac From ce93541f312470b50a5e1bef5ddb1f2874a89c0a Mon Sep 17 00:00:00 2001 From: ganeshvanahalli Date: Thu, 9 Apr 2026 18:28:01 +0530 Subject: [PATCH 5/9] fix lint error --- arbnode/mel/runner/legacy_db_reads_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arbnode/mel/runner/legacy_db_reads_test.go b/arbnode/mel/runner/legacy_db_reads_test.go index deddee21c35..d092690db61 100644 --- a/arbnode/mel/runner/legacy_db_reads_test.go +++ b/arbnode/mel/runner/legacy_db_reads_test.go @@ -24,6 +24,7 @@ import ( // --- Test helpers --- func makeDelayedL1Msg(index uint64) *arbostypes.L1IncomingMessage { + // #nosec G115 reqID := common.BigToHash(big.NewInt(int64(index + 1))) return &arbostypes.L1IncomingMessage{ Header: &arbostypes.L1IncomingMessageHeader{ @@ -321,6 +322,7 @@ func TestLegacyFindBatchCountAtBlock(t *testing.T) { setupBatches := func(t *testing.T, db ethdb.KeyValueStore, blocks []uint64) { t.Helper() for i, block := range blocks { + // #nosec G115 storeBatchMetadata(t, db, uint64(i), mel.BatchMetadata{ParentChainBlock: block}) } } From 2a0eb3cd9883708ba33cab9ac0c84aa81c8278d3 Mon Sep 17 00:00:00 2001 From: ganeshvanahalli Date: Fri, 10 Apr 2026 14:07:51 +0530 Subject: [PATCH 6/9] simplify mel config event --- arbnode/mel/extraction/mel_config_lookup.go | 8 +--- .../extraction/message_extraction_function.go | 41 ++++++------------- arbnode/mel/messages.go | 5 +-- arbnode/mel/state.go | 16 +------- contracts | 2 +- system_tests/message_extraction_layer_test.go | 35 +++++----------- 6 files changed, 29 insertions(+), 78 deletions(-) diff --git a/arbnode/mel/extraction/mel_config_lookup.go b/arbnode/mel/extraction/mel_config_lookup.go index d922c6b4034..77abfe9b7e1 100644 --- a/arbnode/mel/extraction/mel_config_lookup.go +++ b/arbnode/mel/extraction/mel_config_lookup.go @@ -33,13 +33,9 @@ func ParseMELConfigFromBlock( if err := eventUnpacker.UnpackLogTo(event, RollupAdminABI, "MELConfigEvent", *log); err != nil { return nil, err } - if !event.MelVersionActivationBlock.IsUint64() { - return nil, nil - } return &mel.MELConfig{ - Inbox: event.Inbox, - SequencerInbox: event.SequencerInbox, - VersionActivationBlock: event.MelVersionActivationBlock.Uint64(), + Inbox: event.Inbox, + SequencerInbox: event.SequencerInbox, }, nil } return nil, nil diff --git a/arbnode/mel/extraction/message_extraction_function.go b/arbnode/mel/extraction/message_extraction_function.go index c2ff0810c0d..580e5005003 100644 --- a/arbnode/mel/extraction/message_extraction_function.go +++ b/arbnode/mel/extraction/message_extraction_function.go @@ -121,25 +121,6 @@ func extractMessagesImpl( state.ParentChainBlockNumber = parentChainHeader.Number.Uint64() state.ParentChainPreviousBlockHash = parentChainHeader.ParentHash - // Check for MEL config events in this block. - melConfig, err := lookupMELConfig( - ctx, - parentChainHeader, - logsFetcher, - eventUnpacker, - ) - if err != nil { - return nil, nil, nil, nil, fmt.Errorf("failed to lookup MEL config event: %w", err) - } - if melConfig != nil { - if state.VersionActivationBlock >= melConfig.VersionActivationBlock { - return nil, nil, nil, nil, fmt.Errorf("current MEL state VersionActivationBlock: %d cannot be greater than or equal to the next scheduled MEL config's VersionActivationBlock: %d", state.VersionActivationBlock, melConfig.VersionActivationBlock) - } - state.VersionActivationBlock = melConfig.VersionActivationBlock - state.PendingInbox = melConfig.Inbox - state.PendingSequencerInbox = melConfig.SequencerInbox - } - // Now, check for any logs emitted by the sequencer inbox by txs // included in the parent chain block. batches, batchTxs, err := lookupBatches( @@ -289,16 +270,20 @@ func extractMessagesImpl( return nil, nil, nil, nil, fmt.Errorf("batch AfterDelayedCount: %d and MEL state DelayedMessagesRead: %d mismatch", batch.AfterDelayedCount, state.DelayedMessagesRead) } } - // Apply pending config if the activation block has been reached. - if state.ParentChainBlockNumber == state.VersionActivationBlock { - state.DelayedMessagePostingTargetAddress = state.PendingInbox - state.BatchPostingTargetAddress = state.PendingSequencerInbox + // Check for MEL config events in this block. + melConfig, err := lookupMELConfig( + ctx, + parentChainHeader, + logsFetcher, + eventUnpacker, + ) + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("failed to lookup MEL config event: %w", err) + } + if melConfig != nil { state.Version += 1 - // Reset PendingInbox and PendingSequencerInbox because having them non-zero after a version upgrade is incorrect, - // we don't reset the VersionActivationBlock though, to keep in the state when the current version was activated - state.PendingInbox = common.Address{} - state.PendingSequencerInbox = common.Address{} - + state.DelayedMessagePostingTargetAddress = melConfig.Inbox + state.BatchPostingTargetAddress = melConfig.SequencerInbox } return state, messages, delayedMessages, batchMetas, nil } diff --git a/arbnode/mel/messages.go b/arbnode/mel/messages.go index a0bf2da4d3d..21ac5e7b70c 100644 --- a/arbnode/mel/messages.go +++ b/arbnode/mel/messages.go @@ -86,7 +86,6 @@ type MessageSyncProgress struct { } type MELConfig struct { - Inbox common.Address - SequencerInbox common.Address - VersionActivationBlock uint64 + Inbox common.Address + SequencerInbox common.Address } diff --git a/arbnode/mel/state.go b/arbnode/mel/state.go index a52604696c0..eb1790646c9 100644 --- a/arbnode/mel/state.go +++ b/arbnode/mel/state.go @@ -31,9 +31,7 @@ func SplitPreimage(preimage []byte) (left, right common.Hash, err error) { // be deterministically constructed from any start state and parent chain blocks from // that point onwards. type State struct { - Version uint16 - VersionActivationBlock uint64 - + Version uint16 ParentChainId uint64 ParentChainBlockNumber uint64 BatchPostingTargetAddress common.Address @@ -48,11 +46,6 @@ type State struct { DelayedMessageInboxAcc common.Hash DelayedMessageOutboxAcc common.Hash - // Pending value changes to DelayedMessagePostingTargetAddress and - // BatchPostingTargetAddress from a MELConfig upgrade event - PendingInbox common.Address - PendingSequencerInbox common.Address - msgPreimagesDest daprovider.PreimagesMap delayedMsgPreimagesDest daprovider.PreimagesMap // delayedMsgPreimages is always populated during delayed message operations @@ -97,19 +90,14 @@ func (s *State) Clone() *State { parentChainPrevHash := common.Hash{} delayedInboxAcc := common.Hash{} delayedOutboxAcc := common.Hash{} - pendingInbox := common.Address{} - pendingSequencerInbox := common.Address{} copy(batchPostingTarget[:], s.BatchPostingTargetAddress[:]) copy(delayedMessageTarget[:], s.DelayedMessagePostingTargetAddress[:]) copy(parentChainHash[:], s.ParentChainBlockHash[:]) copy(parentChainPrevHash[:], s.ParentChainPreviousBlockHash[:]) copy(delayedInboxAcc[:], s.DelayedMessageInboxAcc[:]) copy(delayedOutboxAcc[:], s.DelayedMessageOutboxAcc[:]) - copy(pendingInbox[:], s.PendingInbox[:]) - copy(pendingSequencerInbox[:], s.PendingSequencerInbox[:]) return &State{ Version: s.Version, - VersionActivationBlock: s.VersionActivationBlock, ParentChainId: s.ParentChainId, ParentChainBlockNumber: s.ParentChainBlockNumber, BatchPostingTargetAddress: batchPostingTarget, @@ -122,8 +110,6 @@ func (s *State) Clone() *State { DelayedMessagesSeen: s.DelayedMessagesSeen, DelayedMessageInboxAcc: delayedInboxAcc, DelayedMessageOutboxAcc: delayedOutboxAcc, - PendingInbox: pendingInbox, - PendingSequencerInbox: pendingSequencerInbox, // LocalMsgAccumulator is intentionally not copied — each cloned state // starts a fresh hash chain for its own batch of accumulated messages. // diff --git a/contracts b/contracts index 546744c05e3..ddf171e6ab5 160000 --- a/contracts +++ b/contracts @@ -1 +1 @@ -Subproject commit 546744c05e35c04d64aa5ef22f36dbb249642aac +Subproject commit ddf171e6ab59ec41802ab3398e17aac29aeb8b38 diff --git a/system_tests/message_extraction_layer_test.go b/system_tests/message_extraction_layer_test.go index 4bb98392066..8982efa0c65 100644 --- a/system_tests/message_extraction_layer_test.go +++ b/system_tests/message_extraction_layer_test.go @@ -800,15 +800,10 @@ func TestMessageExtractionLayer_MELConfigEvent(t *testing.T) { newInbox := common.HexToAddress("0x0000000000000000000000000000000000001111") newSequencerInbox := common.HexToAddress("0x0000000000000000000000000000000000002222") - // Determine an activation block in the future - currentL1Block, err := builder.L1.Client.BlockNumber(ctx) - Require(t, err) - activationBlock := currentL1Block + 20 - // Pack the setMELConfig calldata using the generated rollup ABI rollupABI, err := abi.JSON(strings.NewReader(rollupgen.RollupAdminLogicABI)) Require(t, err) - calldata, err := rollupABI.Pack("setMELConfig", newInbox, newSequencerInbox, new(big.Int).SetUint64(activationBlock)) + calldata, err := rollupABI.Pack("setMELConfig", newInbox, newSequencerInbox) Require(t, err) deployAuth := builder.L1Info.GetDefaultTransactOpts("RollupOwner", ctx) @@ -816,15 +811,14 @@ func TestMessageExtractionLayer_MELConfigEvent(t *testing.T) { Require(t, err) tx, err := upgradeExecutor.ExecuteCall(&deployAuth, builder.addresses.Rollup, calldata) Require(t, err) - _, err = EnsureTxSucceeded(ctx, builder.L1.Client, tx) + receipt, err := EnsureTxSucceeded(ctx, builder.L1.Client, tx) Require(t, err) + eventBlock := receipt.BlockNumber.Uint64() - // Advance L1 past the activation block - // #nosec G115 - blocksToAdvance := int(activationBlock - currentL1Block + 5) - AdvanceL1(t, ctx, builder.L1.Client, builder.L1Info, blocksToAdvance) + // Advance L1 so MEL can process the block containing the event + AdvanceL1(t, ctx, builder.L1.Client, builder.L1Info, 5) - // Wait for MEL to process past the activation block + // Wait for MEL to process past the event block timeout := time.NewTimer(time.Minute) defer timeout.Stop() tick := time.NewTicker(200 * time.Millisecond) @@ -832,24 +826,21 @@ func TestMessageExtractionLayer_MELConfigEvent(t *testing.T) { for { headState, err := msgExtractor.GetHeadState() Require(t, err) - if headState.ParentChainBlockNumber >= activationBlock { + if headState.ParentChainBlockNumber >= eventBlock { break } select { case <-tick.C: case <-timeout.C: - t.Fatal("timed out waiting for MEL to process past activation block") + t.Fatal("timed out waiting for MEL to process past event block") } } - // Verify the config was applied + // Verify the config was applied immediately postConfigState, err := msgExtractor.GetHeadState() Require(t, err) if postConfigState.Version != 1 { - t.Fatalf("Expected version 1 after config activation, got %d", postConfigState.Version) - } - if postConfigState.VersionActivationBlock != activationBlock { - t.Fatalf("Expected VersionActivationBlock %d, got %d", activationBlock, postConfigState.VersionActivationBlock) + t.Fatalf("Expected version 1 after config event, got %d", postConfigState.Version) } if postConfigState.BatchPostingTargetAddress != newSequencerInbox { t.Fatalf("Expected BatchPostingTargetAddress %s, got %s", newSequencerInbox.Hex(), postConfigState.BatchPostingTargetAddress.Hex()) @@ -857,12 +848,6 @@ func TestMessageExtractionLayer_MELConfigEvent(t *testing.T) { if postConfigState.DelayedMessagePostingTargetAddress != newInbox { t.Fatalf("Expected DelayedMessagePostingTargetAddress %s, got %s", newInbox.Hex(), postConfigState.DelayedMessagePostingTargetAddress.Hex()) } - if postConfigState.PendingInbox != (common.Address{}) { - t.Fatalf("Expected PendingInbox to be zero after activation, got %s", postConfigState.PendingInbox.Hex()) - } - if postConfigState.PendingSequencerInbox != (common.Address{}) { - t.Fatalf("Expected PendingSequencerInbox to be zero after activation, got %s", postConfigState.PendingSequencerInbox.Hex()) - } } // TestMELMigrationFromLegacyNode verifies that a node previously running with From 9328dfed7c4c08ce77b3eeadd168b1248bf18c79 Mon Sep 17 00:00:00 2001 From: ganeshvanahalli Date: Fri, 10 Apr 2026 17:56:07 +0530 Subject: [PATCH 7/9] activate mel consensus during the first version upgrade --- .../extraction/message_extraction_function.go | 24 +++++++++++++++++++ changelog/ganeshvanahalli-nit-4779.md | 2 -- .../ganeshvanahalli-nit-4779_and_4720.md | 3 +++ 3 files changed, 27 insertions(+), 2 deletions(-) delete mode 100644 changelog/ganeshvanahalli-nit-4779.md create mode 100644 changelog/ganeshvanahalli-nit-4779_and_4720.md diff --git a/arbnode/mel/extraction/message_extraction_function.go b/arbnode/mel/extraction/message_extraction_function.go index 580e5005003..4e2dbf7aac0 100644 --- a/arbnode/mel/extraction/message_extraction_function.go +++ b/arbnode/mel/extraction/message_extraction_function.go @@ -281,6 +281,30 @@ func extractMessagesImpl( return nil, nil, nil, nil, fmt.Errorf("failed to lookup MEL config event: %w", err) } if melConfig != nil { + // MEL consensus getting activated for the first time + if state.Version == 0 { + var unreadDelayedMsgs []*mel.DelayedInboxMessage + for i := state.DelayedMessagesRead; i < state.DelayedMessagesSeen; i++ { + delayedMsg, err := delayedMsgDatabase.ReadDelayedMessage(state, i) + if err != nil { + return nil, nil, nil, nil, fmt.Errorf("failed creating delayed msg accumulators during MEL consensus activation: %w", err) + } + unreadDelayedMsgs = append(unreadDelayedMsgs, delayedMsg) + } + // Both the accumulators must be now empty + if state.DelayedMessageInboxAcc != (common.Hash{}) || state.DelayedMessageOutboxAcc != (common.Hash{}) { + return nil, nil, nil, nil, fmt.Errorf( + "one of DelayedMessageInboxAcc: %v and DelayedMessageOutboxAcc: %v is non zero after reading all delayed msgs for MEL activation", + state.DelayedMessageInboxAcc, + state.DelayedMessageOutboxAcc, + ) + } + for _, delayedMsg := range unreadDelayedMsgs { + if err := state.AccumulateDelayedMessage(delayedMsg); err != nil { + return nil, nil, nil, nil, fmt.Errorf("failed creating delayed msg accumulators during MEL consensus activation: %w", err) + } + } + } state.Version += 1 state.DelayedMessagePostingTargetAddress = melConfig.Inbox state.BatchPostingTargetAddress = melConfig.SequencerInbox diff --git a/changelog/ganeshvanahalli-nit-4779.md b/changelog/ganeshvanahalli-nit-4779.md deleted file mode 100644 index 294ee7509d0..00000000000 --- a/changelog/ganeshvanahalli-nit-4779.md +++ /dev/null @@ -1,2 +0,0 @@ -### Added - - Make message extractor able to pick up MEL config upgrade events \ No newline at end of file diff --git a/changelog/ganeshvanahalli-nit-4779_and_4720.md b/changelog/ganeshvanahalli-nit-4779_and_4720.md new file mode 100644 index 00000000000..66a63b414a7 --- /dev/null +++ b/changelog/ganeshvanahalli-nit-4779_and_4720.md @@ -0,0 +1,3 @@ +### Added + - Make message extractor able to pick up MEL config upgrade events + - Handle transitioning of node consensus to MEL at MEL activated parent chain block number \ No newline at end of file From 862a56727ff4bf6d857b5f5a5dbd71e8fa756a97 Mon Sep 17 00:00:00 2001 From: ganeshvanahalli Date: Mon, 13 Apr 2026 19:08:36 +0530 Subject: [PATCH 8/9] add unit tests --- .../extraction/message_extraction_function.go | 47 ++++---- .../message_extraction_function_test.go | 104 ++++++++++++++++++ 2 files changed, 131 insertions(+), 20 deletions(-) diff --git a/arbnode/mel/extraction/message_extraction_function.go b/arbnode/mel/extraction/message_extraction_function.go index 4e2dbf7aac0..1cf5b05cb04 100644 --- a/arbnode/mel/extraction/message_extraction_function.go +++ b/arbnode/mel/extraction/message_extraction_function.go @@ -283,26 +283,8 @@ func extractMessagesImpl( if melConfig != nil { // MEL consensus getting activated for the first time if state.Version == 0 { - var unreadDelayedMsgs []*mel.DelayedInboxMessage - for i := state.DelayedMessagesRead; i < state.DelayedMessagesSeen; i++ { - delayedMsg, err := delayedMsgDatabase.ReadDelayedMessage(state, i) - if err != nil { - return nil, nil, nil, nil, fmt.Errorf("failed creating delayed msg accumulators during MEL consensus activation: %w", err) - } - unreadDelayedMsgs = append(unreadDelayedMsgs, delayedMsg) - } - // Both the accumulators must be now empty - if state.DelayedMessageInboxAcc != (common.Hash{}) || state.DelayedMessageOutboxAcc != (common.Hash{}) { - return nil, nil, nil, nil, fmt.Errorf( - "one of DelayedMessageInboxAcc: %v and DelayedMessageOutboxAcc: %v is non zero after reading all delayed msgs for MEL activation", - state.DelayedMessageInboxAcc, - state.DelayedMessageOutboxAcc, - ) - } - for _, delayedMsg := range unreadDelayedMsgs { - if err := state.AccumulateDelayedMessage(delayedMsg); err != nil { - return nil, nil, nil, nil, fmt.Errorf("failed creating delayed msg accumulators during MEL consensus activation: %w", err) - } + if err := moveUnreadDelayedMessagesToInboxAcc(state, delayedMsgDatabase); err != nil { + return nil, nil, nil, nil, err } } state.Version += 1 @@ -311,3 +293,28 @@ func extractMessagesImpl( } return state, messages, delayedMessages, batchMetas, nil } + +func moveUnreadDelayedMessagesToInboxAcc(state *mel.State, delayedMsgDatabase DelayedMessageDatabase) error { + var unreadDelayedMsgs []*mel.DelayedInboxMessage + for i := state.DelayedMessagesRead; i < state.DelayedMessagesSeen; i++ { + delayedMsg, err := delayedMsgDatabase.ReadDelayedMessage(state, i) + if err != nil { + return fmt.Errorf("failed creating delayed msg accumulators during MEL consensus activation: %w", err) + } + unreadDelayedMsgs = append(unreadDelayedMsgs, delayedMsg) + } + // Both the accumulators must be now empty + if state.DelayedMessageInboxAcc != (common.Hash{}) || state.DelayedMessageOutboxAcc != (common.Hash{}) { + return fmt.Errorf( + "one of DelayedMessageInboxAcc: %v and DelayedMessageOutboxAcc: %v is non zero after reading all delayed msgs for MEL activation", + state.DelayedMessageInboxAcc, + state.DelayedMessageOutboxAcc, + ) + } + for _, delayedMsg := range unreadDelayedMsgs { + if err := state.AccumulateDelayedMessage(delayedMsg); err != nil { + return fmt.Errorf("failed creating delayed msg accumulators during MEL consensus activation: %w", err) + } + } + return nil +} diff --git a/arbnode/mel/extraction/message_extraction_function_test.go b/arbnode/mel/extraction/message_extraction_function_test.go index 0024ef3891e..f33bad51ca0 100644 --- a/arbnode/mel/extraction/message_extraction_function_test.go +++ b/arbnode/mel/extraction/message_extraction_function_test.go @@ -464,3 +464,107 @@ func parseReportForSecondBatch( ) (*big.Int, common.Address, common.Hash, uint64, *big.Int, uint64, error) { return nil, common.Address{}, crypto.Keccak256Hash([]byte("batch1")), 0, nil, 0, nil } + +// makeDelayedMsg builds a deterministic DelayedInboxMessage for tests. +func makeDelayedMsg(i uint64) *mel.DelayedInboxMessage { + reqID := common.BigToHash(big.NewInt(int64(i))) + return &mel.DelayedInboxMessage{ + Message: &arbostypes.L1IncomingMessage{ + Header: &arbostypes.L1IncomingMessageHeader{ + RequestId: &reqID, + }, + L2msg: []byte{byte(i)}, + }, + } +} + +func TestMoveUnreadDelayedMessagesToInboxAcc(t *testing.T) { + t.Run("happy path rebuilds inbox accumulator", func(t *testing.T) { + state := &mel.State{ + DelayedMessagesRead: 2, + DelayedMessagesSeen: 5, + } + mockDB := &mockDelayedMessageDB{ + DelayedMessages: map[uint64]*mel.DelayedInboxMessage{ + 2: makeDelayedMsg(2), + 3: makeDelayedMsg(3), + 4: makeDelayedMsg(4), + }, + } + require.NoError(t, moveUnreadDelayedMessagesToInboxAcc(state, mockDB)) + + // Build the expected accumulator independently by running the same + // sequence of AccumulateDelayedMessage calls on a fresh state. + expected := &mel.State{ + DelayedMessagesRead: 2, + DelayedMessagesSeen: 5, + } + for i := uint64(2); i < 5; i++ { + require.NoError(t, expected.AccumulateDelayedMessage(makeDelayedMsg(i))) + } + require.Equal(t, expected.DelayedMessageInboxAcc, state.DelayedMessageInboxAcc) + require.NotEqual(t, common.Hash{}, state.DelayedMessageInboxAcc) + // Outbox is not touched by AccumulateDelayedMessage. + require.Equal(t, common.Hash{}, state.DelayedMessageOutboxAcc) + }) + + t.Run("no unread messages leaves state unchanged", func(t *testing.T) { + state := &mel.State{ + DelayedMessagesRead: 3, + DelayedMessagesSeen: 3, + } + mockDB := &mockDelayedMessageDB{ + DelayedMessages: map[uint64]*mel.DelayedInboxMessage{}, + } + require.NoError(t, moveUnreadDelayedMessagesToInboxAcc(state, mockDB)) + require.Equal(t, common.Hash{}, state.DelayedMessageInboxAcc) + require.Equal(t, common.Hash{}, state.DelayedMessageOutboxAcc) + }) + + t.Run("errors when inbox accumulator is non-zero", func(t *testing.T) { + preExisting := common.HexToHash("0xdeadbeef") + state := &mel.State{ + DelayedMessagesRead: 0, + DelayedMessagesSeen: 1, + DelayedMessageInboxAcc: preExisting, + } + mockDB := &mockDelayedMessageDB{ + DelayedMessages: map[uint64]*mel.DelayedInboxMessage{ + 0: makeDelayedMsg(0), + }, + } + err := moveUnreadDelayedMessagesToInboxAcc(state, mockDB) + require.ErrorContains(t, err, "non zero") + // State must be unchanged on error. + require.Equal(t, preExisting, state.DelayedMessageInboxAcc) + }) + + t.Run("errors when outbox accumulator is non-zero", func(t *testing.T) { + preExisting := common.HexToHash("0xfeedface") + state := &mel.State{ + DelayedMessagesRead: 0, + DelayedMessagesSeen: 1, + DelayedMessageOutboxAcc: preExisting, + } + mockDB := &mockDelayedMessageDB{ + DelayedMessages: map[uint64]*mel.DelayedInboxMessage{ + 0: makeDelayedMsg(0), + }, + } + err := moveUnreadDelayedMessagesToInboxAcc(state, mockDB) + require.ErrorContains(t, err, "non zero") + require.Equal(t, preExisting, state.DelayedMessageOutboxAcc) + }) + + t.Run("propagates DB read errors", func(t *testing.T) { + state := &mel.State{ + DelayedMessagesRead: 0, + DelayedMessagesSeen: 2, + } + dbErr := errors.New("db read boom") + mockDB := &mockDelayedMessageDB{err: dbErr} + err := moveUnreadDelayedMessagesToInboxAcc(state, mockDB) + require.ErrorIs(t, err, dbErr) + require.ErrorContains(t, err, "failed creating delayed msg accumulators") + }) +} From 7a7de7a39a861d0f47a1f0d540e33d09c1356bef Mon Sep 17 00:00:00 2001 From: ganeshvanahalli Date: Tue, 14 Apr 2026 12:56:39 +0530 Subject: [PATCH 9/9] fix lint error --- arbnode/mel/extraction/message_extraction_function_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arbnode/mel/extraction/message_extraction_function_test.go b/arbnode/mel/extraction/message_extraction_function_test.go index f33bad51ca0..90da4a6b4fe 100644 --- a/arbnode/mel/extraction/message_extraction_function_test.go +++ b/arbnode/mel/extraction/message_extraction_function_test.go @@ -467,7 +467,7 @@ func parseReportForSecondBatch( // makeDelayedMsg builds a deterministic DelayedInboxMessage for tests. func makeDelayedMsg(i uint64) *mel.DelayedInboxMessage { - reqID := common.BigToHash(big.NewInt(int64(i))) + reqID := common.BigToHash(big.NewInt(int64(i))) // #nosec G115 return &mel.DelayedInboxMessage{ Message: &arbostypes.L1IncomingMessage{ Header: &arbostypes.L1IncomingMessageHeader{