Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 2 additions & 13 deletions blocksync/msgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (

bcproto "github.com/tendermint/tendermint/proto/tendermint/blocksync"
"github.com/tendermint/tendermint/types"
"github.com/tendermint/tendermint/upgrade"
)

const (
Expand Down Expand Up @@ -88,23 +87,13 @@ func ValidateMsg(pb proto.Message) error {
return errors.New("negative Height")
}
case *bcproto.BlockResponse:
// V1 block format
block, err := types.BlockFromProto(msg.Block)
if err != nil {
if _, err := types.BlockFromProto(msg.Block); err != nil {
return err
}
if upgrade.IsUpgraded(block.Height) {
return fmt.Errorf("unexpected BlockResponse at upgraded height %d", block.Height)
}
case *bcproto.BlockResponseV2:
// V2 block format (sequencer mode)
block, err := types.BlockV2FromProto(msg.Block)
if err != nil {
if _, err := types.BlockV2FromProto(msg.Block); err != nil {
return err
}
if !upgrade.IsUpgraded(block.GetHeight()) {
return fmt.Errorf("unexpected BlockResponseV2 before upgraded height %d", block.GetHeight())
}
case *bcproto.NoBlockResponse:
if msg.Height < 0 {
return errors.New("negative Height")
Expand Down
65 changes: 10 additions & 55 deletions blocksync/msgs_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package blocksync_test

import (
"bytes"
"encoding/hex"
"math"
"math/big"
Expand All @@ -13,10 +12,8 @@ import (
"github.com/stretchr/testify/require"

"github.com/tendermint/tendermint/blocksync"
"github.com/tendermint/tendermint/crypto"
bcproto "github.com/tendermint/tendermint/proto/tendermint/blocksync"
"github.com/tendermint/tendermint/types"
"github.com/tendermint/tendermint/upgrade"
)

func TestBcBlockRequestMessageValidateBasic(t *testing.T) {
Expand Down Expand Up @@ -84,65 +81,23 @@ func TestBcStatusResponseMessageValidateBasic(t *testing.T) {
}
}

func TestValidateMsgBlockResponseRejectsV1AtUpgradeHeight(t *testing.T) {
oldHeight := upgrade.UpgradeBlockHeight
upgrade.SetUpgradeBlockHeight(10)
defer upgrade.SetUpgradeBlockHeight(oldHeight)

lastBlockID := types.BlockID{
Hash: bytes.Repeat([]byte{1}, 32),
PartSetHeader: types.PartSetHeader{
Total: 1,
Hash: bytes.Repeat([]byte{2}, 32),
},
}
lastCommit := types.NewCommit(9, 0, lastBlockID, []types.CommitSig{{
BlockIDFlag: types.BlockIDFlagCommit,
ValidatorAddress: bytes.Repeat([]byte{3}, crypto.AddressSize),
Signature: []byte{1},
}})

block := types.MakeBlock(10, []types.Tx{types.Tx("Hello World")}, nil, nil, nil, lastCommit, nil)
block.ProposerAddress = bytes.Repeat([]byte{4}, crypto.AddressSize)
bpb, err := block.ToProto()
require.NoError(t, err)

err = blocksync.ValidateMsg(&bcproto.BlockResponse{Block: bpb})
require.Error(t, err)
assert.Contains(t, err.Error(), "unexpected BlockResponse")
}

func TestValidateMsgBlockResponseV2RejectsPreUpgradeHeight(t *testing.T) {
oldHeight := upgrade.UpgradeBlockHeight
upgrade.SetUpgradeBlockHeight(10)
defer upgrade.SetUpgradeBlockHeight(oldHeight)

blockV2 := &types.BlockV2{
ParentHash: common.HexToHash("0x1"),
Hash: common.HexToHash("0x2"),
BaseFee: big.NewInt(1),
Number: 9,
}

err := blocksync.ValidateMsg(&bcproto.BlockResponseV2{Block: types.BlockV2ToProto(blockV2)})
require.Error(t, err)
assert.Contains(t, err.Error(), "unexpected BlockResponseV2")
}

func TestValidateMsgBlockResponseV2AllowsUpgradeHeight(t *testing.T) {
oldHeight := upgrade.UpgradeBlockHeight
upgrade.SetUpgradeBlockHeight(10)
defer upgrade.SetUpgradeBlockHeight(oldHeight)

// ValidateMsg validates block responses structurally only. The old "reject V1/V2 by upgrade
// height" checks were removed: the timestamp-driven boundary is unknown at message-validation
// time, so the blocksync reactor guards the V1/V2 type at consume time instead. A structurally
// valid block passes regardless of height; a malformed one is rejected.
func TestValidateMsgBlockResponseV2Structural(t *testing.T) {
blockV2 := &types.BlockV2{
ParentHash: common.HexToHash("0x1"),
Hash: common.HexToHash("0x2"),
BaseFee: big.NewInt(1),
Number: 10,
}
require.NoError(t, blocksync.ValidateMsg(&bcproto.BlockResponseV2{Block: types.BlockV2ToProto(blockV2)}))

err := blocksync.ValidateMsg(&bcproto.BlockResponseV2{Block: types.BlockV2ToProto(blockV2)})
require.NoError(t, err)
// Malformed V2 (bad hash length) -> rejected by structural validation.
bad := types.BlockV2ToProto(blockV2)
bad.Hash = []byte{0x1, 0x2}
require.Error(t, blocksync.ValidateMsg(&bcproto.BlockResponseV2{Block: bad}))
}

//nolint:lll // ignore line length in tests
Expand Down
103 changes: 75 additions & 28 deletions blocksync/reactor.go
Original file line number Diff line number Diff line change
Expand Up @@ -380,24 +380,44 @@ func (bcR *Reactor) L2Node() l2node.L2Node {
return bcR.l2Node
}

// syncBlockV2 handles syncing a single BlockV2 in sequencer mode.
// Verifies block signature using the history-aware verifier, then applies.
func (bcR *Reactor) syncBlockV2(block types.SyncableBlock, blocksSynced *uint64, lastRate *float64, lastHundred *time.Time) bool {
// verifyBlockV2 asserts block is a *types.BlockV2 and verifies its sequencer signature
// (history-aware lookup via IsSequencerAt). Pure check: no pool side effects — the caller
// decides what to redo/ban. Shared by the boundary check and syncBlockV2.
func (bcR *Reactor) verifyBlockV2(block types.SyncableBlock) (*types.BlockV2, error) {
blockV2, ok := block.(*types.BlockV2)
if !ok {
bcR.Logger.Error("Expected BlockV2 after upgrade", "height", block.GetHeight())
bcR.pool.RedoRequest(block.GetHeight())
return false
return nil, fmt.Errorf("expected BlockV2 at height %d, got V1", block.GetHeight())
}

// Verify block signature (uses IsSequencerAt with history-aware lookup)
if err := sequencer.VerifyBlockSignature(bcR.verifier, blockV2); err != nil {
bcR.Logger.Error("Block signature verification failed", "height", blockV2.Number, "err", err)
bcR.pool.RedoRequest(blockV2.GetHeight())
return nil, fmt.Errorf("block signature verification failed at height %d: %w", blockV2.GetHeight(), err)
}
return blockV2, nil
}

// redoAndBan re-queues the block's height and disconnects the peer that served it.
// Single-block granularity: the PBFT path bans only the mismatched block, while the
// one-time boundary check calls it for both blocks.
func (bcR *Reactor) redoAndBan(block types.SyncableBlock, reason string) {
bcR.Logger.Error("blocksync redo+ban", "height", block.GetHeight(), "reason", reason)
peerID := bcR.pool.RedoRequest(block.GetHeight())
if peer := bcR.Switch.Peers().Get(peerID); peer != nil {
bcR.Switch.StopPeerForError(peer, fmt.Errorf("blocksync: %s", reason))
}
}

// syncBlockV2 handles syncing a single BlockV2 in sequencer mode.
// Verifies block signature using the history-aware verifier, then applies.
func (bcR *Reactor) syncBlockV2(block types.SyncableBlock, blocksSynced *uint64, lastRate *float64, lastHundred *time.Time) bool {
blockV2, err := bcR.verifyBlockV2(block)
if err != nil {
// Wrong type or bad signature == the peer served bad/forged data: redo + ban.
bcR.redoAndBan(block, fmt.Sprintf("BlockV2 verification failed: %v", err))
return false
}

// Apply BlockV2 via stateV2
// Apply BlockV2 via stateV2. The block is already signature-verified (authentic), so an
// apply failure is almost always a local issue (geth state), not the peer's fault — redo
// without banning to avoid dropping honest peers during a local hiccup.
if err := bcR.stateV2.ApplyBlock(blockV2); err != nil {
bcR.Logger.Error("Failed to apply BlockV2", "height", blockV2.Number, "err", err)
bcR.pool.RedoRequest(blockV2.GetHeight())
Expand Down Expand Up @@ -612,18 +632,9 @@ FOR_LOOP:
didProcessCh <- struct{}{}
}

if firstSync.GetHeight()+1 == upgrade.UpgradeBlockHeight {
if err := bcR.handleTheLastTMBlock(state, firstSync); err != nil {
bcR.Logger.Error("Error in apply last tendermint block", "err", err)
peerID := bcR.pool.RedoRequest(firstSync.GetHeight())
peer := bcR.Switch.Peers().Get(peerID)
if peer != nil {
bcR.Switch.StopPeerForError(peer, fmt.Errorf("handleTheLastTMBlock error: %v", err))
}
continue FOR_LOOP
}
bcR.pool.PopRequest()
blocksSynced++
// Detect and process the last PBFT block at the timestamp-driven upgrade
// boundary (runs at most once; no-op after the boundary height is recorded).
if bcR.checkAndHandleTheLastTMBlock(state, firstSync, secondSync, &blocksSynced) {
continue FOR_LOOP
}

Expand All @@ -633,8 +644,20 @@ FOR_LOOP:
continue FOR_LOOP
}

// PBFT mode: type assert to *types.Block
first, second := firstSync.(*types.Block), secondSync.(*types.Block)
// PBFT mode: both blocks must be V1. A peer that serves a V2 block in this
// range is malicious or on a bad fork — ban only the mismatched one(s) (this
// is the hot path; avoid false-banning the peer that served a correct block).
first, ok1 := firstSync.(*types.Block)
second, ok2 := secondSync.(*types.Block)
if !ok1 || !ok2 {
if !ok1 {
bcR.redoAndBan(firstSync, "expected V1 block in PBFT range")
}
if !ok2 {
bcR.redoAndBan(secondSync, "expected V1 block in PBFT range")
}
continue FOR_LOOP
}

firstParts, err := first.MakePartSet(types.BlockPartSizeBytes)
if err != nil {
Expand Down Expand Up @@ -727,10 +750,34 @@ func (bcR *Reactor) BroadcastStatusRequest() error {
return nil
}

// just skip the last tendermint block commit check.
// TODO: consider add the commit check in the future or using batch derivation reorg
func (bcR *Reactor) checkAndHandleTheLastTMBlock(state sm.State, first, second types.SyncableBlock, blocksSynced *uint64) bool {
if upgrade.UpgradeBlockHeight() >= 0 {
return false // boundary already discovered; only runs once
}
if first.GetBlockVersion() != types.Version1 || !upgrade.IsUpgradedByTs(first.GetTime()) {
return false // not the boundary; fall through to the normal sync paths
}
if _, err := bcR.verifyBlockV2(second); err != nil {
bcR.redoAndBan(first, fmt.Sprintf("boundary check failed: %v", err))
bcR.redoAndBan(second, fmt.Sprintf("boundary: second must be a valid V2 block: %v", err))
return true
}
if err := bcR.handleTheLastTMBlock(state, first); err != nil {
bcR.redoAndBan(first, fmt.Sprintf("apply last tendermint block: %v", err))
bcR.redoAndBan(second, fmt.Sprintf("boundary rolled back after first failed: %v", err))
return true
}
upgrade.SetUpgradeBlockHeight(first.GetHeight())
bcR.pool.PopRequest()
*blocksSynced++
return true
}

func (bcR *Reactor) handleTheLastTMBlock(state sm.State, lastSyncable types.SyncableBlock) error {
last := lastSyncable.(*types.Block)
last, ok := lastSyncable.(*types.Block)
if !ok {
return fmt.Errorf("handleTheLastTMBlock: expected V1 block at height %d", lastSyncable.GetHeight())
}
lastParts, err := last.MakePartSet(types.BlockPartSizeBytes)
if err != nil {
bcR.Logger.Error(
Expand Down
2 changes: 1 addition & 1 deletion consensus/reactor.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func (conR *Reactor) OnStart() error {
if upgrade.IsUpgraded(conR.conS.Height) {
conR.Logger.Info("Already upgraded to sequencer mode, consensus reactor will not start",
"height", conR.conS.Height,
"upgradeHeight", upgrade.UpgradeBlockHeight)
"upgradeHeight", upgrade.UpgradeBlockHeight())
return nil
}

Expand Down
11 changes: 7 additions & 4 deletions consensus/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -1809,10 +1809,13 @@ func (cs *State) finalizeCommit(height int64) {
}

// Check for upgrade to sequencer mode
if upgrade.IsUpgraded(cs.Height) {
logger.Info("Upgrade height reached, switching to sequencer mode",
"height", cs.Height,
"upgradeHeight", upgrade.UpgradeBlockHeight)
if upgrade.IsUpgradedByTs(block.Time.UnixMilli()) {
// persistent the upgradeHeight, load it into upgradeHeight when startup
upgrade.SetUpgradeBlockHeight(height)

logger.Info("Upgrade time reached, switching to sequencer mode",
"height", height,
"upgradeTime", upgrade.UpgradeBlockTime())

// Stop consensus state first.
//
Expand Down
8 changes: 8 additions & 0 deletions node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,14 @@ func NewNode(
sm.StoreOptions{DiscardABCIResponses: config.Storage.DiscardABCIResponses},
)

// Restore the persisted sequencer-upgrade boundary (if any) and wire the store so future
// SetUpgradeBlockHeight calls (finalizeCommit / blocksync) persist it automatically.
// A DB read failure here is fatal: booting with a reset boundary would let the node
// re-run PBFT past the upgrade.
if err := upgrade.SetStore(stateDB); err != nil {
return nil, err
}

state, genDoc, err := LoadStateFromDBOrGenesisDocProvider(stateDB, genesisDocProvider)
if err != nil {
return nil, err
Expand Down
5 changes: 0 additions & 5 deletions sequencer/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,6 @@ var (
type SequencerVerifier interface {
// IsSequencerAt checks if addr was the valid sequencer at the given L2 block height.
IsSequencerAt(addr common.Address, l2Height uint64) (bool, error)

// VerificationStartHeight returns the L2 block height from which V2 signature
// verification is enforced (= upgradeBlockHeight). Blocks below this height are
// PBFT blocks and skip V2 verification. Returns math.MaxUint64 if not configured.
VerificationStartHeight() uint64
}

// Signer interface for sequencer block signing
Expand Down
2 changes: 1 addition & 1 deletion sequencer/state_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (

const (
// BlockInterval is the fallback interval for empty blocks (no txs).
BlockInterval = 3000 * time.Millisecond
BlockInterval = 5000 * time.Millisecond
// FastBlockInterval is the txpool polling interval.
// When pending txs are found, a block is produced immediately.
FastBlockInterval = 300 * time.Millisecond
Expand Down
5 changes: 0 additions & 5 deletions sequencer/state_v2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ func (m *mockSignerImpl) Address() common.Address {
// mockSequencerVerifier is a mock implementation of SequencerVerifier for testing.
type mockSequencerVerifier struct {
isSequencer bool
startHeight uint64
err error
}

Expand All @@ -45,10 +44,6 @@ func (m *mockSequencerVerifier) IsSequencerAt(addr common.Address, l2Height uint
return m.isSequencer, nil
}

func (m *mockSequencerVerifier) VerificationStartHeight() uint64 {
return m.startHeight
}

// mockSequencerHA is a mock implementation of SequencerHA for testing.
type mockSequencerHA struct {
leader bool
Expand Down
4 changes: 4 additions & 0 deletions types/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ func (b *Block) GetHash() []byte {
return b.Hash().Bytes()
}

func (b *Block) GetTime() int64 { return b.Header.Time.UnixMilli() }

func (b *Block) GetBlockVersion() BlockVersion { return Version1 }

// Ensure Block implements SyncableBlock
var _ SyncableBlock = (*Block)(nil)

Expand Down
18 changes: 16 additions & 2 deletions types/block_v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ package types

import (
"errors"
"math/big"

"github.com/morph-l2/go-ethereum/common"
seqproto "github.com/tendermint/tendermint/proto/tendermint/sequencer"
"math/big"
)

// BlockV2 represents the block format after upgrade to centralized sequencer mode.
Expand Down Expand Up @@ -49,11 +48,26 @@ func (b *BlockV2) GetHash() []byte {
return b.Hash.Bytes()
}

func (b *BlockV2) GetTime() int64 { return int64(b.Timestamp) * 1000 }

func (b *BlockV2) GetBlockVersion() BlockVersion { return Version2 }

// BlockVersion identifies the wire format of a SyncableBlock: Version1 is the
// PBFT-era tendermint block, Version2 is the post-upgrade sequencer block.
type BlockVersion uint8

const (
Version1 BlockVersion = 1
Version2 BlockVersion = 2
)

// SyncableBlock is an interface that both old Block and new BlockV2 can implement
// for compatibility in the block pool.
type SyncableBlock interface {
GetHeight() int64
GetHash() []byte
GetTime() int64
GetBlockVersion() BlockVersion
}

// Ensure BlockV2 implements SyncableBlock
Expand Down
Loading
Loading