From 5d0cad77cfac31c25ffee91b29d0519ce5fb0e0e Mon Sep 17 00:00:00 2001 From: "Masih H. Derkani" Date: Tue, 14 May 2024 20:55:52 +0100 Subject: [PATCH] Introduce fuzz testing to separate unit tests from repetition tests Prior to changes introduced here the majority of time during testing was spent on repeating the same test while changing the seed for generators or latency models. Instead of repeating the same test over and over use the fuzz testing mechanism provided by Go SDK. This change allows the unit tests to run much faster while offering a configurable "fuzztime" that dictates for how long the test cases should be fuzzed. The changes here introduce the following fuzz tests: * `FuzzAbsentAdversary` * `FuzzImmediateDecideAdversary` * `FuzzHonest_AsyncRequireStrongQuorumToProgress` * `FuzzHonest_SyncMajorityCommonPrefix` * `FuzzHonest_AsyncMajorityCommonPrefix` * `FuzzHonestMultiInstance_AsyncDisagreement` * `FuzzHonestMultiInstance_SyncAgreement` * `FuzzHonestMultiInstance_AsyncAgreement` * `FuzzStoragePower_SyncIncreaseMidSimulation` * `FuzzStoragePower_AsyncIncreaseMidSimulation` * `FuzzStoragePower_SyncDecreaseRevertsToBase` * `FuzzStoragePower_AsyncDecreaseRevertsToBase` * `FuzzRepeatAdversary` The CI workflow is updated to run each of the fuzz tests for 30 seconds in a dedicated job. --- .github/workflows/ci.yml | 19 +- Makefile | 17 +- go.mod | 1 - sim/cmd/f3sim/f3sim.go | 7 +- sim/latency/log_normal.go | 16 +- test/absent_test.go | 22 +- test/constants_test.go | 9 +- test/decide_test.go | 34 ++- test/ec_divergence_test.go | 252 ++++++++++----------- test/honest_test.go | 424 ++++++++++++++++------------------- test/multi_instance_test.go | 178 ++++++++------- test/power_evolution_test.go | 275 +++++++++++------------ test/repeat_test.go | 143 ++++++------ test/util_test.go | 43 ---- test/withhold_test.go | 5 +- 15 files changed, 712 insertions(+), 733 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69d32bb4..d2bdecb5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,10 +38,6 @@ jobs: with: go-version: '1.21' - name: Test - env: - # Override test repetition parallelism, since the default runner seem to have a single core. - # This shaves ~30 seconds off the CI test duration. - F3_TEST_REPETITION_PARALLELISM: 2 run: make test/cover - name: Upload coverage to Codecov # Commit SHA corresponds to version v4.4.0 @@ -57,6 +53,21 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: false + fuzz: + name: Fuzz + runs-on: ubuntu-latest + needs: + - test # Do not bother running the fuzz tests if tests fail + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version: '1.21' + - name: Fuzz + env: + FUZZTIME: 30s + run: make fuzz + generate: name: Generate runs-on: ubuntu-latest diff --git a/Makefile b/Makefile index a25fb23d..d7b76f8f 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,9 @@ -all: generate test lint +SHELL := /usr/bin/env bash + +GOGC ?= 1000 # Reduce GC frequency during testing, default to 1000 if unset. + +all: generate test fuzz lint -test: GOGC ?= 1000 # Reduce the GC frequency, default to 1000 if unset. test: GOGC=$(GOGC) go test $(GOTEST_ARGS) ./... .PHONY: test @@ -9,8 +12,18 @@ test/cover: test test/cover: GOTEST_ARGS=-coverprofile=coverage.txt -covermode=atomic -coverpkg=./... .PHONY: test/cover +fuzz: FUZZTIME ?= 10s # The duration to run fuzz testing, default to 10s if unset. +fuzz: # List all fuzz tests across the repo, and run them one at a time with the configured fuzztime. + @go list ./... | while read -r package; do \ + go test -list '^Fuzz' "$$package" | grep '^Fuzz' | while read -r func; do \ + echo "Running $$package $$func for $(FUZZTIME)..."; \ + GOGC=$(GOGC) go test "$$package" -run '^$$' -fuzz="$$func" -fuzztime=$(FUZZTIME) || exit 1; \ + done; \ + done; +.PHONY: fuzz lint: + go mod tidy golangci-lint run ./... .PHONY: lint diff --git a/go.mod b/go.mod index ff2560e0..f4169583 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,6 @@ require ( github.com/whyrusleeping/cbor-gen v0.1.0 go.uber.org/multierr v1.11.0 golang.org/x/crypto v0.21.0 - golang.org/x/sync v0.3.0 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 ) diff --git a/sim/cmd/f3sim/f3sim.go b/sim/cmd/f3sim/f3sim.go index 063fba5e..da4cd2cb 100644 --- a/sim/cmd/f3sim/f3sim.go +++ b/sim/cmd/f3sim/f3sim.go @@ -30,13 +30,8 @@ func main() { seed := *latencySeed + int64(i) fmt.Printf("Iteration %d: seed=%d, mean=%f\n", i, seed, *latencyMean) - latencyModel, err := latency.NewLogNormal(*latencySeed, time.Duration(*latencyMean*float64(time.Second))) - if err != nil { - log.Panicf("failed to instantiate log normal latency model: %c\n", err) - } - options := []sim.Option{ - sim.WithLatencyModel(latencyModel), + sim.WithLatencyModel(latency.NewLogNormal(*latencySeed, time.Duration(*latencyMean*float64(time.Second)))), sim.WithECEpochDuration(30 * time.Second), sim.WithECStabilisationDelay(0), sim.AddHonestParticipants( diff --git a/sim/latency/log_normal.go b/sim/latency/log_normal.go index a919c75b..625462a1 100644 --- a/sim/latency/log_normal.go +++ b/sim/latency/log_normal.go @@ -1,7 +1,6 @@ package latency import ( - "errors" "math" "math/rand" "sync" @@ -25,16 +24,14 @@ type LogNormal struct { } // NewLogNormal instantiates a new latency model of log normal latency -// distribution with the given mean. -func NewLogNormal(seed int64, mean time.Duration) (*LogNormal, error) { - if mean < 0 { - return nil, errors.New("mean duration cannot be negative") - } +// distribution with the given mean. This model will always return zero if mean +// latency duration is less than or equal to zero. +func NewLogNormal(seed int64, mean time.Duration) *LogNormal { return &LogNormal{ rng: rand.New(rand.NewSource(seed)), mean: mean, latencyFromTo: make(map[gpbft.ActorID]map[gpbft.ActorID]time.Duration), - }, nil + } } // Sample returns latency samples that correspond to the log normal distribution @@ -43,9 +40,10 @@ func NewLogNormal(seed int64, mean time.Duration) (*LogNormal, error) { // distribution. Latency from one participant to another may be asymmetric and // once generated remains constant for the lifetime of a simulation. // -// Note, here from and to are the same the latency sample will always be zero. +// Note, where from and to are the same or mean configured latency is not larger +// than zero the latency sample will always be zero. func (l *LogNormal) Sample(_ time.Time, from gpbft.ActorID, to gpbft.ActorID) time.Duration { - if from == to { + if from == to || l.mean <= 0 { return 0 } diff --git a/test/absent_test.go b/test/absent_test.go index 104bb307..fe4b3acc 100644 --- a/test/absent_test.go +++ b/test/absent_test.go @@ -8,19 +8,25 @@ import ( "github.com/stretchr/testify/require" ) -func TestAbsent(t *testing.T) { - t.Parallel() +func FuzzAbsentAdversary(f *testing.F) { + f.Add(98465230) + f.Add(651) + f.Add(-56) + f.Add(22) + f.Add(0) + f.Fuzz(absentAdversaryTest) +} - repeatInParallel(t, 1, func(t *testing.T, repetition int) { - // Total network size of 3 + 1, where the adversary has 1/4 of power. - sm, err := sim.NewSimulation(asyncOptions(t, repetition, +func absentAdversaryTest(t *testing.T, latencySeed int) { + sm, err := sim.NewSimulation( + asyncOptions(latencySeed, + // Total network size of 3 + 1, where the adversary has 1/4 of power. sim.AddHonestParticipants( 3, sim.NewUniformECChainGenerator(tipSetGeneratorSeed, 1, 10), uniformOneStoragePower), sim.WithAdversary(adversary.NewAbsentGenerator(oneStoragePower)), )...) - require.NoError(t, err) - require.NoErrorf(t, sm.Run(1, maxRounds), "%s", sm.Describe()) - }) + require.NoError(t, err) + require.NoErrorf(t, sm.Run(1, maxRounds), "%s", sm.Describe()) } diff --git a/test/constants_test.go b/test/constants_test.go index 5b4877ed..ab92b154 100644 --- a/test/constants_test.go +++ b/test/constants_test.go @@ -1,13 +1,11 @@ package test import ( - "testing" "time" "github.com/filecoin-project/go-f3/gpbft" "github.com/filecoin-project/go-f3/sim" "github.com/filecoin-project/go-f3/sim/latency" - "github.com/stretchr/testify/require" ) const ( @@ -16,7 +14,6 @@ const ( latencyAsync = 100 * time.Millisecond maxRounds = 10 - asyncIterations = 5000 EcEpochDuration = 30 * time.Second EcStabilisationDelay = 3 * time.Second ) @@ -44,11 +41,9 @@ func syncOptions(o ...sim.Option) []sim.Option { ) } -func asyncOptions(t *testing.T, latencySeed int, o ...sim.Option) []sim.Option { - lm, err := latency.NewLogNormal(int64(latencySeed), latencyAsync) - require.NoError(t, err) +func asyncOptions(latencySeed int, o ...sim.Option) []sim.Option { return append(o, - sim.WithLatencyModel(lm), + sim.WithLatencyModel(latency.NewLogNormal(int64(latencySeed), latencyAsync)), sim.WithECEpochDuration(EcEpochDuration), sim.WitECStabilisationDelay(EcStabilisationDelay), sim.WithGpbftOptions(testGpbftOptions...), diff --git a/test/decide_test.go b/test/decide_test.go index 12f010e9..d6bcb407 100644 --- a/test/decide_test.go +++ b/test/decide_test.go @@ -2,6 +2,7 @@ package test import ( "fmt" + "math/rand" "testing" "github.com/filecoin-project/go-f3/gpbft" @@ -10,19 +11,30 @@ import ( "github.com/stretchr/testify/require" ) -func TestImmediateDecide(t *testing.T) { - tsg := sim.NewTipSetGenerator(984615) +func FuzzImmediateDecideAdversary(f *testing.F) { + f.Add(98562314) + f.Add(8) + f.Add(-9554) + f.Add(95) + f.Add(65) + f.Fuzz(immediateDecideAdversaryTest) +} + +func immediateDecideAdversaryTest(t *testing.T, seed int) { + rng := rand.New(rand.NewSource(int64(seed))) + tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) baseChain := generateECChain(t, tsg) adversaryValue := baseChain.Extend(tsg.Sample()) - sm, err := sim.NewSimulation(asyncOptions(t, 1413, - sim.AddHonestParticipants( - 1, - sim.NewUniformECChainGenerator(tipSetGeneratorSeed, 1, 10), - uniformOneStoragePower), - sim.WithBaseChain(&baseChain), - // Add the adversary to the simulation with 3/4 of total power. - sim.WithAdversary(adversary.NewImmediateDecideGenerator(adversaryValue, gpbft.NewStoragePower(3))), - )...) + sm, err := sim.NewSimulation( + asyncOptions(rng.Int(), + sim.AddHonestParticipants( + 1, + sim.NewUniformECChainGenerator(rng.Uint64(), 1, 10), + uniformOneStoragePower), + sim.WithBaseChain(&baseChain), + // Add the adversary to the simulation with 3/4 of total power. + sim.WithAdversary(adversary.NewImmediateDecideGenerator(adversaryValue, gpbft.NewStoragePower(3))), + )...) require.NoError(t, err) err = sm.Run(1, maxRounds) diff --git a/test/ec_divergence_test.go b/test/ec_divergence_test.go index 6794cb16..254f7f80 100644 --- a/test/ec_divergence_test.go +++ b/test/ec_divergence_test.go @@ -25,76 +25,74 @@ func TestEcDivergence_AbsoluteDivergenceConvergesOnBase(t *testing.T) { }, { name: "async", - options: asyncOptions(t, 985623), + options: asyncOptions(985623), }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - repeatInParallel(t, 10, func(t *testing.T, repetition int) { - seedFuzzer := uint64(repetition) - - // uniformECChainGenerator generates different EC chain per instance but the same - // chain for all participants. i.e. with no divergence among votes. - uniformECChainGenerator := sim.NewUniformECChainGenerator(17*seedFuzzer, 5, 10) - - // randomECChainGenerator generates different EC chain per instance per participant, i.e. total disagreement across the network. - randomECChainGenerator := sim.NewRandomECChainGenerator(23*seedFuzzer, 5, 10) - - // divergeAfterECChainGenerator uses uniformECChainGenerator up until - // divergeAtInstance and randomECChainGenerator after it. This simulates a - // scenario where all participants initially propose the same chain at each - // instance. But then totally diverge to propose different chains only sharing - // the same base. - divergeAfterECChainGenerator := &ecChainGeneratorSwitcher{ - switchAtInstance: divergeAtInstance, - before: uniformECChainGenerator, - after: randomECChainGenerator, + seedFuzzer := uint64(985623) + + // uniformECChainGenerator generates different EC chain per instance but the same + // chain for all participants. i.e. with no divergence among votes. + uniformECChainGenerator := sim.NewUniformECChainGenerator(17*seedFuzzer, 5, 10) + + // randomECChainGenerator generates different EC chain per instance per participant, i.e. total disagreement across the network. + randomECChainGenerator := sim.NewRandomECChainGenerator(23*seedFuzzer, 5, 10) + + // divergeAfterECChainGenerator uses uniformECChainGenerator up until + // divergeAtInstance and randomECChainGenerator after it. This simulates a + // scenario where all participants initially propose the same chain at each + // instance. But then totally diverge to propose different chains only sharing + // the same base. + divergeAfterECChainGenerator := &ecChainGeneratorSwitcher{ + switchAtInstance: divergeAtInstance, + before: uniformECChainGenerator, + after: randomECChainGenerator, + } + + sm, err := sim.NewSimulation( + append(test.options, + sim.AddHonestParticipants(20, divergeAfterECChainGenerator, uniformOneStoragePower), + )...) + require.NoError(t, err) + require.NoErrorf(t, sm.Run(instanceCount, maxRounds), "%s", sm.Describe()) + + // Assert that every instance has reached consensus on the expected base chain, + // where: + // * before divergeAtInstance the decision must match the chain generated by + // uniformECChainGenerator, and + // * after it the decision must match the base of last decision made before + // divergeAtInstance. + // + // Because: + // * before divergeAtInstance, all nodes propose the same chain via + // uniformECChainGenerator, and + // * after it, every node proposes a chain (only sharing base tipset as required + // by gPBFT) + instance := sm.GetInstance(0) + require.NotNil(t, instance, "instance 0") + latestBaseECChain := instance.BaseChain + for i := uint64(0); i < instanceCount; i++ { + instance = sm.GetInstance(i + 1) + require.NotNil(t, instance, "instance %d", i) + + var wantDecision gpbft.ECChain + if i < divergeAtInstance { + wantDecision = divergeAfterECChainGenerator.GenerateECChain(i, *latestBaseECChain.Head(), math.MaxUint64) + // Sanity check that the chains generated are not the same but share the same + // base. + require.Equal(t, wantDecision.Base(), latestBaseECChain.Head()) + require.NotEqual(t, wantDecision.Suffix(), latestBaseECChain.Suffix()) + } else { + // After divergeAtInstance all nodes propose different chains. Therefore, the + // only agreeable chain is the base chain of instance before divergeAtInstance. + wantDecision = latestBaseECChain } - sm, err := sim.NewSimulation( - append(test.options, - sim.AddHonestParticipants(20, divergeAfterECChainGenerator, uniformOneStoragePower), - )...) - require.NoError(t, err) - require.NoErrorf(t, sm.Run(instanceCount, maxRounds), "%s", sm.Describe()) - - // Assert that every instance has reached consensus on the expected base chain, - // where: - // * before divergeAtInstance the decision must match the chain generated by - // uniformECChainGenerator, and - // * after it the decision must match the base of last decision made before - // divergeAtInstance. - // - // Because: - // * before divergeAtInstance, all nodes propose the same chain via - // uniformECChainGenerator, and - // * after it, every node proposes a chain (only sharing base tipset as required - // by gPBFT) - instance := sm.GetInstance(0) - require.NotNil(t, instance, "instance 0") - latestBaseECChain := instance.BaseChain - for i := uint64(0); i < instanceCount; i++ { - instance = sm.GetInstance(i + 1) - require.NotNil(t, instance, "instance %d", i) - - var wantDecision gpbft.ECChain - if i < divergeAtInstance { - wantDecision = divergeAfterECChainGenerator.GenerateECChain(i, *latestBaseECChain.Head(), math.MaxUint64) - // Sanity check that the chains generated are not the same but share the same - // base. - require.Equal(t, wantDecision.Base(), latestBaseECChain.Head()) - require.NotEqual(t, wantDecision.Suffix(), latestBaseECChain.Suffix()) - } else { - // After divergeAtInstance all nodes propose different chains. Therefore, the - // only agreeable chain is the base chain of instance before divergeAtInstance. - wantDecision = latestBaseECChain - } - - // Assert the consensus is reached at the head of expected chain. - requireConsensusAtInstance(t, sm, i, *wantDecision.Head()) - latestBaseECChain = instance.BaseChain - } - }) + // Assert the consensus is reached at the head of expected chain. + requireConsensusAtInstance(t, sm, i, *wantDecision.Head()) + latestBaseECChain = instance.BaseChain + } }) } } @@ -115,77 +113,75 @@ func TestEcDivergence_PartitionedNetworkConvergesOnChainWithMostPower(t *testing }, { name: "async", - options: asyncOptions(t, 4656), + options: asyncOptions(4656), }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - repeatInParallel(t, 10, func(t *testing.T, repetition int) { - seedFuzzer := uint64(repetition) - - chainGeneratorBeforePartition := sim.NewUniformECChainGenerator(17*seedFuzzer, 5, 10) - - groupOneChainGeneratorAfterPartition := &ecChainGeneratorSwitcher{ - switchAtInstance: partitionAtInstance, - before: chainGeneratorBeforePartition, - after: sim.NewUniformECChainGenerator(23*seedFuzzer, 8, 9), - } - groupTwoChainGeneratorAfterPartition := &ecChainGeneratorSwitcher{ - switchAtInstance: partitionAtInstance, - before: chainGeneratorBeforePartition, - after: sim.NewUniformECChainGenerator(29*seedFuzzer, 20, 23), + seedFuzzer := uint64(784523) + + chainGeneratorBeforePartition := sim.NewUniformECChainGenerator(17*seedFuzzer, 5, 10) + + groupOneChainGeneratorAfterPartition := &ecChainGeneratorSwitcher{ + switchAtInstance: partitionAtInstance, + before: chainGeneratorBeforePartition, + after: sim.NewUniformECChainGenerator(23*seedFuzzer, 8, 9), + } + groupTwoChainGeneratorAfterPartition := &ecChainGeneratorSwitcher{ + switchAtInstance: partitionAtInstance, + before: chainGeneratorBeforePartition, + after: sim.NewUniformECChainGenerator(29*seedFuzzer, 20, 23), + } + + sm, err := sim.NewSimulation( + append(test.options, + sim.AddHonestParticipants(21, groupOneChainGeneratorAfterPartition, uniformOneStoragePower), + sim.AddHonestParticipants(9, groupTwoChainGeneratorAfterPartition, uniformOneStoragePower), + )...) + require.NoError(t, err) + require.NoErrorf(t, sm.Run(instanceCount, maxRounds), "%s", sm.Describe()) + + // Assert that every instance has reached consensus on the expected base chain, + // where: + // * before partitionAtInstance the decision must match the chain generated by + // chainGeneratorBeforePartition, and + // * after it the decision must match the chains generated by + // groupOneChainGeneratorAfterPartition. + // + // Because: + // * before partitionAtInstance, all nodes propose the same chain at each + // instance via chainGeneratorBeforePartition, and + // * after it, they all should converge on the chains generated by the group with + // most power, i.e. group one + instance := sm.GetInstance(0) + require.NotNil(t, instance, "instance 0") + latestBaseECChain := instance.BaseChain + for i := uint64(0); i < instanceCount; i++ { + instance = sm.GetInstance(i + 1) + require.NotNil(t, instance, "instance %d", i) + + var wantDecision gpbft.ECChain + + if i < partitionAtInstance { + // Before partitionAtInstance all participants should converge on the chains + // generated by chainGeneratorBeforePartition. + wantDecision = chainGeneratorBeforePartition.GenerateECChain(i, *latestBaseECChain.Head(), math.MaxUint64) + } else { + // After partitionAtInstance all participants should converge on the chains + // generated by groupOneChainGeneratorAfterPartition. Because that group has over + // 2/3 of power across the network. + wantDecision = groupOneChainGeneratorAfterPartition.GenerateECChain(i, *latestBaseECChain.Head(), math.MaxUint64) } - sm, err := sim.NewSimulation( - append(test.options, - sim.AddHonestParticipants(21, groupOneChainGeneratorAfterPartition, uniformOneStoragePower), - sim.AddHonestParticipants(9, groupTwoChainGeneratorAfterPartition, uniformOneStoragePower), - )...) - require.NoError(t, err) - require.NoErrorf(t, sm.Run(instanceCount, maxRounds), "%s", sm.Describe()) - - // Assert that every instance has reached consensus on the expected base chain, - // where: - // * before partitionAtInstance the decision must match the chain generated by - // chainGeneratorBeforePartition, and - // * after it the decision must match the chains generated by - // groupOneChainGeneratorAfterPartition. - // - // Because: - // * before partitionAtInstance, all nodes propose the same chain at each - // instance via chainGeneratorBeforePartition, and - // * after it, they all should converge on the chains generated by the group with - // most power, i.e. group one - instance := sm.GetInstance(0) - require.NotNil(t, instance, "instance 0") - latestBaseECChain := instance.BaseChain - for i := uint64(0); i < instanceCount; i++ { - instance = sm.GetInstance(i + 1) - require.NotNil(t, instance, "instance %d", i) - - var wantDecision gpbft.ECChain - - if i < partitionAtInstance { - // Before partitionAtInstance all participants should converge on the chains - // generated by chainGeneratorBeforePartition. - wantDecision = chainGeneratorBeforePartition.GenerateECChain(i, *latestBaseECChain.Head(), math.MaxUint64) - } else { - // After partitionAtInstance all participants should converge on the chains - // generated by groupOneChainGeneratorAfterPartition. Because that group has over - // 2/3 of power across the network. - wantDecision = groupOneChainGeneratorAfterPartition.GenerateECChain(i, *latestBaseECChain.Head(), math.MaxUint64) - } + // Sanity check that the chains generated are not the same but share the same + // base. + require.Equal(t, wantDecision.Base(), latestBaseECChain.Head()) + require.NotEqual(t, wantDecision.Suffix(), latestBaseECChain.Suffix()) - // Sanity check that the chains generated are not the same but share the same - // base. - require.Equal(t, wantDecision.Base(), latestBaseECChain.Head()) - require.NotEqual(t, wantDecision.Suffix(), latestBaseECChain.Suffix()) - - // Assert the consensus is reached at the head of expected chain. - requireConsensusAtInstance(t, sm, i, wantDecision...) - latestBaseECChain = instance.BaseChain - } - }) + // Assert the consensus is reached at the head of expected chain. + requireConsensusAtInstance(t, sm, i, wantDecision...) + latestBaseECChain = instance.BaseChain + } }) } } diff --git a/test/honest_test.go b/test/honest_test.go index 3785c614..466e73b2 100644 --- a/test/honest_test.go +++ b/test/honest_test.go @@ -2,254 +2,177 @@ package test import ( "fmt" + "math/rand" "testing" + "github.com/filecoin-project/go-f3/gpbft" "github.com/filecoin-project/go-f3/sim" "github.com/filecoin-project/go-f3/sim/signing" "github.com/stretchr/testify/require" ) -func TestHonest_ChainAgreement(t *testing.T) { +func TestHonest_Agreement(t *testing.T) { t.Parallel() + maxParticipantCases := 10 + if testing.Short() { + // Reduce max participants to 5 during short test for faster completion. + maxParticipantCases = 5 + } + + // Tests with BLS are super slow; try fewer participant counts for them. + blsParticipantCount := []int{4, 5, 6} + + // The number of honest participants for which every table test is executed. + participantCounts := make([]int, maxParticipantCases) + for i := range participantCounts { + participantCounts[i] = i + 3 + } + tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) + baseChain := generateECChain(t, tsg) + targetChain := baseChain.Extend(tsg.Sample()) + tests := []struct { - name string - options []sim.Option - participantCount int + name string + options []sim.Option + participantCounts []int + wantConsensusOnAnyOf []gpbft.TipSet }{ { - name: "sync singleton no signing", - options: syncOptions(), - participantCount: 1, - }, - { - name: "sync singleton bls", - options: syncOptions(sim.WithSigningBackend(signing.NewBLSBackend())), - participantCount: 1, - }, - { - name: "sync pair no signing", - options: syncOptions(), - participantCount: 2, + name: "sync no signing", + options: syncOptions(), + participantCounts: participantCounts, + wantConsensusOnAnyOf: []gpbft.TipSet{*targetChain.Head()}, }, { - name: "sync pair bls", - options: syncOptions(sim.WithSigningBackend(signing.NewBLSBackend())), - participantCount: 2, + name: "sync bls", + options: syncOptions(sim.WithSigningBackend(signing.NewBLSBackend())), + participantCounts: blsParticipantCount, + wantConsensusOnAnyOf: []gpbft.TipSet{*targetChain.Head()}, }, { - name: "async pair no signing", - options: asyncOptions(t, 1413), - participantCount: 2, + name: "async no signing", + options: asyncOptions(21), + participantCounts: participantCounts, + wantConsensusOnAnyOf: []gpbft.TipSet{*baseChain.Head(), *targetChain.Head()}, }, { - name: "async pair bls", - options: asyncOptions(t, 1413, sim.WithSigningBackend(signing.NewBLSBackend())), - participantCount: 2, + name: "async pair bls", + options: asyncOptions(1413, sim.WithSigningBackend(signing.NewBLSBackend())), + wantConsensusOnAnyOf: []gpbft.TipSet{*baseChain.Head(), *targetChain.Head()}, }, } for _, test := range tests { test := test - t.Run(test.name, func(t *testing.T) { - tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) - baseChain := generateECChain(t, tsg) - targetChain := baseChain.Extend(tsg.Sample()) - test.options = append(test.options, - sim.WithBaseChain(&baseChain), - sim.AddHonestParticipants(test.participantCount, sim.NewFixedECChainGenerator(targetChain), uniformOneStoragePower)) - sm, err := sim.NewSimulation(test.options...) - require.NoError(t, err) - - require.NoErrorf(t, sm.Run(1, maxRounds), "%s", sm.Describe()) - requireConsensusAtFirstInstance(t, sm, *targetChain.Head()) - }) + for _, participantCount := range test.participantCounts { + participantCount := participantCount + name := fmt.Sprintf("%s %d", test.name, participantCount) + t.Run(name, func(t *testing.T) { + test.options = append(test.options, + sim.WithBaseChain(&baseChain), + sim.AddHonestParticipants(participantCount, sim.NewFixedECChainGenerator(targetChain), uniformOneStoragePower)) + sm, err := sim.NewSimulation(test.options...) + require.NoError(t, err) + require.NoErrorf(t, sm.Run(1, maxRounds), "%s", sm.Describe()) + requireConsensusAtFirstInstance(t, sm, test.wantConsensusOnAnyOf...) + }) + } } } -func TestHonest_ChainDisagreement(t *testing.T) { +func TestHonest_Disagreement(t *testing.T) { t.Parallel() + maxParticipantCases := 10 + if testing.Short() { + // Reduce max participants to 1 + maxParticipantCases = 5 + } + + // Tests with BLS are super slow; try fewer participant counts for them. + blsParticipantCount := []int{2, 4, 10} + + participantCounts := make([]int, maxParticipantCases) + for i := range participantCounts { + participantCounts[i] = (i + 1) * 2 + } + + tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) + baseChain := generateECChain(t, tsg) + oneChain := baseChain.Extend(tsg.Sample()) + anotherChain := baseChain.Extend(tsg.Sample()) + tests := []struct { - name string - options []sim.Option + name string + options []sim.Option + participantCounts []int }{ { - name: "sync pair no signing", - options: syncOptions(), + name: "sync no signing", + options: syncOptions(), + participantCounts: participantCounts, }, { - name: "sync pair bls", - options: syncOptions(sim.WithSigningBackend(signing.NewBLSBackend())), + name: "sync bls", + options: syncOptions(sim.WithSigningBackend(signing.NewBLSBackend())), + participantCounts: blsParticipantCount, }, { - name: "async pair no signing", - options: asyncOptions(t, 1413), + name: "async no signing", + options: asyncOptions(1413), + participantCounts: participantCounts, }, { - name: "async pair bls", - options: asyncOptions(t, 1413, sim.WithSigningBackend(signing.NewBLSBackend())), + name: "async bls", + options: asyncOptions(1413, sim.WithSigningBackend(signing.NewBLSBackend())), + participantCounts: blsParticipantCount, }, } + for _, test := range tests { test := test - t.Run(test.name, func(t *testing.T) { - tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) - baseChain := generateECChain(t, tsg) - oneChain := baseChain.Extend(tsg.Sample()) - anotherChain := baseChain.Extend(tsg.Sample()) - test.options = append(test.options, - sim.WithBaseChain(&baseChain), - sim.AddHonestParticipants(1, sim.NewFixedECChainGenerator(oneChain), uniformOneStoragePower), - sim.AddHonestParticipants(1, sim.NewFixedECChainGenerator(anotherChain), uniformOneStoragePower), - ) - sm, err := sim.NewSimulation(test.options...) - require.NoError(t, err) - require.NoErrorf(t, sm.Run(1, maxRounds), "%s", sm.Describe()) - requireConsensusAtFirstInstance(t, sm, baseChain...) - }) - } -} - -func TestSync_AgreementWithRepetition(t *testing.T) { - repeatInParallel(t, 50, func(t *testing.T, repetition int) { - honestCount := 3 + repetition - tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) - baseChain := generateECChain(t, tsg) - someChain := baseChain.Extend(tsg.Sample()) - sm, err := sim.NewSimulation(syncOptions( - sim.WithBaseChain(&baseChain), - sim.AddHonestParticipants(honestCount, sim.NewFixedECChainGenerator(someChain), uniformOneStoragePower), - )...) - require.NoError(t, err) - - require.NoErrorf(t, sm.Run(1, maxRounds), "%s", sm.Describe()) - // Synchronous, agreeing groups always decide the candidate. - requireConsensusAtFirstInstance(t, sm, *someChain.Head()) - }) -} - -func TestAsyncAgreement(t *testing.T) { - if testing.Short() { - t.Skip("skipping test in short mode.") - } + for _, participantCount := range test.participantCounts { + participantCount := participantCount + name := fmt.Sprintf("%s %d", test.name, participantCount) + t.Run(name, func(t *testing.T) { - t.Parallel() - // These iterations are much slower, so we can't test as many participants. - for n := 3; n <= 16; n++ { - honestCount := n - t.Run(fmt.Sprintf("honest count %d", honestCount), func(t *testing.T) { - repeatInParallel(t, asyncIterations, func(t *testing.T, repetition int) { - tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) - baseChain := generateECChain(t, tsg) - someChain := baseChain.Extend(tsg.Sample()) - sm, err := sim.NewSimulation(asyncOptions(t, repetition, + test.options = append(test.options, sim.WithBaseChain(&baseChain), - sim.AddHonestParticipants( - honestCount, - sim.NewFixedECChainGenerator(someChain), - uniformOneStoragePower), - )...) + sim.AddHonestParticipants(participantCount/2, sim.NewFixedECChainGenerator(oneChain), uniformOneStoragePower), + sim.AddHonestParticipants(participantCount/2, sim.NewFixedECChainGenerator(anotherChain), uniformOneStoragePower), + ) + sm, err := sim.NewSimulation(test.options...) require.NoError(t, err) require.NoErrorf(t, sm.Run(1, maxRounds), "%s", sm.Describe()) - requireConsensusAtFirstInstance(t, sm, *baseChain.Head(), *someChain.Head()) + // Insufficient majority means all should decide on base + requireConsensusAtFirstInstance(t, sm, *baseChain.Base()) }) - }) - } -} - -func TestSyncHalves(t *testing.T) { - t.Parallel() - - repeatInParallel(t, 15, func(t *testing.T, repetition int) { - honestCount := repetition*2 + 2 - tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) - baseChain := generateECChain(t, tsg) - oneChain := baseChain.Extend(tsg.Sample()) - anotherChain := baseChain.Extend(tsg.Sample()) - sm, err := sim.NewSimulation(syncOptions( - sim.WithBaseChain(&baseChain), - sim.AddHonestParticipants(honestCount/2, sim.NewFixedECChainGenerator(oneChain), uniformOneStoragePower), - sim.AddHonestParticipants(honestCount/2, sim.NewFixedECChainGenerator(anotherChain), uniformOneStoragePower), - )...) - require.NoError(t, err) - require.NoErrorf(t, sm.Run(1, maxRounds), "%s", sm.Describe()) - // Groups split 50/50 always decide the base. - requireConsensusAtFirstInstance(t, sm, *baseChain.Head()) - }) -} - -func TestSyncHalvesBLS(t *testing.T) { - if testing.Short() { - t.Skip("too slow for testing.Short") + } } - - t.Parallel() - repeatInParallel(t, 3, func(t *testing.T, repetition int) { - honestCount := repetition*2 + 2 - tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) - baseChain := generateECChain(t, tsg) - oneChain := baseChain.Extend(tsg.Sample()) - anotherChain := baseChain.Extend(tsg.Sample()) - sm, err := sim.NewSimulation(syncOptions( - sim.WithSigningBackend(signing.NewBLSBackend()), - sim.WithBaseChain(&baseChain), - sim.AddHonestParticipants(honestCount/2, sim.NewFixedECChainGenerator(oneChain), uniformOneStoragePower), - sim.AddHonestParticipants(honestCount/2, sim.NewFixedECChainGenerator(anotherChain), uniformOneStoragePower), - )...) - require.NoError(t, err) - require.NoErrorf(t, sm.Run(1, maxRounds), "%s", sm.Describe()) - // Groups split 50/50 always decide the base. - requireConsensusAtFirstInstance(t, sm, *baseChain.Head()) - }) } -func TestAsyncHalves(t *testing.T) { - if testing.Short() { - t.Skip("skipping test in short mode.") - } - - t.Parallel() - for n := 4; n <= 20; n += 2 { - honestCount := n - t.Run(fmt.Sprintf("honest count %d", honestCount), func(t *testing.T) { - repeatInParallel(t, asyncIterations, func(t *testing.T, repetition int) { - tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) - baseChain := generateECChain(t, tsg) - oneChain := baseChain.Extend(tsg.Sample()) - anotherChain := baseChain.Extend(tsg.Sample()) - sm, err := sim.NewSimulation(asyncOptions(t, repetition, - sim.WithBaseChain(&baseChain), - sim.AddHonestParticipants(honestCount/2, sim.NewFixedECChainGenerator(oneChain), uniformOneStoragePower), - sim.AddHonestParticipants(honestCount/2, sim.NewFixedECChainGenerator(anotherChain), uniformOneStoragePower), - )...) - require.NoError(t, err) - require.NoErrorf(t, sm.Run(1, maxRounds), "%s", sm.Describe()) - // Groups split 50/50 always decide the base. - requireConsensusAtFirstInstance(t, sm, *baseChain.Head()) - }) - }) - } +func FuzzHonest_AsyncRequireStrongQuorumToProgress(f *testing.F) { + f.Add(8956230) + f.Add(-54546) + f.Add(565) + f.Add(4) + f.Add(-88) + f.Fuzz(requireStrongQuorumToProgressTest) } -func TestRequireStrongQuorumToProgress(t *testing.T) { - if testing.Short() { - t.Skip("skipping test in short mode.") - } +func requireStrongQuorumToProgressTest(t *testing.T, seed int) { + tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) + baseChain := generateECChain(t, tsg) + oneChain := baseChain.Extend(tsg.Sample()) + anotherChain := baseChain.Extend(tsg.Sample()) + sm, err := sim.NewSimulation(asyncOptions(seed, + sim.WithBaseChain(&baseChain), + sim.AddHonestParticipants(10, sim.NewFixedECChainGenerator(oneChain), uniformOneStoragePower), + sim.AddHonestParticipants(20, sim.NewFixedECChainGenerator(anotherChain), uniformOneStoragePower), + )...) + require.NoError(t, err) + require.NoErrorf(t, sm.Run(1, maxRounds), "%s", sm.Describe()) - t.Parallel() - repeatInParallel(t, asyncIterations, func(t *testing.T, repetition int) { - tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) - baseChain := generateECChain(t, tsg) - oneChain := baseChain.Extend(tsg.Sample()) - anotherChain := baseChain.Extend(tsg.Sample()) - sm, err := sim.NewSimulation(asyncOptions(t, repetition, - sim.WithBaseChain(&baseChain), - sim.AddHonestParticipants(10, sim.NewFixedECChainGenerator(oneChain), uniformOneStoragePower), - sim.AddHonestParticipants(20, sim.NewFixedECChainGenerator(anotherChain), uniformOneStoragePower), - )...) - require.NoError(t, err) - require.NoErrorf(t, sm.Run(1, maxRounds), "%s", sm.Describe()) - // Must decide base. - requireConsensusAtFirstInstance(t, sm, *baseChain.Head()) - }) + // Must decide baseChain's head, i.e. the longest common prefix since there is no strong quorum. + requireConsensusAtFirstInstance(t, sm, *baseChain.Head()) } func TestHonest_FixedLongestCommonPrefix(t *testing.T) { @@ -275,34 +198,81 @@ func TestHonest_FixedLongestCommonPrefix(t *testing.T) { requireConsensusAtFirstInstance(t, sm, *commonPrefix.Head()) } -// TestHonest_MajorityCommonPrefix tests that in a network of honest participants, where there is a majority -// that vote for a chain with un-identical but some common prefix, the common prefix of the chain proposed by -// the majority is decided. -func TestHonest_MajorityCommonPrefix(t *testing.T) { +func FuzzHonest_SyncMajorityCommonPrefix(f *testing.F) { const instanceCount = 10 - majorityCommonPrefixGenerator := sim.NewUniformECChainGenerator(8484, 10, 20) - sm, err := sim.NewSimulation(syncOptions( - sim.AddHonestParticipants(20, sim.NewAppendingECChainGenerator( - majorityCommonPrefixGenerator, - sim.NewRandomECChainGenerator(23002354, 1, 8), - ), uniformOneStoragePower), - sim.AddHonestParticipants(5, sim.NewAppendingECChainGenerator( - sim.NewUniformECChainGenerator(84154, 1, 20), - sim.NewRandomECChainGenerator(8965741, 5, 8), - ), uniformOneStoragePower), - sim.AddHonestParticipants(1, sim.NewAppendingECChainGenerator( - sim.NewUniformECChainGenerator(2158, 10, 20), - sim.NewRandomECChainGenerator(154787878, 2, 8), - ), uniformOneStoragePower), - )...) - require.NoError(t, err) - require.NoErrorf(t, sm.Run(instanceCount, maxRounds), "%s", sm.Describe()) + f.Add(69846) + f.Add(2) + f.Add(545) + f.Add(-955846) + f.Add(28) + f.Fuzz(func(t *testing.T, seed int) { + rng := rand.New(rand.NewSource(int64(seed))) + majorityCommonPrefixGenerator := sim.NewUniformECChainGenerator(rng.Uint64(), 10, 20) + sm, err := sim.NewSimulation(append(syncOptions(), + sim.AddHonestParticipants(20, sim.NewAppendingECChainGenerator( + majorityCommonPrefixGenerator, + sim.NewRandomECChainGenerator(rng.Uint64(), 1, 8), + ), uniformOneStoragePower), + sim.AddHonestParticipants(5, sim.NewAppendingECChainGenerator( + sim.NewUniformECChainGenerator(rng.Uint64(), 1, 20), + sim.NewRandomECChainGenerator(rng.Uint64(), 5, 8), + ), uniformOneStoragePower), + sim.AddHonestParticipants(1, sim.NewAppendingECChainGenerator( + sim.NewUniformECChainGenerator(rng.Uint64(), 10, 20), + sim.NewRandomECChainGenerator(rng.Uint64(), 2, 8), + ), uniformOneStoragePower), + )...) + require.NoError(t, err) + require.NoErrorf(t, sm.Run(instanceCount, maxRounds), "%s", sm.Describe()) - // Must decide the longest common prefix proposed by the majority at every instance. - for i := 0; i < instanceCount; i++ { - ii := uint64(i) - instance := sm.GetInstance(ii) - commonPrefix := majorityCommonPrefixGenerator.GenerateECChain(ii, *instance.BaseChain.Base(), 0) - requireConsensusAtInstance(t, sm, ii, *commonPrefix.Head()) - } + // Must decide the longest common prefix's head proposed by the majority at every instance. + for i := uint64(0); i < instanceCount; i++ { + instance := sm.GetInstance(i) + commonPrefix := majorityCommonPrefixGenerator.GenerateECChain(i, *instance.BaseChain.Base(), 0) + requireConsensusAtInstance(t, sm, i, *commonPrefix.Head()) + } + }) +} + +// FuzzHonest_AsyncMajorityCommonPrefix tests that in a network of honest +// participants, where there is a majority that vote for a chain with +// un-identical but some common prefix, the final decision is one of the tipsets +// of that common prefix. Since due to latency it is possible that not all +// participant would decide on the head of the common prefix. +func FuzzHonest_AsyncMajorityCommonPrefix(f *testing.F) { + const instanceCount = 10 + f.Add(5465) + f.Add(54) + f.Add(321565) + f.Add(-7) + f.Add(5) + f.Fuzz(func(t *testing.T, seed int) { + rng := rand.New(rand.NewSource(int64(seed))) + majorityCommonPrefixGenerator := sim.NewUniformECChainGenerator(rng.Uint64(), 10, 20) + sm, err := sim.NewSimulation(append(asyncOptions(rng.Int()), + sim.AddHonestParticipants(20, sim.NewAppendingECChainGenerator( + majorityCommonPrefixGenerator, + sim.NewRandomECChainGenerator(rng.Uint64(), 1, 8), + ), uniformOneStoragePower), + sim.AddHonestParticipants(5, sim.NewAppendingECChainGenerator( + sim.NewUniformECChainGenerator(rng.Uint64(), 1, 20), + sim.NewRandomECChainGenerator(rng.Uint64(), 5, 8), + ), uniformOneStoragePower), + sim.AddHonestParticipants(1, sim.NewAppendingECChainGenerator( + sim.NewUniformECChainGenerator(rng.Uint64(), 10, 20), + sim.NewRandomECChainGenerator(rng.Uint64(), 2, 8), + ), uniformOneStoragePower), + )...) + require.NoError(t, err) + require.NoErrorf(t, sm.Run(instanceCount, maxRounds*2), "%s", sm.Describe()) + + // Must decide on any one of common prefix tipsets. Since, unlike + // FuzzHonest_SyncMajorityCommonPrefix, due to latency not all nodes would + // coverage exactly on common prefix head + for i := uint64(0); i < instanceCount; i++ { + instance := sm.GetInstance(i) + commonPrefix := majorityCommonPrefixGenerator.GenerateECChain(i, *instance.BaseChain.Base(), 0) + requireConsensusAtInstance(t, sm, i, commonPrefix...) + } + }) } diff --git a/test/multi_instance_test.go b/test/multi_instance_test.go index bb389f4e..f2087ff1 100644 --- a/test/multi_instance_test.go +++ b/test/multi_instance_test.go @@ -1,103 +1,119 @@ package test import ( + "fmt" + "math/rand" "testing" "github.com/filecoin-project/go-f3/sim" "github.com/stretchr/testify/require" ) -///// Tests for multiple chained instances of the protocol, no adversaries. +// TestHonestMultiInstance_Agreement tests for multiple chained instances of the protocol with no adversaries. +func TestHonestMultiInstance_Agreement(t *testing.T) { + t.Parallel() + const ( + instanceCount = 4000 + testRNGSeed = 8965130 + latencySeed = testRNGSeed * 7 + maxHonestCount = 10 + ) + // The number of honest participants for which every table test is executed. + participantCounts := make([]int, maxHonestCount) + for i := range participantCounts { + participantCounts[i] = i + 1 + } -const instanceCount = 4000 + tests := []struct { + name string + options []sim.Option + }{ + { + name: "sync", + options: syncOptions(), + }, + { + name: "async", + options: asyncOptions(latencySeed), + }, + } + for _, participantCount := range participantCounts { + participantCount := participantCount + for _, test := range tests { + test := test + name := fmt.Sprintf("%s %d", test.name, participantCount) + t.Run(name, func(t *testing.T) { + multiAgreementTest(t, testRNGSeed, participantCount, instanceCount, maxRounds, test.options...) + }) + } + } +} -func TestMultiSingleton(t *testing.T) { - sm, err := sim.NewSimulation(syncOptions( - sim.AddHonestParticipants(1, sim.NewUniformECChainGenerator(tipSetGeneratorSeed, 1, 10), uniformOneStoragePower), - )...) - require.NoError(t, err) - require.NoErrorf(t, sm.Run(instanceCount, maxRounds), "%s", sm.Describe()) - instance := sm.GetInstance(instanceCount) - require.NotNil(t, instance) - expected := instance.BaseChain - requireConsensusAtInstance(t, sm, instanceCount-1, *expected.Head()) +// FuzzHonestMultiInstance_AsyncDisagreement tests a scenario where two groups of equal +// participants, both in terms of count and power, vote for randomly generated +// chains at each instance, where the chain is uniform among all participants +// within the same group. It then asserts that over multiple instances the +// consensus is never reached and all nodes converge on the base chain of the +// first instance. +func FuzzHonestMultiInstance_AsyncDisagreement(f *testing.F) { + const ( + instanceCount = 1000 + honestCount = 6 + ) + f.Add(981) + f.Fuzz(func(t *testing.T, seed int) { + tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) + baseChain := generateECChain(t, tsg) + sm, err := sim.NewSimulation(asyncOptions(seed, + sim.WithBaseChain(&baseChain), + sim.AddHonestParticipants(honestCount/2, sim.NewUniformECChainGenerator(rand.Uint64(), 4, 10), uniformOneStoragePower), + sim.AddHonestParticipants(honestCount/2, sim.NewUniformECChainGenerator(rand.Uint64(), 1, 5), uniformOneStoragePower), + )...) + require.NoError(t, err) + require.NoErrorf(t, sm.Run(instanceCount, maxRounds), "%s", sm.Describe()) + // Insufficient majority means all should decide on base + requireConsensusAtFirstInstance(t, sm, *baseChain.Base()) + }) } -func TestMultiSyncPair(t *testing.T) { - sm, err := sim.NewSimulation(syncOptions( - sim.AddHonestParticipants(2, sim.NewUniformECChainGenerator(tipSetGeneratorSeed, 1, 10), uniformOneStoragePower), - )...) - require.NoError(t, err) - require.NoErrorf(t, sm.Run(instanceCount, maxRounds), "%s", sm.Describe()) - instance := sm.GetInstance(instanceCount) - require.NotNil(t, instance) - expected := instance.BaseChain - requireConsensusAtInstance(t, sm, instanceCount-1, *expected.Head()) +func FuzzHonestMultiInstance_SyncAgreement(f *testing.F) { + const ( + instanceCount = 4000 + honestCount = 5 + ) + f.Add(-47) + f.Fuzz(func(t *testing.T, seed int) { + multiAgreementTest(t, seed, honestCount, instanceCount, maxRounds, syncOptions()...) + }) } -func TestMultiASyncPair(t *testing.T) { - sm, err := sim.NewSimulation(asyncOptions(t, 1413, - sim.AddHonestParticipants(2, sim.NewUniformECChainGenerator(tipSetGeneratorSeed, 1, 10), uniformOneStoragePower), +func FuzzHonestMultiInstance_AsyncAgreement(f *testing.F) { + const ( + instanceCount = 4000 + honestCount = 5 + ) + f.Add(-7) + f.Fuzz(func(t *testing.T, seed int) { + multiAgreementTest(t, seed, honestCount, instanceCount, maxRounds*2, asyncOptions(seed)...) + }) +} + +func multiAgreementTest(t *testing.T, seed int, honestCount int, instanceCount uint64, maxRounds uint64, opts ...sim.Option) { + rng := rand.New(rand.NewSource(int64(seed))) + sm, err := sim.NewSimulation(append(opts, + sim.AddHonestParticipants( + honestCount, + // Generate a random EC chain for all participant that changes randomly at each + // instance. + sim.NewUniformECChainGenerator(rng.Uint64(), 1, 10), uniformOneStoragePower), )...) require.NoError(t, err) - require.NoErrorf(t, sm.Run(instanceCount, maxRounds), "%s", sm.Describe()) - // Note: when async, the decision is not always the latest possible value, - // but should be something recent. - // This expectation may need to be relaxed. instance := sm.GetInstance(instanceCount) require.NotNil(t, instance) expected := instance.BaseChain - requireConsensusAtInstance(t, sm, instanceCount-1, expected...) -} - -func TestMultiSyncAgreement(t *testing.T) { - if testing.Short() { - t.Skip("too slow for testing.Short") - } - - t.Parallel() - repeatInParallel(t, 9, func(t *testing.T, repetition int) { - honestCount := repetition + 3 - sm, err := sim.NewSimulation(syncOptions( - sim.AddHonestParticipants( - honestCount, - sim.NewUniformECChainGenerator(tipSetGeneratorSeed, 1, 10), - uniformOneStoragePower), - )...) - require.NoError(t, err) - // All nodes start with the same chain and will observe the same extensions of that chain - // in subsequent instances. - require.NoErrorf(t, sm.Run(instanceCount, maxRounds), "%s", sm.Describe()) - // Synchronous, agreeing groups always decide the candidate. - instance := sm.GetInstance(instanceCount) - require.NotNil(t, instance) - expected := instance.BaseChain - requireConsensusAtInstance(t, sm, instanceCount-1, expected...) - }) -} - -func TestMultiAsyncAgreement(t *testing.T) { - if testing.Short() { - t.Skip("too slow for testing.Short") - } - - t.Parallel() - repeatInParallel(t, 9, func(t *testing.T, repetition int) { - honestCount := repetition + 3 - sm, err := sim.NewSimulation(asyncOptions(t, 1414, - sim.AddHonestParticipants( - honestCount, - sim.NewUniformECChainGenerator(tipSetGeneratorSeed, 1, 10), - uniformOneStoragePower), - )...) - require.NoError(t, err) - require.NoErrorf(t, sm.Run(instanceCount, maxRounds), "%s", sm.Describe()) - // Note: The expected decision only needs to be something recent. - // Relax this expectation when the simEC chain is less clean. - instance := sm.GetInstance(instanceCount) - require.NotNil(t, instance) - expected := instance.BaseChain - requireConsensusAtInstance(t, sm, instanceCount-1, expected...) - }) + // Assert that the network reaches a decision at last completed instance, and the + // decision always matches the head of instance after it, which is initialised + // but not executed by the simulation due to hitting the instanceCount limit. + requireConsensusAtInstance(t, sm, instanceCount-1, *expected.Head()) } diff --git a/test/power_evolution_test.go b/test/power_evolution_test.go index 6ae8a5b0..0c991546 100644 --- a/test/power_evolution_test.go +++ b/test/power_evolution_test.go @@ -2,6 +2,7 @@ package test import ( "math" + "math/rand" "testing" "github.com/filecoin-project/go-f3/gpbft" @@ -9,14 +10,36 @@ import ( "github.com/stretchr/testify/require" ) -func TestStoragePower_IncreaseMidSimulation(t *testing.T) { +func FuzzStoragePower_SyncIncreaseMidSimulation(f *testing.F) { + f.Add(651651) + f.Add(-8) + f.Add(77) + f.Add(0) + f.Fuzz(func(t *testing.T, seed int) { + storagePowerIncreaseMidSimulationTest(t, seed, 8, maxRounds, syncOptions()...) + }) +} + +func FuzzStoragePower_AsyncIncreaseMidSimulation(f *testing.F) { + f.Add(151) + f.Add(-784) // Requires 29 rounds to succeed. Investigate further for potential issues. + f.Add(5460) // Requires 62 rounds to succeed. Investigate further for potential issues. + f.Add(-563) // Requires 71 rounds to succeed. Investigate further for potential issues. + + f.Fuzz(func(t *testing.T, seed int) { + storagePowerIncreaseMidSimulationTest(t, seed, 8, maxRounds*8, asyncOptions(seed)...) + }) +} + +func storagePowerIncreaseMidSimulationTest(t *testing.T, seed int, instanceCount uint64, maxRounds uint64, o ...sim.Option) { const ( - instanceCount = 8 - powerIncreaseAfterInstance = 4 groupOneStoragePower = 5 groupTwoStoragePowerBeforeIncrease = 2 groupTwoStoragePowerAfterIncrease = 21 ) + var powerIncreaseAfterInstance = instanceCount / 2 + rng := rand.New(rand.NewSource(int64(seed))) + groupOneStoragePowerer := sim.UniformStoragePower(gpbft.NewStoragePower(groupOneStoragePower)) groupTwoStoragePowerer := func(instance uint64, id gpbft.ActorID) *gpbft.StoragePower { switch { @@ -27,143 +50,119 @@ func TestStoragePower_IncreaseMidSimulation(t *testing.T) { } } - tests := []struct { - name string - options []sim.Option - }{ - { - name: "sync", - options: syncOptions(), - }, - { - name: "async", - options: asyncOptions(t, 55452), - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - repeatInParallel(t, 1, func(t *testing.T, repetition int) { - seedFuzzer := uint64(repetition) - tsg := sim.NewTipSetGenerator(7 * seedFuzzer) - baseChain := generateECChain(t, tsg) - groupOneEcGenerator := sim.NewUniformECChainGenerator(17*seedFuzzer, 5, 10) - groupTwoEcGenerator := sim.NewUniformECChainGenerator(23*seedFuzzer, 5, 10) - sm, err := sim.NewSimulation( - append(test.options, - sim.WithBaseChain(&baseChain), - // Group 1: 10 participants with fixed storage power throughout the simulation - sim.AddHonestParticipants( - 10, - groupOneEcGenerator, - groupOneStoragePowerer), - // Group 2: 10 participants with smaller storage power up to instance 4 and larger - // after that. - sim.AddHonestParticipants( - 10, - groupTwoEcGenerator, - groupTwoStoragePowerer), - )...) - require.NoError(t, err) - require.NoErrorf(t, sm.Run(instanceCount, maxRounds), "%s", sm.Describe()) - - // Assert that the chains agreed upon belong to group 1 before instance 4 and - // to group 2 after that. - base := *baseChain.Head() - for i := uint64(0); i < instanceCount-1; i++ { - instance := sm.GetInstance(i + 1) - require.NotNil(t, instance, "instance %d", i) - - var chainBackedByMostPower, chainBackedByLeastPower gpbft.ECChain - // UniformECChainGenerator caches the generated chains for each instance and disregards participant IDs. - if i < powerIncreaseAfterInstance { - chainBackedByMostPower = groupOneEcGenerator.GenerateECChain(i, base, math.MaxUint64) - chainBackedByLeastPower = groupTwoEcGenerator.GenerateECChain(i, base, math.MaxUint64) - } else { - chainBackedByMostPower = groupTwoEcGenerator.GenerateECChain(i, base, math.MaxUint64) - chainBackedByLeastPower = groupOneEcGenerator.GenerateECChain(i, base, math.MaxUint64) - } - - // Sanity check that the chains generated by either group are not the same but - // share the same base. - require.Equal(t, chainBackedByMostPower.Base(), chainBackedByLeastPower.Base()) - require.NotEqual(t, chainBackedByMostPower.Suffix(), chainBackedByLeastPower.Suffix()) - - // Assert the consensus is reached on the chain with most power. - requireConsensusAtInstance(t, sm, i, chainBackedByMostPower...) - base = *instance.BaseChain.Head() - } - }) - }) + tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) + baseChain := generateECChain(t, tsg) + groupOneEcGenerator := sim.NewUniformECChainGenerator(rng.Uint64(), 5, 10) + groupTwoEcGenerator := sim.NewUniformECChainGenerator(rng.Uint64(), 5, 10) + sm, err := sim.NewSimulation( + append(o, + sim.WithBaseChain(&baseChain), + // Group 1: 10 participants with fixed storage power throughout the simulation + sim.AddHonestParticipants( + 10, + groupOneEcGenerator, + groupOneStoragePowerer), + // Group 2: 10 participants with smaller storage power up to instance 4 and larger + // after that. + sim.AddHonestParticipants( + 10, + groupTwoEcGenerator, + groupTwoStoragePowerer), + )...) + require.NoError(t, err) + require.NoErrorf(t, sm.Run(instanceCount, maxRounds), "%s", sm.Describe()) + + // Assert that the chains agreed upon belong to group 1 before instance 4 and + // to group 2 after that. + base := *baseChain.Head() + for i := uint64(0); i < instanceCount-1; i++ { + instance := sm.GetInstance(i + 1) + require.NotNil(t, instance, "instance %d", i) + + var chainBackedByMostPower, chainBackedByLeastPower gpbft.ECChain + // UniformECChainGenerator caches the generated chains for each instance and disregards participant IDs. + if i < powerIncreaseAfterInstance { + chainBackedByMostPower = groupOneEcGenerator.GenerateECChain(i, base, math.MaxUint64) + chainBackedByLeastPower = groupTwoEcGenerator.GenerateECChain(i, base, math.MaxUint64) + } else { + chainBackedByMostPower = groupTwoEcGenerator.GenerateECChain(i, base, math.MaxUint64) + chainBackedByLeastPower = groupOneEcGenerator.GenerateECChain(i, base, math.MaxUint64) + } + + // Sanity check that the chains generated by either group are not the same but + // share the same base. + require.Equal(t, chainBackedByMostPower.Base(), chainBackedByLeastPower.Base()) + require.NotEqual(t, chainBackedByMostPower.Suffix(), chainBackedByLeastPower.Suffix()) + + // Assert the consensus is reached on the chain with most power. + requireConsensusAtInstance(t, sm, i, chainBackedByMostPower...) + base = *instance.BaseChain.Head() } } -func TestStoragePower_DecreaseRevertsToBase(t *testing.T) { - const ( - instanceCount = 8 - ) - tests := []struct { - name string - options []sim.Option - }{ - { - name: "sync", - options: syncOptions(), - }, - { - name: "async", - options: asyncOptions(t, 55452), - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - repeatInParallel(t, 1, func(t *testing.T, repetition int) { - seedFuzzer := uint64(repetition) - tsg := sim.NewTipSetGenerator(7 * seedFuzzer) - baseChain := generateECChain(t, tsg) - sm, err := sim.NewSimulation( - append(test.options, - sim.WithBaseChain(&baseChain), - // Group 1: 10 participants with fixed storage power of 2 per participant - // throughout the simulation. - sim.AddHonestParticipants( - 10, - sim.NewBaseECChainGenerator(), - sim.UniformStoragePower(gpbft.NewStoragePower(3)), - ), - // Group 2: 10 participants with decreasing storage power starting from 8 and - // decreasing by 1 per instance per participant. - sim.AddHonestParticipants( - 10, - sim.NewUniformECChainGenerator(17*seedFuzzer, 5, 10), - func(instance uint64, id gpbft.ActorID) *gpbft.StoragePower { - // Decrease storage power of each participant by 1 per instance. - // The plus one is there to avoid zero powered actors as it is an error. - return gpbft.NewStoragePower(int64(instanceCount - instance + 1)) - }, - ), - )...) - require.NoError(t, err) - require.NoErrorf(t, sm.Run(instanceCount, maxRounds), "%s", sm.Describe()) - - // Assert that the last three instances have exactly one tipset in their base - // chain to which all participants converge. Because, by the fifth instance the - // total storage power of group 2 should have sufficiently decreased to no longer - // influence consensus. As a result, the chain proposed by group 1 should become - // the dominant one and that group always proposes the base chain, which should - // have exactly one tipset. - for i := uint64(instanceCount - 3); i < instanceCount; i++ { - instance := sm.GetInstance(i) - require.NotNil(t, instance) - - // Assert that the base chain only has one tipset, i.e. the chain proposed by - // group 1 is the dominant one. - require.Len(t, instance.BaseChain, 1) - - // Assert that the head tipset of all decisions made by participants is the base - // of instance's base-chain. - requireConsensusAtInstance(t, sm, i, *instance.BaseChain.Base()) - } - }) - }) +func FuzzStoragePower_SyncDecreaseRevertsToBase(f *testing.F) { + f.Add(545444) + f.Add(654) + f.Add(-151) + f.Fuzz(func(t *testing.T, seed int) { + storagePowerDecreaseRevertsToBaseTest(t, seed, 100, maxRounds, syncOptions()...) + }) +} + +func FuzzStoragePower_AsyncDecreaseRevertsToBase(f *testing.F) { + f.Add(24) + f.Add(-44) + f.Add(11151) + f.Fuzz(func(t *testing.T, seed int) { + storagePowerDecreaseRevertsToBaseTest(t, seed, 100, maxRounds, asyncOptions(seed)...) + }) +} + +func storagePowerDecreaseRevertsToBaseTest(t *testing.T, seed int, instanceCount uint64, maxRounds uint64, o ...sim.Option) { + rng := rand.New(rand.NewSource(int64(seed))) + tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) + baseChain := generateECChain(t, tsg) + sm, err := sim.NewSimulation( + append(o, + sim.WithBaseChain(&baseChain), + // Group 1: 10 participants with fixed storage power of 2 per participant + // throughout the simulation. + sim.AddHonestParticipants( + 10, + sim.NewBaseECChainGenerator(), + sim.UniformStoragePower(gpbft.NewStoragePower(int64(instanceCount/2))), + ), + // Group 2: 10 participants with decreasing storage power starting from 8 and + // decreasing by 1 per instance per participant. + sim.AddHonestParticipants( + 10, + sim.NewUniformECChainGenerator(rng.Uint64(), 5, 10), + func(instance uint64, id gpbft.ActorID) *gpbft.StoragePower { + // Decrease storage power of each participant by 1 per instance. + // The plus one is there to avoid zero powered actors as it is an error. + return gpbft.NewStoragePower(int64(instanceCount - instance + 1)) + }, + ), + )...) + require.NoError(t, err) + require.NoErrorf(t, sm.Run(instanceCount, maxRounds), "%s", sm.Describe()) + + // Assert that the last three instances have exactly one tipset in their base + // chain to which all participants converge. Because, by the fifth instance the + // total storage power of group 2 should have sufficiently decreased to no longer + // influence consensus. As a result, the chain proposed by group 1 should become + // the dominant one and that group always proposes the base chain, which should + // have exactly one tipset. + for i := instanceCount/2 + 1; i < instanceCount; i++ { + instance := sm.GetInstance(i) + require.NotNil(t, instance) + + // Assert that the base chain only has one tipset, i.e. the chain proposed by + // group 1 is the dominant one. + require.Len(t, instance.BaseChain, 1) + + // Assert that the head tipset of all decisions made by participants is the base + // of instance's base-chain. + requireConsensusAtInstance(t, sm, i, *instance.BaseChain.Base()) } } diff --git a/test/repeat_test.go b/test/repeat_test.go index 76df0071..2366d243 100644 --- a/test/repeat_test.go +++ b/test/repeat_test.go @@ -10,99 +10,112 @@ import ( "github.com/stretchr/testify/require" ) -func TestRepeat(t *testing.T) { - if testing.Short() { - t.Skip("too slow for testing.Short") +var ( + repeatOnce = func(int) adversary.RepetitionSampler { + return func(*gpbft.GMessage) int { + return 1 + } } - - t.Parallel() - - honestCounts := []int{ + repeatBoundedRandom = func(seed int) adversary.RepetitionSampler { + return newBoundedRepeater(int64(seed), 10, 50) + } + repeatZipF = func(seed int) adversary.RepetitionSampler { + rng := rand.New(rand.NewSource(int64(seed))) + zipf := rand.NewZipf(rng, 1.2, 1.0, 100) + return func(*gpbft.GMessage) int { + return int(zipf.Uint64()) + } + } + repeatBoundedQuality = func(seed int) adversary.RepetitionSampler { + boundedRepeater := newBoundedRepeater(int64(seed), 10, 50) + return func(msg *gpbft.GMessage) int { + if msg.Vote.Step != gpbft.QUALITY_PHASE { + return 0 + } + return boundedRepeater(msg) + } + } + repeatBoundedCommit = func(seed int) adversary.RepetitionSampler { + boundedRepeater := newBoundedRepeater(int64(seed), 10, 50) + return func(msg *gpbft.GMessage) int { + if msg.Vote.Step != gpbft.COMMIT_PHASE { + return 0 + } + return boundedRepeater(msg) + } + } + repeatAdversaryTestHonestCounts = []int{ 2, // 1/3 adversary power 3, // 1/4 adversary power 4, // 1/5 adversary power } +) + +func TestRepeatAdversary(t *testing.T) { tests := []struct { name string repetitionSampler func(int) adversary.RepetitionSampler - maxRounds uint64 }{ { - name: "once", - repetitionSampler: func(int) adversary.RepetitionSampler { - return func(*gpbft.GMessage) int { - return 1 - } - }, - maxRounds: maxRounds, + name: "once", + repetitionSampler: repeatOnce, }, { - name: "bounded uniform random", - repetitionSampler: func(repetition int) adversary.RepetitionSampler { - return newBoundedRepeater(int64(repetition), 10, 50) - }, - maxRounds: maxRounds, + name: "bounded uniform random", + repetitionSampler: repeatBoundedRandom, }, { - name: "zipf", - repetitionSampler: func(repetition int) adversary.RepetitionSampler { - rng := rand.New(rand.NewSource(int64(repetition))) - zipf := rand.NewZipf(rng, 1.2, 1.0, 100) - return func(*gpbft.GMessage) int { - return int(zipf.Uint64()) - } - }, - maxRounds: maxRounds, + name: "zipf", + repetitionSampler: repeatZipF, }, { - name: "QUALITY Repeater", - repetitionSampler: func(repetition int) adversary.RepetitionSampler { - boundedRepeater := newBoundedRepeater(int64(repetition), 10, 50) - return func(msg *gpbft.GMessage) int { - if msg.Vote.Step != gpbft.QUALITY_PHASE { - return 0 - } - return boundedRepeater(msg) - } - }, - maxRounds: maxRounds, + name: "QUALITY Repeater", + repetitionSampler: repeatBoundedQuality, }, { - name: "COMMIT Repeater", - repetitionSampler: func(repetition int) adversary.RepetitionSampler { - boundedRepeater := newBoundedRepeater(int64(repetition), 10, 50) - return func(msg *gpbft.GMessage) int { - if msg.Vote.Step != gpbft.COMMIT_PHASE { - return 0 - } - return boundedRepeater(msg) - } - }, - maxRounds: 100, + name: "COMMIT Repeater", + repetitionSampler: repeatBoundedCommit, }, } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { - for _, hc := range honestCounts { - repeatInParallel(t, asyncIterations, func(t *testing.T, repetition int) { - dist := test.repetitionSampler(repetition) - sm, err := sim.NewSimulation(asyncOptions(t, repetition, - sim.AddHonestParticipants( - hc, - sim.NewUniformECChainGenerator(tipSetGeneratorSeed, 1, 10), - uniformOneStoragePower), - sim.WithAdversary(adversary.NewRepeatGenerator(oneStoragePower, dist)), - )...) - require.NoError(t, err) - - require.NoErrorf(t, sm.Run(1, test.maxRounds), "%s", sm.Describe()) - }) + for _, hc := range repeatAdversaryTestHonestCounts { + repeatAdversaryTest(t, 98461, hc, maxRounds, test.repetitionSampler) } }) } } +func FuzzRepeatAdversary(f *testing.F) { + f.Add(68465) + f.Add(-5) + f.Add(-5454) + f.Fuzz(func(t *testing.T, seed int) { + for _, hc := range repeatAdversaryTestHonestCounts { + repeatAdversaryTest(t, seed, hc, maxRounds, repeatOnce) + repeatAdversaryTest(t, seed, hc, maxRounds, repeatZipF) + repeatAdversaryTest(t, seed, hc, maxRounds, repeatBoundedRandom) + repeatAdversaryTest(t, seed, hc, maxRounds*2, repeatBoundedQuality) + repeatAdversaryTest(t, seed, hc, maxRounds*2, repeatBoundedCommit) + } + }) +} + +func repeatAdversaryTest(t *testing.T, seed int, honestCount int, maxRounds uint64, repetitionSampler func(int) adversary.RepetitionSampler) { + rng := rand.New(rand.NewSource(int64(seed))) + dist := repetitionSampler(seed) + sm, err := sim.NewSimulation(asyncOptions(rng.Int(), + sim.AddHonestParticipants( + honestCount, + sim.NewUniformECChainGenerator(rng.Uint64(), 1, 10), + uniformOneStoragePower), + sim.WithAdversary(adversary.NewRepeatGenerator(oneStoragePower, dist)), + )...) + require.NoError(t, err) + require.NoErrorf(t, sm.Run(1, maxRounds), "%s", sm.Describe()) +} + func newBoundedRepeater(rngSeed int64, min, max int) adversary.RepetitionSampler { rng := rand.New(rand.NewSource(rngSeed)) return func(*gpbft.GMessage) int { diff --git a/test/util_test.go b/test/util_test.go index 3680910f..e489d335 100644 --- a/test/util_test.go +++ b/test/util_test.go @@ -1,32 +1,13 @@ package test import ( - "os" - "runtime" - "strconv" "testing" "github.com/filecoin-project/go-f3/gpbft" "github.com/filecoin-project/go-f3/sim" "github.com/stretchr/testify/require" - "golang.org/x/sync/errgroup" ) -// repetitionParallelism sets the limit to the maximum degree of parallelism by which repetitions are executed. -// By default this value is set to runtime.NumCPU, and is overridable via F3_TEST_REPETITION_PARALLELISM environment variable. -// A negative value indicates no limit. -var repetitionParallelism int - -func init() { - repetitionParallelism = runtime.NumCPU() - ps, found := os.LookupEnv("F3_TEST_REPETITION_PARALLELISM") - if found { - if v, err := strconv.ParseInt(ps, 10, 32); err == nil { - repetitionParallelism = int(v) - } - } -} - // Expects the decision in the first instance to be one of the given tipsets. func requireConsensusAtFirstInstance(t *testing.T, sm *sim.Simulation, expectAnyOf ...gpbft.TipSet) { requireConsensusAtInstance(t, sm, 0, expectAnyOf...) @@ -51,27 +32,3 @@ func generateECChain(t *testing.T, tsg *sim.TipSetGenerator) gpbft.ECChain { require.NoError(t, err) return chain } - -// repeatInParallel repeats target for the given number of repetitions. -// Set F3_TEST_REPETITION_PARALLELISM=1 to run repetitions sequentially. -// See repetitionParallelism. -func repeatInParallel(t *testing.T, repetitions int, target func(t *testing.T, repetition int)) { - // When no parallelism is requested, run repetitions sequentially so their logs are readable. - if repetitionParallelism <= 1 { - for i := 0; i <= repetitions; i++ { - t.Log("repetition", i) - target(t, i) - } - } else { - var eg errgroup.Group - eg.SetLimit(repetitionParallelism) - for i := 1; i <= repetitions; i++ { - repetition := i - eg.Go(func() error { - target(t, repetition) - return nil - }) - } - require.NoError(t, eg.Wait()) - } -} diff --git a/test/withhold_test.go b/test/withhold_test.go index 327c4e93..b026297a 100644 --- a/test/withhold_test.go +++ b/test/withhold_test.go @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestWitholdCommit1(t *testing.T) { +func TestWitholdCommitAdversary(t *testing.T) { tests := []struct { name string gst time.Duration @@ -37,8 +37,7 @@ func TestWitholdCommit1(t *testing.T) { for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { - nearSynchrony, err := latency.NewLogNormal(1413, 10*time.Millisecond) - require.NoError(t, err) + nearSynchrony := latency.NewLogNormal(1413, 10*time.Millisecond) tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) baseChain := generateECChain(t, tsg) a := baseChain.Extend(tsg.Sample())