Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
7a0e4c2
fix double start round bug and add test
samliok Jan 5, 2026
33831c6
dont send message to self
samliok Jan 5, 2026
f407d8d
initial mempool
samliok Jan 5, 2026
06cf6b5
add mempool, block, transaction
samliok Jan 6, 2026
fae084f
node
samliok Jan 7, 2026
7642681
reduce block building api
samliok Jan 7, 2026
24ab202
refactor test node
samliok Jan 8, 2026
e732758
refactor node and network
samliok Jan 8, 2026
3fe5aa3
finish refactor of all tests and network
samliok Jan 8, 2026
0b2410f
add first test, not passing
samliok Jan 8, 2026
167042c
failing test when it shouldn't
samliok Jan 9, 2026
6a69e4a
fix bug
samliok Jan 9, 2026
c2b04a6
save
samliok Jan 9, 2026
93e1792
save
samliok Jan 9, 2026
6274ce3
fix: test hang (#324)
JonathanOppenheimer Jan 9, 2026
a968dfc
save
samliok Jan 12, 2026
ee55ad4
working test now
samliok Jan 12, 2026
1fef374
index
samliok Jan 12, 2026
19f922d
wait for issuance
samliok Jan 12, 2026
0cde180
ctx cancel
samliok Jan 13, 2026
316171a
added run ability and crash
samliok Jan 13, 2026
73dcda3
add crashes
samliok Jan 14, 2026
d06c5c7
crashing works
samliok Jan 14, 2026
c98d1ea
remove node from network before re-adding after crash
samliok Jan 14, 2026
7305061
remove diff
samliok Jan 14, 2026
3c24868
merge conflicts
samliok Jan 20, 2026
474b9b9
Merge branch 'main' into random-network
samliok Jan 20, 2026
3e1dc18
refactor test utils
samliok Jan 20, 2026
a811c69
separate out network and node
samliok Jan 20, 2026
20a3842
lint
samliok Jan 20, 2026
1b56415
merge
samliok Jan 20, 2026
3f3ddad
fix test failure
samliok Jan 21, 2026
6fb79ee
build a block
samliok Jan 21, 2026
9f3784f
sequence count
samliok Jan 21, 2026
cb097aa
rename
samliok Jan 21, 2026
d0a73a7
ierge branch 'refactor-testutil' into random-network
samliok Jan 21, 2026
965f081
clean up + integrate randomness
samliok Jan 21, 2026
a944f1b
Merge branch 'main' into refactor-testutil
samliok Jan 21, 2026
e0bedfe
cleanup and logging
samliok Jan 21, 2026
9ac9040
tx not found
samliok Jan 21, 2026
16d1f9b
add locking for race condition
samliok Jan 21, 2026
bab8db1
Merge remote-tracking branch 'origin/refactor-testutil' into random-n…
samliok Jan 22, 2026
1963835
Merge branch 'main' into random-network
samliok Feb 3, 2026
11b67e3
lint and licesne
samliok Feb 3, 2026
41f7a94
copy message
samliok Feb 3, 2026
a46f5e7
move wait
samliok Feb 3, 2026
daf8058
remove the use of go routines
samliok Feb 4, 2026
09391a7
save
samliok Feb 4, 2026
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,7 @@ go.work.sum

.DS_Store
.idea/

# tmp folder

/tmp
Copy link
Collaborator

Choose a reason for hiding this comment

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

What's the use of adding /tmp?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

the logs are written to /tmp, and i would like to save the logs for debugging but not commit them to github

4 changes: 4 additions & 0 deletions epoch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1727,6 +1727,10 @@ func TestQuorum(t *testing.T) {
n: 12, f: 3,
q: 8,
},
{
n: 7, f: 2,
q: 5,
},
} {
t.Run(fmt.Sprintf("%d", testCase.n), func(t *testing.T) {
require.Equal(t, testCase.q, Quorum(testCase.n))
Expand Down
15 changes: 15 additions & 0 deletions fuzz_network_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package simplex_test

import (
"testing"

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

func TestNetworkSimpleFuzz(t *testing.T) {
config := random_network.DefaultFuzzConfig()
config.RandomSeed = 1770220909588
network := random_network.NewNetwork(config, t)
network.Run()
network.PrintStatus()
}
71 changes: 47 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 Down Expand Up @@ -96,13 +97,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 @@ -120,28 +124,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()}
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 @@ -152,16 +176,15 @@ 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{Logger: logger, traceVerboseLogger: traceVerboseLogger,
atomicLevel: atomicLevel,
return &TestLogger{
Logger: logger,
traceVerboseLogger: traceVerboseLogger,
atomicLevel: atomicLevel,
}

return l
}

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

package random_network

import (
"bytes"
"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[:]})
}

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() {
var bb bytes.Buffer
tbBytes, err := b.Bytes()
if err != nil {
panic(fmt.Sprintf("failed to serialize test block: %v", err))
}

bb.Write(tbBytes)
b.digest = sha256.Sum256(bb.Bytes())
}

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
}
54 changes: 54 additions & 0 deletions testutil/random_network/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package random_network

import "time"

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

// The probability that a transaction verification will fail. Default is .1%.
TxVerificationFailure float64

// The minimum and maximum number of transactions per block. Default is between 5 and 20.
MinTxsPerBlock int
MaxTxsPerBlock int

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

RandomSeed int64

// Chance that a node will be randomly crashed. Default is 10%.
NodeCrashPercentage float64

// Chance that a crashed node will be restarted. Default is 50%.
NodeRecoverPercentage float64

// Amount to advance the time by. Default is 1000ms.
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 is "tmp".
LogDirectory string
}

func DefaultFuzzConfig() *FuzzConfig {
return &FuzzConfig{
MinNodes: 3,
MaxNodes: 10,
TxVerificationFailure: .001,
MinTxsPerBlock: 5,
MaxTxsPerBlock: 20,
NumFinalizedBlocks: 100,
RandomSeed: time.Now().UnixMilli(),
NodeCrashPercentage: 0.1,
NodeRecoverPercentage: 0.5,
AdvanceTimeTickAmount: 1000 * time.Millisecond,
LogDirectory: "tmp",
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we need to create a temporary folder and if the test succeeds, delete the logs.

Copy link
Collaborator

Choose a reason for hiding this comment

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

This way you don't need to delete the directory at the beginning of the test

}
}
Loading
Loading