@@ -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
2526const dataStoreName = "bft.engine"
2627
27- var finalizedKey = []byte ("finalized" )
28+ var (
29+ finalizedKey = []byte ("finalized" )
30+ logger = log .WithContext ("pkg" , "bft" )
31+ )
2832
2933type 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.
89207func (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