diff --git a/sim/adversary/absent.go b/sim/adversary/absent.go index 50c86e29..a9a7379d 100644 --- a/sim/adversary/absent.go +++ b/sim/adversary/absent.go @@ -19,6 +19,15 @@ func NewAbsent(id gpbft.ActorID, host gpbft.Host) *Absent { } } +func NewAbsentGenerator(power *gpbft.StoragePower) Generator { + return func(id gpbft.ActorID, host Host) *Adversary { + return &Adversary{ + Receiver: NewAbsent(id, host), + Power: power, + } + } +} + func (a *Absent) ID() gpbft.ActorID { return a.id } diff --git a/sim/adversary/api.go b/sim/adversary/adversary.go similarity index 82% rename from sim/adversary/api.go rename to sim/adversary/adversary.go index 263e00c6..12bbdd41 100644 --- a/sim/adversary/api.go +++ b/sim/adversary/adversary.go @@ -15,3 +15,10 @@ type Host interface { // before messages are actually received. BroadcastSynchronous(sender gpbft.ActorID, msg gpbft.GMessage) } + +type Generator func(gpbft.ActorID, Host) *Adversary + +type Adversary struct { + Receiver + Power *gpbft.StoragePower +} diff --git a/sim/adversary/decide.go b/sim/adversary/decide.go index 3c0cd108..b210ebd7 100644 --- a/sim/adversary/decide.go +++ b/sim/adversary/decide.go @@ -22,31 +22,20 @@ func NewImmediateDecide(id gpbft.ActorID, host Host, value gpbft.ECChain) *Immed } } +func NewImmediateDecideGenerator(value gpbft.ECChain, power *gpbft.StoragePower) Generator { + return func(id gpbft.ActorID, host Host) *Adversary { + return &Adversary{ + Receiver: NewImmediateDecide(id, host, value), + Power: power, + } + } +} + func (i *ImmediateDecide) ID() gpbft.ActorID { return i.id } func (i *ImmediateDecide) Start() error { - return nil -} - -func (i *ImmediateDecide) ReceiveECChain(gpbft.ECChain) error { - return nil -} - -func (i *ImmediateDecide) ValidateMessage(_ *gpbft.GMessage) (bool, error) { - return true, nil -} - -func (i *ImmediateDecide) ReceiveMessage(_ *gpbft.GMessage, _ bool) (bool, error) { - return true, nil -} - -func (i *ImmediateDecide) ReceiveAlarm() error { - return nil -} - -func (i *ImmediateDecide) Begin() { _, powertable, _ := i.host.GetCanonicalChain() // Immediately send a DECIDE message payload := gpbft.Payload{ @@ -82,6 +71,23 @@ func (i *ImmediateDecide) Begin() { } i.broadcast(payload, &justification, powertable) + return nil +} + +func (i *ImmediateDecide) ReceiveECChain(gpbft.ECChain) error { + return nil +} + +func (i *ImmediateDecide) ValidateMessage(_ *gpbft.GMessage) (bool, error) { + return true, nil +} + +func (i *ImmediateDecide) ReceiveMessage(_ *gpbft.GMessage, _ bool) (bool, error) { + return true, nil +} + +func (i *ImmediateDecide) ReceiveAlarm() error { + return nil } func (i *ImmediateDecide) AllowMessage(_ gpbft.ActorID, _ gpbft.ActorID, _ gpbft.GMessage) bool { diff --git a/sim/adversary/repeat.go b/sim/adversary/repeat.go index f5c42633..7cb8317a 100644 --- a/sim/adversary/repeat.go +++ b/sim/adversary/repeat.go @@ -60,6 +60,15 @@ func NewRepeat(id gpbft.ActorID, host gpbft.Host, sampler RepetitionSampler) *Re } } +func NewRepeatGenerator(power *gpbft.StoragePower, sampler RepetitionSampler) Generator { + return func(id gpbft.ActorID, host Host) *Adversary { + return &Adversary{ + Receiver: NewRepeat(id, host, sampler), + Power: power, + } + } +} + func (r *Repeat) ReceiveMessage(msg *gpbft.GMessage, _ bool) (bool, error) { echoCount := r.repetitionSampler(msg) if echoCount <= 0 { diff --git a/sim/adversary/withhold.go b/sim/adversary/withhold.go index a12bc253..ecefbd8f 100644 --- a/sim/adversary/withhold.go +++ b/sim/adversary/withhold.go @@ -1,6 +1,7 @@ package adversary import ( + "errors" "sort" "github.com/filecoin-project/go-bitfield" @@ -28,6 +29,17 @@ func NewWitholdCommit(id gpbft.ActorID, host Host) *WithholdCommit { } } +func NewWitholdCommitGenerator(power *gpbft.StoragePower, victims []gpbft.ActorID, victimValue gpbft.ECChain) Generator { + return func(id gpbft.ActorID, host Host) *Adversary { + wc := NewWitholdCommit(id, host) + wc.SetVictim(victims, victimValue) + return &Adversary{ + Receiver: wc, + Power: power, + } + } +} + func (w *WithholdCommit) SetVictim(victims []gpbft.ActorID, victimValue gpbft.ECChain) { w.victims = victims w.victimValue = victimValue @@ -38,26 +50,11 @@ func (w *WithholdCommit) ID() gpbft.ActorID { } func (w *WithholdCommit) Start() error { - return nil -} - -func (w *WithholdCommit) ReceiveECChain(gpbft.ECChain) error { - return nil -} -func (a *WithholdCommit) ValidateMessage(_ *gpbft.GMessage) (bool, error) { - return true, nil -} - -func (a *WithholdCommit) ReceiveMessage(_ *gpbft.GMessage, _ bool) (bool, error) { - return true, nil -} - -func (w *WithholdCommit) ReceiveAlarm() error { - return nil -} + if len(w.victims) == 0 { + return errors.New("victims must be set") + } -func (w *WithholdCommit) Begin() { _, powertable, _ := w.host.GetCanonicalChain() broadcast := w.broadcastHelper(w.id, powertable) // All victims need to see QUALITY and PREPARE in order to send their COMMIT, @@ -114,6 +111,23 @@ func (w *WithholdCommit) Begin() { } broadcast(commitPayload, &justification) + return nil +} + +func (w *WithholdCommit) ReceiveECChain(gpbft.ECChain) error { + return nil +} + +func (a *WithholdCommit) ValidateMessage(_ *gpbft.GMessage) (bool, error) { + return true, nil +} + +func (a *WithholdCommit) ReceiveMessage(_ *gpbft.GMessage, _ bool) (bool, error) { + return true, nil +} + +func (w *WithholdCommit) ReceiveAlarm() error { + return nil } func (w *WithholdCommit) AllowMessage(_ gpbft.ActorID, to gpbft.ActorID, msg gpbft.GMessage) bool { diff --git a/sim/cmd/f3sim/f3sim.go b/sim/cmd/f3sim/f3sim.go index 7c97a2f4..c8aafd5d 100644 --- a/sim/cmd/f3sim/f3sim.go +++ b/sim/cmd/f3sim/f3sim.go @@ -9,6 +9,8 @@ import ( "github.com/filecoin-project/go-f3/gpbft" "github.com/filecoin-project/go-f3/sim" + "github.com/filecoin-project/go-f3/sim/latency" + "github.com/filecoin-project/go-f3/sim/signing" ) func main() { @@ -28,24 +30,46 @@ func main() { seed := *latencySeed + int64(i) fmt.Printf("Iteration %d: seed=%d, mean=%f\n", i, seed, *latencyMean) - simConfig := sim.Config{ - HonestCount: *participantCount, - LatencySeed: *latencySeed, - LatencyMean: time.Duration(*latencyMean * float64(time.Second)), - ECEpochDuration: 30 * time.Second, - ECStabilisationDelay: 0, + 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) + } + + tsg := sim.NewTipSetGenerator(uint64(seed)) + baseChain, err := gpbft.NewChain(tsg.Sample()) + if err != nil { + log.Fatalf("failed to generate base chain: %v\n", err) } - graniteConfig := gpbft.GraniteConfig{ + + graniteConfig := &gpbft.GraniteConfig{ Delta: time.Duration(*graniteDelta * float64(time.Second)), DeltaBackOffExponent: *deltaBackOffExponent, } - sm, err := sim.NewSimulation(simConfig, graniteConfig, *traceLevel) + options := []sim.Option{ + sim.WithHonestParticipantCount(*participantCount), + sim.WithLatencyModel(latencyModel), + sim.WithECEpochDuration(30 * time.Second), + sim.WithECStabilisationDelay(0), + sim.WithTipSetGenerator(tsg), + sim.WithBaseChain(&baseChain), + sim.WithTraceLevel(*traceLevel), + sim.WithGraniteConfig(graniteConfig), + } + + if os.Getenv("F3_TEST_USE_BLS") == "1" { + options = append(options, sim.WithSigningBackend(signing.NewBLSBackend())) + } + + sm, err := sim.NewSimulation(options...) + if err != nil { + return + } if err != nil { log.Panicf("failed to instantiate simulation: %v\n", err) } // Same chain for everyone. - candidate := sm.Base(0).Extend(sm.TipGen.Sample()) + candidate := baseChain.Extend(tsg.Sample()) sm.SetChains(sim.ChainCount{Count: *participantCount, Chain: candidate}) if err := sm.Run(1, *maxRounds); err != nil { diff --git a/sim/config.go b/sim/config.go deleted file mode 100644 index 865e0501..00000000 --- a/sim/config.go +++ /dev/null @@ -1,27 +0,0 @@ -package sim - -import ( - "time" - - "github.com/filecoin-project/go-f3/sim/signing" -) - -type Config struct { - // Honest participant count. - // Honest participants have one unit of power each. - HonestCount int - LatencySeed int64 - // Mean delivery latency for messages. - LatencyMean time.Duration - // Duration of EC epochs. - ECEpochDuration time.Duration - // Time to wait after EC epoch before starting next instance. - ECStabilisationDelay time.Duration - // If nil then FakeSigningBackend is used unless overridden by F3_TEST_USE_BLS - SigningBacked signing.Backend -} - -func (c Config) UseBLS() Config { - c.SigningBacked = signing.NewBLSBackend() - return c -} diff --git a/sim/decision_log.go b/sim/decision_log.go index f319c695..4e1d3612 100644 --- a/sim/decision_log.go +++ b/sim/decision_log.go @@ -9,8 +9,9 @@ import ( // Receives and validates finality decisions type DecisionLog struct { - Network gpbft.Network - Verifier gpbft.Verifier + netName gpbft.NetworkName + verifier gpbft.Verifier + // Base tipset for each instance. Bases []gpbft.TipSet // Powertable for each instance. @@ -20,10 +21,10 @@ type DecisionLog struct { err error } -func NewDecisionLog(ntwk gpbft.Network, verifier gpbft.Verifier) *DecisionLog { +func newDecisionLog(opts *options) *DecisionLog { return &DecisionLog{ - Network: ntwk, - Verifier: verifier, + netName: opts.networkName, + verifier: opts.signingBacked, } } @@ -155,8 +156,8 @@ func (dl *DecisionLog) verifyDecision(decision *gpbft.Justification) error { return fmt.Errorf("decision lacks strong quorum: %v", decision) } // Verify aggregate signature - payload := decision.Vote.MarshalForSigning(dl.Network.NetworkName()) - if err := dl.Verifier.VerifyAggregate(payload, decision.Signature, signers); err != nil { + payload := decision.Vote.MarshalForSigning(dl.netName) + if err := dl.verifier.VerifyAggregate(payload, decision.Signature, signers); err != nil { return xerrors.Errorf("invalid aggregate signature: %v: %w", decision, err) } return nil diff --git a/sim/ec.go b/sim/ec.go index 4c2ac3c4..baf8eb20 100644 --- a/sim/ec.go +++ b/sim/ec.go @@ -24,15 +24,15 @@ type ECInstance struct { Beacon []byte } -func NewEC(base gpbft.ECChain, beacon []byte) *EC { +func newEC(opts *options) *EC { return &EC{ BaseTimestamp: time.Time{}, Instances: []*ECInstance{ { - Base: base, + Base: *opts.baseChain, Chains: make(map[gpbft.ActorID]gpbft.ECChain), PowerTable: gpbft.NewPowerTable(), - Beacon: beacon, + Beacon: opts.beacon, }, }, } diff --git a/sim/host.go b/sim/host.go index eaa52f44..19320ddb 100644 --- a/sim/host.go +++ b/sim/host.go @@ -5,66 +5,85 @@ import ( "time" "github.com/filecoin-project/go-f3/gpbft" + "github.com/filecoin-project/go-f3/sim/adversary" ) +var _ gpbft.Host = (*simHost)(nil) +var _ adversary.Host = (*simHost)(nil) + // One participant's host // This provides methods that know the caller's participant ID and can provide its view of the world. -type SimHost struct { - *Config - *Network - *EC - *DecisionLog - *TipGen - id gpbft.ActorID -} +type simHost struct { + gpbft.Network + gpbft.Signer + gpbft.Verifier + gpbft.Clock -var _ gpbft.Host = (*SimHost)(nil) + sim *Simulation + id gpbft.ActorID +} -///// Chain interface +func newHost(id gpbft.ActorID, sim *Simulation) *simHost { + return &simHost{ + Network: sim.network, + Verifier: sim.signingBacked, + Signer: sim.signingBacked, + sim: sim, + id: id, + } +} -func (v *SimHost) GetCanonicalChain() (chain gpbft.ECChain, power gpbft.PowerTable, beacon []byte) { +func (v *simHost) GetCanonicalChain() (chain gpbft.ECChain, power gpbft.PowerTable, beacon []byte) { // Find the instance after the last instance finalised by the participant. - instance := uint64(0) - for i := len(v.Decisions) - 1; i >= 0; i-- { - if v.Decisions[i][v.id] != nil { + var instance uint64 + decisions := v.sim.decisions.Decisions + for i := len(decisions) - 1; i >= 0; i-- { + if decisions[i][v.id] != nil { instance = uint64(i + 1) break } } - - i := v.Instances[instance] + i := v.sim.ec.Instances[instance] chain = i.Chains[v.id] power = *i.PowerTable beacon = i.Beacon return } -func (v *SimHost) SetAlarm(at time.Time) { - v.Network.SetAlarm(v.id, at) +func (v *simHost) SetAlarm(at time.Time) { + v.sim.network.SetAlarm(v.id, at) +} + +func (v *simHost) Time() time.Time { + return v.sim.network.Time() } -func (v *SimHost) ReceiveDecision(decision *gpbft.Justification) time.Time { - firstForInstance := v.DecisionLog.ReceiveDecision(v.id, decision) +func (v *simHost) ReceiveDecision(decision *gpbft.Justification) time.Time { + firstForInstance := v.sim.decisions.ReceiveDecision(v.id, decision) if firstForInstance { // When the first valid decision is received for an instance, prepare for the next one. nextBase := decision.Vote.Value.Head() // Copy the previous instance power table. // The simulator doesn't have any facility to evolve the power table. // See https://github.com/filecoin-project/go-f3/issues/114. - nextPowerTable := v.EC.Instances[decision.Vote.Instance].PowerTable.Copy() + nextPowerTable := v.sim.ec.Instances[decision.Vote.Instance].PowerTable.Copy() nextBeacon := []byte(fmt.Sprintf("beacon %d", decision.Vote.Instance+1)) // Create a new chain for all participants. // There's no facility yet for them to observe different chains after the first instance. // See https://github.com/filecoin-project/go-f3/issues/115. - newTip := v.TipGen.Sample() + newTip := v.sim.tipSetGenerator.Sample() nextChain, _ := gpbft.NewChain(nextBase, newTip) - v.EC.AddInstance(nextChain, nextPowerTable, nextBeacon) - v.DecisionLog.BeginInstance(decision.Vote.Instance+1, nextBase, nextPowerTable) + v.sim.ec.AddInstance(nextChain, nextPowerTable, nextBeacon) + v.sim.decisions.BeginInstance(decision.Vote.Instance+1, nextBase, nextPowerTable) } elapsedEpochs := 1 //decision.Vote.Value.Head().Epoch - v.EC.BaseEpoch - finalTimestamp := v.EC.BaseTimestamp.Add(time.Duration(elapsedEpochs) * v.Config.ECEpochDuration) + finalTimestamp := v.sim.ec.BaseTimestamp.Add(time.Duration(elapsedEpochs) * v.sim.ecEpochDuration) // Next instance starts some fixed time after the next EC epoch is due. - nextInstanceStart := finalTimestamp.Add(v.Config.ECEpochDuration).Add(v.Config.ECStabilisationDelay) + nextInstanceStart := finalTimestamp.Add(v.sim.ecEpochDuration).Add(v.sim.ecStabilisationDelay) return nextInstanceStart } + +func (v *simHost) BroadcastSynchronous(sender gpbft.ActorID, msg gpbft.GMessage) { + v.sim.network.BroadcastSynchronous(sender, msg) +} diff --git a/sim/latency/none.go b/sim/latency/none.go new file mode 100644 index 00000000..f9ba2f10 --- /dev/null +++ b/sim/latency/none.go @@ -0,0 +1,19 @@ +package latency + +import ( + "time" + + "github.com/filecoin-project/go-f3/gpbft" +) + +var ( + _ Model = (*none)(nil) + + // None represents zero no-op latency model. + None = none{} +) + +// None represents zero latency model. +type none struct{} + +func (l none) Sample(time.Time, gpbft.ActorID, gpbft.ActorID) time.Duration { return 0 } diff --git a/sim/network.go b/sim/network.go index bd3dba94..6a177883 100644 --- a/sim/network.go +++ b/sim/network.go @@ -8,7 +8,6 @@ import ( "github.com/filecoin-project/go-f3/gpbft" "github.com/filecoin-project/go-f3/sim/adversary" "github.com/filecoin-project/go-f3/sim/latency" - "github.com/filecoin-project/go-f3/sim/signing" ) const ( @@ -20,8 +19,6 @@ const ( ) type Network struct { - signing.Backend - // Participants by ID. participants map[gpbft.ActorID]gpbft.Receiver // Participant IDs for deterministic iteration @@ -34,39 +31,27 @@ type Network struct { // Whether global stabilisation time has passed, so adversary can't control network. globalStabilisationElapsed bool // Trace level. - traceLevel int - - actor2PubKey map[gpbft.ActorID]gpbft.PubKey - + traceLevel int networkName gpbft.NetworkName } -func NewNetwork(latency latency.Model, traceLevel int, sb signing.Backend, nn gpbft.NetworkName) *Network { +func newNetwork(opts *options) *Network { return &Network{ - Backend: sb, - participants: map[gpbft.ActorID]gpbft.Receiver{}, - participantIDs: []gpbft.ActorID{}, - queue: messageQueue{}, - clock: time.Time{}, - latency: latency, - globalStabilisationElapsed: false, - traceLevel: traceLevel, - actor2PubKey: map[gpbft.ActorID]gpbft.PubKey{}, - networkName: nn, + participants: make(map[gpbft.ActorID]gpbft.Receiver), + latency: opts.latencyModel, + traceLevel: opts.traceLevel, + networkName: opts.networkName, } } -func (n *Network) AddParticipant(p gpbft.Receiver, pubKey gpbft.PubKey) { +func (n *Network) AddParticipant(p gpbft.Receiver) { if n.participants[p.ID()] != nil { panic("duplicate participant ID") } n.participantIDs = append(n.participantIDs, p.ID()) n.participants[p.ID()] = p - n.actor2PubKey[p.ID()] = pubKey } -////// Network interface - func (n *Network) NetworkName() gpbft.NetworkName { return n.networkName } @@ -87,8 +72,6 @@ func (n *Network) Broadcast(msg *gpbft.GMessage) { } } -///// Clock interface - func (n *Network) Time() time.Time { return n.clock } @@ -110,8 +93,6 @@ func (n *Network) Log(format string, args ...interface{}) { n.log(TraceLogic, format, args...) } -///// Adversary network interface - func (n *Network) BroadcastSynchronous(sender gpbft.ActorID, msg gpbft.GMessage) { n.log(TraceSent, "P%d ↗ %v", sender, msg) for _, k := range n.participantIDs { @@ -128,7 +109,7 @@ func (n *Network) BroadcastSynchronous(sender gpbft.ActorID, msg gpbft.GMessage) } // Returns whether there are any more messages to process. -func (n *Network) Tick(adv adversary.Receiver) (bool, error) { +func (n *Network) Tick(adv *adversary.Adversary) (bool, error) { // Find first message the adversary will allow. i := 0 if adv != nil && !n.globalStabilisationElapsed { diff --git a/sim/options.go b/sim/options.go new file mode 100644 index 00000000..6d12d4b5 --- /dev/null +++ b/sim/options.go @@ -0,0 +1,184 @@ +package sim + +import ( + "time" + + "github.com/filecoin-project/go-f3/gpbft" + "github.com/filecoin-project/go-f3/sim/adversary" + "github.com/filecoin-project/go-f3/sim/latency" + "github.com/filecoin-project/go-f3/sim/signing" +) + +const ( + defaultSimNetworkName = "sim" + defaultTipSetGeneratorSeed = 0x264803e715714f95 // Seed from Drand. + defaultHonestCount = 3 +) + +var ( + defaultBaseChain gpbft.ECChain + defaultBeacon = []byte("beacon") + defaultGraniteConfig = gpbft.GraniteConfig{ + Delta: 2.0, + DeltaBackOffExponent: 1.3, + } + defaultLatencyModel latency.Model +) + +func init() { + var err error + defaultBaseChain, err = gpbft.NewChain([]byte(("genesis"))) + if err != nil { + panic("failed to instantiate default simulation base chain") + } + defaultLatencyModel, err = latency.NewLogNormal(time.Now().UnixMilli(), time.Second*5) + if err != nil { + panic("failed to instantiate default simulation latency model") + } +} + +type Option func(*options) error + +type options struct { + // latencyModel models the cross participant communication latencyModel throughout a + // simulation. + latencyModel latency.Model + // honestCount is the honest participant count. Honest participants have one unit + // of power each. + honestCount int + // Duration of EC epochs. + ecEpochDuration time.Duration + // Time to wait after EC epoch before starting next instance. + ecStabilisationDelay time.Duration + // If nil then FakeSigningBackend is used unless overridden by F3_TEST_USE_BLS + signingBacked signing.Backend + graniteConfig *gpbft.GraniteConfig + traceLevel int + networkName gpbft.NetworkName + tipSetGenerator *TipSetGenerator + initialInstance uint64 + baseChain *gpbft.ECChain + beacon []byte + adversaryGenerator adversary.Generator + adversaryCount uint64 +} + +func newOptions(o ...Option) (*options, error) { + var opts options + for _, apply := range o { + if err := apply(&opts); err != nil { + return nil, err + } + } + if opts.honestCount == 0 { + opts.honestCount = defaultHonestCount + } + if opts.latencyModel == nil { + opts.latencyModel = defaultLatencyModel + } + if opts.graniteConfig == nil { + opts.graniteConfig = &defaultGraniteConfig + } + if opts.signingBacked == nil { + opts.signingBacked = signing.NewFakeBackend() + } + if opts.networkName == "" { + opts.networkName = defaultSimNetworkName + } + if opts.tipSetGenerator == nil { + opts.tipSetGenerator = NewTipSetGenerator(defaultTipSetGeneratorSeed) + } + if opts.baseChain == nil { + opts.baseChain = &defaultBaseChain + } + if opts.beacon == nil { + opts.beacon = defaultBeacon + } + return &opts, nil +} + +func WithHonestParticipantCount(i int) Option { + return func(o *options) error { + o.honestCount = i + return nil + } +} + +// WithSigningBackend sets the signing backend to be used by all participants in +// the simulation. Defaults to signing.FakeBackend if unset. +// +// See signing.FakeBackend, signing.BLSBackend. +func WithSigningBackend(sb signing.Backend) Option { + return func(o *options) error { + o.signingBacked = sb + return nil + } +} + +func WithLatencyModel(lm latency.Model) Option { + return func(o *options) error { + o.latencyModel = lm + return nil + } +} + +func WithECEpochDuration(d time.Duration) Option { + return func(o *options) error { + o.ecEpochDuration = d + return nil + } +} + +func WitECStabilisationDelay(d time.Duration) Option { + return func(o *options) error { + o.ecStabilisationDelay = d + return nil + } +} + +func WithGraniteConfig(c *gpbft.GraniteConfig) Option { + return func(o *options) error { + o.graniteConfig = c + return nil + } +} + +func WithAdversary(generator adversary.Generator) Option { + return func(o *options) error { + // TODO: parameterise number of adversary counts. + + // Hard-coded to 1 in order to reduce the LOC up for review. Future work will + // parameterise this for multiple adversary instances in a simulation + o.adversaryCount = 1 + o.adversaryGenerator = generator + return nil + } +} + +func WithBaseChain(base *gpbft.ECChain) Option { + return func(o *options) error { + o.baseChain = base + return nil + } +} + +func WithTipSetGenerator(tsg *TipSetGenerator) Option { + return func(o *options) error { + o.tipSetGenerator = tsg + return nil + } +} + +func WithECStabilisationDelay(d time.Duration) Option { + return func(o *options) error { + o.ecStabilisationDelay = d + return nil + } +} + +func WithTraceLevel(i int) Option { + return func(o *options) error { + o.traceLevel = i + return nil + } +} diff --git a/sim/sim.go b/sim/sim.go index 326fba95..139d13d7 100644 --- a/sim/sim.go +++ b/sim/sim.go @@ -3,102 +3,68 @@ package sim import ( "fmt" "math" - "os" "strings" "github.com/filecoin-project/go-f3/gpbft" "github.com/filecoin-project/go-f3/sim/adversary" - "github.com/filecoin-project/go-f3/sim/latency" - "github.com/filecoin-project/go-f3/sim/signing" ) type Simulation struct { - Config Config - Network *Network - EC *EC - Participants []*gpbft.Participant - Adversary adversary.Receiver - Decisions *DecisionLog - TipGen *TipGen + *options + network *Network + ec *EC + participants []*gpbft.Participant + adversary *adversary.Adversary + decisions *DecisionLog } -func NewSimulation(simConfig Config, graniteConfig gpbft.GraniteConfig, traceLevel int) (*Simulation, error) { - // Create a network to deliver messages. - lat, err := latency.NewLogNormal(simConfig.LatencySeed, simConfig.LatencyMean) +func NewSimulation(o ...Option) (*Simulation, error) { + opts, err := newOptions(o...) if err != nil { return nil, err } - sb := simConfig.SigningBacked - if sb == nil { - if os.Getenv("F3_TEST_USE_BLS") != "1" { - sb = signing.NewFakeBackend() - } else { - sb = signing.NewBLSBackend() - } - } + network := newNetwork(opts) + ec := newEC(opts) + decisions := newDecisionLog(opts) - ntwk := NewNetwork(lat, traceLevel, sb, "sim") - baseChain, _ := gpbft.NewChain([]byte(("genesis"))) - beacon := []byte("beacon") - ec := NewEC(baseChain, beacon) - tipGen := NewTipGen(0x264803e715714f95) // Seed from Drand - decisions := NewDecisionLog(ntwk, sb) - - // Create participants. - participants := make([]*gpbft.Participant, simConfig.HonestCount) - for i := 0; i < len(participants); i++ { - id := gpbft.ActorID(i) - host := &SimHost{ - Config: &simConfig, - Network: ntwk, - EC: ec, - DecisionLog: decisions, - TipGen: tipGen, - id: id, - } - participants[i] = gpbft.NewParticipant(id, graniteConfig, host, host, 0) - pubKey, _ := sb.GenerateKey() - ntwk.AddParticipant(participants[i], pubKey) - ec.AddParticipant(id, gpbft.NewStoragePower(1), pubKey) + s := &Simulation{ + options: opts, + network: network, + ec: ec, + decisions: decisions, } - decisions.BeginInstance(0, baseChain.Head(), ec.Instances[0].PowerTable) - return &Simulation{ - Config: simConfig, - Network: ntwk, - EC: ec, - Participants: participants, - Adversary: nil, - Decisions: decisions, - TipGen: tipGen, - }, nil -} + var nextID gpbft.ActorID + s.participants = make([]*gpbft.Participant, s.honestCount) + for i := range s.participants { + host := newHost(nextID, s) + s.participants[i] = gpbft.NewParticipant(nextID, *s.graniteConfig, host, s.network, s.initialInstance) + pubKey, _ := s.signingBacked.GenerateKey() + s.network.AddParticipant(s.participants[i]) + s.ec.AddParticipant(nextID, gpbft.NewStoragePower(1), pubKey) + nextID++ + } -func (s *Simulation) Base(instance uint64) gpbft.ECChain { - return s.EC.Instances[instance].Base -} + // TODO: expand the simulation to accommodate more than one adversary for + // a more realistic simulation. + // Limit adversaryCount to exactly one for now to reduce LOC up for review. + // Future PRs will expand this to support a group of adversaries. + if s.adversaryGenerator != nil && s.adversaryCount == 1 { + host := newHost(nextID, s) + s.adversary = s.adversaryGenerator(nextID, host) + pubKey, _ := s.signingBacked.GenerateKey() + s.network.AddParticipant(s.adversary) + s.ec.AddParticipant(s.adversary.ID(), s.adversary.Power, pubKey) + } -func (s *Simulation) PowerTable(instance uint64) *gpbft.PowerTable { - return s.EC.Instances[instance].PowerTable -} + s.decisions.BeginInstance(0, (*s.baseChain).Head(), s.ec.Instances[0].PowerTable) -func (s *Simulation) SetAdversary(adv adversary.Receiver, power uint) { - s.Adversary = adv - pubKey, _ := s.Network.GenerateKey() - s.Network.AddParticipant(adv, pubKey) - s.EC.AddParticipant(adv.ID(), gpbft.NewStoragePower(int64(power)), pubKey) + return s, nil } -func (s *Simulation) HostFor(id gpbft.ActorID) *SimHost { - return &SimHost{ - Config: &s.Config, - Network: s.Network, - EC: s.EC, - DecisionLog: s.Decisions, - TipGen: s.TipGen, - id: id, - } +func (s *Simulation) PowerTable(instance uint64) *gpbft.PowerTable { + return s.ec.Instances[instance].PowerTable } type ChainCount struct { @@ -111,27 +77,36 @@ func (s *Simulation) SetChains(chains ...ChainCount) { pidx := 0 for _, chain := range chains { for i := 0; i < chain.Count; i++ { - s.EC.Instances[0].Chains[s.Participants[pidx].ID()] = chain.Chain + s.ec.Instances[0].Chains[s.participants[pidx].ID()] = chain.Chain pidx += 1 } } - if pidx != len(s.Participants) { - panic(fmt.Errorf("%d participants but %d chains", len(s.Participants), pidx)) + honestParticipantsCount := len(s.network.participantIDs) + if pidx != s.HonestParticipantsCount() { + panic(fmt.Errorf("%d participants but %d chains", honestParticipantsCount, pidx)) } } // Runs simulation, and returns whether all participants decided on the same value. func (s *Simulation) Run(instanceCount uint64, maxRounds uint64) error { + // Start participants. - for _, p := range s.Participants { + for _, p := range s.participants { if err := p.Start(); err != nil { panic(fmt.Errorf("participant %d failed starting: %w", p.ID(), err)) } } + // Start adversary + if s.adversary != nil { + if err := s.adversary.Start(); err != nil { + panic(fmt.Errorf("adversary %d failed starting: %w", s.adversary.ID(), err)) + } + } + adversaryID := gpbft.ActorID(math.MaxUint64) - if s.Adversary != nil { - adversaryID = s.Adversary.ID() + if s.adversary != nil { + adversaryID = s.adversary.ID() } finalInstance := instanceCount - 1 currentInstance := uint64(0) @@ -140,15 +115,15 @@ func (s *Simulation) Run(instanceCount uint64, maxRounds uint64) error { moreTicks := true for moreTicks { var err error - if s.Decisions.err != nil { - return fmt.Errorf("error in decision: %w", s.Decisions.err) + if s.decisions.err != nil { + return fmt.Errorf("error in decision: %w", s.decisions.err) } - if s.Participants[0].CurrentRound() >= maxRounds { + if s.participants[0].CurrentRound() >= maxRounds { return fmt.Errorf("reached maximum number of %d rounds", maxRounds) } // Verify the current instance as soon as it completes. - if s.Decisions.HasCompletedInstance(currentInstance, adversaryID) { - if err := s.Decisions.VerifyInstance(currentInstance, adversaryID); err != nil { + if s.decisions.HasCompletedInstance(currentInstance, adversaryID) { + if err := s.decisions.VerifyInstance(currentInstance, adversaryID); err != nil { return fmt.Errorf("invalid decisions for instance %d: %w", currentInstance, err) } @@ -158,7 +133,7 @@ func (s *Simulation) Run(instanceCount uint64, maxRounds uint64) error { } currentInstance += 1 } - moreTicks, err = s.Network.Tick(s.Adversary) + moreTicks, err = s.network.Tick(s.adversary) if err != nil { return fmt.Errorf("error performing simulation step: %w", err) } @@ -168,7 +143,7 @@ func (s *Simulation) Run(instanceCount uint64, maxRounds uint64) error { // Returns the decision for a participant in an instance. func (s *Simulation) GetDecision(instance uint64, participant gpbft.ActorID) (gpbft.ECChain, bool) { - v, ok := s.Decisions.Decisions[instance][participant] + v, ok := s.decisions.Decisions[instance][participant] if ok { return v.Vote.Value, ok } @@ -176,14 +151,29 @@ func (s *Simulation) GetDecision(instance uint64, participant gpbft.ActorID) (gp } func (s *Simulation) PrintResults() { - s.Decisions.PrintInstance(0) + s.decisions.PrintInstance(0) } func (s *Simulation) Describe() string { b := strings.Builder{} - for _, p := range s.Participants { + for _, p := range s.participants { b.WriteString(p.Describe()) b.WriteString("\n") } return b.String() } + +func (s *Simulation) ListParticipantIDs() []gpbft.ActorID { + return s.network.participantIDs +} + +func (s *Simulation) HonestParticipantsCount() int { + return len(s.participants) +} + +func (s *Simulation) GetInstance(i int) *ECInstance { + if i < 0 || len(s.ec.Instances) <= i { + return nil + } + return s.ec.Instances[i] +} diff --git a/sim/tipgen.go b/sim/tipset_gen.go similarity index 72% rename from sim/tipgen.go rename to sim/tipset_gen.go index 17968370..5111644d 100644 --- a/sim/tipgen.go +++ b/sim/tipset_gen.go @@ -7,15 +7,15 @@ var alphanum = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234 // A tipset generator. // This uses a fast xorshift PRNG to generate random tipset IDs. // The statistical properties of these are not important to correctness. -type TipGen struct { +type TipSetGenerator struct { xorshiftState uint64 } -func NewTipGen(seed uint64) *TipGen { - return &TipGen{seed} +func NewTipSetGenerator(seed uint64) *TipSetGenerator { + return &TipSetGenerator{seed} } -func (c *TipGen) Sample() gpbft.TipSet { +func (c *TipSetGenerator) Sample() gpbft.TipSet { b := make([]byte, 8) for i := range b { b[i] = alphanum[c.nextN(len(alphanum))] @@ -23,7 +23,7 @@ func (c *TipGen) Sample() gpbft.TipSet { return b } -func (c *TipGen) nextN(n int) uint64 { +func (c *TipSetGenerator) nextN(n int) uint64 { bucketSize := uint64(1<<63) / uint64(n) limit := bucketSize * uint64(n) for { @@ -34,7 +34,7 @@ func (c *TipGen) nextN(n int) uint64 { } } -func (c *TipGen) next() uint64 { +func (c *TipSetGenerator) next() uint64 { x := c.xorshiftState x ^= x << 13 x ^= x >> 7 diff --git a/test/absent_test.go b/test/absent_test.go index 0efe13dd..52dc477a 100644 --- a/test/absent_test.go +++ b/test/absent_test.go @@ -10,15 +10,23 @@ import ( func TestAbsent(t *testing.T) { t.Parallel() - repeatInParallel(t, ASYNC_ITERS, func(t *testing.T, repetition int) { - sm, err := sim.NewSimulation(AsyncConfig(3, repetition), GraniteConfig(), sim.TraceNone) + + const honestParticipantsCount = 3 + repeatInParallel(t, 1, func(t *testing.T, repetition int) { + // Total network size of 3 + 1, where the adversary has 1/4 of power. + tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) + baseChain := generateECChain(t, tsg) + sm, err := sim.NewSimulation(asyncOptions(t, repetition, + sim.WithHonestParticipantCount(honestParticipantsCount), + sim.WithTipSetGenerator(tsg), + sim.WithBaseChain(&baseChain), + sim.WithAdversary(adversary.NewAbsentGenerator(oneStoragePower)), + )...) require.NoError(t, err) - // Adversary has 1/4 of power. - sm.SetAdversary(adversary.NewAbsent(99, sm.HostFor(99)), 1) - a := sm.Base(0).Extend(sm.TipGen.Sample()) - sm.SetChains(sim.ChainCount{Count: len(sm.Participants), Chain: a}) + a := baseChain.Extend(tsg.Sample()) + sm.SetChains(sim.ChainCount{Count: honestParticipantsCount, Chain: a}) - require.NoErrorf(t, sm.Run(1, MAX_ROUNDS), "%s", sm.Describe()) + require.NoErrorf(t, sm.Run(1, maxRounds), "%s", sm.Describe()) }) } diff --git a/test/constants.go b/test/constants.go deleted file mode 100644 index 6cc3116b..00000000 --- a/test/constants.go +++ /dev/null @@ -1,52 +0,0 @@ -package test - -import ( - "time" - - "github.com/filecoin-project/go-f3/gpbft" - "github.com/filecoin-project/go-f3/sim" -) - -// Configuration constants used across most tests. -// These values are not intended to reflect real-world conditions. -// The latency and delta values are similar in order to stress "slow" message paths and interleaving. -// The values are not appropriate for benchmarks. -// Granite configuration. -const DELTA = 200 * time.Millisecond -const DELTA_BACK_OFF_EXPONENT = 1.300 - -// Returns a default Granite configuration. -func GraniteConfig() gpbft.GraniteConfig { - return gpbft.GraniteConfig{ - Delta: DELTA, - DeltaBackOffExponent: DELTA_BACK_OFF_EXPONENT, - } -} - -// Simulator configuration. -const LATENCY_SYNC = 0 -const LATENCY_ASYNC = 100 * time.Millisecond -const MAX_ROUNDS = 10 -const ASYNC_ITERS = 5000 -const EC_EPOCH_DURATION = 30 * time.Second -const EC_STABILISATION_DELAY = 3 * time.Second - -func SyncConfig(honestCount int) sim.Config { - return sim.Config{ - HonestCount: honestCount, - LatencySeed: 0, - LatencyMean: LATENCY_SYNC, - ECEpochDuration: EC_EPOCH_DURATION, - ECStabilisationDelay: EC_STABILISATION_DELAY, - } -} - -func AsyncConfig(honestCount, latencySeed int) sim.Config { - return sim.Config{ - HonestCount: honestCount, - LatencySeed: int64(latencySeed), - LatencyMean: LATENCY_ASYNC, - ECEpochDuration: EC_EPOCH_DURATION, - ECStabilisationDelay: EC_STABILISATION_DELAY, - } -} diff --git a/test/constants_test.go b/test/constants_test.go new file mode 100644 index 00000000..c40480d4 --- /dev/null +++ b/test/constants_test.go @@ -0,0 +1,55 @@ +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 ( + // tipSetGeneratorSeed is a test random seed from Drand. + tipSetGeneratorSeed = 0x264803e715714f95 + + latencyAsync = 100 * time.Millisecond + maxRounds = 10 + asyncInterations = 5000 + EcEpochDuration = 30 * time.Second + EcStabilisationDelay = 3 * time.Second +) + +var ( + oneStoragePower = gpbft.NewStoragePower(1) + + // testGraniteConfig is configuration constants used across most tests. + // These values are not intended to reflect real-world conditions. + // The latency and delta values are similar in order to stress "slow" message paths and interleaving. + // The values are not appropriate for benchmarks. + testGraniteConfig = &gpbft.GraniteConfig{ + Delta: 200 * time.Millisecond, + DeltaBackOffExponent: 1.300, + } +) + +func syncOptions(o ...sim.Option) []sim.Option { + return append(o, + sim.WithLatencyModel(latency.None), + sim.WithECEpochDuration(EcEpochDuration), + sim.WitECStabilisationDelay(EcStabilisationDelay), + sim.WithGraniteConfig(testGraniteConfig), + ) +} + +func asyncOptions(t *testing.T, latencySeed int, o ...sim.Option) []sim.Option { + lm, err := latency.NewLogNormal(int64(latencySeed), latencyAsync) + require.NoError(t, err) + return append(o, + sim.WithLatencyModel(lm), + sim.WithECEpochDuration(EcEpochDuration), + sim.WitECStabilisationDelay(EcStabilisationDelay), + sim.WithGraniteConfig(testGraniteConfig), + ) +} diff --git a/test/decide_test.go b/test/decide_test.go index 9ce1ff5b..0a6d7b96 100644 --- a/test/decide_test.go +++ b/test/decide_test.go @@ -4,26 +4,28 @@ import ( "fmt" "testing" + "github.com/filecoin-project/go-f3/gpbft" "github.com/filecoin-project/go-f3/sim" "github.com/filecoin-project/go-f3/sim/adversary" "github.com/stretchr/testify/require" ) func TestImmediateDecide(t *testing.T) { - sm, err := sim.NewSimulation(AsyncConfig(1, 0), GraniteConfig(), sim.TraceNone) + tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) + baseChain := generateECChain(t, tsg) + adversaryValue := baseChain.Extend(tsg.Sample()) + sm, err := sim.NewSimulation(asyncOptions(t, 1413, + sim.WithHonestParticipantCount(1), + sim.WithBaseChain(&baseChain), + sim.WithTipSetGenerator(tsg), + // Add the adversary to the simulation with 3/4 of total power. + sim.WithAdversary(adversary.NewImmediateDecideGenerator(adversaryValue, gpbft.NewStoragePower(3))), + )...) require.NoError(t, err) - // Create adversarial node - value := sm.Base(0).Extend(sm.TipGen.Sample()) - adv := adversary.NewImmediateDecide(99, sm.HostFor(99), value) - - // Add the adversary to the simulation with 3/4 of total power. - sm.SetAdversary(adv, 3) - // The honest node starts with a different chain (on the same base). - sm.SetChains(sim.ChainCount{Count: 1, Chain: sm.Base(0).Extend(sm.TipGen.Sample())}) - adv.Begin() - err = sm.Run(1, MAX_ROUNDS) + sm.SetChains(sim.ChainCount{Count: 1, Chain: baseChain.Extend(tsg.Sample())}) + err = sm.Run(1, maxRounds) if err != nil { fmt.Printf("%s", sm.Describe()) sm.PrintResults() @@ -32,5 +34,5 @@ func TestImmediateDecide(t *testing.T) { decision, ok := sm.GetDecision(0, 0) require.True(t, ok, "no decision") - require.Equal(t, value.Head(), decision.Head(), "honest node did not decide the right value") + require.Equal(t, adversaryValue.Head(), decision.Head(), "honest node did not decide the right value") } diff --git a/test/honest_test.go b/test/honest_test.go index ff27f79c..b53dadd5 100644 --- a/test/honest_test.go +++ b/test/honest_test.go @@ -5,118 +5,125 @@ import ( "testing" "github.com/filecoin-project/go-f3/sim" + "github.com/filecoin-project/go-f3/sim/signing" "github.com/stretchr/testify/require" ) -///// Tests for a single instance with no adversaries. - -func TestSingleton(t *testing.T) { - sm, err := sim.NewSimulation(SyncConfig(1), GraniteConfig(), sim.TraceNone) - require.NoError(t, err) - a := sm.Base(0).Extend(sm.TipGen.Sample()) - sm.SetChains(sim.ChainCount{Count: 1, Chain: a}) - - require.NoErrorf(t, sm.Run(1, MAX_ROUNDS), "%s", sm.Describe()) - expectDecision(t, sm, a.Head()) -} - -func TestSyncPair(t *testing.T) { +func TestHonest_ChainAgreement(t *testing.T) { t.Parallel() tests := []struct { - name string - config sim.Config + name string + options []sim.Option }{ { - name: "no signing", - config: SyncConfig(2), + name: "sync singleton no signing", + options: syncOptions(sim.WithHonestParticipantCount(1)), + }, + { + name: "sync singleton bls", + options: syncOptions( + sim.WithHonestParticipantCount(1), + sim.WithSigningBackend(signing.NewBLSBackend())), }, { - name: "bls", - config: SyncConfig(2).UseBLS(), + name: "sync pair no signing", + options: syncOptions(sim.WithHonestParticipantCount(2)), + }, + { + name: "sync pair bls", + options: syncOptions( + sim.WithHonestParticipantCount(2), + sim.WithSigningBackend(signing.NewBLSBackend())), + }, + { + name: "async pair no signing", + options: asyncOptions(t, 1413, sim.WithHonestParticipantCount(2)), + }, + { + name: "async pair bls", + options: asyncOptions(t, 1413, + sim.WithHonestParticipantCount(2), + sim.WithSigningBackend(signing.NewBLSBackend())), }, } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { - sm, err := sim.NewSimulation(test.config, GraniteConfig(), sim.TraceNone) + tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) + baseChain := generateECChain(t, tsg) + targetChain := baseChain.Extend(tsg.Sample()) + test.options = append(test.options, sim.WithBaseChain(&baseChain), sim.WithTipSetGenerator(tsg)) + sm, err := sim.NewSimulation(test.options...) require.NoError(t, err) - a := sm.Base(0).Extend(sm.TipGen.Sample()) - sm.SetChains(sim.ChainCount{Count: len(sm.Participants), Chain: a}) + sm.SetChains(sim.ChainCount{Count: sm.HonestParticipantsCount(), Chain: targetChain}) - require.NoErrorf(t, sm.Run(1, MAX_ROUNDS), "%s", sm.Describe()) - expectDecision(t, sm, a.Head()) + require.NoErrorf(t, sm.Run(1, maxRounds), "%s", sm.Describe()) + requireConsensus(t, sm, targetChain.Head()) }) } } -func TestASyncPair(t *testing.T) { - t.Parallel() - repeatInParallel(t, ASYNC_ITERS, func(t *testing.T, repetition int) { - sm, err := sim.NewSimulation(AsyncConfig(2, repetition), GraniteConfig(), sim.TraceNone) - require.NoError(t, err) - a := sm.Base(0).Extend(sm.TipGen.Sample()) - sm.SetChains(sim.ChainCount{Count: len(sm.Participants), Chain: a}) - - require.NoErrorf(t, sm.Run(1, MAX_ROUNDS), "%s", sm.Describe()) - expectDecision(t, sm, a.Head(), sm.Base(0).Head()) - }) -} - -func TestSyncPairDisagree(t *testing.T) { +func TestHonest_ChainDisagreement(t *testing.T) { t.Parallel() tests := []struct { - name string - config sim.Config + name string + options []sim.Option }{ { - name: "no signing", - config: SyncConfig(2), + name: "sync pair no signing", + options: syncOptions(sim.WithHonestParticipantCount(2)), + }, + { + name: "sync pair bls", + options: syncOptions( + sim.WithHonestParticipantCount(2), + sim.WithSigningBackend(signing.NewBLSBackend())), + }, + { + name: "async pair no signing", + options: asyncOptions(t, 1413, sim.WithHonestParticipantCount(2)), }, { - name: "bls", - config: SyncConfig(2).UseBLS(), + name: "async pair bls", + options: asyncOptions(t, 1413, + sim.WithHonestParticipantCount(2), + sim.WithSigningBackend(signing.NewBLSBackend())), }, } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { - sm, err := sim.NewSimulation(test.config, GraniteConfig(), sim.TraceNone) + tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) + baseChain := generateECChain(t, tsg) + test.options = append(test.options, sim.WithBaseChain(&baseChain), sim.WithTipSetGenerator(tsg)) + sm, err := sim.NewSimulation(test.options...) require.NoError(t, err) - a := sm.Base(0).Extend(sm.TipGen.Sample()) - b := sm.Base(0).Extend(sm.TipGen.Sample()) - sm.SetChains(sim.ChainCount{Count: 1, Chain: a}, sim.ChainCount{Count: 1, Chain: b}) + oneChain := baseChain.Extend(tsg.Sample()) + anotherChain := baseChain.Extend(tsg.Sample()) + participantsCount := sm.HonestParticipantsCount() + sm.SetChains(sim.ChainCount{Count: participantsCount / 2, Chain: oneChain}, sim.ChainCount{Count: participantsCount / 2, Chain: anotherChain}) - require.NoErrorf(t, sm.Run(1, MAX_ROUNDS), "%s", sm.Describe()) - // Decide base chain as the only common value. - expectDecision(t, sm, sm.Base(0).Head()) + require.NoErrorf(t, sm.Run(1, maxRounds), "%s", sm.Describe()) + requireConsensus(t, sm, baseChain...) }) } } -func TestAsyncPairDisagree(t *testing.T) { - repeatInParallel(t, ASYNC_ITERS, func(t *testing.T, repetition int) { - sm, err := sim.NewSimulation(AsyncConfig(2, repetition), GraniteConfig(), sim.TraceNone) - require.NoError(t, err) - a := sm.Base(0).Extend(sm.TipGen.Sample()) - b := sm.Base(0).Extend(sm.TipGen.Sample()) - sm.SetChains(sim.ChainCount{Count: 1, Chain: a}, sim.ChainCount{Count: 1, Chain: b}) - - require.NoErrorf(t, sm.Run(1, MAX_ROUNDS), "%s", sm.Describe()) - // Decide base chain as the only common value. - expectDecision(t, sm, sm.Base(0).Head()) - }) -} - -func TestSyncAgreement(t *testing.T) { +func TestSync_AgreementWithRepetition(t *testing.T) { repeatInParallel(t, 50, func(t *testing.T, repetition int) { honestCount := 3 + repetition - sm, err := sim.NewSimulation(SyncConfig(honestCount), GraniteConfig(), sim.TraceNone) + tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) + baseChain := generateECChain(t, tsg) + sm, err := sim.NewSimulation(syncOptions( + sim.WithHonestParticipantCount(honestCount), + sim.WithTipSetGenerator(tsg), + sim.WithBaseChain(&baseChain))...) require.NoError(t, err) - a := sm.Base(0).Extend(sm.TipGen.Sample()) - sm.SetChains(sim.ChainCount{Count: len(sm.Participants), Chain: a}) - require.NoErrorf(t, sm.Run(1, MAX_ROUNDS), "%s", sm.Describe()) + a := baseChain.Extend(tsg.Sample()) + sm.SetChains(sim.ChainCount{Count: sm.HonestParticipantsCount(), Chain: a}) + require.NoErrorf(t, sm.Run(1, maxRounds), "%s", sm.Describe()) // Synchronous, agreeing groups always decide the candidate. - expectDecision(t, sm, a.Head()) + requireConsensus(t, sm, a.Head()) }) } @@ -130,14 +137,20 @@ func TestAsyncAgreement(t *testing.T) { for n := 3; n <= 16; n++ { honestCount := n t.Run(fmt.Sprintf("honest count %d", honestCount), func(t *testing.T) { - repeatInParallel(t, ASYNC_ITERS, func(t *testing.T, repetition int) { - sm, err := sim.NewSimulation(AsyncConfig(honestCount, repetition), GraniteConfig(), sim.TraceNone) + repeatInParallel(t, asyncInterations, func(t *testing.T, repetition int) { + tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) + baseChain := generateECChain(t, tsg) + sm, err := sim.NewSimulation(asyncOptions(t, repetition, + sim.WithHonestParticipantCount(honestCount), + sim.WithTipSetGenerator(tsg), + sim.WithBaseChain(&baseChain), + )...) require.NoError(t, err) - a := sm.Base(0).Extend(sm.TipGen.Sample()) - sm.SetChains(sim.ChainCount{Count: len(sm.Participants), Chain: a}) + a := baseChain.Extend(tsg.Sample()) + sm.SetChains(sim.ChainCount{Count: sm.HonestParticipantsCount(), Chain: a}) - require.NoErrorf(t, sm.Run(1, MAX_ROUNDS), "%s", sm.Describe()) - expectDecision(t, sm, sm.Base(0).Head(), a.Head()) + require.NoErrorf(t, sm.Run(1, maxRounds), "%s", sm.Describe()) + requireConsensus(t, sm, baseChain.Head(), a.Head()) }) }) } @@ -145,17 +158,23 @@ func TestAsyncAgreement(t *testing.T) { func TestSyncHalves(t *testing.T) { t.Parallel() + repeatInParallel(t, 15, func(t *testing.T, repetition int) { honestCount := repetition*2 + 2 - sm, err := sim.NewSimulation(SyncConfig(honestCount), GraniteConfig(), sim.TraceNone) + tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) + baseChain := generateECChain(t, tsg) + sm, err := sim.NewSimulation(syncOptions( + sim.WithHonestParticipantCount(honestCount), + sim.WithTipSetGenerator(tsg), + sim.WithBaseChain(&baseChain))...) require.NoError(t, err) - a := sm.Base(0).Extend(sm.TipGen.Sample()) - b := sm.Base(0).Extend(sm.TipGen.Sample()) + a := baseChain.Extend(tsg.Sample()) + b := baseChain.Extend(tsg.Sample()) sm.SetChains(sim.ChainCount{Count: honestCount / 2, Chain: a}, sim.ChainCount{Count: honestCount / 2, Chain: b}) - require.NoErrorf(t, sm.Run(1, MAX_ROUNDS), "%s", sm.Describe()) + require.NoErrorf(t, sm.Run(1, maxRounds), "%s", sm.Describe()) // Groups split 50/50 always decide the base. - expectDecision(t, sm, sm.Base(0).Head()) + requireConsensus(t, sm, baseChain.Head()) }) } @@ -167,15 +186,21 @@ func TestSyncHalvesBLS(t *testing.T) { t.Parallel() repeatInParallel(t, 3, func(t *testing.T, repetition int) { honestCount := repetition*2 + 2 - sm, err := sim.NewSimulation(SyncConfig(honestCount).UseBLS(), GraniteConfig(), sim.TraceNone) + tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) + baseChain := generateECChain(t, tsg) + sm, err := sim.NewSimulation(syncOptions( + sim.WithHonestParticipantCount(honestCount), + sim.WithTipSetGenerator(tsg), + sim.WithSigningBackend(signing.NewBLSBackend()), + sim.WithBaseChain(&baseChain))...) require.NoError(t, err) - a := sm.Base(0).Extend(sm.TipGen.Sample()) - b := sm.Base(0).Extend(sm.TipGen.Sample()) + a := baseChain.Extend(tsg.Sample()) + b := baseChain.Extend(tsg.Sample()) sm.SetChains(sim.ChainCount{Count: honestCount / 2, Chain: a}, sim.ChainCount{Count: honestCount / 2, Chain: b}) - require.NoErrorf(t, sm.Run(1, MAX_ROUNDS), "%s", sm.Describe()) + require.NoErrorf(t, sm.Run(1, maxRounds), "%s", sm.Describe()) // Groups split 50/50 always decide the base. - expectDecision(t, sm, sm.Base(0).Head()) + requireConsensus(t, sm, baseChain.Head()) }) } @@ -188,16 +213,21 @@ func TestAsyncHalves(t *testing.T) { for n := 4; n <= 20; n += 2 { honestCount := n t.Run(fmt.Sprintf("honest count %d", honestCount), func(t *testing.T) { - repeatInParallel(t, ASYNC_ITERS, func(t *testing.T, repetition int) { - sm, err := sim.NewSimulation(AsyncConfig(honestCount, repetition), GraniteConfig(), sim.TraceNone) + repeatInParallel(t, asyncInterations, func(t *testing.T, repetition int) { + tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) + baseChain := generateECChain(t, tsg) + sm, err := sim.NewSimulation(asyncOptions(t, repetition, + sim.WithHonestParticipantCount(honestCount), + sim.WithTipSetGenerator(tsg), + sim.WithBaseChain(&baseChain))...) require.NoError(t, err) - a := sm.Base(0).Extend(sm.TipGen.Sample()) - b := sm.Base(0).Extend(sm.TipGen.Sample()) + a := baseChain.Extend(tsg.Sample()) + b := baseChain.Extend(tsg.Sample()) sm.SetChains(sim.ChainCount{Count: honestCount / 2, Chain: a}, sim.ChainCount{Count: honestCount / 2, Chain: b}) - require.NoErrorf(t, sm.Run(1, MAX_ROUNDS), "%s", sm.Describe()) + require.NoErrorf(t, sm.Run(1, maxRounds), "%s", sm.Describe()) // Groups split 50/50 always decide the base. - expectDecision(t, sm, sm.Base(0).Head()) + requireConsensus(t, sm, baseChain.Head()) }) }) } @@ -209,30 +239,41 @@ func TestRequireStrongQuorumToProgress(t *testing.T) { } t.Parallel() - repeatInParallel(t, ASYNC_ITERS, func(t *testing.T, repetition int) { - sm, err := sim.NewSimulation(AsyncConfig(30, repetition), GraniteConfig(), sim.TraceNone) + repeatInParallel(t, asyncInterations, func(t *testing.T, repetition int) { + tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) + baseChain := generateECChain(t, tsg) + sm, err := sim.NewSimulation(asyncOptions(t, repetition, + sim.WithHonestParticipantCount(30), + sim.WithTipSetGenerator(tsg), + sim.WithBaseChain(&baseChain))...) require.NoError(t, err) - a := sm.Base(0).Extend(sm.TipGen.Sample()) - b := sm.Base(0).Extend(sm.TipGen.Sample()) + a := baseChain.Extend(tsg.Sample()) + b := baseChain.Extend(tsg.Sample()) // No strict > quorum. sm.SetChains(sim.ChainCount{Count: 20, Chain: a}, sim.ChainCount{Count: 10, Chain: b}) - require.NoErrorf(t, sm.Run(1, MAX_ROUNDS), "%s", sm.Describe()) + require.NoErrorf(t, sm.Run(1, maxRounds), "%s", sm.Describe()) // Must decide base. - expectDecision(t, sm, sm.Base(0).Head()) + requireConsensus(t, sm, baseChain.Head()) }) } func TestLongestCommonPrefix(t *testing.T) { // This test uses a synchronous configuration to ensure timely message delivery. // If async, it is possible to decide the base chain if QUALITY messages are delayed. - sm, err := sim.NewSimulation(SyncConfig(4), GraniteConfig(), sim.TraceNone) + tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) + baseChain := generateECChain(t, tsg) + sm, err := sim.NewSimulation(syncOptions( + sim.WithHonestParticipantCount(4), + sim.WithTipSetGenerator(tsg), + sim.WithBaseChain(&baseChain), + )...) require.NoError(t, err) - ab := sm.Base(0).Extend(sm.TipGen.Sample()) - abc := ab.Extend(sm.TipGen.Sample()) - abd := ab.Extend(sm.TipGen.Sample()) - abe := ab.Extend(sm.TipGen.Sample()) - abf := ab.Extend(sm.TipGen.Sample()) + ab := baseChain.Extend(tsg.Sample()) + abc := ab.Extend(tsg.Sample()) + abd := ab.Extend(tsg.Sample()) + abe := ab.Extend(tsg.Sample()) + abf := ab.Extend(tsg.Sample()) sm.SetChains( sim.ChainCount{Count: 1, Chain: abc}, sim.ChainCount{Count: 1, Chain: abd}, @@ -240,7 +281,7 @@ func TestLongestCommonPrefix(t *testing.T) { sim.ChainCount{Count: 1, Chain: abf}, ) - require.NoErrorf(t, sm.Run(1, MAX_ROUNDS), "%s", sm.Describe()) + require.NoErrorf(t, sm.Run(1, maxRounds), "%s", sm.Describe()) // Must decide ab, the longest common prefix. - expectDecision(t, sm, ab.Head()) + requireConsensus(t, sm, ab.Head()) } diff --git a/test/multi_instance_test.go b/test/multi_instance_test.go index 61f3d74e..40162854 100644 --- a/test/multi_instance_test.go +++ b/test/multi_instance_test.go @@ -9,42 +9,59 @@ import ( ///// Tests for multiple chained instances of the protocol, no adversaries. -const INSTANCE_COUNT = 4000 +const instanceCount = 4000 func TestMultiSingleton(t *testing.T) { - sm, err := sim.NewSimulation(SyncConfig(1), GraniteConfig(), sim.TraceNone) + tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) + baseChain := generateECChain(t, tsg) + sm, err := sim.NewSimulation(syncOptions( + sim.WithHonestParticipantCount(1), + sim.WithTipSetGenerator(tsg), + sim.WithBaseChain(&baseChain))...) require.NoError(t, err) - a := sm.Base(0).Extend(sm.TipGen.Sample()) + a := baseChain.Extend(tsg.Sample()) sm.SetChains(sim.ChainCount{Count: 1, Chain: a}) - require.NoErrorf(t, sm.Run(INSTANCE_COUNT, MAX_ROUNDS), "%s", sm.Describe()) - expected := sm.EC.Instances[INSTANCE_COUNT].Base - expectInstanceDecision(t, sm, INSTANCE_COUNT-1, expected.Head()) + require.NoErrorf(t, sm.Run(instanceCount, maxRounds), "%s", sm.Describe()) + instance := sm.GetInstance(instanceCount) + require.NotNil(t, instance) + expected := instance.Base + requireConsensusAtInstance(t, sm, instanceCount-1, expected.Head()) } func TestMultiSyncPair(t *testing.T) { - sm, err := sim.NewSimulation(SyncConfig(2), GraniteConfig(), sim.TraceNone) + tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) + baseChain := generateECChain(t, tsg) + sm, err := sim.NewSimulation(syncOptions( + sim.WithHonestParticipantCount(2), + sim.WithTipSetGenerator(tsg), + sim.WithBaseChain(&baseChain))...) require.NoError(t, err) - a := sm.Base(0).Extend(sm.TipGen.Sample()) - sm.SetChains(sim.ChainCount{Count: len(sm.Participants), Chain: a}) + a := baseChain.Extend(tsg.Sample()) + sm.SetChains(sim.ChainCount{Count: sm.HonestParticipantsCount(), Chain: a}) - require.NoErrorf(t, sm.Run(INSTANCE_COUNT, MAX_ROUNDS), "%s", sm.Describe()) - expected := sm.EC.Instances[INSTANCE_COUNT].Base - expectInstanceDecision(t, sm, INSTANCE_COUNT-1, expected.Head()) + require.NoErrorf(t, sm.Run(instanceCount, maxRounds), "%s", sm.Describe()) + expected := sm.GetInstance(instanceCount).Base + requireConsensusAtInstance(t, sm, instanceCount-1, expected.Head()) } func TestMultiASyncPair(t *testing.T) { - sm, err := sim.NewSimulation(AsyncConfig(2, 0), GraniteConfig(), sim.TraceNone) + tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) + baseChain := generateECChain(t, tsg) + sm, err := sim.NewSimulation(asyncOptions(t, 1413, + sim.WithHonestParticipantCount(2), + sim.WithTipSetGenerator(tsg), + sim.WithBaseChain(&baseChain))...) require.NoError(t, err) - a := sm.Base(0).Extend(sm.TipGen.Sample()) - sm.SetChains(sim.ChainCount{Count: len(sm.Participants), Chain: a}) + a := baseChain.Extend(tsg.Sample()) + sm.SetChains(sim.ChainCount{Count: sm.HonestParticipantsCount(), Chain: a}) - require.NoErrorf(t, sm.Run(INSTANCE_COUNT, MAX_ROUNDS), "%s", sm.Describe()) + 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. - expected := sm.EC.Instances[INSTANCE_COUNT].Base - expectInstanceDecision(t, sm, INSTANCE_COUNT-1, expected...) + expected := sm.GetInstance(instanceCount).Base + requireConsensusAtInstance(t, sm, instanceCount-1, expected...) } func TestMultiSyncAgreement(t *testing.T) { @@ -55,16 +72,21 @@ func TestMultiSyncAgreement(t *testing.T) { t.Parallel() repeatInParallel(t, 9, func(t *testing.T, repetition int) { honestCount := repetition + 3 - sm, err := sim.NewSimulation(SyncConfig(honestCount), GraniteConfig(), sim.TraceNone) + tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) + baseChain := generateECChain(t, tsg) + sm, err := sim.NewSimulation(syncOptions( + sim.WithHonestParticipantCount(honestCount), + sim.WithTipSetGenerator(tsg), + sim.WithBaseChain(&baseChain))...) require.NoError(t, err) - a := sm.Base(0).Extend(sm.TipGen.Sample()) + a := baseChain.Extend(tsg.Sample()) // All nodes start with the same chain and will observe the same extensions of that chain // in subsequent instances. - sm.SetChains(sim.ChainCount{Count: len(sm.Participants), Chain: a}) - require.NoErrorf(t, sm.Run(INSTANCE_COUNT, MAX_ROUNDS), "%s", sm.Describe()) + sm.SetChains(sim.ChainCount{Count: sm.HonestParticipantsCount(), Chain: a}) + require.NoErrorf(t, sm.Run(instanceCount, maxRounds), "%s", sm.Describe()) // Synchronous, agreeing groups always decide the candidate. - expected := sm.EC.Instances[INSTANCE_COUNT].Base - expectInstanceDecision(t, sm, INSTANCE_COUNT-1, expected...) + expected := sm.GetInstance(instanceCount).Base + requireConsensusAtInstance(t, sm, instanceCount-1, expected...) }) } @@ -76,14 +98,20 @@ func TestMultiAsyncAgreement(t *testing.T) { t.Parallel() repeatInParallel(t, 9, func(t *testing.T, repetition int) { honestCount := repetition + 3 - sm, err := sim.NewSimulation(AsyncConfig(honestCount, 0), GraniteConfig(), sim.TraceNone) + tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) + baseChain := generateECChain(t, tsg) + sm, err := sim.NewSimulation(asyncOptions(t, 1413, + sim.WithHonestParticipantCount(honestCount), + sim.WithTipSetGenerator(tsg), + sim.WithBaseChain(&baseChain))...) require.NoError(t, err) - sm.SetChains(sim.ChainCount{Count: honestCount, Chain: sm.Base(0).Extend(sm.TipGen.Sample())}) + a := baseChain.Extend(tsg.Sample()) + sm.SetChains(sim.ChainCount{Count: honestCount, Chain: a}) - require.NoErrorf(t, sm.Run(INSTANCE_COUNT, MAX_ROUNDS), "%s", sm.Describe()) + 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 EC chain is less clean. - expected := sm.EC.Instances[INSTANCE_COUNT].Base - expectInstanceDecision(t, sm, INSTANCE_COUNT-1, expected...) + expected := sm.GetInstance(instanceCount).Base + requireConsensusAtInstance(t, sm, instanceCount-1, expected...) }) } diff --git a/test/repeat_test.go b/test/repeat_test.go index 139b75d7..596fa37a 100644 --- a/test/repeat_test.go +++ b/test/repeat_test.go @@ -34,14 +34,14 @@ func TestRepeat(t *testing.T) { return 1 } }, - maxRounds: MAX_ROUNDS, + maxRounds: maxRounds, }, { name: "bounded uniform random", repetitionSampler: func(repetition int) adversary.RepetitionSampler { return newBoundedRepeater(int64(repetition), 10, 50) }, - maxRounds: MAX_ROUNDS, + maxRounds: maxRounds, }, { name: "zipf", @@ -52,7 +52,7 @@ func TestRepeat(t *testing.T) { return int(zipf.Uint64()) } }, - maxRounds: MAX_ROUNDS, + maxRounds: maxRounds, }, { name: "QUALITY Repeater", @@ -65,7 +65,7 @@ func TestRepeat(t *testing.T) { return boundedRepeater(msg) } }, - maxRounds: MAX_ROUNDS, + maxRounds: maxRounds, }, { name: "COMMIT Repeater", @@ -85,20 +85,22 @@ func TestRepeat(t *testing.T) { test := test t.Run(test.name, func(t *testing.T) { for _, hc := range honestCounts { - repeatInParallel(t, ASYNC_ITERS, func(t *testing.T, repetition int) { - sm, err := sim.NewSimulation(AsyncConfig(hc, repetition), GraniteConfig(), sim.TraceNone) - require.NoError(t, err) + repeatInParallel(t, asyncInterations, func(t *testing.T, repetition int) { dist := test.repetitionSampler(repetition) - repeat := adversary.NewRepeat(99, sm.HostFor(99), dist) - sm.SetAdversary(repeat, 1) - - a := sm.Base(0).Extend(sm.TipGen.Sample()) - sm.SetChains(sim.ChainCount{Count: len(sm.Participants), Chain: a}) + tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) + baseChain := generateECChain(t, tsg) + sm, err := sim.NewSimulation(asyncOptions(t, repetition, + sim.WithHonestParticipantCount(hc), + sim.WithBaseChain(&baseChain), + sim.WithAdversary(adversary.NewRepeatGenerator(oneStoragePower, dist)), + )...) + require.NoError(t, err) + a := baseChain.Extend(tsg.Sample()) + sm.SetChains(sim.ChainCount{Count: sm.HonestParticipantsCount(), Chain: a}) require.NoErrorf(t, sm.Run(1, test.maxRounds), "%s", sm.Describe()) }) } - }) } } diff --git a/test/util.go b/test/util_test.go similarity index 67% rename from test/util.go rename to test/util_test.go index c0f54c2a..9a5e2e68 100644 --- a/test/util.go +++ b/test/util_test.go @@ -29,26 +29,34 @@ func init() { } // Expects the decision in the first instance to be one of the given tipsets. -func expectDecision(t *testing.T, sm *sim.Simulation, expected ...gpbft.TipSet) { - expectInstanceDecision(t, sm, 0, expected...) +func requireConsensus(t *testing.T, sm *sim.Simulation, expected ...gpbft.TipSet) { + requireConsensusAtInstance(t, sm, 0, expected...) } // Expects the decision in an instance to be one of the given tipsets. -func expectInstanceDecision(t *testing.T, sm *sim.Simulation, instance uint64, expected ...gpbft.TipSet) { +func requireConsensusAtInstance(t *testing.T, sm *sim.Simulation, instance uint64, expected ...gpbft.TipSet) { nextParticipant: - for _, participant := range sm.Participants { - decision, ok := sm.GetDecision(instance, participant.ID()) - require.True(t, ok, "no decision for participant %d in instance %d", participant.ID(), instance) + for _, pid := range sm.ListParticipantIDs() { + decision, ok := sm.GetDecision(instance, pid) + require.True(t, ok, "no decision for participant %d in instance %d", pid, instance) for _, e := range expected { if bytes.Equal(decision.Head(), e) { continue nextParticipant } } - require.Fail(t, "participant %d decided %s in instance %d, expected one of %s", - participant.ID(), decision, instance, expected) + require.Fail(t, "consensus not reached", "participant %d decided %s in instance %d, expected one of %s", + pid, decision, instance, expected) } } +func generateECChain(t *testing.T, tsg *sim.TipSetGenerator) gpbft.ECChain { + t.Helper() + // TODO: add stochastic chain generation. + chain, err := gpbft.NewChain(tsg.Sample()) + require.NoError(t, err) + return chain +} + // repeatInParallel repeats target for the given number of repetitions. // See repetitionParallelism. func repeatInParallel(t *testing.T, repetitions int, target func(t *testing.T, repetition int)) { diff --git a/test/withhold_test.go b/test/withhold_test.go index f14185fb..03ef15fb 100644 --- a/test/withhold_test.go +++ b/test/withhold_test.go @@ -8,31 +8,38 @@ import ( "github.com/filecoin-project/go-f3/gpbft" "github.com/filecoin-project/go-f3/sim" "github.com/filecoin-project/go-f3/sim/adversary" + "github.com/filecoin-project/go-f3/sim/latency" "github.com/stretchr/testify/require" ) func TestWitholdCommit1(t *testing.T) { - i := 0 - simConfig := AsyncConfig(7, i) - simConfig.LatencyMean = 10 * time.Millisecond // Near-synchrony - sm, err := sim.NewSimulation(simConfig, GraniteConfig(), sim.TraceNone) + nearSynchrony, err := latency.NewLogNormal(1413, 10*time.Millisecond) require.NoError(t, err) - adv := adversary.NewWitholdCommit(99, sm.HostFor(99)) - sm.SetAdversary(adv, 3) // Adversary has 30% of 10 total power. - - a := sm.Base(0).Extend(sm.TipGen.Sample()) - b := sm.Base(0).Extend(sm.TipGen.Sample()) - // Of 7 nodes, 4 victims will prefer chain A, 3 others will prefer chain B. - // The adversary will target the first to decide, and withhold COMMIT from the rest. - // After the victim decides in round 0, the adversary stops participating. - // Now there are 3 nodes on each side (and one decided), with total power 6/10, less than quorum. - // The B side must be swayed to the A side by observing that some nodes on the A side reached a COMMIT. + tsg := sim.NewTipSetGenerator(tipSetGeneratorSeed) + baseChain := generateECChain(t, tsg) + a := baseChain.Extend(tsg.Sample()) + b := baseChain.Extend(tsg.Sample()) victims := []gpbft.ActorID{0, 1, 2, 3} - adv.SetVictim(victims, a) + sm, err := sim.NewSimulation( + sim.WithHonestParticipantCount(7), + sim.WithLatencyModel(nearSynchrony), + sim.WithECEpochDuration(EcEpochDuration), + sim.WitECStabilisationDelay(EcStabilisationDelay), + sim.WithGraniteConfig(testGraniteConfig), + sim.WithTipSetGenerator(tsg), + sim.WithBaseChain(&baseChain), + // Adversary has 30% of 10 total power. + // Of 7 nodes, 4 victims will prefer chain A, 3 others will prefer chain B. + // The adversary will target the first to decide, and withhold COMMIT from the rest. + // After the victim decides in round 0, the adversary stops participating. + // Now there are 3 nodes on each side (and one decided), with total power 6/10, less than quorum. + // The B side must be swayed to the A side by observing that some nodes on the A side reached a COMMIT. + sim.WithAdversary(adversary.NewWitholdCommitGenerator(gpbft.NewStoragePower(3), victims, a)), + ) + require.NoError(t, err) - adv.Begin() sm.SetChains(sim.ChainCount{Count: 4, Chain: a}, sim.ChainCount{Count: 3, Chain: b}) - err = sm.Run(1, MAX_ROUNDS) + err = sm.Run(1, maxRounds) if err != nil { fmt.Printf("%s", sm.Describe()) sm.PrintResults()