From d23b7560adae4396a5383855c4a70797746143f7 Mon Sep 17 00:00:00 2001 From: Andrew Nguyen Date: Wed, 2 Sep 2026 16:14:32 -0400 Subject: [PATCH] fix: bootstrap first consensus block (#583) --- controller/block.go | 24 ++++++++++++++---------- fsm/state.go | 13 +++++++++---- fsm/state_test.go | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 14 deletions(-) diff --git a/controller/block.go b/controller/block.go index 7aafb31252..a8ef183b97 100644 --- a/controller/block.go +++ b/controller/block.go @@ -141,13 +141,22 @@ func (c *Controller) ProduceProposal(evidence *bft.ByzantineEvidence, vdf *crypt if e != nil { return false, e } - // load the previous quorum height quorum certificate from the indexer - lastCertificate, e := c.FSM.LoadCertificateHashesOnly(c.FSM.Height() - 1) - if e != nil { - return false, e + // The first consensus block has no prior certificate or indexed block. + // Later heights must resolve both predecessors from the indexer. + var lastCertificate *lib.QuorumCertificate + lastBlock := &lib.BlockResult{BlockHeader: new(lib.BlockHeader)} + if c.FSM.Height() > 1 { + lastCertificate, e = c.FSM.LoadCertificateHashesOnly(c.FSM.Height() - 1) + if e != nil { + return false, e + } + lastBlock, e = c.FSM.LoadBlock(c.FSM.Height() - 1) + if e != nil { + return false, e + } } // validate the verifiable delay function from the bft module - if vdf != nil { + if c.FSM.Height() > 1 && vdf != nil { // if the verifiable delay function is NOT valid for using the last block hash if !crypto.VerifyVDF(lastCertificate.BlockHash, vdf.Output, vdf.Proof, int(vdf.Iterations)) { // nullify the bad VDF @@ -156,11 +165,6 @@ func (c *Controller) ProduceProposal(evidence *bft.ByzantineEvidence, vdf *crypt c.log.Error(lib.ErrInvalidVDF().Error()) } } - // load the last block from the indexer - lastBlock, e := c.FSM.LoadBlock(c.FSM.Height() - 1) - if e != nil { - return false, e - } // replace the VDF and last certificate in the header p.Block.BlockHeader.LastQuorumCertificate, p.Block.BlockHeader.Vdf = lastCertificate, vdf p.Block.BlockHeader.TotalVdfIterations = vdf.GetIterations() + lastBlock.BlockHeader.TotalVdfIterations diff --git a/fsm/state.go b/fsm/state.go index 137211f267..f31aa995d2 100644 --- a/fsm/state.go +++ b/fsm/state.go @@ -221,10 +221,15 @@ func (s *StateMachine) ApplyBlock(ctx context.Context, b *lib.Block, allowOversi if !rootStartTime.IsZero() { s.Metrics.UpdateFSMApplyBlockRootTime(rootStartTime) } - // load the last block from the indexer - lastBlock, err := s.LoadBlock(s.height - 1) - if err != nil { - return nil, nil, err + // The state committed from genesis is version 1, but there is no indexed + // block yet. Treat the predecessor of the first consensus block as an empty + // genesis boundary; later heights must always resolve their prior block. + lastBlock := &lib.BlockResult{BlockHeader: new(lib.BlockHeader)} + if s.height > 1 { + lastBlock, err = s.LoadBlock(s.height - 1) + if err != nil { + return nil, nil, err + } } // get the transaction root transactionRoot, err := r.TransactionRoot() diff --git a/fsm/state_test.go b/fsm/state_test.go index a6c4f50a11..22e03fa4cf 100644 --- a/fsm/state_test.go +++ b/fsm/state_test.go @@ -1,6 +1,7 @@ package fsm import ( + "bytes" "context" "encoding/hex" "encoding/json" @@ -287,6 +288,43 @@ func TestApplyBlock(t *testing.T) { } } +func TestApplyFirstBlockWithoutIndexedPredecessor(t *testing.T) { + log := lib.NewDefaultLogger() + db, err := store.NewStoreInMemory(log) + require.NoError(t, err) + defer db.Close() + sm := StateMachine{ + store: db, + ProtocolVersion: CurrentProtocolVersion, + NetworkID: 1, + height: 1, + slashTracker: NewSlashTracker(), + events: new(lib.EventsTracker), + Config: lib.Config{ + MainConfig: lib.DefaultMainConfig(), + StateMachineConfig: lib.DefaultStateMachineConfig(), + }, + log: log, + cache: &cache{ + accounts: make(map[uint64]*Account), + pools: make(map[uint64]*Pool), + }, + } + require.NoError(t, sm.SetParams(DefaultParams())) + + block := &lib.Block{BlockHeader: &lib.BlockHeader{ + Time: uint64(time.Now().UnixMicro()), + ProposerAddress: newTestAddressBytes(t), + }} + header, result, err := sm.ApplyBlock(context.Background(), block, false) + require.NoError(t, err) + require.Empty(t, result.Failed) + require.EqualValues(t, 1, header.Height) + require.Zero(t, header.TotalTxs) + require.Zero(t, header.TotalVdfIterations) + require.Equal(t, bytes.Repeat([]byte("F"), crypto.HashSize), []byte(header.LastBlockHash)) +} + func TestApplyTransactions_DoesNotReturnCheckErrors(t *testing.T) { sm := newTestStateMachine(t) kg := newTestKeyGroup(t)