Skip to content

Commit 26dc364

Browse files
marcello33lucca30
authored andcommitted
eth: fix overflow impacting milestone lock and syncing (#2111)
* eth: fix overflow impacting milestone lock and syncing * eth: address comments (cherry picked from commit 27130c2)
1 parent 2fcc861 commit 26dc364

7 files changed

Lines changed: 218 additions & 10 deletions

File tree

cmd/keeper/go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ require (
5050
github.com/yusufpapurcu/wmi v1.2.4 // indirect
5151
golang.org/x/crypto v0.46.0 // indirect
5252
golang.org/x/sync v0.19.0 // indirect
53-
golang.org/x/sys v0.39.0 // indirect
53+
golang.org/x/sys v0.40.0 // indirect
54+
golang.org/x/time v0.12.0 // indirect
5455
gopkg.in/yaml.v2 v2.4.0 // indirect
5556
)
5657

cmd/keeper/go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
169169
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
170170
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
171171
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
172+
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
172173
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
173174
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
174175
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

eth/bor_api_backend.go

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"math"
78

89
"github.com/ethereum/go-ethereum"
910
"github.com/ethereum/go-ethereum/common"
@@ -16,10 +17,15 @@ import (
1617
"github.com/ethereum/go-ethereum/rpc"
1718
)
1819

19-
var errBorEngineNotAvailable error = errors.New("Only available in Bor engine")
20+
const tipConfirmationOffset uint64 = 16
21+
22+
var (
23+
errBorEngineNotAvailable = errors.New("Only available in Bor engine")
24+
errInvalidBlockNumber = errors.New("end block number is out of safe range")
25+
)
2026

2127
// GetRootHash returns root hash for given start and end block
22-
func (b *EthAPIBackend) GetRootHash(ctx context.Context, starBlockNr uint64, endBlockNr uint64) (string, error) {
28+
func (b *EthAPIBackend) GetRootHash(_ context.Context, starBlockNr uint64, endBlockNr uint64) (string, error) {
2329
var api *bor.API
2430

2531
for _, _api := range b.eth.Engine().APIs(b.eth.BlockChain()) {
@@ -41,7 +47,12 @@ func (b *EthAPIBackend) GetRootHash(ctx context.Context, starBlockNr uint64, end
4147
}
4248

4349
// GetVoteOnHash returns the vote on hash
44-
func (b *EthAPIBackend) GetVoteOnHash(ctx context.Context, starBlockNr uint64, endBlockNr uint64, hash string, milestoneId string) (bool, error) {
50+
func (b *EthAPIBackend) GetVoteOnHash(ctx context.Context, _ uint64, endBlockNr uint64, hash string, milestoneId string) (bool, error) {
51+
// Reject invalid block numbers (overflowing with the confirmation offset or exceeding the valid range).
52+
if endBlockNr > math.MaxInt64-tipConfirmationOffset {
53+
return false, errInvalidBlockNumber
54+
}
55+
4556
var api *bor.API
4657

4758
for _, _api := range b.eth.Engine().APIs(b.eth.BlockChain()) {
@@ -54,16 +65,16 @@ func (b *EthAPIBackend) GetVoteOnHash(ctx context.Context, starBlockNr uint64, e
5465
return false, errBorEngineNotAvailable
5566
}
5667

57-
// Confirmation of 16 blocks on the endblock
58-
tipConfirmationBlockNr := endBlockNr + uint64(16)
68+
// Confirmation of tipConfirmationOffset blocks on the endblock
69+
tipConfirmationBlockNr := endBlockNr + tipConfirmationOffset
5970

60-
// Check if tipConfirmation block exit
61-
_, err := b.BlockByNumber(ctx, rpc.BlockNumber(tipConfirmationBlockNr))
62-
if err != nil {
71+
// Check if the tipConfirmation block exists
72+
tipBlock, err := b.BlockByNumber(ctx, rpc.BlockNumber(tipConfirmationBlockNr))
73+
if err != nil || tipBlock == nil {
6374
return false, errTipConfirmationBlock
6475
}
6576

66-
// Check if end block exist
77+
// Check if the end block exists
6778
localEndBlock, err := b.BlockByNumber(ctx, rpc.BlockNumber(endBlockNr))
6879
if err != nil || localEndBlock == nil {
6980
return false, errEndBlock

eth/bor_api_backend_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package eth
2+
3+
import (
4+
"context"
5+
"errors"
6+
"math"
7+
"testing"
8+
)
9+
10+
// TestGetVoteOnHashRejectsOutOfRangeBlockNumbers verifies that GetVoteOnHash returns an error when endBlockNr is outside the safe range.
11+
func TestGetVoteOnHashRejectsOutOfRangeBlockNumbers(t *testing.T) {
12+
t.Parallel()
13+
14+
backend := &EthAPIBackend{}
15+
16+
rejectCases := []struct {
17+
name string
18+
endBlockNr uint64
19+
}{
20+
{"max uint64", math.MaxUint64},
21+
{"max uint64 minus 15", math.MaxUint64 - 15},
22+
{"max int64", math.MaxInt64},
23+
{"max int64 minus 15", math.MaxInt64 - 15},
24+
}
25+
26+
for _, tt := range rejectCases {
27+
t.Run(tt.name, func(t *testing.T) {
28+
t.Parallel()
29+
30+
_, err := backend.GetVoteOnHash(context.Background(), 0, tt.endBlockNr, "0x00", "test")
31+
if err == nil {
32+
t.Fatalf("expected error for endBlockNr=%d, got nil", tt.endBlockNr)
33+
}
34+
if !errors.Is(err, errInvalidBlockNumber) {
35+
t.Fatalf("expected errInvalidBlockNumber, got %v", err)
36+
}
37+
})
38+
}
39+
40+
// Boundary value: math.MaxInt64 - tipConfirmationOffset is the highest accepted endBlockNr.
41+
// The call passes the range guard and then panics on nil backend internals,
42+
// which confirms the guard did not reject it.
43+
t.Run("max int64 minus tipConfirmationOffset (boundary, should pass guard)", func(t *testing.T) {
44+
t.Parallel()
45+
46+
defer func() {
47+
if r := recover(); r == nil {
48+
// No panic means the function returned normally — check that
49+
// the error is not errInvalidBlockNumber.
50+
}
51+
// A panic here means the boundary value passed the guard and
52+
// proceeded into backend logic (which is nil in this test). That's
53+
// the expected outcome.
54+
}()
55+
56+
_, err := backend.GetVoteOnHash(context.Background(), 0, math.MaxInt64-tipConfirmationOffset, "0x00", "test")
57+
if errors.Is(err, errInvalidBlockNumber) {
58+
t.Fatal("expected boundary value to pass the range check, but got errInvalidBlockNumber")
59+
}
60+
})
61+
}

eth/downloader/whitelist/milestone_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package whitelist
22

33
import (
4+
"math"
45
"math/big"
56
"sync"
67
"sync/atomic"
@@ -59,6 +60,66 @@ func TestUnlockSprintThreshold(t *testing.T) {
5960
m.finality.RUnlock()
6061
}
6162

63+
// TestIsReorgAllowedWithMaxLockedNumber verifies that IsReorgAllowed correctly
64+
// handles the case where LockedMilestoneNumber is set to an extremely large value.
65+
// No real chain tip can exceed such a number, so IsReorgAllowed must return false.
66+
func TestIsReorgAllowedWithMaxLockedNumber(t *testing.T) {
67+
db := rawdb.NewMemoryDatabase()
68+
svc := NewService(db, false, 0)
69+
70+
m, ok := svc.milestoneService.(*milestone)
71+
if !ok {
72+
t.Fatalf("expected milestoneService to be *milestone, got %T", svc.milestoneService)
73+
}
74+
75+
chain := []*types.Header{
76+
{Number: new(big.Int).SetUint64(100)},
77+
{Number: new(big.Int).SetUint64(200)},
78+
{Number: new(big.Int).SetUint64(300)},
79+
}
80+
81+
// With a normal locked milestone below the chain tip and not in the chain, reorg is allowed
82+
if !m.IsReorgAllowed(chain, 50, common.Hash{}) {
83+
t.Fatal("expected reorg to be allowed when chain tip exceeds locked milestone not in chain")
84+
}
85+
86+
// With max uint64 as the locked milestone, no chain tip can exceed it, so the reorg is blocked
87+
if m.IsReorgAllowed(chain, math.MaxUint64, common.Hash{}) {
88+
t.Fatal("expected reorg to be blocked when locked milestone is max uint64")
89+
}
90+
}
91+
92+
// TestIsValidChainWithMaxLockedNumber verifies that a milestone locked at an
93+
// unreachable block number causes IsValidChain to reject all chains.
94+
func TestIsValidChainWithMaxLockedNumber(t *testing.T) {
95+
db := rawdb.NewMemoryDatabase()
96+
svc := NewService(db, false, 0)
97+
98+
m, ok := svc.milestoneService.(*milestone)
99+
if !ok {
100+
t.Fatalf("expected milestoneService to be *milestone, got %T", svc.milestoneService)
101+
}
102+
103+
chain := []*types.Header{
104+
{Number: new(big.Int).SetUint64(100)},
105+
{Number: new(big.Int).SetUint64(200)},
106+
}
107+
currentHeader := chain[len(chain)-1]
108+
109+
// Set locked milestone to max uint64 under write lock
110+
m.finality.Lock()
111+
m.Locked = true
112+
m.LockedMilestoneNumber = math.MaxUint64
113+
m.LockedMilestoneHash = common.Hash{0x01}
114+
m.LockedMilestoneIDs = map[string]struct{}{"test": {}}
115+
m.finality.Unlock()
116+
117+
valid, _ := m.IsValidChain(currentHeader, chain)
118+
if valid {
119+
t.Fatal("expected chain to be invalid when locked milestone number is max uint64")
120+
}
121+
}
122+
62123
// TestMilestoneUnlockSprintRace exercises concurrent readers and writers
63124
// of milestone lock state and future milestone lists.
64125
//

eth/downloader/whitelist/service.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package whitelist
33
import (
44
"errors"
55
"fmt"
6+
"math"
67
"sync"
78

89
"github.com/ethereum/go-ethereum/common"
@@ -78,6 +79,19 @@ func NewService(db ethdb.Database, disableBlindForkValidation bool, maxBlindFork
7879
lockedMilestoneIDs = make(map[string]struct{})
7980
}
8081

82+
// Discard the locked state if the stored milestone number is out of the safe range (corrupted data).
83+
if locked && lockedMilestoneNumber > math.MaxInt64 {
84+
log.Warn("Discarding invalid locked milestone loaded from DB", "lockedMilestoneNumber", lockedMilestoneNumber)
85+
locked = false
86+
lockedMilestoneNumber = 0
87+
lockedMilestoneHash = common.Hash{}
88+
lockedMilestoneIDs = make(map[string]struct{})
89+
90+
if err := rawdb.WriteLockField(db, locked, lockedMilestoneNumber, lockedMilestoneHash, lockedMilestoneIDs); err != nil {
91+
log.Error("Error clearing invalid lock data from db", "err", err)
92+
}
93+
}
94+
8195
order, list, err := rawdb.ReadFutureMilestoneList(db)
8296
if err != nil {
8397
order = make([]uint64, 0)

eth/downloader/whitelist/service_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package whitelist
44
import (
55
"errors"
66
"fmt"
7+
"math"
78
"math/big"
89
"reflect"
910
"sort"
@@ -1533,3 +1534,61 @@ func TestForkCorrectness(t *testing.T) {
15331534
require.Equal(t, chain3[1].Number.Uint64(), s.lastValidForkBlock, "expected last known valid block to be unchanged")
15341535
})
15351536
}
1537+
1538+
// TestNewServiceDiscardsInvalidLockedMilestone verifies that NewService detects and clears
1539+
// a LockedMilestoneNumber that is beyond the safe range when loading from DB.
1540+
func TestNewServiceDiscardsInvalidLockedMilestone(t *testing.T) {
1541+
db := rawdb.NewMemoryDatabase()
1542+
1543+
// Write a locked milestone with an unreachable block number to the DB,
1544+
// simulating a previously corrupted state.
1545+
lockedIDs := map[string]struct{}{"bad-id": {}}
1546+
err := rawdb.WriteLockField(db, true, math.MaxUint64, common.Hash{0xAB}, lockedIDs)
1547+
require.NoError(t, err)
1548+
1549+
// NewService should detect the out-of-range value and clear it.
1550+
svc := NewService(db, false, 0)
1551+
1552+
m, ok := svc.milestoneService.(*milestone)
1553+
require.True(t, ok)
1554+
1555+
m.finality.RLock()
1556+
defer m.finality.RUnlock()
1557+
1558+
require.False(t, m.Locked, "expected Locked to be false after discarding invalid milestone")
1559+
require.Equal(t, uint64(0), m.LockedMilestoneNumber, "expected LockedMilestoneNumber to be reset to 0")
1560+
require.Equal(t, common.Hash{}, m.LockedMilestoneHash, "expected LockedMilestoneHash to be reset")
1561+
require.Empty(t, m.LockedMilestoneIDs, "expected LockedMilestoneIDs to be cleared")
1562+
1563+
// Verify the corrected state was persisted to DB.
1564+
locked, lockedNum, lockedHash, ids, err := rawdb.ReadLockField(db)
1565+
require.NoError(t, err)
1566+
require.False(t, locked)
1567+
require.Equal(t, uint64(0), lockedNum)
1568+
require.Equal(t, common.Hash{}, lockedHash)
1569+
require.Empty(t, ids)
1570+
}
1571+
1572+
// TestNewServicePreservesValidLockedMilestone verifies that NewService does not
1573+
// interfere with a legitimate locked milestone stored in DB.
1574+
func TestNewServicePreservesValidLockedMilestone(t *testing.T) {
1575+
db := rawdb.NewMemoryDatabase()
1576+
1577+
expectedHash := common.Hash{0x42}
1578+
lockedIDs := map[string]struct{}{"valid-id": {}}
1579+
err := rawdb.WriteLockField(db, true, 1000, expectedHash, lockedIDs)
1580+
require.NoError(t, err)
1581+
1582+
svc := NewService(db, false, 0)
1583+
1584+
m, ok := svc.milestoneService.(*milestone)
1585+
require.True(t, ok)
1586+
1587+
m.finality.RLock()
1588+
defer m.finality.RUnlock()
1589+
1590+
require.True(t, m.Locked, "expected Locked to remain true for valid milestone")
1591+
require.Equal(t, uint64(1000), m.LockedMilestoneNumber)
1592+
require.Equal(t, expectedHash, m.LockedMilestoneHash)
1593+
require.Len(t, m.LockedMilestoneIDs, 1)
1594+
}

0 commit comments

Comments
 (0)