Files
zeekayandHanzo Dev d678c2d213 transform: publish upstream simplex as luxfi/bft on luxfi/log
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]>
2026-08-06 04:21:17 -07:00

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)
}
}
}