Skip to content
60 changes: 13 additions & 47 deletions adapters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"testing"
"time"

"github.com/ava-labs/simplex/avalanchego"
"github.com/ava-labs/simplex/common"
metadata "github.com/ava-labs/simplex/msm"
"github.com/ava-labs/simplex/testutil"
Expand Down Expand Up @@ -39,7 +38,7 @@ func newTestParsedBlock(num uint64, payload string) *ParsedBlock {
// and a verified but not yet indexed block at seq 5. A zero digest matches on
// seq alone, a non-zero digest must match the block's digest exactly.
func TestCachedStorageRetrieve(t *testing.T) {
cs := NewCachedStorage(NewMockStorage(t))
cs := NewCachedStorage(NewMockStorage(t, &testInnerBlockDeserializer{}))
indexedBlock := newTestParsedBlock(0, "indexed")
require.NoError(t, cs.Index(t.Context(), indexedBlock, common.Finalization{}))

Expand Down Expand Up @@ -114,7 +113,7 @@ func TestCachedStorageRetrieve(t *testing.T) {
// a zero-digest Retrieve of that seq returns the finalized block with its
// finalization, even when a verified fork at the same seq was cached.
func TestCachedStorageIndexEvictsSameSeqFork(t *testing.T) {
cs := NewCachedStorage(NewMockStorage(t))
cs := NewCachedStorage(NewMockStorage(t, &testInnerBlockDeserializer{}))
require.NoError(t, cs.Index(t.Context(), newTestParsedBlock(0, "genesis"), common.Finalization{}))

equivocatedBlock := &cachedBlock{
Expand All @@ -137,72 +136,39 @@ func TestCachedStorageIndexEvictsSameSeqFork(t *testing.T) {
// startup ends up in the instance's CachedStorage, retrievable by seq before it
// is finalized and indexed.
func TestCachedStoragePopulatedByWal(t *testing.T) {
const basePChainHeight = uint64(1)

// Four equal-weight validators; the node under test is the first.
numNodes := 4
validatorSet := make(metadata.NodeBLSMappings, numNodes)
for i := range numNodes {
validatorSet[i] = metadata.NodeBLSMapping{NodeID: avalanchego.NodeID{byte(i + 1)}, BLSKey: []byte{byte(i + 1)}, Weight: 1}
// Four equal-weight validators; only the first runs, so no quorum forms
// and the restored block stays unfinalized.
validatorSet := make(metadata.NodeBLSMappings, 4)
for i := range validatorSet {
validatorSet[i] = newBLSMapping(i + 1)
}
pChain := newTestPlatformChain(basePChainHeight, map[uint64]metadata.NodeBLSMappings{
basePChainHeight: validatorSet,
})

vm := newTestVM()
vm.pause()
cops := &testCryptoOps{}
genesisBlock := &testInnerBlock{Height_: 0, TS: time.Now(), Payload: []byte("genesis")}
storage := newStorageWithGenesis(t, genesisBlock)
nodeIDs := validatorSet.Nodes().NodeIDs()
comm := testutil.NewNoopComm(nodeIDs)
logger := testutil.MakeLogger(t, 1)
testWAL := testutil.NewTestWAL(t)

// The first Simplex block on top of the genesis block.
genesis := &ParsedBlock{StateMachineBlock: metadata.StateMachineBlock{InnerBlock: genesisBlock}}
block := newTestParsedBlock(1, "wal block")
block.Metadata.SimplexProtocolMetadata.Epoch = 1
block.Metadata.SimplexProtocolMetadata.Prev = genesis.BlockHeader().Digest

testWAL := testutil.NewTestWAL(t)
blockRecord, err := common.BlockRecord(block.BlockHeader(), block.Bytes())
require.NoError(t, err)

// write block record to wal
require.NoError(t, testWAL.Append(blockRecord))

// notarize the block so restoring the WAL keeps it as the round in progress
cops := &testCryptoOps{}
quorum := common.Quorum(len(nodeIDs))
notarizationRecord, err := testutil.NewNotarizationRecord(logger, cops.CreateSignatureAggregator(validatorSet.Nodes()), block, nodeIDs[:quorum])
notarizationRecord, err := testutil.NewNotarizationRecord(testutil.MakeLogger(t, 1), cops.CreateSignatureAggregator(validatorSet.Nodes()), block, nodeIDs[:quorum])
require.NoError(t, err)
require.NoError(t, testWAL.Append(notarizationRecord))

config := Config{
Logger: logger,
ID: nodeIDs[0],
VM: vm,
Storage: storage,
Sender: comm,
Broadcaster: comm,
PlatformChain: pChain,
CryptoOps: cops,
LastNonSimplexInnerBlock: genesisBlock,
WalCreator: storage.CreateWAL,
ParameterConfig: ParameterConfig{
MaxNetworkDelay: 500 * time.Millisecond,
MaxRoundWindow: 100,
WALMaxSizeBytes: 1024,
},
WALs: []wal.DeletableWAL{testWAL},
}
instance := NewInstance(config)
require.NoError(t, instance.Start(t.Context()))
t.Cleanup(instance.Stop)
chain := newNetwork(t, newTestPChain(validatorSet))
node := chain.addNodeWithConfig(nodeIDs[0], nodeConfig{wals: []wal.DeletableWAL{testWAL}})

// The restored block is verified asynchronously and not indexed, so poll until
// a seq-only lookup serves it from the cache.
require.Eventually(t, func() bool {
got, fin, err := instance.cs.Retrieve(1, common.Digest{})
got, fin, err := node.inst.cs.Retrieve(1, common.Digest{})
if err != nil || fin != nil {
return false
}
Expand Down
112 changes: 112 additions & 0 deletions external_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package simplex

import (
"sync"
"testing"
"time"

"github.com/ava-labs/simplex/common"
metadata "github.com/ava-labs/simplex/msm"
"github.com/stretchr/testify/require"
)

func TestParseBlockSizeMatchesBytes(t *testing.T) {
// Case 1: Bytes() first, Size() second, size returns the cached length.
pb := &ParsedBlock{
StateMachineBlock: metadata.StateMachineBlock{
Metadata: metadata.StateMachineMetadata{
SimplexProtocolMetadata: common.ProtocolMetadata{
Version: 1,
Prev: common.Digest{},
Round: 1,
Epoch: 4,
Seq: 2,
},
SimplexBlacklist: common.Blacklist{
Updates: common.BlacklistUpdates{{NodeIndex: 1, Type: 1}},
NodeCount: 2,
},
PChainHeight: 6,
},
InnerBlock: &testInnerBlock{
Height_: 7,
TS: time.UnixMilli(8),
Payload: []byte("payload"),
},
},
}
bytes := pb.Bytes()
require.Equal(t, len(bytes), pb.Size())

// Case 2: Size() first on a non serialized block. it will
// compute the size and match a later Byte() call.
pb2 := &ParsedBlock{
StateMachineBlock: metadata.StateMachineBlock{
Metadata: metadata.StateMachineMetadata{
SimplexProtocolMetadata: common.ProtocolMetadata{
Version: 1,
Prev: common.Digest{},
Round: 1,
Epoch: 4,
Seq: 2,
},
SimplexBlacklist: common.Blacklist{
Updates: common.BlacklistUpdates{{NodeIndex: 1, Type: 1}},
NodeCount: 2,
},
PChainHeight: 6,
},
InnerBlock: &testInnerBlock{
Height_: 9,
TS: time.UnixMilli(10),
Payload: []byte("other payload"),
},
},
}
size := pb2.Size()
require.NotZero(t, size)
bytes2 := pb2.Bytes()
require.Equal(t, len(bytes2), size)

// case 3: concurrent Size() calls on a block that was never serialized.
// the goroutines rase to compute the size, the lock must make this
// safe and every call must return the correct value

pb3 := &ParsedBlock{
StateMachineBlock: metadata.StateMachineBlock{
Metadata: metadata.StateMachineMetadata{
SimplexProtocolMetadata: common.ProtocolMetadata{
Version: 1,
Prev: common.Digest{},
Round: 1,
Epoch: 4,
Seq: 2,
},
SimplexBlacklist: common.Blacklist{
Updates: common.BlacklistUpdates{{NodeIndex: 1, Type: 1}},
NodeCount: 2,
},
PChainHeight: 6,
},
InnerBlock: &testInnerBlock{
Height_: 11,
TS: time.UnixMilli(12),
Payload: []byte("concurrent"),
},
},
}
var wg sync.WaitGroup
sizes := make([]int, 4)
for i := range sizes {
wg.Add(1)
go func() {
defer wg.Done()
sizes[i] = pb3.Size()
}()
}
wg.Wait()
bytes3 := pb3.Bytes()
for _, size := range sizes {
require.Equal(t, len(bytes3), size)
}
}
1 change: 1 addition & 0 deletions instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ func (i *Instance) createNonValidatorConfig() (nonvalidator.Config, error) {
}

func (i *Instance) notifyEpochChange(epoch uint64, validators common.Nodes) {
i.Config.Logger.Debug("Notifying the instance of an epoch change", zap.Uint64("Epoch", epoch), zap.Stringers("Validators", validators.NodeIDs()))
ec := epochChange{
epoch: epoch,
validators: validators,
Expand Down
Loading
Loading