Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
WALMaxEntryCount: 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 @@ -199,6 +199,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