diff --git a/arbnode/mel/extraction/abis.go b/arbnode/mel/extraction/abis.go index 65aacf8d2d3..a3e13b26e06 100644 --- a/arbnode/mel/extraction/abis.go +++ b/arbnode/mel/extraction/abis.go @@ -7,13 +7,16 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/offchainlabs/nitro/solgen/go/bridgegen" + "github.com/offchainlabs/nitro/solgen/go/rollupgen" ) 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 RollupAdminABI *abi.ABI var iInboxABI *abi.ABI var iDelayedMessageProviderABI *abi.ABI @@ -45,4 +48,11 @@ func init() { panic(err) } iInboxABI = parsedIInboxABI + + parsedRollupAdminABI, err := rollupgen.RollupAdminLogicMetaData.GetAbi() + if err != nil { + panic(err) + } + RollupAdminABI = parsedRollupAdminABI + MELConfigEventID = parsedRollupAdminABI.Events["MELConfigSet"].ID } diff --git a/arbnode/mel/extraction/mel_config_lookup.go b/arbnode/mel/extraction/mel_config_lookup.go new file mode 100644 index 00000000000..6c26ad13744 --- /dev/null +++ b/arbnode/mel/extraction/mel_config_lookup.go @@ -0,0 +1,44 @@ +// Copyright 2026, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md +package melextraction + +import ( + "context" + + "github.com/ethereum/go-ethereum/core/types" + + "github.com/offchainlabs/nitro/arbnode/mel" + "github.com/offchainlabs/nitro/solgen/go/rollupgen" +) + +// 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(rollupgen.RollupAdminLogicMELConfigSet) + if err := eventUnpacker.UnpackLogTo(event, RollupAdminABI, "MELConfigSet", *log); err != nil { + return nil, err + } + return &mel.MELConfig{ + MelVersion: event.MelVersion, + Inbox: event.Inbox, + SequencerInbox: event.SequencerInbox, + ActivationBlock: event.ActivationBlock, + }, nil + } + return nil, nil +} diff --git a/arbnode/mel/extraction/message_extraction_function.go b/arbnode/mel/extraction/message_extraction_function.go index 4c3064ec1a0..a5c523bb52d 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,7 @@ func extractMessagesImpl( state.ParentChainBlockHash = parentChainHeader.Hash() state.ParentChainBlockNumber = parentChainHeader.Number.Uint64() state.ParentChainPreviousBlockHash = parentChainHeader.ParentHash + // Now, check for any logs emitted by the sequencer inbox by txs // included in the parent chain block. batches, batchTxs, err := lookupBatches( @@ -267,5 +270,59 @@ func extractMessagesImpl( return nil, nil, nil, nil, fmt.Errorf("batch AfterDelayedCount: %d and MEL state DelayedMessagesRead: %d mismatch", batch.AfterDelayedCount, state.DelayedMessagesRead) } } + // 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 { + // Sanity check: the contract sets activationBlock = block.number at emission time, + // so the event must be observed in the same parent chain block it was emitted. + if melConfig.ActivationBlock != parentChainHeader.Number.Uint64() { + return nil, nil, nil, nil, fmt.Errorf( + "MELConfigSet activation block %d does not match current parent chain block %d", + melConfig.ActivationBlock, parentChainHeader.Number.Uint64(), + ) + } + // MEL consensus getting activated for the first time + if state.Version == 0 { + if err := moveUnreadDelayedMessagesToInboxAcc(state, delayedMsgDatabase); err != nil { + return nil, nil, nil, nil, err + } + } + state.Version = melConfig.MelVersion + state.DelayedMessagePostingTargetAddress = melConfig.Inbox + state.BatchPostingTargetAddress = melConfig.SequencerInbox + } 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 14dc1b7a47c..90da4a6b4fe 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, ) } @@ -458,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))) // #nosec G115 + 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") + }) +} 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..3db9d2a6df3 100644 --- a/arbnode/mel/messages.go +++ b/arbnode/mel/messages.go @@ -84,3 +84,10 @@ type MessageSyncProgress struct { BatchProcessed uint64 MsgCount arbutil.MessageIndex } + +type MELConfig struct { + MelVersion uint16 + Inbox common.Address + SequencerInbox common.Address + ActivationBlock 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..bae5be7071f 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.fetchMELConfigUpdateLogs(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) fetchMELConfigUpdateLogs(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 cd0edb6606d..0cbcc513f40 100644 --- a/arbnode/mel/runner/mel.go +++ b/arbnode/mel/runner/mel.go @@ -457,7 +457,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/node.go b/arbnode/node.go index 8d4dfc44be4..15b0743eb35 100644 --- a/arbnode/node.go +++ b/arbnode/node.go @@ -119,6 +119,9 @@ func (c *Config) Validate() error { if err := c.MELValidator.Validate(); err != nil { return err } + if c.MELValidator.Enable && !c.MessageExtraction.Enable { + return errors.New("cannot enable mel-validator without enabling message-extraction") + } if err := c.MessageExtraction.Validate(); err != nil { return err } diff --git a/bold/protocol/sol/assertion_chain.go b/bold/protocol/sol/assertion_chain.go index 341cee608a8..8542429ed7c 100644 --- a/bold/protocol/sol/assertion_chain.go +++ b/bold/protocol/sol/assertion_chain.go @@ -555,7 +555,7 @@ func (a *AssertionChain) NewStakeOnNewAssertion( assertionInputs rollupgen.AssertionInputs, expectedAssertionHash [32]byte, ) (*types.Transaction, error) { - return a.userLogic.NewStakeOnNewAssertion50f32f68( + return a.userLogic.NewStakeOnNewAssertion611c3d80( opts, tokenAmount, assertionInputs, @@ -621,11 +621,11 @@ func (a *AssertionChain) createAndStakeOnAssertion( if err != nil { return nil, ErrBatchNotYetFound } + _ = inboxBatchAcc // PR 427: inboxAcc no longer used in ComputeAssertionHash computedHash, err := a.userLogic.ComputeAssertionHash( a.GetCallOptsWithDesiredRpcHeadBlockNumber(&bind.CallOpts{Context: ctx}), parentAssertionCreationInfo.AssertionHash.Hash, postState.AsSolidityStruct(), - inboxBatchAcc, ) if err != nil { return nil, errors.Wrap(err, "could not compute assertion hash") @@ -656,12 +656,13 @@ func (a *AssertionChain) createAndStakeOnAssertion( BeforeStateData: rollupgen.BeforeStateData{ PrevPrevAssertionHash: parentAssertionCreationInfo.ParentAssertionHash.Hash, SequencerBatchAcc: parentAssertionCreationInfo.AfterInboxBatchAcc, + // PR 427: ConfigData.NextInboxPosition replaced by NextParentChainBlockHash. + // Left zero — assertion logic wiring will be completed in a follow-up PR. ConfigData: rollupgen.ConfigData{ RequiredStake: parentAssertionCreationInfo.RequiredStake, ChallengeManager: parentAssertionCreationInfo.ChallengeManager, ConfirmPeriodBlocks: parentAssertionCreationInfo.ConfirmPeriodBlocks, WasmModuleRoot: parentAssertionCreationInfo.WasmModuleRoot, - NextInboxPosition: parentAssertionCreationInfo.InboxMaxCount.Uint64(), }, }, BeforeState: parentAssertionCreationInfo.AfterState, @@ -845,6 +846,8 @@ func (a *AssertionChain) ConfirmAssertionByChallengeWinner( return errors.New("assertion prev creation info inbox max count was not a uint64") } receipt, err := a.transact(ctx, a.backend, func(opts *bind.TransactOpts) (*types.Transaction, error) { + // PR 427: ConfirmAssertion dropped inboxAcc arg; ConfigData.NextInboxPosition + // replaced by NextParentChainBlockHash. Left zero pending follow-up PR. return a.userLogic.ConfirmAssertion( opts, b, @@ -856,9 +859,7 @@ func (a *AssertionChain) ConfirmAssertionByChallengeWinner( ConfirmPeriodBlocks: prevCreationInfo.ConfirmPeriodBlocks, RequiredStake: prevCreationInfo.RequiredStake, ChallengeManager: prevCreationInfo.ChallengeManager, - NextInboxPosition: prevCreationInfo.InboxMaxCount.Uint64(), }, - creationInfo.AfterInboxBatchAcc, ) }) if err != nil { @@ -879,12 +880,12 @@ func (a *AssertionChain) FastConfirmAssertion( return a.fastConfirmSafe.fastConfirmAssertion(ctx, assertionCreationInfo) } receipt, err := a.transact(ctx, a.backend, func(opts *bind.TransactOpts) (*types.Transaction, error) { + // PR 427: FastConfirmAssertion dropped inboxAcc arg. return a.userLogic.FastConfirmAssertion( opts, assertionCreationInfo.AssertionHash.Hash, assertionCreationInfo.ParentAssertionHash.Hash, assertionCreationInfo.AfterState, - assertionCreationInfo.AfterInboxBatchAcc, ) }) if err != nil { @@ -1077,8 +1078,10 @@ func (a *AssertionChain) ReadAssertionCreationInfo( ParentAssertionHash: protocol.AssertionHash{Hash: parsedLog.ParentAssertionHash}, BeforeState: parsedLog.Assertion.BeforeState, AfterState: afterState, - InboxMaxCount: parsedLog.InboxMaxCount, - AfterInboxBatchAcc: parsedLog.AfterInboxBatchAcc, + // PR 427: InboxMaxCount / AfterInboxBatchAcc dropped from AssertionCreated event. + // Left zero pending follow-up PR that rewires against NextParentChainBlockHash. + InboxMaxCount: big.NewInt(0), + AfterInboxBatchAcc: common.Hash{}, AssertionHash: protocol.AssertionHash{Hash: parsedLog.AssertionHash}, WasmModuleRoot: parsedLog.WasmModuleRoot, ChallengeManager: parsedLog.ChallengeManager, diff --git a/bold/protocol/sol/edge_challenge_manager.go b/bold/protocol/sol/edge_challenge_manager.go index c617aef35b3..d14ab0a8e30 100644 --- a/bold/protocol/sol/edge_challenge_manager.go +++ b/bold/protocol/sol/edge_challenge_manager.go @@ -831,12 +831,13 @@ func (cm *specChallengeManager) ConfirmEdgeByOneStepProof( BeforeHash: oneStepData.BeforeHash, Proof: oneStepData.Proof, }, + // PR 427: ConfigData.NextInboxPosition replaced by NextParentChainBlockHash. + // Left zero pending follow-up PR. challengeV2gen.ConfigData{ WasmModuleRoot: creationInfo.WasmModuleRoot, RequiredStake: creationInfo.RequiredStake, ChallengeManager: creationInfo.ChallengeManager, ConfirmPeriodBlocks: creationInfo.ConfirmPeriodBlocks, - NextInboxPosition: creationInfo.InboxMaxCount.Uint64(), }, pre, post, 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 diff --git a/cmd/nitro/init/init.go b/cmd/nitro/init/init.go index deb1285ba11..c74cb78e7a6 100644 --- a/cmd/nitro/init/init.go +++ b/cmd/nitro/init/init.go @@ -1135,7 +1135,7 @@ func getGenesisAssertionCreationInfo(ctx context.Context, rollupAddress common.A EndHistoryRoot: [32]byte{}, } - assertionHash, err = userLogic.ComputeAssertionHash(&bind.CallOpts{Context: ctx}, common.Hash{}, genesisAssertionState, common.Hash{}) + assertionHash, err = userLogic.ComputeAssertionHash(&bind.CallOpts{Context: ctx}, common.Hash{}, genesisAssertionState) if err != nil { return nil, assertionHash, false, err } diff --git a/contracts b/contracts index 4341b132cfb..6ef1a11357e 160000 --- a/contracts +++ b/contracts @@ -1 +1 @@ -Subproject commit 4341b132cfbdcc980ead03765ca5224ff6cb5d97 +Subproject commit 6ef1a11357ea4214fce9b03666141a5ef2176c51 diff --git a/staker/bold_assertioncreation.go b/staker/bold_assertioncreation.go index a4739daaa59..16823763b56 100644 --- a/staker/bold_assertioncreation.go +++ b/staker/bold_assertioncreation.go @@ -93,8 +93,10 @@ func ReadBoldAssertionCreationInfo( ParentAssertionHash: protocol.AssertionHash{Hash: parsedLog.ParentAssertionHash}, BeforeState: parsedLog.Assertion.BeforeState, AfterState: afterState, - InboxMaxCount: parsedLog.InboxMaxCount, - AfterInboxBatchAcc: parsedLog.AfterInboxBatchAcc, + // PR 427: InboxMaxCount / AfterInboxBatchAcc dropped from AssertionCreated event. + // Left zero pending follow-up PR that rewires against NextParentChainBlockHash. + InboxMaxCount: big.NewInt(0), + AfterInboxBatchAcc: common.Hash{}, AssertionHash: protocol.AssertionHash{Hash: parsedLog.AssertionHash}, WasmModuleRoot: parsedLog.WasmModuleRoot, ChallengeManager: parsedLog.ChallengeManager, diff --git a/system_tests/message_extraction_layer_test.go b/system_tests/message_extraction_layer_test.go index 39261101d49..845f5988a24 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" "github.com/offchainlabs/nitro/util/headerreader" "github.com/offchainlabs/nitro/util/testhelpers" @@ -763,6 +766,94 @@ 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") + + // Pack the setMELConfig calldata using the generated rollup ABI. + // PR 427 signature: setMELConfig(uint16 _melVersion, address _inbox, address _sequencerInbox) + // Use a non-trivial version (42) to explicitly check that MEL reads + // the version from the event rather than, say, incrementing from 0. + rollupABI, err := abi.JSON(strings.NewReader(rollupgen.RollupAdminLogicABI)) + Require(t, err) + melVersion := uint16(42) + calldata, err := rollupABI.Pack("setMELConfig", melVersion, newInbox, newSequencerInbox) + 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) + receipt, err := EnsureTxSucceeded(ctx, builder.L1.Client, tx) + Require(t, err) + eventBlock := receipt.BlockNumber.Uint64() + + // 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 event 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 >= eventBlock { + break + } + select { + case <-tick.C: + case <-timeout.C: + t.Fatal("timed out waiting for MEL to process past event block") + } + } + + // Verify the config was applied immediately + postConfigState, err := msgExtractor.GetHeadState() + Require(t, err) + if postConfigState.Version != melVersion { + t.Fatalf("Expected version %d after config event, got %d", melVersion, postConfigState.Version) + } + 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()) + } +} + // TestMELMigrationFromLegacyNode verifies that a node previously running with // the legacy inbox reader/tracker can be seamlessly migrated to MEL. // @@ -912,8 +1003,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) @@ -986,7 +1077,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() @@ -995,7 +1086,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", diff --git a/system_tests/message_extraction_layer_validation_test.go b/system_tests/message_extraction_layer_validation_test.go index 4e662c495fa..1f0a2c7c37b 100644 --- a/system_tests/message_extraction_layer_validation_test.go +++ b/system_tests/message_extraction_layer_validation_test.go @@ -31,6 +31,7 @@ func testValidationPostMEL(t *testing.T, useJit bool) { builder.nodeConfig.BatchPoster.IgnoreBlobPrice = true builder.nodeConfig.BatchPoster.MaxDelay = time.Hour // set high max-delay so we can test the delay buffer builder.nodeConfig.BatchPoster.PollInterval = time.Hour // set a high poll interval to avoid continuous polling + builder.nodeConfig.MessageExtraction.Enable = true builder.nodeConfig.MELValidator.Enable = true builder.nodeConfig.BlockValidator.Enable = true builder.nodeConfig.BlockValidator.EnableMEL = true