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

202 lines
5.1 KiB
Go

// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package wal
import (
"encoding/base64"
"fmt"
"github.com/luxfi/bft/common"
)
const (
defaultMaxWALSize = 100 * 1024 * 1024 // 100 MB
)
// Creator creates a DeletableWAL. Returns an error upon failure.
type Creator func() (DeletableWAL, error)
// Reader reads the retention term of a WAL payload.
// Returns an error upon failure.
type Reader interface {
RetentionTerm([]byte) (uint64, error)
}
// TruncateableWAL is a DeletableWAL that can be recentlyTruncated to a given retention term.
// It removes all entries below the given retention term.
type TruncateableWAL interface {
DeletableWAL
Truncate(retentionTerm uint64) error
}
// DeletableWAL is a WAL that can be deleted.
type DeletableWAL interface {
common.WriteAheadLog
// Delete deletes the WAL file and after it is called,
// it should no longer be used.
Delete() error
}
type garbageCollectedWAL struct {
retentionTerm uint64
size int
DeletableWAL
}
type GarbageCollectedWAL struct {
wals []garbageCollectedWAL
createWAL Creator
maxWalSize int
reader Reader
}
// NewGarbageCollectedWAL creates a new GarbageCollectedWAL.
// It takes in a list of existing WALs to load, a Creator to create new WALs,
// a Reader to read the retention term of entries, and a maximum WAL size.
// A new WAL is created when the given maximum WAL size of a WAL is reached.
func NewGarbageCollectedWAL(WALs []DeletableWAL, creator Creator, reader Reader, maxWALSize int) (*GarbageCollectedWAL, error) {
wals := make([]garbageCollectedWAL, 0, len(WALs))
for _, wal := range WALs {
wal, err := loadGarbageCollectedWAL(wal, reader)
if err != nil {
return nil, err
}
wals = append(wals, wal)
}
if maxWALSize == 0 {
maxWALSize = defaultMaxWALSize
}
return &GarbageCollectedWAL{
maxWalSize: maxWALSize,
reader: reader,
createWAL: creator,
wals: wals,
}, nil
}
func loadGarbageCollectedWAL(wal DeletableWAL, reader Reader) (garbageCollectedWAL, error) {
entries, err := wal.ReadAll()
if err != nil {
return garbageCollectedWAL{}, fmt.Errorf("error reading entries from WAL: %v", err)
}
var highestRetentionTerm uint64
var size int
for _, entry := range entries {
size += len(entry)
rt, err := reader.RetentionTerm(entry)
if err != nil {
data := base64.StdEncoding.EncodeToString(entry)
return garbageCollectedWAL{}, fmt.Errorf("error reading retention term of entry %s: %v", data, err)
}
if rt > highestRetentionTerm {
highestRetentionTerm = rt
}
}
return garbageCollectedWAL{
retentionTerm: highestRetentionTerm,
DeletableWAL: wal,
size: size,
}, nil
}
func (gcw *GarbageCollectedWAL) Append(payload []byte) error {
rt, err := gcw.reader.RetentionTerm(payload)
if err != nil {
data := base64.StdEncoding.EncodeToString(payload)
return fmt.Errorf("error reading retention term of entry %s: %v", data, err)
}
if gcw.shouldWeCreateNewWAL(payload) {
// Close the last WAL in use because we won't be needing it anymore
if len(gcw.wals) > 0 {
if err := gcw.wals[len(gcw.wals)-1].Close(); err != nil {
return fmt.Errorf("error closing last WAL: %v", err)
}
}
newWAL, err := gcw.createWAL()
if err != nil {
return fmt.Errorf("error creating new WAL: %v", err)
}
gcw.wals = append(gcw.wals, garbageCollectedWAL{
DeletableWAL: newWAL,
retentionTerm: rt,
size: len(payload),
})
} else {
// We're still using the last WAL, so we need to update its size
gcw.wals[len(gcw.wals)-1].size += len(payload)
}
last := len(gcw.wals) - 1
if rt > gcw.wals[last].retentionTerm {
gcw.wals[last].retentionTerm = rt
}
return gcw.wals[last].Append(payload)
}
func (gcw *GarbageCollectedWAL) shouldWeCreateNewWAL(payload []byte) bool {
var shouldCreateNewWAL bool
if len(gcw.wals) == 0 {
shouldCreateNewWAL = true
} else {
lastWALSize := gcw.wals[len(gcw.wals)-1].size
shouldCreateNewWAL = lastWALSize+len(payload) > gcw.maxWalSize
}
return shouldCreateNewWAL
}
func (gcw *GarbageCollectedWAL) ReadAll() ([][]byte, error) {
var allEntries [][]byte
for i, wal := range gcw.wals {
entries, err := wal.ReadAll()
if err != nil {
return nil, fmt.Errorf("error reading entries from WAL %d: %v", i, err)
}
if err := wal.Close(); err != nil {
return nil, fmt.Errorf("error closing WAL %d: %v", i, err)
}
allEntries = append(allEntries, entries...)
}
return allEntries, nil
}
func (gcw *GarbageCollectedWAL) GarbageCollect(retentionTerm uint64) error {
newWALs := make([]garbageCollectedWAL, 0, len(gcw.wals))
for i, wal := range gcw.wals {
if wal.retentionTerm < retentionTerm {
// This WAL is below the retention term, we can delete it
if err := wal.Delete(); err != nil {
return fmt.Errorf("error deleting WAL %d: %v", i, err)
}
continue
}
// Else, this WAL is still needed
newWALs = append(newWALs, wal)
}
gcw.wals = newWALs
return nil
}
func (gcw *GarbageCollectedWAL) Close() error {
for i, wal := range gcw.wals {
if err := wal.Close(); err != nil {
return fmt.Errorf("error closing WAL %d: %v", i, err)
}
}
gcw.wals = nil
return nil
}