mirror of
https://github.com/luxfi/node.git
synced 2026-10-30 15:24:05 +00:00
816 lines
26 KiB
Go
816 lines
26 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package chain
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/luxfi/metric"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
hash "github.com/luxfi/crypto/hash"
|
|
"github.com/luxfi/database"
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/vm/chain"
|
|
"github.com/luxfi/vm/chain/blocktest"
|
|
)
|
|
|
|
const (
|
|
defaultBlockCacheSize = 256
|
|
)
|
|
|
|
var (
|
|
errCantBuildBlock = errors.New("can't build new block")
|
|
errVerify = errors.New("verify failed")
|
|
errAccept = errors.New("accept failed")
|
|
errReject = errors.New("reject failed")
|
|
errUnexpectedBlockBytes = errors.New("unexpected block bytes")
|
|
)
|
|
|
|
// testBlockAdapter wraps a blocktest.Block to implement the Block interface
|
|
type testBlockAdapter struct {
|
|
*blocktest.Block
|
|
verifyErr error
|
|
acceptErr error
|
|
rejectErr error
|
|
}
|
|
|
|
// Override Verify to handle the test error
|
|
func (b *testBlockAdapter) Verify(ctx context.Context) error {
|
|
if b.verifyErr != nil {
|
|
return b.verifyErr
|
|
}
|
|
return b.Block.Verify(ctx)
|
|
}
|
|
|
|
// Override Accept to handle the test error
|
|
func (b *testBlockAdapter) Accept(ctx context.Context) error {
|
|
if b.acceptErr != nil {
|
|
return b.acceptErr
|
|
}
|
|
return b.Block.Accept(ctx)
|
|
}
|
|
|
|
// Override Reject to handle the test error
|
|
func (b *testBlockAdapter) Reject(ctx context.Context) error {
|
|
if b.rejectErr != nil {
|
|
return b.rejectErr
|
|
}
|
|
return b.Block.Reject(ctx)
|
|
}
|
|
|
|
// NewTestBlock returns a new test block with height, bytes, and ID derived from [i]
|
|
// and using [parentID] as the parent block ID
|
|
func NewTestBlock(i uint64, parentID ids.ID) *blocktest.Block {
|
|
b := []byte{byte(i)}
|
|
id := hash.ComputeHash256Array(b)
|
|
return &blocktest.Block{
|
|
IDV: id,
|
|
HeightV: i,
|
|
ParentV: parentID,
|
|
BytesV: b,
|
|
StatusV: blocktest.Unknown,
|
|
}
|
|
}
|
|
|
|
// NewTestBlocks generates [numBlocks] consecutive blocks
|
|
func NewTestBlocks(numBlocks uint64) []*blocktest.Block {
|
|
blks := make([]*blocktest.Block, 0, numBlocks)
|
|
parentID := ids.Empty
|
|
for i := uint64(0); i < numBlocks; i++ {
|
|
blks = append(blks, NewTestBlock(i, parentID))
|
|
parent := blks[len(blks)-1]
|
|
parentID = parent.ID()
|
|
}
|
|
|
|
return blks
|
|
}
|
|
|
|
func createInternalBlockFuncs(blks []*blocktest.Block) (
|
|
func(ctx context.Context, blkID ids.ID) (chain.Block, error),
|
|
func(ctx context.Context, b []byte) (chain.Block, error),
|
|
) {
|
|
blkMap := make(map[ids.ID]*blocktest.Block)
|
|
blkBytesMap := make(map[string]*blocktest.Block)
|
|
adapterMap := make(map[ids.ID]*testBlockAdapter)
|
|
|
|
// Create adapters for all blocks upfront
|
|
for _, blk := range blks {
|
|
blkMap[blk.ID()] = blk
|
|
blkBytes := blk.Bytes()
|
|
blkBytesMap[string(blkBytes)] = blk
|
|
adapterMap[blk.ID()] = &testBlockAdapter{Block: blk}
|
|
}
|
|
|
|
getBlock := func(_ context.Context, id ids.ID) (chain.Block, error) {
|
|
blk, ok := blkMap[id]
|
|
if !ok || blk.StatusV == blocktest.Unknown {
|
|
return nil, database.ErrNotFound
|
|
}
|
|
|
|
return adapterMap[id], nil
|
|
}
|
|
|
|
parseBlk := func(_ context.Context, b []byte) (chain.Block, error) {
|
|
blk, ok := blkBytesMap[string(b)]
|
|
if !ok {
|
|
return nil, fmt.Errorf("%w: %x", errUnexpectedBlockBytes, b)
|
|
}
|
|
if blk.StatusV == blocktest.Unknown {
|
|
blk.StatusV = blocktest.Processing
|
|
}
|
|
blkMap[blk.ID()] = blk
|
|
|
|
return adapterMap[blk.ID()], nil
|
|
}
|
|
|
|
return getBlock, parseBlk
|
|
}
|
|
|
|
func cantBuildBlock(context.Context) (chain.Block, error) {
|
|
return nil, errCantBuildBlock
|
|
}
|
|
|
|
// checkProcessingBlock checks that [blk] is of the correct type and is
|
|
// correctly uniquified when calling GetBlock and ParseBlock.
|
|
func checkProcessingBlock(t *testing.T, s *State, blk chain.Block) {
|
|
require := require.New(t)
|
|
|
|
require.IsType(&BlockWrapper{}, blk)
|
|
|
|
parsedBlk, err := s.ParseBlock(context.Background(), blk.Bytes())
|
|
require.NoError(err)
|
|
require.Equal(blk.ID(), parsedBlk.ID())
|
|
require.Equal(blk.Bytes(), parsedBlk.Bytes())
|
|
require.Equal(blk, parsedBlk)
|
|
|
|
getBlk, err := s.GetBlock(context.Background(), blk.ID())
|
|
require.NoError(err)
|
|
require.Equal(parsedBlk, getBlk)
|
|
}
|
|
|
|
// checkDecidedBlock asserts that [blk] is returned with the correct status by ParseBlock
|
|
// and GetBlock.
|
|
// expectedStatus should be either Accepted or Rejected.
|
|
func checkDecidedBlock(t *testing.T, s *State, blk chain.Block, cached bool) {
|
|
require := require.New(t)
|
|
|
|
require.IsType(&BlockWrapper{}, blk)
|
|
|
|
if cached {
|
|
_, ok := s.decidedBlocks.Get(blk.ID())
|
|
require.True(ok)
|
|
}
|
|
|
|
parsedBlk, err := s.ParseBlock(context.Background(), blk.Bytes())
|
|
require.NoError(err)
|
|
require.Equal(blk.ID(), parsedBlk.ID())
|
|
require.Equal(blk.Bytes(), parsedBlk.Bytes())
|
|
|
|
_, ok := s.decidedBlocks.Get(blk.ID())
|
|
require.True(ok)
|
|
|
|
// If the block should be in the cache, assert that the returned block is identical to [blk]
|
|
if cached {
|
|
require.Equal(blk, parsedBlk)
|
|
}
|
|
|
|
getBlk, err := s.GetBlock(context.Background(), blk.ID())
|
|
require.NoError(err)
|
|
require.Equal(blk.ID(), getBlk.ID())
|
|
require.Equal(blk.Bytes(), getBlk.Bytes())
|
|
|
|
// Since ParseBlock should have triggered a cache hit, assert that the block is identical
|
|
// to the parsed block.
|
|
require.Equal(parsedBlk, getBlk)
|
|
}
|
|
|
|
func TestState(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
testBlks := NewTestBlocks(3)
|
|
genesisBlock := testBlks[0]
|
|
genesisBlock.StatusV = blocktest.Accepted
|
|
blk1 := testBlks[1]
|
|
blk2 := testBlks[2]
|
|
// Need to create a block with a different bytes and hash here
|
|
// to generate a conflict with blk2
|
|
blk3 := NewTestBlock(3, blk1.ID())
|
|
testBlks = append(testBlks, blk3)
|
|
|
|
getBlock, parseBlock := createInternalBlockFuncs(testBlks)
|
|
chainState := NewState(&Config{
|
|
DecidedCacheSize: defaultBlockCacheSize,
|
|
MissingCacheSize: defaultBlockCacheSize,
|
|
UnverifiedCacheSize: defaultBlockCacheSize,
|
|
BytesToIDCacheSize: defaultBlockCacheSize,
|
|
LastAcceptedBlock: genesisBlock,
|
|
GetBlock: getBlock,
|
|
UnmarshalBlock: parseBlock,
|
|
BuildBlock: cantBuildBlock,
|
|
})
|
|
|
|
lastAccepted, err := chainState.LastAccepted(context.Background())
|
|
require.NoError(err)
|
|
require.Equal(genesisBlock.ID(), lastAccepted)
|
|
|
|
wrappedGenesisBlk, err := chainState.GetBlock(context.Background(), genesisBlock.ID())
|
|
require.NoError(err)
|
|
|
|
// Check that a cache miss on a block is handled correctly
|
|
_, err = chainState.GetBlock(context.Background(), blk1.ID())
|
|
require.ErrorIs(err, database.ErrNotFound)
|
|
|
|
// Parse and verify blk1 and blk2
|
|
parsedBlk1, err := chainState.ParseBlock(context.Background(), blk1.Bytes())
|
|
require.NoError(err)
|
|
require.NoError(parsedBlk1.Verify(context.Background()))
|
|
|
|
parsedBlk2, err := chainState.ParseBlock(context.Background(), blk2.Bytes())
|
|
require.NoError(err)
|
|
require.NoError(parsedBlk2.Verify(context.Background()))
|
|
|
|
// Check that the verified blocks have been placed in the processing map
|
|
require.Len(chainState.verifiedBlocks, 2)
|
|
|
|
parsedBlk3, err := chainState.ParseBlock(context.Background(), blk3.Bytes())
|
|
require.NoError(err)
|
|
getBlk3, err := chainState.GetBlock(context.Background(), blk3.ID())
|
|
require.NoError(err)
|
|
require.Equal(parsedBlk3.ID(), getBlk3.ID(), "State GetBlock returned the wrong block")
|
|
|
|
// Check that parsing blk3 does not add it to processing blocks since it has
|
|
// not been verified.
|
|
require.Len(chainState.verifiedBlocks, 2)
|
|
|
|
require.NoError(parsedBlk3.Verify(context.Background()))
|
|
// Check that blk3 has been added to processing blocks.
|
|
require.Len(chainState.verifiedBlocks, 3)
|
|
|
|
// Decide the blocks and ensure they are removed from the processing blocks map
|
|
require.NoError(parsedBlk1.Accept(context.Background()))
|
|
require.NoError(parsedBlk2.Accept(context.Background()))
|
|
require.NoError(parsedBlk3.Reject(context.Background()))
|
|
|
|
require.Empty(chainState.verifiedBlocks)
|
|
|
|
// Check that the last accepted block was updated correctly
|
|
lastAcceptedID, err := chainState.LastAccepted(context.Background())
|
|
require.NoError(err)
|
|
require.Equal(blk2.ID(), lastAcceptedID)
|
|
require.Equal(blk2.ID(), chainState.LastAcceptedBlock().ID())
|
|
|
|
// Flush the caches to ensure decided blocks are handled correctly on cache misses.
|
|
chainState.Flush()
|
|
checkDecidedBlock(t, chainState, wrappedGenesisBlk, false)
|
|
checkDecidedBlock(t, chainState, parsedBlk1, false)
|
|
checkDecidedBlock(t, chainState, parsedBlk2, false)
|
|
// parsedBlk3 was rejected - check it exists in storage
|
|
getBlk3AfterFlush, err := chainState.GetBlock(context.Background(), blk3.ID())
|
|
require.NoError(err)
|
|
require.Equal(parsedBlk3.ID(), getBlk3AfterFlush.ID())
|
|
}
|
|
|
|
func TestBuildBlock(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
testBlks := NewTestBlocks(2)
|
|
genesisBlock := testBlks[0]
|
|
genesisBlock.StatusV = blocktest.Accepted
|
|
blk1 := testBlks[1]
|
|
|
|
getBlock, parseBlock := createInternalBlockFuncs(testBlks)
|
|
buildBlock := func(context.Context) (chain.Block, error) {
|
|
// Once the block is built, mark it as processing
|
|
blk1.StatusV = blocktest.Processing
|
|
return blk1, nil
|
|
}
|
|
|
|
chainState := NewState(&Config{
|
|
DecidedCacheSize: defaultBlockCacheSize,
|
|
MissingCacheSize: defaultBlockCacheSize,
|
|
UnverifiedCacheSize: defaultBlockCacheSize,
|
|
BytesToIDCacheSize: defaultBlockCacheSize,
|
|
LastAcceptedBlock: genesisBlock,
|
|
GetBlock: getBlock,
|
|
UnmarshalBlock: parseBlock,
|
|
BuildBlock: buildBlock,
|
|
})
|
|
|
|
builtBlk, err := chainState.BuildBlock(context.Background())
|
|
require.NoError(err)
|
|
require.Empty(chainState.verifiedBlocks)
|
|
|
|
require.NoError(builtBlk.Verify(context.Background()))
|
|
require.Len(chainState.verifiedBlocks, 1)
|
|
|
|
checkProcessingBlock(t, chainState, builtBlk)
|
|
|
|
require.NoError(builtBlk.Accept(context.Background()))
|
|
|
|
checkDecidedBlock(t, chainState, builtBlk, true)
|
|
}
|
|
|
|
func TestStateDecideBlock(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
testBlks := NewTestBlocks(4)
|
|
genesisBlock := testBlks[0]
|
|
genesisBlock.StatusV = blocktest.Accepted
|
|
badAcceptBlk := testBlks[1]
|
|
badVerifyBlk := testBlks[2]
|
|
badVerifyBlk.ErrV = errVerify
|
|
badRejectBlk := testBlks[3]
|
|
getBlock, parseBlock := createInternalBlockFuncs(testBlks)
|
|
|
|
// Custom wrapper to make badAcceptBlk fail on Accept but not Verify
|
|
originalGetBlock := getBlock
|
|
getBlock = func(ctx context.Context, id ids.ID) (chain.Block, error) {
|
|
blk, err := originalGetBlock(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if id == badAcceptBlk.ID() {
|
|
return &testBlockAdapter{Block: badAcceptBlk, acceptErr: errAccept}, nil
|
|
}
|
|
if id == badRejectBlk.ID() {
|
|
return &testBlockAdapter{Block: badRejectBlk, rejectErr: errReject}, nil
|
|
}
|
|
return blk, nil
|
|
}
|
|
|
|
originalParseBlock := parseBlock
|
|
parseBlock = func(ctx context.Context, b []byte) (chain.Block, error) {
|
|
blk, err := originalParseBlock(ctx, b)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
id := blk.ID()
|
|
if id == badAcceptBlk.ID() {
|
|
return &testBlockAdapter{Block: badAcceptBlk, acceptErr: errAccept}, nil
|
|
}
|
|
if id == badRejectBlk.ID() {
|
|
return &testBlockAdapter{Block: badRejectBlk, rejectErr: errReject}, nil
|
|
}
|
|
return blk, nil
|
|
}
|
|
|
|
chainState := NewState(&Config{
|
|
DecidedCacheSize: defaultBlockCacheSize,
|
|
MissingCacheSize: defaultBlockCacheSize,
|
|
UnverifiedCacheSize: defaultBlockCacheSize,
|
|
BytesToIDCacheSize: defaultBlockCacheSize,
|
|
LastAcceptedBlock: genesisBlock,
|
|
GetBlock: getBlock,
|
|
UnmarshalBlock: parseBlock,
|
|
BuildBlock: cantBuildBlock,
|
|
})
|
|
|
|
// Parse badVerifyBlk (which should fail verification)
|
|
badBlk, err := chainState.ParseBlock(context.Background(), badVerifyBlk.Bytes())
|
|
require.NoError(err)
|
|
err = badBlk.Verify(context.Background())
|
|
require.ErrorIs(err, errVerify)
|
|
// Ensure a block that fails verification is not marked as processing
|
|
require.Empty(chainState.verifiedBlocks)
|
|
|
|
// Ensure that an error during block acceptance is propagated correctly
|
|
badBlk, err = chainState.ParseBlock(context.Background(), badAcceptBlk.Bytes())
|
|
require.NoError(err)
|
|
require.NoError(badBlk.Verify(context.Background()))
|
|
require.Len(chainState.verifiedBlocks, 1)
|
|
|
|
err = badBlk.Accept(context.Background())
|
|
require.ErrorIs(err, errAccept)
|
|
|
|
// Ensure that an error during block reject is propagated correctly
|
|
badBlk, err = chainState.ParseBlock(context.Background(), badRejectBlk.Bytes())
|
|
require.NoError(err)
|
|
require.NoError(badBlk.Verify(context.Background()))
|
|
// Note: an error during block Accept/Reject is fatal, so it is undefined whether
|
|
// the block that failed on Accept should be removed from processing or not. We allow
|
|
// either case here to make this test more flexible.
|
|
numProcessing := len(chainState.verifiedBlocks)
|
|
require.Contains([]int{1, 2}, numProcessing)
|
|
|
|
err = badBlk.Reject(context.Background())
|
|
require.ErrorIs(err, errReject)
|
|
}
|
|
|
|
func TestStateParent(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
testBlks := NewTestBlocks(3)
|
|
genesisBlock := testBlks[0]
|
|
genesisBlock.StatusV = blocktest.Accepted
|
|
blk1 := testBlks[1]
|
|
blk2 := testBlks[2]
|
|
|
|
getBlock, parseBlock := createInternalBlockFuncs(testBlks)
|
|
chainState := NewState(&Config{
|
|
DecidedCacheSize: defaultBlockCacheSize,
|
|
MissingCacheSize: defaultBlockCacheSize,
|
|
UnverifiedCacheSize: defaultBlockCacheSize,
|
|
BytesToIDCacheSize: defaultBlockCacheSize,
|
|
LastAcceptedBlock: genesisBlock,
|
|
GetBlock: getBlock,
|
|
UnmarshalBlock: parseBlock,
|
|
BuildBlock: cantBuildBlock,
|
|
})
|
|
|
|
parsedBlk2, err := chainState.ParseBlock(context.Background(), blk2.Bytes())
|
|
require.NoError(err)
|
|
|
|
missingBlk1ID := parsedBlk2.Parent()
|
|
|
|
_, err = chainState.GetBlock(context.Background(), missingBlk1ID)
|
|
require.ErrorIs(err, database.ErrNotFound)
|
|
|
|
parsedBlk1, err := chainState.ParseBlock(context.Background(), blk1.Bytes())
|
|
require.NoError(err)
|
|
|
|
genesisBlkParentID := parsedBlk1.Parent()
|
|
genesisBlkParent, err := chainState.GetBlock(context.Background(), genesisBlkParentID)
|
|
require.NoError(err)
|
|
checkDecidedBlock(t, chainState, genesisBlkParent, true)
|
|
|
|
parentBlk1ID := parsedBlk2.Parent()
|
|
parentBlk1, err := chainState.GetBlock(context.Background(), parentBlk1ID)
|
|
require.NoError(err)
|
|
checkProcessingBlock(t, chainState, parentBlk1)
|
|
}
|
|
|
|
func TestGetBlockInternal(t *testing.T) {
|
|
require := require.New(t)
|
|
testBlks := NewTestBlocks(1)
|
|
genesisBlock := testBlks[0]
|
|
genesisBlock.StatusV = blocktest.Accepted
|
|
|
|
getBlock, parseBlock := createInternalBlockFuncs(testBlks)
|
|
chainState := NewState(&Config{
|
|
DecidedCacheSize: defaultBlockCacheSize,
|
|
MissingCacheSize: defaultBlockCacheSize,
|
|
UnverifiedCacheSize: defaultBlockCacheSize,
|
|
BytesToIDCacheSize: defaultBlockCacheSize,
|
|
LastAcceptedBlock: genesisBlock,
|
|
GetBlock: getBlock,
|
|
UnmarshalBlock: parseBlock,
|
|
BuildBlock: cantBuildBlock,
|
|
})
|
|
|
|
genesisBlockInternal := chainState.LastAcceptedBlockInternal()
|
|
// The internal block might be wrapped in an adapter
|
|
if adapter, ok := genesisBlockInternal.(*testBlockAdapter); ok {
|
|
genesisBlockInternal = adapter.Block
|
|
}
|
|
require.IsType(&blocktest.Block{}, genesisBlockInternal)
|
|
require.Equal(genesisBlock.ID(), genesisBlockInternal.ID())
|
|
|
|
blk, err := chainState.GetBlockInternal(context.Background(), genesisBlock.ID())
|
|
require.NoError(err)
|
|
|
|
// The internal block might be wrapped in an adapter
|
|
if adapter, ok := blk.(*testBlockAdapter); ok {
|
|
blk = adapter.Block
|
|
}
|
|
require.IsType(&blocktest.Block{}, blk)
|
|
require.Equal(genesisBlock.ID(), blk.ID())
|
|
}
|
|
|
|
func TestGetBlockError(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
testBlks := NewTestBlocks(2)
|
|
genesisBlock := testBlks[0]
|
|
genesisBlock.StatusV = blocktest.Accepted
|
|
blk1 := testBlks[1]
|
|
|
|
getBlock, parseBlock := createInternalBlockFuncs(testBlks)
|
|
wrappedGetBlock := func(ctx context.Context, id ids.ID) (chain.Block, error) {
|
|
blk, err := getBlock(ctx, id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("wrapping error to prevent caching miss: %w", err)
|
|
}
|
|
return blk, nil
|
|
}
|
|
chainState := NewState(&Config{
|
|
DecidedCacheSize: defaultBlockCacheSize,
|
|
MissingCacheSize: defaultBlockCacheSize,
|
|
UnverifiedCacheSize: defaultBlockCacheSize,
|
|
BytesToIDCacheSize: defaultBlockCacheSize,
|
|
LastAcceptedBlock: genesisBlock,
|
|
GetBlock: wrappedGetBlock,
|
|
UnmarshalBlock: parseBlock,
|
|
BuildBlock: cantBuildBlock,
|
|
})
|
|
|
|
_, err := chainState.GetBlock(context.Background(), blk1.ID())
|
|
require.ErrorIs(err, database.ErrNotFound)
|
|
|
|
// Update the status to Processing, so that it will be returned by the
|
|
// internal get block function.
|
|
blk1.StatusV = blocktest.Processing
|
|
blk, err := chainState.GetBlock(context.Background(), blk1.ID())
|
|
require.NoError(err)
|
|
require.Equal(blk1.ID(), blk.ID())
|
|
checkProcessingBlock(t, chainState, blk)
|
|
}
|
|
|
|
func TestParseBlockError(t *testing.T) {
|
|
testBlks := NewTestBlocks(1)
|
|
genesisBlock := testBlks[0]
|
|
genesisBlock.StatusV = blocktest.Accepted
|
|
|
|
getBlock, parseBlock := createInternalBlockFuncs(testBlks)
|
|
chainState := NewState(&Config{
|
|
DecidedCacheSize: defaultBlockCacheSize,
|
|
MissingCacheSize: defaultBlockCacheSize,
|
|
UnverifiedCacheSize: defaultBlockCacheSize,
|
|
BytesToIDCacheSize: defaultBlockCacheSize,
|
|
LastAcceptedBlock: genesisBlock,
|
|
GetBlock: getBlock,
|
|
UnmarshalBlock: parseBlock,
|
|
BuildBlock: cantBuildBlock,
|
|
})
|
|
|
|
_, err := chainState.ParseBlock(context.Background(), []byte{255})
|
|
require.ErrorIs(t, err, errUnexpectedBlockBytes)
|
|
}
|
|
|
|
func TestBuildBlockError(t *testing.T) {
|
|
testBlks := NewTestBlocks(1)
|
|
genesisBlock := testBlks[0]
|
|
genesisBlock.StatusV = blocktest.Accepted
|
|
|
|
getBlock, parseBlock := createInternalBlockFuncs(testBlks)
|
|
chainState := NewState(&Config{
|
|
DecidedCacheSize: defaultBlockCacheSize,
|
|
MissingCacheSize: defaultBlockCacheSize,
|
|
UnverifiedCacheSize: defaultBlockCacheSize,
|
|
BytesToIDCacheSize: defaultBlockCacheSize,
|
|
LastAcceptedBlock: genesisBlock,
|
|
GetBlock: getBlock,
|
|
UnmarshalBlock: parseBlock,
|
|
BuildBlock: cantBuildBlock,
|
|
})
|
|
|
|
_, err := chainState.BuildBlock(context.Background())
|
|
require.ErrorIs(t, err, errCantBuildBlock)
|
|
}
|
|
|
|
func TestMeteredCache(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
testBlks := NewTestBlocks(1)
|
|
genesisBlock := testBlks[0]
|
|
genesisBlock.StatusV = blocktest.Accepted
|
|
|
|
getBlock, parseBlock := createInternalBlockFuncs(testBlks)
|
|
|
|
// First MeteredState with its own registry
|
|
registry1 := metric.NewRegistry()
|
|
config1 := &Config{
|
|
DecidedCacheSize: defaultBlockCacheSize,
|
|
MissingCacheSize: defaultBlockCacheSize,
|
|
UnverifiedCacheSize: defaultBlockCacheSize,
|
|
BytesToIDCacheSize: defaultBlockCacheSize,
|
|
LastAcceptedBlock: genesisBlock,
|
|
GetBlock: getBlock,
|
|
UnmarshalBlock: parseBlock,
|
|
BuildBlock: cantBuildBlock,
|
|
}
|
|
_, err := NewMeteredState(registry1, config1)
|
|
require.NoError(err)
|
|
|
|
// Second MeteredState with a different registry - should succeed
|
|
registry2 := metric.NewRegistry()
|
|
config2 := &Config{
|
|
DecidedCacheSize: defaultBlockCacheSize,
|
|
MissingCacheSize: defaultBlockCacheSize,
|
|
UnverifiedCacheSize: defaultBlockCacheSize,
|
|
BytesToIDCacheSize: defaultBlockCacheSize,
|
|
LastAcceptedBlock: genesisBlock,
|
|
GetBlock: getBlock,
|
|
UnmarshalBlock: parseBlock,
|
|
BuildBlock: cantBuildBlock,
|
|
}
|
|
_, err = NewMeteredState(registry2, config2)
|
|
require.NoError(err)
|
|
}
|
|
|
|
// Test the bytesToIDCache
|
|
func TestStateBytesToIDCache(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
testBlks := NewTestBlocks(3)
|
|
genesisBlock := testBlks[0]
|
|
genesisBlock.StatusV = blocktest.Accepted
|
|
blk1 := testBlks[1]
|
|
blk2 := testBlks[2]
|
|
|
|
getBlock, parseBlock := createInternalBlockFuncs(testBlks)
|
|
buildBlock := func(context.Context) (chain.Block, error) {
|
|
require.FailNow("shouldn't have been called")
|
|
return nil, nil
|
|
}
|
|
|
|
chainState := NewState(&Config{
|
|
DecidedCacheSize: 0,
|
|
MissingCacheSize: 0,
|
|
UnverifiedCacheSize: 0,
|
|
BytesToIDCacheSize: 1 + ids.IDLen, // Size of one block
|
|
LastAcceptedBlock: &testBlockAdapter{Block: genesisBlock},
|
|
GetBlock: getBlock,
|
|
UnmarshalBlock: parseBlock,
|
|
BuildBlock: buildBlock,
|
|
})
|
|
|
|
// Shouldn't have blk1 ID to start with
|
|
_, err := chainState.GetBlock(context.Background(), blk1.ID())
|
|
require.ErrorIs(err, database.ErrNotFound)
|
|
_, ok := chainState.bytesToIDCache.Get(string(blk1.Bytes()))
|
|
require.False(ok)
|
|
|
|
// Parse blk1 from bytes
|
|
_, err = chainState.ParseBlock(context.Background(), blk1.Bytes())
|
|
require.NoError(err)
|
|
|
|
// blk1 should be in cache now
|
|
_, ok = chainState.bytesToIDCache.Get(string(blk1.Bytes()))
|
|
require.True(ok)
|
|
|
|
// Parse another block
|
|
_, err = chainState.ParseBlock(context.Background(), blk2.Bytes())
|
|
require.NoError(err)
|
|
|
|
// Should have bumped blk1 from cache
|
|
_, ok = chainState.bytesToIDCache.Get(string(blk2.Bytes()))
|
|
require.True(ok)
|
|
_, ok = chainState.bytesToIDCache.Get(string(blk1.Bytes()))
|
|
require.False(ok)
|
|
}
|
|
|
|
// TestSetLastAcceptedBlock ensures chainState's last accepted block
|
|
// can be updated by calling [SetLastAcceptedBlock].
|
|
func TestSetLastAcceptedBlock(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
testBlks := NewTestBlocks(1)
|
|
genesisBlock := testBlks[0]
|
|
genesisBlock.StatusV = blocktest.Accepted
|
|
|
|
postSetBlk1ParentID := hash.ComputeHash256Array([]byte{byte(199)})
|
|
postSetBlk1 := NewTestBlock(200, postSetBlk1ParentID)
|
|
postSetBlk2 := NewTestBlock(201, postSetBlk1.ID())
|
|
|
|
// note we do not need to parse postSetBlk1 so it is omitted here
|
|
testBlks = append(testBlks, postSetBlk2)
|
|
|
|
getBlock, parseBlock := createInternalBlockFuncs(testBlks)
|
|
chainState := NewState(&Config{
|
|
DecidedCacheSize: defaultBlockCacheSize,
|
|
MissingCacheSize: defaultBlockCacheSize,
|
|
UnverifiedCacheSize: defaultBlockCacheSize,
|
|
BytesToIDCacheSize: defaultBlockCacheSize,
|
|
LastAcceptedBlock: genesisBlock,
|
|
GetBlock: getBlock,
|
|
UnmarshalBlock: parseBlock,
|
|
BuildBlock: cantBuildBlock,
|
|
})
|
|
lastAcceptedID, err := chainState.LastAccepted(context.Background())
|
|
require.NoError(err)
|
|
require.Equal(genesisBlock.ID(), lastAcceptedID)
|
|
|
|
// call SetLastAcceptedBlock for postSetBlk1
|
|
// Need to create adapter for postSetBlk1 since it's not in the original testBlks
|
|
postSetBlk1Adapter := &testBlockAdapter{Block: postSetBlk1}
|
|
require.NoError(chainState.SetLastAcceptedBlock(postSetBlk1Adapter))
|
|
lastAcceptedID, err = chainState.LastAccepted(context.Background())
|
|
require.NoError(err)
|
|
require.Equal(postSetBlk1.ID(), lastAcceptedID)
|
|
require.Equal(postSetBlk1.ID(), chainState.LastAcceptedBlock().ID())
|
|
|
|
// ensure further blocks can be accepted
|
|
parsedpostSetBlk2, err := chainState.ParseBlock(context.Background(), postSetBlk2.Bytes())
|
|
require.NoError(err)
|
|
require.NoError(parsedpostSetBlk2.Verify(context.Background()))
|
|
require.NoError(parsedpostSetBlk2.Accept(context.Background()))
|
|
lastAcceptedID, err = chainState.LastAccepted(context.Background())
|
|
require.NoError(err)
|
|
require.Equal(postSetBlk2.ID(), lastAcceptedID)
|
|
require.Equal(postSetBlk2.ID(), chainState.LastAcceptedBlock().ID())
|
|
|
|
checkDecidedBlock(t, chainState, parsedpostSetBlk2, false)
|
|
}
|
|
|
|
func TestSetLastAcceptedBlockWithProcessingBlocksErrors(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
testBlks := NewTestBlocks(5)
|
|
genesisBlock := testBlks[0]
|
|
genesisBlock.StatusV = blocktest.Accepted
|
|
blk1 := testBlks[1]
|
|
resetBlk := testBlks[4]
|
|
|
|
getBlock, parseBlock := createInternalBlockFuncs(testBlks)
|
|
buildBlock := func(context.Context) (chain.Block, error) {
|
|
// Once the block is built, mark it as processing
|
|
genesisBlock.StatusV = blocktest.Processing
|
|
return blk1, nil
|
|
}
|
|
|
|
chainState := NewState(&Config{
|
|
DecidedCacheSize: defaultBlockCacheSize,
|
|
MissingCacheSize: defaultBlockCacheSize,
|
|
UnverifiedCacheSize: defaultBlockCacheSize,
|
|
BytesToIDCacheSize: defaultBlockCacheSize,
|
|
LastAcceptedBlock: genesisBlock,
|
|
GetBlock: getBlock,
|
|
UnmarshalBlock: parseBlock,
|
|
BuildBlock: buildBlock,
|
|
})
|
|
|
|
builtBlk, err := chainState.BuildBlock(context.Background())
|
|
require.NoError(err)
|
|
require.Empty(chainState.verifiedBlocks)
|
|
|
|
require.NoError(builtBlk.Verify(context.Background()))
|
|
require.Len(chainState.verifiedBlocks, 1)
|
|
|
|
checkProcessingBlock(t, chainState, builtBlk)
|
|
|
|
err = chainState.SetLastAcceptedBlock(&testBlockAdapter{Block: resetBlk})
|
|
require.ErrorIs(err, errSetAcceptedWithProcessing)
|
|
}
|
|
|
|
func TestStateParseTransitivelyAcceptedBlock(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
testBlks := NewTestBlocks(3)
|
|
genesisBlock := testBlks[0]
|
|
genesisBlock.StatusV = blocktest.Accepted
|
|
blk1 := testBlks[1]
|
|
blk2 := testBlks[2]
|
|
blk2.StatusV = blocktest.Accepted
|
|
|
|
getBlock, parseBlock := createInternalBlockFuncs(testBlks)
|
|
chainState := NewState(&Config{
|
|
DecidedCacheSize: defaultBlockCacheSize,
|
|
MissingCacheSize: defaultBlockCacheSize,
|
|
UnverifiedCacheSize: defaultBlockCacheSize,
|
|
BytesToIDCacheSize: defaultBlockCacheSize,
|
|
LastAcceptedBlock: blk2,
|
|
GetBlock: getBlock,
|
|
UnmarshalBlock: parseBlock,
|
|
BuildBlock: cantBuildBlock,
|
|
})
|
|
|
|
parsedBlk1, err := chainState.ParseBlock(context.Background(), blk1.Bytes())
|
|
require.NoError(err)
|
|
require.Equal(blk1.Height(), parsedBlk1.Height())
|
|
}
|
|
|
|
func TestIsProcessing(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
testBlks := NewTestBlocks(2)
|
|
genesisBlock := testBlks[0]
|
|
genesisBlock.StatusV = blocktest.Accepted
|
|
blk1 := testBlks[1]
|
|
|
|
getBlock, parseBlock := createInternalBlockFuncs(testBlks)
|
|
chainState := NewState(&Config{
|
|
DecidedCacheSize: defaultBlockCacheSize,
|
|
MissingCacheSize: defaultBlockCacheSize,
|
|
UnverifiedCacheSize: defaultBlockCacheSize,
|
|
BytesToIDCacheSize: defaultBlockCacheSize,
|
|
LastAcceptedBlock: genesisBlock,
|
|
GetBlock: getBlock,
|
|
UnmarshalBlock: parseBlock,
|
|
BuildBlock: cantBuildBlock,
|
|
})
|
|
|
|
// Parse blk1
|
|
parsedBlk1, err := chainState.ParseBlock(context.Background(), blk1.Bytes())
|
|
require.NoError(err)
|
|
|
|
// Check that it is not processing in consensus
|
|
require.False(chainState.IsProcessing(parsedBlk1.ID()))
|
|
|
|
// Verify blk1
|
|
require.NoError(parsedBlk1.Verify(context.Background()))
|
|
|
|
// Check that it is processing in consensus
|
|
require.True(chainState.IsProcessing(parsedBlk1.ID()))
|
|
|
|
// Accept blk1
|
|
require.NoError(parsedBlk1.Accept(context.Background()))
|
|
|
|
// Check that it is no longer processing in consensus
|
|
require.False(chainState.IsProcessing(parsedBlk1.ID()))
|
|
}
|