Skip to content

Commit 2eed8ef

Browse files
libotonyotherview
andauthored
bft: use post-housekeep state for PoS threshold (#1630)
Co-authored-by: otherview <otherview@gmail.com>
1 parent c94330a commit 2eed8ef

12 files changed

Lines changed: 872 additions & 124 deletions

File tree

bft/casts.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,14 @@ func (engine *Engine) newCasts() error {
3434
return err
3535
}
3636

37-
header := sum.Header
38-
signer, _ := header.Signer()
37+
signer, _ := sum.Header.Signer()
3938
if signer == engine.master {
40-
st, err := engine.computeState(header)
39+
st, err := engine.computeState(sum)
4140
if err != nil {
4241
return err
4342
}
4443

45-
checkpoint, err := chain.GetBlockID(getCheckPoint(header.Number()))
44+
checkpoint, err := chain.GetBlockID(getCheckPoint(sum.Header.Number()))
4645
if err != nil {
4746
return err
4847
}

bft/engine.go

Lines changed: 170 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/vechain/thor/v2/cache"
1616
"github.com/vechain/thor/v2/chain"
1717
"github.com/vechain/thor/v2/kv"
18+
"github.com/vechain/thor/v2/log"
1819
"github.com/vechain/thor/v2/muxdb"
1920
"github.com/vechain/thor/v2/state"
2021
"github.com/vechain/thor/v2/thor"
@@ -24,14 +25,19 @@ import (
2425

2526
const dataStoreName = "bft.engine"
2627

27-
var finalizedKey = []byte("finalized")
28+
var (
29+
finalizedKey = []byte("finalized")
30+
logger = log.WithContext("pkg", "bft")
31+
)
2832

2933
type Committer interface {
3034
Finalized() thor.Bytes32
3135
Justified() (thor.Bytes32, error)
3236
Accepts(parentID thor.Bytes32) (bool, error)
33-
Select(header *block.Header) (bool, error)
34-
CommitBlock(header *block.Header, isPacking bool) error
37+
// Select chooses between the given block and the current best; header +
38+
// conflicts address the block's post-housekeep state before it is persisted.
39+
Select(header *block.Header, conflicts uint32) (bool, error)
40+
CommitBlock(header *block.Header, conflicts uint32, isPacking bool) error
3541
ShouldVote(parentID thor.Bytes32) (bool, error)
3642
}
3743

@@ -85,6 +91,118 @@ func NewEngine(repo *chain.Repository, mainDB *muxdb.MuxDB, forkConfig *thor.For
8591
return &engine, nil
8692
}
8793

94+
// Resync recomputes and persists BFT quality for every storePoint from the first
95+
// post-HAYABUSA+HayabusaTP epoch to head, correcting values left out of sync by the
96+
// pre-housekeep threshold. Idempotent via currentResyncVersion.
97+
//
98+
// onProgress, if non-nil, is called with (0, total) before the first storePoint and
99+
// (done, total) after each; the final call has done == total.
100+
func (engine *Engine) Resync(onProgress func(done, total uint32)) error {
101+
version, err := loadResyncVersion(engine.data)
102+
if err != nil {
103+
return errors.Wrap(err, "load resync version")
104+
}
105+
if version >= currentResyncVersion {
106+
return nil
107+
}
108+
109+
logger.Info("running bft resync", "version", currentResyncVersion)
110+
111+
// Drop all caches so stale entries can't leak into recomputation. Quality cache
112+
// included: relying on in-order write-back is fragile and a cold cache is cheap.
113+
engine.caches.state.Purge()
114+
engine.caches.quality.Purge()
115+
engine.caches.justifier = cache.NewPrioCache(16)
116+
117+
head := engine.repo.BestBlockSummary()
118+
headNum := head.Header.Number()
119+
120+
// PoS vote weights apply after HAYABUSA+HayabusaTP(); start at that block's
121+
// storePoint. Covering one pre-PoS epoch is harmless — quality is unchanged.
122+
firstPosBlock := engine.forkConfig.HAYABUSA + thor.HayabusaTP()
123+
if firstPosBlock > headNum {
124+
if err := saveResyncVersion(engine.data, currentResyncVersion); err != nil {
125+
return errors.Wrap(err, "save resync version")
126+
}
127+
return nil
128+
}
129+
130+
start := getStorePoint(firstPosBlock)
131+
total := (headNum-start)/thor.EpochLength() + 1
132+
133+
chain := engine.repo.NewChain(head.Header.ID())
134+
135+
if onProgress != nil {
136+
onProgress(0, total)
137+
}
138+
139+
var done, mismatches uint32
140+
for sp := start; sp <= headNum; sp += thor.EpochLength() {
141+
storeID, err := chain.GetBlockID(sp)
142+
if err != nil {
143+
return errors.Wrapf(err, "get block id at storePoint %d", sp)
144+
}
145+
146+
sum, err := engine.repo.GetBlockSummary(storeID)
147+
if err != nil {
148+
return errors.Wrapf(err, "get block summary at storePoint %d", sp)
149+
}
150+
151+
engine.caches.state.Remove(storeID)
152+
153+
prevQuality, prevErr := loadQuality(engine.data, storeID)
154+
hasPrev := prevErr == nil
155+
if prevErr != nil && !engine.data.IsNotFound(prevErr) {
156+
return errors.Wrapf(prevErr, "load prev quality at storePoint %d", sp)
157+
}
158+
159+
st, err := engine.computeState(sum)
160+
if err != nil {
161+
return errors.Wrapf(err, "compute state at storePoint %d", sp)
162+
}
163+
164+
if hasPrev && prevQuality != st.Quality {
165+
mismatches++
166+
logger.Warn("bft resync: quality mismatch",
167+
"storePoint", sp, "id", storeID, "stored", prevQuality, "recomputed", st.Quality)
168+
}
169+
170+
if err := saveQuality(engine.data, storeID, st.Quality); err != nil {
171+
return errors.Wrapf(err, "save quality at storePoint %d", sp)
172+
}
173+
engine.caches.quality.Add(storeID, st.Quality)
174+
175+
// Advance finalized forward only. Skip when storeID is in finalized's epoch
176+
// or earlier: findCheckpointByQuality would underflow or miss the target
177+
// (already absorbed by the current finalized).
178+
if st.Committed && st.Quality > 1 && getCheckPoint(block.Number(storeID)) > block.Number(engine.Finalized()) {
179+
id, err := engine.findCheckpointByQuality(st.Quality-1, engine.Finalized(), storeID)
180+
if err != nil {
181+
return errors.Wrapf(err, "find checkpoint by quality at storePoint %d", sp)
182+
}
183+
if block.Number(id) > block.Number(engine.Finalized()) {
184+
if err := engine.data.Put(finalizedKey, id[:]); err != nil {
185+
return err
186+
}
187+
engine.finalized.Store(id)
188+
}
189+
}
190+
191+
done++
192+
if onProgress != nil {
193+
onProgress(done, total)
194+
}
195+
logger.Debug("bft resync: storePoint processed", "num", sp, "quality", st.Quality)
196+
}
197+
198+
if err := saveResyncVersion(engine.data, currentResyncVersion); err != nil {
199+
return errors.Wrap(err, "save resync version")
200+
}
201+
202+
logger.Info("bft resync done", "version", currentResyncVersion, "total", total, "mismatches", mismatches)
203+
return nil
204+
}
205+
88206
// Finalized returns the finalized checkpoint.
89207
func (engine *Engine) Finalized() thor.Bytes32 {
90208
return engine.finalized.Load().(thor.Bytes32)
@@ -150,13 +268,15 @@ func (engine *Engine) Accepts(parentID thor.Bytes32) (bool, error) {
150268
}
151269

152270
// Select selects between the new block and the current best, return true if new one is better.
153-
func (engine *Engine) Select(header *block.Header) (bool, error) {
154-
newSt, err := engine.computeState(header)
271+
// header + conflicts address the new block's post-housekeep state before it is persisted.
272+
func (engine *Engine) Select(header *block.Header, conflicts uint32) (bool, error) {
273+
newSum := &chain.BlockSummary{Header: header, Conflicts: conflicts}
274+
newSt, err := engine.computeState(newSum)
155275
if err != nil {
156276
return false, err
157277
}
158278

159-
best := engine.repo.BestBlockSummary().Header
279+
best := engine.repo.BestBlockSummary()
160280
bestSt, err := engine.computeState(best)
161281
if err != nil {
162282
return false, err
@@ -166,14 +286,19 @@ func (engine *Engine) Select(header *block.Header) (bool, error) {
166286
return newSt.Quality > bestSt.Quality, nil
167287
}
168288

169-
return header.BetterThan(best), nil
289+
return header.BetterThan(best.Header), nil
170290
}
171291

172292
// CommitBlock commits bft state to storage.
173-
func (engine *Engine) CommitBlock(header *block.Header, isPacking bool) error {
293+
func (engine *Engine) CommitBlock(header *block.Header, conflicts uint32, isPacking bool) error {
294+
// The block is already persisted by this point (CommitBlock runs after
295+
// repo.AddBlock); header + conflicts address its post-housekeep state the
296+
// same way Select does, without a repo lookup.
297+
sum := &chain.BlockSummary{Header: header, Conflicts: conflicts}
298+
174299
// save quality and finalized at the end of each round
175300
if getStorePoint(header.Number()) == header.Number() {
176-
state, err := engine.computeState(header)
301+
state, err := engine.computeState(sum)
177302
if err != nil {
178303
return err
179304
}
@@ -199,7 +324,7 @@ func (engine *Engine) CommitBlock(header *block.Header, isPacking bool) error {
199324

200325
// mark voted if packing
201326
if isPacking {
202-
state, err := engine.computeState(header)
327+
state, err := engine.computeState(sum)
203328
if err != nil {
204329
return err
205330
}
@@ -233,7 +358,7 @@ func (engine *Engine) ShouldVote(parentID thor.Bytes32) (bool, error) {
233358
if err != nil {
234359
return false, err
235360
}
236-
st, err := engine.computeState(sum.Header)
361+
st, err := engine.computeState(sum)
237362
if err != nil {
238363
return false, err
239364
}
@@ -290,8 +415,11 @@ func (engine *Engine) ShouldVote(parentID thor.Bytes32) (bool, error) {
290415
return true, nil
291416
}
292417

293-
// computeState computes the bft state regarding the given block header to the closest checkpoint.
294-
func (engine *Engine) computeState(header *block.Header) (*bftState, error) {
418+
// computeState computes the bft state for sum back to the closest checkpoint.
419+
// sum may describe a not-yet-persisted block (during bft.Select); Header + Conflicts
420+
// still address its post-housekeep state.
421+
func (engine *Engine) computeState(sum *chain.BlockSummary) (*bftState, error) {
422+
header := sum.Header
295423
if cached, ok := engine.caches.state.Get(header.ID()); ok {
296424
return cached.(*bftState), nil
297425
}
@@ -311,57 +439,47 @@ func (engine *Engine) computeState(header *block.Header) (*bftState, error) {
311439
} else {
312440
// create a new vote set if cache missed or new block is checkpoint
313441
var err error
314-
js, err = engine.newJustifier(header.ParentID())
442+
js, err = engine.newJustifier(sum)
315443
if err != nil {
316444
return nil, errors.Wrap(err, "failed to create vote set")
317445
}
318446
end = js.checkpoint
319447
}
320448

449+
// Weights only change at Housekeep, so weighing every vote against the
450+
// checkpoint's post-housekeep state matches each block's parent state — except
451+
// the checkpoint block itself, whose parent state holds stale weights.
452+
var weightState *state.State
453+
if js.posActive {
454+
weightState = engine.stater.NewState(js.weightRoot)
455+
}
456+
321457
h := header
322458
for h.Number() >= engine.forkConfig.FINALITY {
323459
signer, _ := h.Signer()
324460

325-
var parentBlockSummary *chain.BlockSummary
326-
var err error
327-
328-
if h.Number() > engine.forkConfig.HAYABUSA+thor.HayabusaTP() {
329-
parentBlockSummary, err = engine.repo.GetBlockSummary(h.ParentID())
461+
var weight uint64
462+
if js.posActive {
463+
validator, err := builtin.Staker.Native(weightState).GetValidation(signer)
330464
if err != nil {
331465
return nil, err
332466
}
333-
state := engine.stater.NewState(parentBlockSummary.Root())
334-
staker := builtin.Staker.Native(state)
335-
336-
var weight uint64
337-
if posActive, _ := staker.IsPoSActive(); posActive {
338-
// PoS is active, get validator weight
339-
validator, err := staker.GetValidation(signer)
340-
if err != nil {
341-
return nil, err
342-
}
343-
if validator == nil {
344-
return nil, errors.New("validator not found")
345-
}
346-
weight = validator.Weight
467+
468+
if validator == nil {
469+
return nil, errors.Errorf("signer %s absent from committee at block %d", signer, h.Number())
347470
}
348-
// If PoS is not active or error occurred, weight remains 0
349-
js.AddBlock(signer, h.COM(), weight)
350-
} else {
351-
js.AddBlock(signer, h.COM(), 0)
471+
weight = validator.Weight
352472
}
473+
js.AddBlock(signer, h.COM(), weight)
353474

354475
if h.Number() <= end {
355476
break
356477
}
357478

358-
if parentBlockSummary == nil {
359-
parentBlockSummary, err = engine.repo.GetBlockSummary(h.ParentID())
360-
if err != nil {
361-
return nil, err
362-
}
479+
parentBlockSummary, err := engine.repo.GetBlockSummary(h.ParentID())
480+
if err != nil {
481+
return nil, err
363482
}
364-
365483
h = parentBlockSummary.Header
366484
}
367485

@@ -381,6 +499,11 @@ func (engine *Engine) findCheckpointByQuality(target uint32, finalized, headID t
381499
}
382500
}()
383501

502+
// Guard uint32 underflow below: callers must search forward from finalized.
503+
if block.Number(headID) < block.Number(finalized) {
504+
return thor.Bytes32{}, errors.New("headID precedes finalized")
505+
}
506+
384507
searchStart := block.Number(finalized)
385508
if searchStart == 0 {
386509
searchStart = getCheckPoint(engine.forkConfig.FINALITY)
@@ -498,11 +621,11 @@ func (e *mockedEngine) Accepts(parentID thor.Bytes32) (bool, error) {
498621
return true, nil
499622
}
500623

501-
func (e *mockedEngine) Select(header *block.Header) (bool, error) {
624+
func (e *mockedEngine) Select(_ *block.Header, _ uint32) (bool, error) {
502625
return true, nil
503626
}
504627

505-
func (e *mockedEngine) CommitBlock(header *block.Header, isPacking bool) error {
628+
func (e *mockedEngine) CommitBlock(_ *block.Header, _ uint32, _ bool) error {
506629
return nil
507630
}
508631

@@ -606,13 +729,13 @@ func (e *soloMockedEngine) Accepts(parentID thor.Bytes32) (bool, error) {
606729

607730
// Select always returns true in solo mode since there's no fork choice rule
608731
// needed - the single node always extends its own chain.
609-
func (e *soloMockedEngine) Select(header *block.Header) (bool, error) {
732+
func (e *soloMockedEngine) Select(_ *block.Header, _ uint32) (bool, error) {
610733
return true, nil
611734
}
612735

613736
// CommitBlock is a no-op in solo mode since there's no BFT voting or
614737
// commitment protocol needed.
615-
func (e *soloMockedEngine) CommitBlock(header *block.Header, isPacking bool) error {
738+
func (e *soloMockedEngine) CommitBlock(_ *block.Header, _ uint32, _ bool) error {
616739
return nil
617740
}
618741

0 commit comments

Comments
 (0)