diff --git a/arbnode/node.go b/arbnode/node.go index 15b0743eb35..40391be5dff 100644 --- a/arbnode/node.go +++ b/arbnode/node.go @@ -1083,6 +1083,7 @@ func getStaker( blockValidator *staker.BlockValidator, dapRegistry *daprovider.DAProviderRegistry, messageExtractor *melrunner.MessageExtractor, + melValidator *staker.MELValidator, ) (*multiprotocolstaker.MultiProtocolStaker, *MessagePruner, common.Address, error) { var stakerObj *multiprotocolstaker.MultiProtocolStaker var messagePruner *MessagePruner @@ -1153,7 +1154,7 @@ func getStaker( if tracker == nil || reader == nil { return nil, nil, common.Address{}, errors.New("staker requires either message extractor or inbox tracker/reader") } - stakerObj, err = multiprotocolstaker.NewMultiProtocolStaker(stack, l1Reader, wallet, bind.CallOpts{}, func() *legacystaker.L1ValidatorConfig { return &configFetcher.Get().Staker }, &configFetcher.Get().Bold, blockValidator, statelessBlockValidator, nil, deployInfo.StakeToken, deployInfo.Rollup, confirmedNotifiers, deployInfo.ValidatorUtils, deployInfo.Bridge, txStreamer, tracker, reader, dapRegistry, fatalErrChan) + stakerObj, err = multiprotocolstaker.NewMultiProtocolStaker(stack, l1Reader, wallet, bind.CallOpts{}, func() *legacystaker.L1ValidatorConfig { return &configFetcher.Get().Staker }, &configFetcher.Get().Bold, blockValidator, statelessBlockValidator, nil, deployInfo.StakeToken, deployInfo.Rollup, confirmedNotifiers, deployInfo.ValidatorUtils, deployInfo.Bridge, txStreamer, tracker, reader, dapRegistry, fatalErrChan, melValidator) if err != nil { return nil, nil, common.Address{}, err } @@ -1551,7 +1552,7 @@ func createNodeImpl( batchMetaFetcher = messageExtractor } - stakerObj, messagePruner, stakerAddr, err := getStaker(ctx, config, configFetcher, consensusDB, l1Reader, txOptsValidator, syncMonitor, parentChain, l1client, deployInfo, txStreamer, validatorInboxReader, validatorInboxTracker, batchMetaFetcher, stack, fatalErrChan, statelessBlockValidator, blockValidator, dapRegistry, messageExtractor) + stakerObj, messagePruner, stakerAddr, err := getStaker(ctx, config, configFetcher, consensusDB, l1Reader, txOptsValidator, syncMonitor, parentChain, l1client, deployInfo, txStreamer, validatorInboxReader, validatorInboxTracker, batchMetaFetcher, stack, fatalErrChan, statelessBlockValidator, blockValidator, dapRegistry, messageExtractor, melValidator) if err != nil { return nil, err } diff --git a/bold/assertions/manager.go b/bold/assertions/manager.go index 8d162df18c9..4539c61e253 100644 --- a/bold/assertions/manager.go +++ b/bold/assertions/manager.go @@ -73,6 +73,7 @@ type Manager struct { chain protocol.AssertionChain backend protocol.ChainBackend execProvider state.ExecutionProvider + melLookup state.ValidatedMELStateLookup // nil when MEL is not active times timings rollupAddr common.Address validatorName string @@ -111,6 +112,15 @@ func WithPostingDisabled() Opt { } } +// WithMELStateLookup enables MEL-based assertion determinism. When set, +// assertions use NextParentChainBlockHash instead of InboxMaxCount. +func WithMELStateLookup(lookup state.ValidatedMELStateLookup) Opt { + return func(m *Manager) { + m.melLookup = lookup + } +} + + func WithFastConfirmation() Opt { return func(m *Manager) { m.enableFastConfirmation = true @@ -383,7 +393,28 @@ func (m *Manager) checkLatestDesiredBlock(ctx context.Context) { func (m *Manager) ExecutionStateAfterParent(ctx context.Context, parentInfo *protocol.AssertionCreatedInfo) (*protocol.ExecutionState, error) { goGlobalState := protocol.GoGlobalStateFromSolidity(parentInfo.AfterState.GlobalState) - return m.execProvider.ExecutionStateAfterPreviousState(ctx, parentInfo.InboxMaxCount.Uint64(), goGlobalState) + batchCount, err := m.batchCountFromAssertion(ctx, parentInfo) + if err != nil { + return nil, err + } + return m.execProvider.ExecutionStateAfterPreviousState(ctx, batchCount, goGlobalState) +} + +// batchCountFromAssertion determines the batch count for the next assertion. +// Post-MEL (melLookup != nil): derives it from NextParentChainBlockHash via validated MEL state. +// Pre-MEL: uses InboxMaxCount directly. +func (m *Manager) batchCountFromAssertion(ctx context.Context, info *protocol.AssertionCreatedInfo) (uint64, error) { + if m.melLookup != nil { + melInfo, err := m.melLookup.GetValidatedMELStateByBlockHash(ctx, info.NextParentChainBlockHash) + if err != nil { + return 0, err + } + return melInfo.BatchCount, nil + } + if !info.InboxMaxCount.IsUint64() { + return 0, errors.New("inbox max count not a uint64") + } + return info.InboxMaxCount.Uint64(), nil } func (m *Manager) ForksDetected() uint64 { diff --git a/bold/assertions/poster.go b/bold/assertions/poster.go index e23074b2012..9689c6db41f 100644 --- a/bold/assertions/poster.go +++ b/bold/assertions/poster.go @@ -186,33 +186,43 @@ func (m *Manager) PostAssertionBasedOnParent( ) (protocol.Assertion, error), ) (option.Option[protocol.Assertion], error) { none := option.None[protocol.Assertion]() - if !parentCreationInfo.InboxMaxCount.IsUint64() { - return none, errors.New("inbox max count not a uint64") - } - // The parent assertion tells us what the next posted assertion's batch should be. - // We read this value and use it to compute the required execution state we must post. - batchCount := parentCreationInfo.InboxMaxCount.Uint64() - parentBlockHash := protocol.GoGlobalStateFromSolidity(parentCreationInfo.AfterState.GlobalState).BlockHash - newState, err := m.ExecutionStateAfterParent(ctx, parentCreationInfo) + // Derive the batch count from the parent assertion. Post-MEL this uses + // NextParentChainBlockHash via validated MEL state; pre-MEL it uses InboxMaxCount. + batchCount, err := m.batchCountFromAssertion(ctx, parentCreationInfo) if err != nil { if errors.Is(err, state.ErrChainCatchingUp) { chainCatchingUpCounter.Inc(1) + parentBlockHash := protocol.GoGlobalStateFromSolidity(parentCreationInfo.AfterState.GlobalState).BlockHash log.Info( "Waiting for more batches to post next assertion", "latestStakedAssertionBatchCount", batchCount, "latestStakedAssertionBlockHash", containers.Trunc(parentBlockHash[:]), ) - // If the chain is catching up, we wait for a bit and try again. time.Sleep(m.times.avgBlockTime / 10) return none, nil } - return none, errors.Wrapf(err, "could not get execution state at batch count %d with parent block hash %v", batchCount, parentBlockHash) + return none, errors.Wrapf(err, "could not derive batch count from parent assertion") + } + newState, err := m.ExecutionStateAfterParent(ctx, parentCreationInfo) + if err != nil { + if errors.Is(err, state.ErrChainCatchingUp) { + chainCatchingUpCounter.Inc(1) + log.Info( + "Waiting for execution state to catch up", + "batchCount", batchCount, + ) + time.Sleep(m.times.avgBlockTime / 10) + return none, nil + } + return none, errors.Wrapf(err, "could not get execution state at batch count %d", batchCount) } // If the assertion is not an overflow assertion i.e !(newState.GlobalState.Batch < batchCount) derived from // contracts check for overflow assertion => assertion.afterState.globalState.u64Vals[0] < assertion.beforeStateData.configData.nextInboxPosition) // then should check if we need to wait for the minimum number of blocks between assertions and a minimum time since parent assertion creation. // Overflow ones are not subject to this check onchain. + // Note: post-MEL, overflow assertions are removed from the contracts, but we keep this check + // for backwards compatibility with pre-MEL chains. isOverflowAssertion := newState.MachineStatus != protocol.MachineStatusErrored && newState.GlobalState.Batch < batchCount if !isOverflowAssertion { if err = m.waitToPostIfNeeded(ctx, parentCreationInfo); err != nil { @@ -221,8 +231,8 @@ func (m *Manager) PostAssertionBasedOnParent( } log.Info( - "Posting assertion for batch we agree with", - "requiredInboxMaxCount", batchCount, + "Posting assertion", + "batchCount", batchCount, "validatorName", m.validatorName, ) assertion, err := submitFn( @@ -241,7 +251,7 @@ func (m *Manager) PostAssertionBasedOnParent( assertionPostedCounter.Inc(1) log.Info("Successfully submitted assertion", "validatorName", m.validatorName, - "requiredInboxMaxCount", batchCount, + "batchCount", batchCount, "postedExecutionState", fmt.Sprintf("%+v", newState), "assertionHash", assertion.Id(), ) diff --git a/bold/challenge/manager.go b/bold/challenge/manager.go index bf0d7ca698b..e8764af96bd 100644 --- a/bold/challenge/manager.go +++ b/bold/challenge/manager.go @@ -60,6 +60,7 @@ type Manager struct { assertionManager AssertionManager watcher *chain.Watcher stateManager state.Provider + melLookup state.ValidatedMELStateLookup // nil when MEL is not active name string headerProvider HeaderProvider timeRef clock.Reference @@ -110,6 +111,14 @@ func WithHeaderProvider(provider HeaderProvider) Opt { } } +// WithMELStateLookup enables MEL-based determinism for challenge metadata. +func WithMELStateLookup(lookup state.ValidatedMELStateLookup) Opt { + return func(val *Manager) { + val.melLookup = lookup + } +} + + // New sets up a challenge manager instance provided a protocol, state manager, // chain watcher, assertion manager, and additional options. func New( diff --git a/bold/challenge/stack.go b/bold/challenge/stack.go index cfd9911ade2..f2ff946e845 100644 --- a/bold/challenge/stack.go +++ b/bold/challenge/stack.go @@ -38,6 +38,7 @@ type stackParams struct { delegatedStaking bool autoDeposit bool autoAllowanceApproval bool + melLookup state.ValidatedMELStateLookup } var defaultStackParams = stackParams{ @@ -191,6 +192,14 @@ func OverrideAssertionManager(asm *assertions.Manager) StackOpt { } } +// StackWithMELStateLookup enables MEL-based assertion determinism for both the +// assertion manager and challenge manager in the stack. +func StackWithMELStateLookup(lookup state.ValidatedMELStateLookup) StackOpt { + return func(p *stackParams) { + p.melLookup = lookup + } +} + // NewChallengeStack creates a new ChallengeManager and all of the dependencies // wiring them together. func NewChallengeStack( @@ -270,6 +279,9 @@ func NewChallengeStack( if !params.autoAllowanceApproval { amOpts = append(amOpts, assertions.WithoutAutoAllowanceApproval()) } + if params.melLookup != nil { + amOpts = append(amOpts, assertions.WithMELStateLookup(params.melLookup)) + } asm, err = assertions.NewManager( parent, provider, @@ -292,6 +304,9 @@ func NewChallengeStack( if params.headerProvider != nil { cmOpts = append(cmOpts, WithHeaderProvider(params.headerProvider)) } + if params.melLookup != nil { + cmOpts = append(cmOpts, WithMELStateLookup(params.melLookup)) + } if params.apiAddr != "" { cmOpts = append(cmOpts, WithAPIServer(api)) } diff --git a/bold/protocol/execution_state.go b/bold/protocol/execution_state.go index de54146e78e..066e8302962 100644 --- a/bold/protocol/execution_state.go +++ b/bold/protocol/execution_state.go @@ -27,10 +27,12 @@ type GoGlobalState struct { func GoGlobalStateFromSolidity(globalState rollupgen.GlobalState) GoGlobalState { return GoGlobalState{ - BlockHash: globalState.Bytes32Vals[0], - SendRoot: globalState.Bytes32Vals[1], - Batch: globalState.U64Vals[0], - PosInBatch: globalState.U64Vals[1], + BlockHash: globalState.Bytes32Vals[0], + SendRoot: globalState.Bytes32Vals[1], + MELStateHash: globalState.Bytes32Vals[2], + MELMsgHash: globalState.Bytes32Vals[3], + Batch: globalState.U64Vals[0], + PosInBatch: globalState.U64Vals[1], } } @@ -48,8 +50,21 @@ func ComputeSimpleMachineChallengeHash( func (s GoGlobalState) Hash() common.Hash { data := []byte("Global state:") - data = append(data, s.BlockHash.Bytes()...) - data = append(data, s.SendRoot.Bytes()...) + // Include bytes32 values up to the last non-zero index, with a minimum of + // index 1 (BlockHash + SendRoot always included). This matches the Rust + // prover's bytes32_last_non_zero_index() for backwards compatibility: + // pre-MEL states (MEL fields zero) produce the same hash as before. + bytes32Vals := [4]common.Hash{s.BlockHash, s.SendRoot, s.MELStateHash, s.MELMsgHash} + endIdx := 1 // always include at least BlockHash and SendRoot + for i := len(bytes32Vals) - 1; i > 1; i-- { + if bytes32Vals[i] != (common.Hash{}) { + endIdx = i + break + } + } + for i := 0; i <= endIdx; i++ { + data = append(data, bytes32Vals[i].Bytes()...) + } data = append(data, u64ToBe(s.Batch)...) data = append(data, u64ToBe(s.PosInBatch)...) return crypto.Keccak256Hash(data) @@ -57,7 +72,7 @@ func (s GoGlobalState) Hash() common.Hash { func (s GoGlobalState) AsSolidityStruct() challengeV2gen.GlobalState { return challengeV2gen.GlobalState{ - Bytes32Vals: [2][32]byte{s.BlockHash, s.SendRoot}, + Bytes32Vals: [4][32]byte{s.BlockHash, s.SendRoot, s.MELStateHash, s.MELMsgHash}, U64Vals: [2]uint64{s.Batch, s.PosInBatch}, } } diff --git a/bold/protocol/interfaces.go b/bold/protocol/interfaces.go index 2ef1e1f553b..c8a93c54cb6 100644 --- a/bold/protocol/interfaces.go +++ b/bold/protocol/interfaces.go @@ -118,14 +118,17 @@ type AssertionCreatedInfo struct { ParentAssertionHash AssertionHash BeforeState rollupgen.AssertionState AfterState rollupgen.AssertionState - InboxMaxCount *big.Int - AfterInboxBatchAcc common.Hash - AssertionHash AssertionHash - WasmModuleRoot common.Hash - ChallengeManager common.Address - TransactionHash common.Hash - CreationParentBlock uint64 - CreationL1Block uint64 + // Pre-MEL: InboxMaxCount is the batch count the next assertion must consume. + // Post-MEL: InboxMaxCount is unused (zero); NextParentChainBlockHash is used instead. + InboxMaxCount *big.Int + AfterInboxBatchAcc common.Hash + NextParentChainBlockHash common.Hash + AssertionHash AssertionHash + WasmModuleRoot common.Hash + ChallengeManager common.Address + TransactionHash common.Hash + CreationParentBlock uint64 + CreationL1Block uint64 } func (i AssertionCreatedInfo) ExecutionHash() common.Hash { diff --git a/bold/protocol/sol/assertion_chain.go b/bold/protocol/sol/assertion_chain.go index 8542429ed7c..7879acfe6b7 100644 --- a/bold/protocol/sol/assertion_chain.go +++ b/bold/protocol/sol/assertion_chain.go @@ -555,12 +555,11 @@ func (a *AssertionChain) NewStakeOnNewAssertion( assertionInputs rollupgen.AssertionInputs, expectedAssertionHash [32]byte, ) (*types.Transaction, error) { - return a.userLogic.NewStakeOnNewAssertion611c3d80( + return a.userLogic.NewStakeOnNewAssertion7f62c2af( opts, tokenAmount, assertionInputs, expectedAssertionHash, - a.withdrawalAddress, ) } return a.createAndStakeOnAssertion( @@ -1073,21 +1072,20 @@ func (a *AssertionChain) ReadAssertionCreationInfo( } creationL1Block := res.CreatedAtBlock return &protocol.AssertionCreatedInfo{ - ConfirmPeriodBlocks: parsedLog.ConfirmPeriodBlocks, - RequiredStake: parsedLog.RequiredStake, - ParentAssertionHash: protocol.AssertionHash{Hash: parsedLog.ParentAssertionHash}, - BeforeState: parsedLog.Assertion.BeforeState, - AfterState: afterState, - // 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, - TransactionHash: ethLog.TxHash, - CreationParentBlock: ethLog.BlockNumber, - CreationL1Block: creationL1Block, + ConfirmPeriodBlocks: parsedLog.ConfirmPeriodBlocks, + RequiredStake: parsedLog.RequiredStake, + ParentAssertionHash: protocol.AssertionHash{Hash: parsedLog.ParentAssertionHash}, + BeforeState: parsedLog.Assertion.BeforeState, + AfterState: afterState, + InboxMaxCount: big.NewInt(0), + AfterInboxBatchAcc: common.Hash{}, + NextParentChainBlockHash: parsedLog.NextParentChainBlockHash, + AssertionHash: protocol.AssertionHash{Hash: parsedLog.AssertionHash}, + WasmModuleRoot: parsedLog.WasmModuleRoot, + ChallengeManager: parsedLog.ChallengeManager, + TransactionHash: ethLog.TxHash, + CreationParentBlock: ethLog.BlockNumber, + CreationL1Block: creationL1Block, }, nil } diff --git a/bold/state/provider.go b/bold/state/provider.go index 4054a0ae2fb..064172fc38d 100644 --- a/bold/state/provider.go +++ b/bold/state/provider.go @@ -21,6 +21,25 @@ import ( var ErrChainCatchingUp = errors.New("chain is catching up to the execution state") +// MELStateInfo contains the key fields from a validated MEL state +// needed by the BOLD assertion and challenge system. +type MELStateInfo struct { + BatchCount uint64 + MsgCount uint64 + ParentChainBlockNumber uint64 + StateHash common.Hash +} + +// ValidatedMELStateLookup translates a parent chain block hash (the post-MEL +// determinism rule) into MEL batch/message counts. It only returns data for +// states that have been validated by the MEL validator. +type ValidatedMELStateLookup interface { + // GetValidatedMELStateByBlockHash returns MEL state info at the given + // parent chain block hash. Returns ErrChainCatchingUp if MEL validation + // hasn't reached this block yet. + GetValidatedMELStateByBlockHash(ctx context.Context, blockHash common.Hash) (*MELStateInfo, error) +} + // Batch index for an Arbitrum L2 state. type Batch uint64 @@ -36,11 +55,12 @@ type StepSize uint64 // ConfigSnapshot for an assertion on Arbitrum. type ConfigSnapshot struct { - RequiredStake *big.Int - ChallengeManagerAddress common.Address - ConfirmPeriodBlocks uint64 - WasmModuleRoot [32]byte - InboxMaxCount *big.Int + RequiredStake *big.Int + ChallengeManagerAddress common.Address + ConfirmPeriodBlocks uint64 + WasmModuleRoot [32]byte + InboxMaxCount *big.Int // Pre-MEL determinism rule. + NextParentChainBlockHash common.Hash // Post-MEL determinism rule. } type History struct { diff --git a/staker/bold/bold_staker.go b/staker/bold/bold_staker.go index af83a4a4d3b..d9b80f2facb 100644 --- a/staker/bold/bold_staker.go +++ b/staker/bold/bold_staker.go @@ -223,6 +223,7 @@ func NewBOLDStaker( inboxReader staker.InboxReaderInterface, dapRegistry *daprovider.DAProviderRegistry, fatalErr chan<- error, + melValidator staker.MELValidatorInterface, // nil when MEL is not active ) (*BOLDStaker, error) { if err := config.Validate(); err != nil { return nil, err @@ -235,7 +236,7 @@ func NewBOLDStaker( } l1reader := l1Reader.Client() - manager, err := newBOLDChallengeManager(ctx, stack, rollupAddress, txOpts, l1Reader, l1reader, blockValidator, statelessBlockValidator, config, strategy, dataPoster, inboxTracker, inboxStreamer, inboxReader, proofEnhancer) + manager, err := newBOLDChallengeManager(ctx, stack, rollupAddress, txOpts, l1Reader, l1reader, blockValidator, statelessBlockValidator, config, strategy, dataPoster, inboxTracker, inboxStreamer, inboxReader, proofEnhancer, melValidator) if err != nil { return nil, err } @@ -516,6 +517,7 @@ func newBOLDChallengeManager( inboxStreamer staker.TransactionStreamerInterface, inboxReader staker.InboxReaderInterface, proofEnhancer proofenhancement.ProofEnhancer, + melValidator staker.MELValidatorInterface, // nil when MEL is not active ) (*challenge.Manager, error) { // Initializes the BOLD contract bindings and the assertion chain abstraction. rollupBindings, err := rollupgen.NewRollupUserLogic(rollupAddress, client) @@ -662,6 +664,10 @@ func newBOLDChallengeManager( if config.EnableFastConfirmation { stackOpts = append(stackOpts, challenge.StackWithFastConfirmationEnabled()) } + if melValidator != nil { + melLookup := NewMELStateLookup(melValidator, l1Reader.Client()) + stackOpts = append(stackOpts, challenge.StackWithMELStateLookup(melLookup)) + } manager, err := challenge.NewChallengeStack( assertionChain, diff --git a/staker/bold/mel_state_lookup.go b/staker/bold/mel_state_lookup.go new file mode 100644 index 00000000000..0865bd9a793 --- /dev/null +++ b/staker/bold/mel_state_lookup.go @@ -0,0 +1,67 @@ +// Copyright 2023-2026, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md + +package bold + +import ( + "context" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + + "github.com/offchainlabs/nitro/bold/state" + "github.com/offchainlabs/nitro/staker" +) + +// Compile-time check that melStateLookup implements ValidatedMELStateLookup. +var _ state.ValidatedMELStateLookup = (*melStateLookup)(nil) + +// melStateLookup implements state.ValidatedMELStateLookup by resolving parent +// chain block hashes to block numbers and delegating to the MEL validator for +// validated state retrieval. +type melStateLookup struct { + melValidator staker.MELValidatorInterface + parentChainRPC *ethclient.Client +} + +// NewMELStateLookup creates a ValidatedMELStateLookup backed by the given MEL +// validator and parent chain client. +func NewMELStateLookup( + melValidator staker.MELValidatorInterface, + parentChainRPC *ethclient.Client, +) state.ValidatedMELStateLookup { + return &melStateLookup{ + melValidator: melValidator, + parentChainRPC: parentChainRPC, + } +} + +// GetValidatedMELStateByBlockHash resolves a parent chain block hash to a block +// number, then retrieves the MEL state at that block — but only if MEL +// validation has reached it. Returns state.ErrChainCatchingUp if not. +func (m *melStateLookup) GetValidatedMELStateByBlockHash(ctx context.Context, blockHash common.Hash) (*state.MELStateInfo, error) { + if blockHash == (common.Hash{}) { + return nil, fmt.Errorf("cannot look up MEL state for zero block hash") + } + header, err := m.parentChainRPC.HeaderByHash(ctx, blockHash) + if err != nil { + return nil, fmt.Errorf("could not resolve parent chain block hash %s: %w", blockHash, err) + } + if !header.Number.IsInt64() { + return nil, fmt.Errorf("parent chain block number not representable: %s", header.Number) + } + blockNum := new(big.Int).Set(header.Number).Uint64() + + melState, err := m.melValidator.GetValidatedMELStateAtBlock(ctx, blockNum) + if err != nil { + return nil, err + } + return &state.MELStateInfo{ + BatchCount: melState.BatchCount, + MsgCount: melState.MsgCount, + ParentChainBlockNumber: melState.ParentChainBlockNumber, + StateHash: melState.Hash(), + }, nil +} diff --git a/staker/bold_assertioncreation.go b/staker/bold_assertioncreation.go index 16823763b56..3b8fcf7d914 100644 --- a/staker/bold_assertioncreation.go +++ b/staker/bold_assertioncreation.go @@ -88,20 +88,19 @@ func ReadBoldAssertionCreationInfo( return nil, err } return &protocol.AssertionCreatedInfo{ - ConfirmPeriodBlocks: parsedLog.ConfirmPeriodBlocks, - RequiredStake: parsedLog.RequiredStake, - ParentAssertionHash: protocol.AssertionHash{Hash: parsedLog.ParentAssertionHash}, - BeforeState: parsedLog.Assertion.BeforeState, - AfterState: afterState, - // 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, - TransactionHash: ethLog.TxHash, - CreationParentBlock: ethLog.BlockNumber, - CreationL1Block: creationL1Block, + ConfirmPeriodBlocks: parsedLog.ConfirmPeriodBlocks, + RequiredStake: parsedLog.RequiredStake, + ParentAssertionHash: protocol.AssertionHash{Hash: parsedLog.ParentAssertionHash}, + BeforeState: parsedLog.Assertion.BeforeState, + AfterState: afterState, + InboxMaxCount: big.NewInt(0), + AfterInboxBatchAcc: common.Hash{}, + NextParentChainBlockHash: parsedLog.NextParentChainBlockHash, + AssertionHash: protocol.AssertionHash{Hash: parsedLog.AssertionHash}, + WasmModuleRoot: parsedLog.WasmModuleRoot, + ChallengeManager: parsedLog.ChallengeManager, + TransactionHash: ethLog.TxHash, + CreationParentBlock: ethLog.BlockNumber, + CreationL1Block: creationL1Block, }, nil } diff --git a/staker/mel_validator.go b/staker/mel_validator.go index b525f299392..adfb3f80625 100644 --- a/staker/mel_validator.go +++ b/staker/mel_validator.go @@ -32,6 +32,7 @@ import ( "github.com/offchainlabs/nitro/arbos/arbostypes" "github.com/offchainlabs/nitro/arbstate" "github.com/offchainlabs/nitro/arbutil" + "github.com/offchainlabs/nitro/bold/state" "github.com/offchainlabs/nitro/daprovider" "github.com/offchainlabs/nitro/util/rpcclient" "github.com/offchainlabs/nitro/util/stopwaiter" @@ -419,6 +420,19 @@ func (mv *MELValidator) LatestValidatedMELState(ctx context.Context) (*mel.State return mv.messageExtractor.GetState(mv.latestValidatedParentChainBlock.Load()) } +// GetValidatedMELStateAtBlock returns the MEL state at a specific parent chain +// block number, but only if MEL validation has reached that block. +func (mv *MELValidator) GetValidatedMELStateAtBlock(_ context.Context, blockNum uint64) (*mel.State, error) { + latestValidated := mv.latestValidatedParentChainBlock.Load() + if blockNum > latestValidated { + return nil, fmt.Errorf( + "%w: MEL validated up to block %d, requested block %d", + state.ErrChainCatchingUp, blockNum, latestValidated, + ) + } + return mv.messageExtractor.GetState(blockNum) +} + func (mv *MELValidator) SetCurrentWasmModuleRoot(hash common.Hash) error { mv.moduleMutex.Lock() defer mv.moduleMutex.Unlock() diff --git a/staker/multi_protocol/multi_protocol_staker.go b/staker/multi_protocol/multi_protocol_staker.go index f8aa0cc3f15..5a6c5304d07 100644 --- a/staker/multi_protocol/multi_protocol_staker.go +++ b/staker/multi_protocol/multi_protocol_staker.go @@ -58,6 +58,7 @@ type MultiProtocolStaker struct { inboxStreamer staker.TransactionStreamerInterface inboxReader staker.InboxReaderInterface dapRegistry *daprovider.DAProviderRegistry + melValidator staker.MELValidatorInterface fatalErr chan<- error } @@ -81,6 +82,7 @@ func NewMultiProtocolStaker( inboxReader staker.InboxReaderInterface, dapRegistry *daprovider.DAProviderRegistry, fatalErr chan<- error, + melValidator staker.MELValidatorInterface, // nil when MEL is not active ) (*MultiProtocolStaker, error) { if err := legacyConfig().Validate(); err != nil { return nil, err @@ -130,6 +132,7 @@ func NewMultiProtocolStaker( inboxStreamer: inboxStreamer, inboxReader: inboxReader, dapRegistry: dapRegistry, + melValidator: melValidator, fatalErr: fatalErr, }, nil } @@ -293,6 +296,7 @@ func (m *MultiProtocolStaker) setupBoldStaker( m.inboxReader, m.dapRegistry, m.fatalErr, + m.melValidator, ) if err != nil { return err diff --git a/staker/stateless_block_validator.go b/staker/stateless_block_validator.go index 5fe0956c5bc..d6bd091f7c3 100644 --- a/staker/stateless_block_validator.go +++ b/staker/stateless_block_validator.go @@ -75,6 +75,10 @@ type TransactionStreamerInterface interface { type MELValidatorInterface interface { UpdateValidationTarget(pos arbutil.MessageIndex) LatestValidatedMELState(context.Context) (*mel.State, error) + // GetValidatedMELStateAtBlock returns the MEL state at a given parent chain + // block number, but only if validation has reached that block. Returns an + // error wrapping state.ErrChainCatchingUp if validation hasn't caught up. + GetValidatedMELStateAtBlock(ctx context.Context, blockNum uint64) (*mel.State, error) FetchMsgPreimagesAndRelevantState(ctx context.Context, l2BlockNum arbutil.MessageIndex) (*MsgPreimagesAndRelevantState, error) FetchMessageOriginMELStateHash(pos arbutil.MessageIndex) (common.Hash, error) ClearValidatedMsgPreimages(lastValidatedL2BlockNumber arbutil.MessageIndex) diff --git a/system_tests/message_extraction_layer_validation_test.go b/system_tests/message_extraction_layer_validation_test.go index 1f0a2c7c37b..83084a55a6c 100644 --- a/system_tests/message_extraction_layer_validation_test.go +++ b/system_tests/message_extraction_layer_validation_test.go @@ -71,12 +71,9 @@ func testValidationPostMEL(t *testing.T, useJit bool) { } } -func TestValidationPostMELReorgHandleInJitMode(t *testing.T) { - testValidationPostMELReorgHandle(t, true) -} - -func TestValidationPostMELReorgHandleInArbitratorMode(t *testing.T) { - testValidationPostMELReorgHandle(t, false) +func TestValidationPostMELReorgHandle(t *testing.T) { + t.Run("TestValidationPostMELReorgHandle-in-jit-mode", func(t *testing.T) { testValidationPostMELReorgHandle(t, true) }) + t.Run("TestValidationPostMELReorgHandle-in-arbitrator-mode", func(t *testing.T) { testValidationPostMELReorgHandle(t, false) }) } func testValidationPostMELReorgHandle(t *testing.T, useJit bool) { @@ -85,7 +82,7 @@ func testValidationPostMELReorgHandle(t *testing.T, useJit bool) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - builder := NewNodeBuilder(ctx).DefaultConfig(t, true) + builder := NewNodeBuilder(ctx).DefaultConfig(t, true).DontParalellise() builder.useJit = useJit builder.nodeConfig.MessageExtraction.Enable = true builder.nodeConfig.MessageExtraction.RetryInterval = 100 * time.Millisecond diff --git a/validator/execution_state.go b/validator/execution_state.go index 99d774dc449..9d7b6f60a43 100644 --- a/validator/execution_state.go +++ b/validator/execution_state.go @@ -52,8 +52,21 @@ func u64ToBe(x uint64) []byte { func (s GoGlobalState) Hash() common.Hash { data := []byte("Global state:") - data = append(data, s.BlockHash.Bytes()...) - data = append(data, s.SendRoot.Bytes()...) + // Include bytes32 values up to the last non-zero index, with a minimum of + // index 1 (BlockHash + SendRoot always included). This matches the Rust + // prover's bytes32_last_non_zero_index() for backwards compatibility: + // pre-MEL states (MEL fields zero) produce the same hash as before. + bytes32Vals := [4]common.Hash{s.BlockHash, s.SendRoot, s.MELStateHash, s.MELMsgHash} + endIdx := 1 // always include at least BlockHash and SendRoot + for i := len(bytes32Vals) - 1; i > 1; i-- { + if bytes32Vals[i] != (common.Hash{}) { + endIdx = i + break + } + } + for i := 0; i <= endIdx; i++ { + data = append(data, bytes32Vals[i].Bytes()...) + } data = append(data, u64ToBe(s.Batch)...) data = append(data, u64ToBe(s.PosInBatch)...) return crypto.Keccak256Hash(data)