Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@ go.work.sum

.DS_Store
.idea/

# tmp folder
/tmp
20 changes: 20 additions & 0 deletions fuzz_network_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package simplex_test

import (
"testing"
"time"

"github.com/ava-labs/simplex/testutil/random_network"
)

func TestNetworkSimpleFuzz(t *testing.T) {
for i := 0; i < 10; i++ {
t.Run("", func(t *testing.T) {
config := random_network.DefaultFuzzConfig()
config.RandomSeed = time.Now().UnixMilli()
network := random_network.NewNetwork(config, t)
network.Run()
network.PrintStatus()
})
}
}
74 changes: 50 additions & 24 deletions testutil/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package testutil

import (
"fmt"
"io"
"os"
"strings"
"testing"
Expand All @@ -14,6 +15,11 @@ import (
"go.uber.org/zap/zapcore"
)

const (
LOG_LEVEL = "log_level"
INFO_LOG_LEVEL = "info"
)

type TestLogger struct {
*zap.Logger
t *testing.T
Expand Down Expand Up @@ -94,13 +100,16 @@ func (tl *TestLogger) Error(msg string, fields ...zap.Field) {
}

func MakeLogger(t *testing.T, node ...int) *TestLogger {
return MakeLoggerWithFile(t, nil, node...)
// Preserve existing behavior: logs to stdout by default.
return MakeLoggerWithFile(t, nil, true, node...)
}

// MakeLoggerWithFile creates a TestLogger that optionally writes to a file in addition to stdout.
// If fileWriter is nil, logs only to stdout (same as MakeLogger).
// If fileWriter is provided, logs to both stdout and the file.
func MakeLoggerWithFile(t *testing.T, fileWriter zapcore.WriteSyncer, node ...int) *TestLogger {
// MakeLoggerWithFile creates a TestLogger that can write to a file and optionally to stdout.
// - If writeStdout is true, logs may be written to stdout.
// - If fileWriter is non-nil, logs may be written to that fileWriter.
// - If both are enabled, logs go to both.
// - If neither is enabled, logs are discarded.
func MakeLoggerWithFile(t *testing.T, fileWriter zapcore.WriteSyncer, writeStdout bool, node ...int) *TestLogger {
defaultEncoderConfig := zapcore.EncoderConfig{
TimeKey: "timestamp",
LevelKey: "level",
Expand All @@ -118,28 +127,48 @@ func MakeLoggerWithFile(t *testing.T, fileWriter zapcore.WriteSyncer, node ...in
config.EncodeTime = zapcore.TimeEncoderOfLayout("[01-02|15:04:05.000]")
config.ConsoleSeparator = " "

// Create stdout encoder
stdoutEncoder := zapcore.NewConsoleEncoder(config)
if strings.ToLower(os.Getenv("LOG_LEVEL")) == "info" {
stdoutEncoder = &DebugSwallowingEncoder{consoleEncoder: stdoutEncoder, ObjectEncoder: stdoutEncoder, pool: buffer.NewPool()}
}

atomicLevel := zap.NewAtomicLevelAt(zapcore.DebugLevel)

// Create stdout core
stdoutCore := zapcore.NewCore(stdoutEncoder, zapcore.AddSync(os.Stdout), atomicLevel)
var cores []zapcore.Core

// If file writer is provided, create a tee core with both stdout and file
var core zapcore.Core
// Stdout core only if explicitly enabled
if writeStdout {
stdoutEncoder := zapcore.NewConsoleEncoder(config)
if strings.ToLower(os.Getenv("LOG_LEVEL")) == "info" {
stdoutEncoder = &DebugSwallowingEncoder{
consoleEncoder: stdoutEncoder,
ObjectEncoder: stdoutEncoder,
pool: buffer.NewPool(),
}
}
stdoutCore := zapcore.NewCore(stdoutEncoder, zapcore.AddSync(os.Stdout), atomicLevel)
cores = append(cores, stdoutCore)
}

// File core only if provided
if fileWriter != nil {
fileEncoder := zapcore.NewConsoleEncoder(config)
if strings.ToLower(os.Getenv("LOG_LEVEL")) == "info" {
fileEncoder = &DebugSwallowingEncoder{consoleEncoder: fileEncoder, ObjectEncoder: fileEncoder, pool: buffer.NewPool()}
if strings.ToLower(os.Getenv(LOG_LEVEL)) == INFO_LOG_LEVEL {
fileEncoder = &DebugSwallowingEncoder{
consoleEncoder: fileEncoder,
ObjectEncoder: fileEncoder,
pool: buffer.NewPool(),
}
}
fileCore := zapcore.NewCore(fileEncoder, fileWriter, atomicLevel)
core = zapcore.NewTee(stdoutCore, fileCore)
} else {
core = stdoutCore
cores = append(cores, fileCore)
}

// If neither stdout nor file enabled, discard logs.
var core zapcore.Core
switch len(cores) {
case 0:
discardEncoder := zapcore.NewConsoleEncoder(config)
core = zapcore.NewCore(discardEncoder, zapcore.AddSync(io.Discard), atomicLevel)
case 1:
core = cores[0]
default:
core = zapcore.NewTee(cores...)
}

logger := zap.New(core, zap.AddCaller())
Expand All @@ -150,16 +179,13 @@ func MakeLoggerWithFile(t *testing.T, fileWriter zapcore.WriteSyncer, node ...in

traceVerboseLogger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1))
traceVerboseLogger = traceVerboseLogger.With(zap.String("test", t.Name()))

if len(node) > 0 {
traceVerboseLogger = traceVerboseLogger.With(zap.Int("myNodeID", node[0]))
}

l := &TestLogger{t: t, Logger: logger, traceVerboseLogger: traceVerboseLogger,
return &TestLogger{t: t, Logger: logger, traceVerboseLogger: traceVerboseLogger,
atomicLevel: atomicLevel,
}

return l
}

type DebugSwallowingEncoder struct {
Expand Down
140 changes: 140 additions & 0 deletions testutil/random_network/block.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package random_network

import (
"context"
"crypto/sha256"
"encoding/asn1"
"fmt"

"github.com/ava-labs/simplex"
)

var _ simplex.Block = (*Block)(nil)

type Block struct {
blacklist simplex.Blacklist

// contents
txs []*TX

// protocol metadata
metadata simplex.ProtocolMetadata
digest simplex.Digest

// mempool access
mempool *Mempool
}

func NewBlock(metadata simplex.ProtocolMetadata, blacklist simplex.Blacklist, mempool *Mempool, txs []*TX) *Block {
b := &Block{
mempool: mempool,
txs: txs,
metadata: metadata,
blacklist: blacklist,
}

b.ComputeAndSetDigest()
return b
}

func (b *Block) Verify(ctx context.Context) (simplex.VerifiedBlock, error) {
return b, b.mempool.VerifyBlock(ctx, b)
}

func (b *Block) Blacklist() simplex.Blacklist {
return b.blacklist
}

func (b *Block) BlockHeader() simplex.BlockHeader {
return simplex.BlockHeader{
ProtocolMetadata: b.metadata,
Digest: b.digest,
}
}

type encodedBlock struct {
ProtocolMetadata []byte
TXs []asn1TX
Blacklist []byte
}

func (b *Block) Bytes() ([]byte, error) {
mdBytes := b.metadata.Bytes()

var asn1TXs []asn1TX
for _, tx := range b.txs {
asn1TXs = append(asn1TXs, asn1TX{ID: tx.ID[:], ShouldFailVerification: tx.shouldFailVerification})
}

blacklistBytes := b.blacklist.Bytes()

encodedB := encodedBlock{
ProtocolMetadata: mdBytes,
TXs: asn1TXs,
Blacklist: blacklistBytes,
}

return asn1.Marshal(encodedB)
}

func (b *Block) containsTX(txID txID) bool {
for _, tx := range b.txs {
if tx.ID == txID {
return true
}
}
return false
}

func (b *Block) ComputeAndSetDigest() {
tbBytes, err := b.Bytes()
if err != nil {
panic(fmt.Sprintf("failed to serialize test block: %v", err))
}

b.digest = sha256.Sum256(tbBytes)
}

type BlockDeserializer struct {
mempool *Mempool
}

var _ simplex.BlockDeserializer = (*BlockDeserializer)(nil)

func (bd *BlockDeserializer) DeserializeBlock(ctx context.Context, buff []byte) (simplex.Block, error) {
var encodedBlock encodedBlock
_, err := asn1.Unmarshal(buff, &encodedBlock)
if err != nil {
return nil, err
}

md, err := simplex.ProtocolMetadataFromBytes(encodedBlock.ProtocolMetadata)
if err != nil {
return nil, err
}

var blacklist simplex.Blacklist
if err := blacklist.FromBytes(encodedBlock.Blacklist); err != nil {
return nil, err
}

txs := make([]*TX, len(encodedBlock.TXs))
for i, asn1Tx := range encodedBlock.TXs {
tx := asn1Tx.toTX()
txs[i] = tx
}

b := &Block{
metadata: *md,
txs: txs,
blacklist: blacklist,
mempool: bd.mempool,
}

b.ComputeAndSetDigest()

return b, nil
}
59 changes: 59 additions & 0 deletions testutil/random_network/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package random_network

import (
"time"

"github.com/ava-labs/simplex"
)

type FuzzConfig struct {
// The minimum and maximum number of nodes in the network.
MinNodes int // Default is 3.
MaxNodes int // Default is 10.

// The minimum and maximum number of transactions to be issued at a block. Default is between 5 and 20.
MinTxsPerIssue int
Copy link
Collaborator

@yacovm yacovm Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the benefit of having a random number of transactions issued? Doesn't this make the test even more non-deterministic? Why not just use 1 (issue a single transaction) all the time?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i was hoping that we would occasionally issue more than a blocks worth of transactions at a time. this way we when we are waiting for txs to be accepted we need to wait multiple rounds. Potentially even more rounds than 2 if nodes are blacklisted. This way we hit more code paths and hopefully more edge cases.

MaxTxsPerIssue int

// Number of transactions per block. Default is 15.
TxsPerBlock int

// The number of blocks that must be finalized before ending the fuzz test. Default is 100.
NumFinalizedBlocks int

RandomSeed int64

// Probability that a node will be randomly crashed. Default is .1 (10%).
NodeCrashProbability float64

// Probability that a crashed node will be restarted. Default is .5 (50%).
NodeRecoverProbability float64

// Amount to advance the time by. Default is simplex.DefaultMaxProposalWaitTime / 5.
AdvanceTimeTickAmount time.Duration

// Creates main.log for network logs and {nodeID-short}.log for each node.
// NodeID is represented as a 16-character hex string (first 8 bytes).
// Default directory is "tmp".
// If empty, logging to files is disabled and logs will only be printed to console.
LogDirectory string
}

func DefaultFuzzConfig() *FuzzConfig {
return &FuzzConfig{
MinNodes: 3,
MaxNodes: 10,
MinTxsPerIssue: 5,
MaxTxsPerIssue: 20,
TxsPerBlock: 15,
NumFinalizedBlocks: 100,
RandomSeed: time.Now().UnixMilli(),
NodeCrashProbability: 0.1,
NodeRecoverProbability: 0.5,
AdvanceTimeTickAmount: simplex.DefaultMaxProposalWaitTime / 5,
LogDirectory: "tmp",
}
}
Loading
Loading