diff --git a/vms/proposervm/state_sync_block_backfilling_test.go b/vms/proposervm/state_sync_block_backfilling_test.go index e4a955017e0e..4ae55f94a8ea 100644 --- a/vms/proposervm/state_sync_block_backfilling_test.go +++ b/vms/proposervm/state_sync_block_backfilling_test.go @@ -24,6 +24,135 @@ import ( statelessblock "github.com/ava-labs/avalanchego/vms/proposervm/block" ) +// Pre Fork section +func TestBlockBackfillPreForkSuccess(t *testing.T) { + // setup VM with backfill enabled + require := require.New(t) + toEngineCh := make(chan common.Message) + innerVM, vm, fromInnerVMCh := setupBlockBackfillingVM(t, toEngineCh) + defer func() { + require.NoError(vm.Shutdown(context.Background())) + }() + + var ( + forkHeight = uint64(2000) + blkCount = 10 + startBlkHeight = uint64(100) + + // create a list of consecutive blocks and build state summary of top of them + // proBlks should all be preForkBlocks + proBlks, innerBlks = createTestBlocks(t, vm, forkHeight, blkCount, startBlkHeight) + + innerTopBlk = innerBlks[len(innerBlks)-1] + preForkTopBlk = proBlks[len(proBlks)-1] + stateSummaryHeight = innerTopBlk.Height() + 1 + ) + + stateSummary := &block.TestStateSummary{ + IDV: ids.ID{'s', 'u', 'm', 'm', 'a', 'r', 'y', 'I', 'D'}, + HeightV: stateSummaryHeight, + BytesV: []byte{'i', 'n', 'n', 'e', 'r'}, + } + innerStateSyncedBlk := &snowman.TestBlock{ + TestDecidable: choices.TestDecidable{ + IDV: ids.ID{'i', 'n', 'n', 'e', 'r', 'S', 'y', 'n', 'c', 'e', 'd'}, + }, + ParentV: innerTopBlk.ID(), + HeightV: stateSummary.Height(), + BytesV: []byte("inner state synced block"), + } + stateSummary.AcceptF = func(ctx context.Context) (block.StateSyncMode, error) { + return block.StateSyncStatic, nil + } + + ctx := context.Background() + _, err := stateSummary.Accept(ctx) + require.NoError(err) + + innerVM.LastAcceptedF = func(context.Context) (ids.ID, error) { + return innerStateSyncedBlk.ID(), nil + } + innerVM.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { + switch blkID { + case innerStateSyncedBlk.ID(): + return innerStateSyncedBlk, nil + default: + return nil, database.ErrNotFound + } + } + + fromInnerVMCh <- common.StateSyncDone + <-toEngineCh + + innerVM.BackfillBlocksEnabledF = func(ctx context.Context) (ids.ID, uint64, error) { + return innerStateSyncedBlk.ID(), innerStateSyncedBlk.Height() - 1, nil + } + innerVM.GetBlockIDAtHeightF = func(ctx context.Context, height uint64) (ids.ID, error) { + if height == innerStateSyncedBlk.Height() { + return innerStateSyncedBlk.ID(), nil + } + return ids.Empty, database.ErrNotFound + } + + blkID, _, err := vm.BackfillBlocksEnabled(ctx) + require.NoError(err) + require.Equal(preForkTopBlk.ID(), blkID) + + // Backfill some blocks + innerVM.ParseBlockF = func(_ context.Context, b []byte) (snowman.Block, error) { + for _, blk := range innerBlks { + if bytes.Equal(b, blk.Bytes()) { + return blk, nil + } + } + return nil, database.ErrNotFound + } + innerVM.GetBlockF = func(_ context.Context, blkID ids.ID) (snowman.Block, error) { + for _, blk := range innerBlks { + if blkID == blk.ID() { + return blk, nil + } + } + return nil, database.ErrNotFound + } + innerVM.GetBlockIDAtHeightF = func(ctx context.Context, height uint64) (ids.ID, error) { + for _, blk := range innerBlks { + if height == blk.Height() { + return blk.ID(), nil + } + } + return ids.Empty, database.ErrNotFound + } + innerVM.BackfillBlocksF = func(_ context.Context, b [][]byte) (ids.ID, uint64, error) { + lowestblk := innerBlks[0] + for _, blk := range innerBlks { + if blk.Height() < lowestblk.Height() { + lowestblk = blk + } + } + return lowestblk.Parent(), lowestblk.Height() - 1, nil + } + + blkBytes := make([][]byte, 0, len(proBlks)) + for _, blk := range proBlks { + blkBytes = append(blkBytes, blk.Bytes()) + } + nextBlkID, nextBlkHeight, err := vm.BackfillBlocks(ctx, blkBytes) + require.NoError(err) + require.Equal(proBlks[0].Parent(), nextBlkID) + require.Equal(proBlks[0].Height()-1, nextBlkHeight) + + // check proBlocks have been indexed + for _, blk := range proBlks { + blkID, err := vm.GetBlockIDAtHeight(ctx, blk.Height()) + require.NoError(err) + require.Equal(blk.ID(), blkID) + + _, err = vm.GetBlock(ctx, blkID) + require.NoError(err) + } +} + func TestBlockBackfillEnabledPreFork(t *testing.T) { require := require.New(t) toEngineCh := make(chan common.Message) @@ -104,6 +233,7 @@ func TestBlockBackfillEnabledPreFork(t *testing.T) { require.ErrorIs(err, block.ErrBlockBackfillingNotEnabled) } +// Post Fork section func TestBlockBackfillEnabledPostFork(t *testing.T) { require := require.New(t) toEngineCh := make(chan common.Message) diff --git a/vms/proposervm/state_syncable_vm.go b/vms/proposervm/state_syncable_vm.go index 188a72759119..bcd651d2cbbf 100644 --- a/vms/proposervm/state_syncable_vm.go +++ b/vms/proposervm/state_syncable_vm.go @@ -189,17 +189,32 @@ func (vm *VM) BackfillBlocks(ctx context.Context, blksBytes [][]byte) (ids.ID, u blks[blk.Height()] = blk } - // 2. Validate outer blocks + // 2. Validate blocks, checking that they are continguous blkHeights := maps.Keys(blks) sort.Slice(blkHeights, func(i, j int) bool { return blkHeights[i] < blkHeights[j] // sort in ascending order by heights }) - topBlk, err := vm.getBlock(ctx, vm.latestBackfilledBlock) - if err != nil { - return ids.Empty, 0, fmt.Errorf("failed retrieving latest backfilled block, %s, %w", vm.latestBackfilledBlock, err) + var ( + topBlk = blks[blkHeights[len(blkHeights)-1]] + topIdx = len(blkHeights) - 2 + ) + + // vm.latestBackfilledBlock is non nil only if proposerVM has forked + if vm.latestBackfilledBlock != ids.Empty { + latestBackfilledBlk, err := vm.getBlock(ctx, vm.latestBackfilledBlock) + if err != nil { + return ids.Empty, 0, fmt.Errorf("failed retrieving latest backfilled block, %s, %w", vm.latestBackfilledBlock, err) + } + if latestBackfilledBlk.Parent() != topBlk.ID() { + return ids.Empty, 0, fmt.Errorf("unexpected backfilled block %s, expected child' parent is %s", topBlk.ID(), latestBackfilledBlk.Parent()) + } + + topBlk = latestBackfilledBlk + topIdx = len(blkHeights) - 1 } - for i := len(blkHeights) - 1; i >= 0; i-- { + + for i := topIdx; i >= 0; i-- { blk := blks[blkHeights[i]] if topBlk.Parent() != blk.ID() { return ids.Empty, 0, fmt.Errorf("unexpected backfilled block %s, expected child' parent is %s", blk.ID(), topBlk.Parent()) @@ -272,7 +287,7 @@ func (vm *VM) nextBlockBackfillData(ctx context.Context, innerBlkHeight uint64) } - return childBlk.Parent(), innerBlkHeight, nil + return childBlk.Parent(), childBlk.Height() - 1, nil } func (vm *VM) revertBackfilledBlock(blk Block) error { diff --git a/vms/proposervm/vm.go b/vms/proposervm/vm.go index ed85d4514151..137263e1ee8d 100644 --- a/vms/proposervm/vm.go +++ b/vms/proposervm/vm.go @@ -135,6 +135,9 @@ type VM struct { stateSyncDone utils.Atomic[bool] + // latestBackfilledBlock track the latest post fork block + // indexed via block backfilling. Will be ids.Empty if proposerVM + // fork is not active latestBackfilledBlock ids.ID }