mirror of
https://github.com/luxfi/bft.git
synced 2026-10-30 17:01:54 +00:00
Reapplies the fork's whole delta on top of the merge, as one reviewable diff rather than 43 conflict resolutions. Nothing here changes consensus behaviour. - module github.com/luxfi/bft, Go 1.26.4; root package simplex -> bft, which would otherwise collide with the simplex/ subpackage under this module name. - go.uber.org/zap -> github.com/luxfi/log, so common.Logger takes ...log.Field. zap.Error maps to log.Err: log.Error is luxfi/log's package-level logging function, not a field constructor, and the compiler is the only thing that catches that. Needs luxfi/log v1.6.1 for the Stringers constructor. - avalanchego/ -> host/. The package holds NodeID, VMBlock, Bitmask and size limits — the host node's types, vendored to avoid importing a whole node — so it is named for what it holds. Renaming it luxd/ would have swapped one brand for another and said just as little. - testutil/logger.go reimplemented without zapcore. Upstream layers a keyword-filtering Core, a debug-swallowing Encoder and a Tee because that is zap's extension model; luxfi/log has no such seam, so every policy is applied in one emit. That also fixes two defects the layering caused: SilenceExceptKeywords rebuilt the logger and discarded an installed Intercept hook, and the two sinks read different env vars (LOG_LEVEL vs log_level) so they disagreed about suppressing debug. Hooks run outside the lock, so a hook that logs fails the test instead of deadlocking it. Fatal panics rather than exiting, because exiting takes the suite down with no attribution. - simplex/monitor_test.go keeps a local logger: testutil imports simplex, so an internal test cannot import testutil back without closing a cycle. - README keeps the name Simplex throughout — it is the protocol's own name and the subject of reference [1]. Only the deployment target changed. The HyperSDK sentence still says Avalanche because that sentence is true. - Drops the fork's Makefile: it duplicated unit-tests.sh and fuzz.sh, and ran the suite with -timeout 30s when simplex alone needs over 100s. Co-authored-by: Hanzo Dev <[email protected]>
295 lines
6.9 KiB
Go
295 lines
6.9 KiB
Go
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package testutil
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/luxfi/bft/common"
|
|
"github.com/luxfi/bft/wal"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type TestWAL struct {
|
|
common.WriteAheadLog
|
|
t *testing.T
|
|
lock sync.Mutex
|
|
signal sync.Cond
|
|
}
|
|
|
|
func NewTestWAL(t *testing.T) *TestWAL {
|
|
var tw TestWAL
|
|
tw.WriteAheadLog = wal.NewMemWAL(t)
|
|
tw.signal = sync.Cond{L: &tw.lock}
|
|
tw.t = t
|
|
return &tw
|
|
}
|
|
|
|
func (tw *TestWAL) Clone() *TestWAL {
|
|
rawWAL, err := tw.ReadAll()
|
|
require.NoError(tw.t, err)
|
|
|
|
wal := NewTestWAL(tw.t)
|
|
|
|
for _, entry := range rawWAL {
|
|
wal.Append(entry)
|
|
}
|
|
|
|
return wal
|
|
}
|
|
|
|
func (tw *TestWAL) Delete() error {
|
|
return nil
|
|
}
|
|
|
|
func (tw *TestWAL) Append(b []byte) error {
|
|
tw.lock.Lock()
|
|
defer tw.lock.Unlock()
|
|
|
|
err := tw.WriteAheadLog.Append(b)
|
|
tw.signal.Signal()
|
|
return err
|
|
}
|
|
|
|
func (tw *TestWAL) ReadAll() ([][]byte, error) {
|
|
tw.lock.Lock()
|
|
defer tw.lock.Unlock()
|
|
|
|
return tw.WriteAheadLog.ReadAll()
|
|
}
|
|
|
|
func (tw *TestWAL) AssertWALSize(n int) {
|
|
tw.lock.Lock()
|
|
defer tw.lock.Unlock()
|
|
|
|
for {
|
|
rawRecords, err := tw.WriteAheadLog.ReadAll()
|
|
require.NoError(tw.t, err)
|
|
|
|
if len(rawRecords) == n {
|
|
return
|
|
}
|
|
|
|
tw.signal.Wait()
|
|
}
|
|
}
|
|
|
|
func (tw *TestWAL) AssertNotarization(round uint64) uint16 {
|
|
tw.lock.Lock()
|
|
defer tw.lock.Unlock()
|
|
|
|
for {
|
|
rawRecords, err := tw.WriteAheadLog.ReadAll()
|
|
require.NoError(tw.t, err)
|
|
|
|
for _, rawRecord := range rawRecords {
|
|
if binary.BigEndian.Uint16(rawRecord[:2]) == common.NotarizationRecordType {
|
|
_, vote, err := common.ParseNotarizationRecord(rawRecord)
|
|
require.NoError(tw.t, err)
|
|
|
|
if vote.Round == round {
|
|
return common.NotarizationRecordType
|
|
}
|
|
}
|
|
if binary.BigEndian.Uint16(rawRecord[:2]) == common.EmptyNotarizationRecordType {
|
|
_, vote, err := common.ParseEmptyNotarizationRecord(rawRecord)
|
|
require.NoError(tw.t, err)
|
|
|
|
if vote.Round == round {
|
|
return common.EmptyNotarizationRecordType
|
|
}
|
|
}
|
|
}
|
|
|
|
tw.signal.Wait()
|
|
}
|
|
|
|
}
|
|
|
|
func (tw *TestWAL) AssertEmptyVote(round uint64) {
|
|
tw.lock.Lock()
|
|
defer tw.lock.Unlock()
|
|
|
|
for {
|
|
rawRecords, err := tw.WriteAheadLog.ReadAll()
|
|
require.NoError(tw.t, err)
|
|
|
|
for _, rawRecord := range rawRecords {
|
|
if binary.BigEndian.Uint16(rawRecord[:2]) == common.EmptyVoteRecordType {
|
|
vote, err := common.ParseEmptyVoteRecord(rawRecord)
|
|
require.NoError(tw.t, err)
|
|
|
|
if vote.Round == round {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
tw.signal.Wait()
|
|
}
|
|
|
|
}
|
|
|
|
func (tw *TestWAL) AssertBlockProposal(round uint64) {
|
|
tw.lock.Lock()
|
|
defer tw.lock.Unlock()
|
|
|
|
for {
|
|
rawRecords, err := tw.WriteAheadLog.ReadAll()
|
|
require.NoError(tw.t, err)
|
|
|
|
for _, rawRecord := range rawRecords {
|
|
if binary.BigEndian.Uint16(rawRecord[:2]) == common.BlockRecordType {
|
|
bh, _, err := common.ParseBlockRecord(rawRecord)
|
|
require.NoError(tw.t, err)
|
|
|
|
if bh.Round == round {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
tw.signal.Wait()
|
|
}
|
|
|
|
}
|
|
|
|
func (tw *TestWAL) ContainsNotarization(round uint64) bool {
|
|
tw.lock.Lock()
|
|
defer tw.lock.Unlock()
|
|
|
|
rawRecords, err := tw.WriteAheadLog.ReadAll()
|
|
require.NoError(tw.t, err)
|
|
|
|
for _, rawRecord := range rawRecords {
|
|
if binary.BigEndian.Uint16(rawRecord[:2]) == common.NotarizationRecordType {
|
|
_, vote, err := common.ParseNotarizationRecord(rawRecord)
|
|
require.NoError(tw.t, err)
|
|
|
|
if vote.Round == round {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (tw *TestWAL) ContainsEmptyVote(round uint64) bool {
|
|
tw.lock.Lock()
|
|
defer tw.lock.Unlock()
|
|
|
|
rawRecords, err := tw.WriteAheadLog.ReadAll()
|
|
require.NoError(tw.t, err)
|
|
|
|
for _, rawRecord := range rawRecords {
|
|
if binary.BigEndian.Uint16(rawRecord[:2]) == common.EmptyVoteRecordType {
|
|
vote, err := common.ParseEmptyVoteRecord(rawRecord)
|
|
require.NoError(tw.t, err)
|
|
|
|
if vote.Round == round {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (tw *TestWAL) ContainsEmptyNotarization(round uint64) bool {
|
|
tw.lock.Lock()
|
|
defer tw.lock.Unlock()
|
|
|
|
rawRecords, err := tw.WriteAheadLog.ReadAll()
|
|
require.NoError(tw.t, err)
|
|
|
|
for _, rawRecord := range rawRecords {
|
|
if binary.BigEndian.Uint16(rawRecord[:2]) == common.EmptyNotarizationRecordType {
|
|
_, vote, err := common.ParseEmptyNotarizationRecord(rawRecord)
|
|
require.NoError(tw.t, err)
|
|
|
|
if vote.Round == round {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
type walRound struct {
|
|
round uint64
|
|
blockRecord bool
|
|
notarizationRecord bool
|
|
finalizationRecord bool
|
|
emptyNotarizationRecord bool
|
|
emptyVoteRecord bool
|
|
}
|
|
|
|
// AssertHealthy checks that the WAL has at most one of each record type per round.
|
|
func (tw *TestWAL) AssertHealthy(bd common.BlockDeserializer, qcd common.QCDeserializer) {
|
|
ctx := context.Background()
|
|
records, err := tw.ReadAll()
|
|
require.NoError(tw.t, err)
|
|
|
|
rounds := make(map[uint64]*walRound)
|
|
|
|
for _, r := range records {
|
|
recordType := binary.BigEndian.Uint16(r)
|
|
|
|
switch recordType {
|
|
case common.BlockRecordType:
|
|
block, err := common.BlockFromRecord(ctx, bd, r)
|
|
require.NoError(tw.t, err)
|
|
round := block.BlockHeader().Round
|
|
if _, exists := rounds[round]; !exists {
|
|
rounds[round] = &walRound{round: round}
|
|
}
|
|
require.False(tw.t, rounds[round].blockRecord, "duplicate block record for round %d", round)
|
|
rounds[round].blockRecord = true
|
|
case common.NotarizationRecordType:
|
|
_, vote, err := common.ParseNotarizationRecord(r)
|
|
require.NoError(tw.t, err)
|
|
round := vote.Round
|
|
if _, exists := rounds[round]; !exists {
|
|
rounds[round] = &walRound{round: round}
|
|
}
|
|
require.False(tw.t, rounds[round].notarizationRecord, "duplicate notarization record for round %d", round)
|
|
rounds[round].notarizationRecord = true
|
|
case common.EmptyNotarizationRecordType:
|
|
_, vote, err := common.ParseEmptyNotarizationRecord(r)
|
|
require.NoError(tw.t, err)
|
|
round := vote.Round
|
|
if _, exists := rounds[round]; !exists {
|
|
rounds[round] = &walRound{round: round}
|
|
}
|
|
require.False(tw.t, rounds[round].emptyNotarizationRecord, "duplicate empty notarization record for round %d", round)
|
|
rounds[round].emptyNotarizationRecord = true
|
|
case common.EmptyVoteRecordType:
|
|
vote, err := common.ParseEmptyVoteRecord(r)
|
|
require.NoError(tw.t, err)
|
|
round := vote.Round
|
|
if _, exists := rounds[round]; !exists {
|
|
rounds[round] = &walRound{round: round}
|
|
}
|
|
require.False(tw.t, rounds[round].emptyVoteRecord, "duplicate empty vote record for round %d", round)
|
|
rounds[round].emptyVoteRecord = true
|
|
case common.FinalizationRecordType:
|
|
finalization, err := common.FinalizationFromRecord(r, qcd)
|
|
require.NoError(tw.t, err)
|
|
round := finalization.Finalization.Round
|
|
if _, exists := rounds[round]; !exists {
|
|
rounds[round] = &walRound{round: round}
|
|
}
|
|
require.False(tw.t, rounds[round].finalizationRecord, "duplicate finalization record for round %d", round)
|
|
rounds[round].finalizationRecord = true
|
|
default:
|
|
tw.t.Fatalf("undefined record type: %d", recordType)
|
|
}
|
|
}
|
|
}
|