diff --git a/node/node.go b/node/node.go index 1ebc7cdbda56..a073358548d4 100644 --- a/node/node.go +++ b/node/node.go @@ -140,9 +140,9 @@ func (n *Node) initNetworking() error { tlsConfig := &tls.Config{ Certificates: []tls.Certificate{cert}, ClientAuth: tls.RequireAnyClientCert, - // We do not use TLS's CA functionality, we just require an - // authenticated channel. Therefore, we can safely skip verification - // here. + // We do not use TLS's CA functionality to authenticate a hostname. + // We only require an authenticated channel based on the peer's + // public key. Therefore, we can safely skip CA verification. // // TODO: Security audit required InsecureSkipVerify: true, diff --git a/snow/consensus/avalanche/consensus.go b/snow/consensus/avalanche/consensus.go index a4ee7c52003b..154ddf5a9653 100644 --- a/snow/consensus/avalanche/consensus.go +++ b/snow/consensus/avalanche/consensus.go @@ -23,7 +23,7 @@ type Consensus interface { // called, the status maps should be immediately updated accordingly. // Assumes each element in the accepted frontier will return accepted from // the join status map. - Initialize(*snow.Context, Parameters, []Vertex) + Initialize(*snow.Context, Parameters, []Vertex) error // Returns the parameters that describe this avalanche instance Parameters() Parameters diff --git a/snow/consensus/avalanche/consensus_test.go b/snow/consensus/avalanche/consensus_test.go index 9cbddbe8f4ac..99bb89a6b3f1 100644 --- a/snow/consensus/avalanche/consensus_test.go +++ b/snow/consensus/avalanche/consensus_test.go @@ -122,18 +122,21 @@ func ParamsTest(t *testing.T, factory Factory) { ctx := snow.DefaultContextTest() params := Parameters{ Parameters: snowball.Parameters{ - Namespace: fmt.Sprintf("gecko_%s", ctx.ChainID.String()), - Metrics: prometheus.NewRegistry(), - K: 2, - Alpha: 2, - BetaVirtuous: 1, - BetaRogue: 2, + Namespace: fmt.Sprintf("gecko_%s", ctx.ChainID.String()), + Metrics: prometheus.NewRegistry(), + K: 2, + Alpha: 2, + BetaVirtuous: 1, + BetaRogue: 2, + ConcurrentRepolls: 1, }, Parents: 2, BatchSize: 1, } - avl.Initialize(ctx, params, nil) + if err := avl.Initialize(ctx, params, nil); err != nil { + t.Fatal(err) + } if p := avl.Parameters(); p.K != params.K { t.Fatalf("Wrong K parameter") @@ -153,11 +156,12 @@ func AddTest(t *testing.T, factory Factory) { params := Parameters{ Parameters: snowball.Parameters{ - Metrics: prometheus.NewRegistry(), - K: 2, - Alpha: 2, - BetaVirtuous: 1, - BetaRogue: 2, + Metrics: prometheus.NewRegistry(), + K: 2, + Alpha: 2, + BetaVirtuous: 1, + BetaRogue: 2, + ConcurrentRepolls: 1, }, Parents: 2, BatchSize: 1, @@ -174,7 +178,9 @@ func AddTest(t *testing.T, factory Factory) { } utxos := []ids.ID{ids.GenerateTestID()} - avl.Initialize(snow.DefaultContextTest(), params, vts) + if err := avl.Initialize(snow.DefaultContextTest(), params, vts); err != nil { + t.Fatal(err) + } if !avl.Finalized() { t.Fatalf("An empty avalanche instance is not finalized") @@ -248,11 +254,12 @@ func VertexIssuedTest(t *testing.T, factory Factory) { params := Parameters{ Parameters: snowball.Parameters{ - Metrics: prometheus.NewRegistry(), - K: 2, - Alpha: 2, - BetaVirtuous: 1, - BetaRogue: 2, + Metrics: prometheus.NewRegistry(), + K: 2, + Alpha: 2, + BetaVirtuous: 1, + BetaRogue: 2, + ConcurrentRepolls: 1, }, Parents: 2, BatchSize: 1, @@ -269,7 +276,9 @@ func VertexIssuedTest(t *testing.T, factory Factory) { } utxos := []ids.ID{ids.GenerateTestID()} - avl.Initialize(snow.DefaultContextTest(), params, vts) + if err := avl.Initialize(snow.DefaultContextTest(), params, vts); err != nil { + t.Fatal(err) + } if !avl.VertexIssued(vts[0]) { t.Fatalf("Genesis Vertex not reported as issued") @@ -305,11 +314,12 @@ func TxIssuedTest(t *testing.T, factory Factory) { params := Parameters{ Parameters: snowball.Parameters{ - Metrics: prometheus.NewRegistry(), - K: 2, - Alpha: 2, - BetaVirtuous: 1, - BetaRogue: 2, + Metrics: prometheus.NewRegistry(), + K: 2, + Alpha: 2, + BetaVirtuous: 1, + BetaRogue: 2, + ConcurrentRepolls: 1, }, Parents: 2, BatchSize: 1, @@ -334,7 +344,9 @@ func TxIssuedTest(t *testing.T, factory Factory) { }} tx1.InputIDsV.Add(utxos[0]) - avl.Initialize(snow.DefaultContextTest(), params, vts) + if err := avl.Initialize(snow.DefaultContextTest(), params, vts); err != nil { + t.Fatal(err) + } if !avl.TxIssued(tx0) { t.Fatalf("Genesis Tx not reported as issued") @@ -675,11 +687,12 @@ func IgnoreInvalidVotingTest(t *testing.T, factory Factory) { params := Parameters{ Parameters: snowball.Parameters{ - Metrics: prometheus.NewRegistry(), - K: 3, - Alpha: 2, - BetaVirtuous: 1, - BetaRogue: 1, + Metrics: prometheus.NewRegistry(), + K: 3, + Alpha: 2, + BetaVirtuous: 1, + BetaRogue: 1, + ConcurrentRepolls: 1, }, Parents: 2, BatchSize: 1, @@ -697,7 +710,9 @@ func IgnoreInvalidVotingTest(t *testing.T, factory Factory) { } utxos := []ids.ID{ids.GenerateTestID()} - avl.Initialize(snow.DefaultContextTest(), params, vts) + if err := avl.Initialize(snow.DefaultContextTest(), params, vts); err != nil { + t.Fatal(err) + } tx0 := &snowstorm.TestTx{TestDecidable: choices.TestDecidable{ IDV: ids.GenerateTestID(), diff --git a/snow/consensus/avalanche/topological.go b/snow/consensus/avalanche/topological.go index a0447c03ff5a..eca9162702d3 100644 --- a/snow/consensus/avalanche/topological.go +++ b/snow/consensus/avalanche/topological.go @@ -59,26 +59,34 @@ type kahnNode struct { } // Initialize implements the Avalanche interface -func (ta *Topological) Initialize(ctx *snow.Context, params Parameters, frontier []Vertex) { - ctx.Log.AssertDeferredNoError(params.Valid) +func (ta *Topological) Initialize( + ctx *snow.Context, + params Parameters, + frontier []Vertex, +) error { + if err := params.Valid(); err != nil { + return err + } ta.ctx = ctx ta.params = params if err := ta.metrics.Initialize(ctx.Log, params.Namespace, params.Metrics); err != nil { - ta.ctx.Log.Error("%s", err) + return err } ta.nodes = make(map[[32]byte]Vertex, minMapSize) ta.cg = &snowstorm.Directed{} - ta.cg.Initialize(ctx, params.Parameters) + if err := ta.cg.Initialize(ctx, params.Parameters); err != nil { + return err + } ta.frontier = make(map[[32]byte]Vertex, minMapSize) for _, vtx := range frontier { ta.frontier[vtx.ID().Key()] = vtx } - ctx.Log.AssertNoError(ta.updateFrontiers()) + return ta.updateFrontiers() } // Parameters implements the Avalanche interface diff --git a/snow/consensus/snowstorm/benchmark_test.go b/snow/consensus/snowstorm/benchmark_test.go index 914167dd14c9..58e25ca0ba50 100644 --- a/snow/consensus/snowstorm/benchmark_test.go +++ b/snow/consensus/snowstorm/benchmark_test.go @@ -9,12 +9,12 @@ import ( "github.com/prometheus/client_golang/prometheus" - "github.com/ava-labs/gecko/snow/consensus/snowball" + sbcon "github.com/ava-labs/gecko/snow/consensus/snowball" ) func Simulate( numColors, colorsPerConsumer, maxInputConflicts, numNodes int, - params snowball.Parameters, + params sbcon.Parameters, seed int64, fact Factory, ) { @@ -53,7 +53,7 @@ func BenchmarkVirtuousDirected(b *testing.B) { /*colorsPerConsumer=*/ 1, /*maxInputConflicts=*/ 1, /*numNodes=*/ 50, - /*params=*/ snowball.Parameters{ + /*params=*/ sbcon.Parameters{ Metrics: prometheus.NewRegistry(), K: 20, Alpha: 11, @@ -73,7 +73,7 @@ func BenchmarkVirtuousInput(b *testing.B) { /*colorsPerConsumer=*/ 1, /*maxInputConflicts=*/ 1, /*numNodes=*/ 50, - /*params=*/ snowball.Parameters{ + /*params=*/ sbcon.Parameters{ Metrics: prometheus.NewRegistry(), K: 20, Alpha: 11, @@ -99,7 +99,7 @@ func BenchmarkRogueDirected(b *testing.B) { /*colorsPerConsumer=*/ 1, /*maxInputConflicts=*/ 3, /*numNodes=*/ 50, - /*params=*/ snowball.Parameters{ + /*params=*/ sbcon.Parameters{ Metrics: prometheus.NewRegistry(), K: 20, Alpha: 11, @@ -119,7 +119,7 @@ func BenchmarkRogueInput(b *testing.B) { /*colorsPerConsumer=*/ 1, /*maxInputConflicts=*/ 3, /*numNodes=*/ 50, - /*params=*/ snowball.Parameters{ + /*params=*/ sbcon.Parameters{ Metrics: prometheus.NewRegistry(), K: 20, Alpha: 11, @@ -145,7 +145,7 @@ func BenchmarkMultiDirected(b *testing.B) { /*colorsPerConsumer=*/ 10, /*maxInputConflicts=*/ 1, /*numNodes=*/ 50, - /*params=*/ snowball.Parameters{ + /*params=*/ sbcon.Parameters{ Metrics: prometheus.NewRegistry(), K: 20, Alpha: 11, @@ -165,7 +165,7 @@ func BenchmarkMultiInput(b *testing.B) { /*colorsPerConsumer=*/ 10, /*maxInputConflicts=*/ 1, /*numNodes=*/ 50, - /*params=*/ snowball.Parameters{ + /*params=*/ sbcon.Parameters{ Metrics: prometheus.NewRegistry(), K: 20, Alpha: 11, @@ -191,7 +191,7 @@ func BenchmarkMultiRogueDirected(b *testing.B) { /*colorsPerConsumer=*/ 10, /*maxInputConflicts=*/ 3, /*numNodes=*/ 50, - /*params=*/ snowball.Parameters{ + /*params=*/ sbcon.Parameters{ Metrics: prometheus.NewRegistry(), K: 20, Alpha: 11, @@ -211,7 +211,7 @@ func BenchmarkMultiRogueInput(b *testing.B) { /*colorsPerConsumer=*/ 10, /*maxInputConflicts=*/ 3, /*numNodes=*/ 50, - /*params=*/ snowball.Parameters{ + /*params=*/ sbcon.Parameters{ Metrics: prometheus.NewRegistry(), K: 20, Alpha: 11, diff --git a/snow/consensus/snowstorm/common.go b/snow/consensus/snowstorm/common.go index a64685dc2217..f84ce612f97f 100644 --- a/snow/consensus/snowstorm/common.go +++ b/snow/consensus/snowstorm/common.go @@ -4,11 +4,14 @@ package snowstorm import ( + "fmt" + "github.com/ava-labs/gecko/ids" "github.com/ava-labs/gecko/snow" - "github.com/ava-labs/gecko/snow/consensus/snowball" "github.com/ava-labs/gecko/snow/events" "github.com/ava-labs/gecko/utils/wrappers" + + sbcon "github.com/ava-labs/gecko/snow/consensus/snowball" ) type common struct { @@ -19,7 +22,7 @@ type common struct { ctx *snow.Context // params describes how this instance was parameterized - params snowball.Parameters + params sbcon.Parameters // each element of preferences is the ID of a transaction that is preferred preferences ids.Set @@ -44,19 +47,18 @@ type common struct { } // Initialize implements the ConflictGraph interface -func (c *common) Initialize(ctx *snow.Context, params snowball.Parameters) { - ctx.Log.AssertDeferredNoError(params.Valid) - +func (c *common) Initialize(ctx *snow.Context, params sbcon.Parameters) error { c.ctx = ctx c.params = params if err := c.metrics.Initialize(params.Namespace, params.Metrics); err != nil { - ctx.Log.Error("failed to initialize metrics: %s", err) + return fmt.Errorf("failed to initialize metrics: %s", err) } + return params.Valid() } // Parameters implements the Snowstorm interface -func (c *common) Parameters() snowball.Parameters { return c.params } +func (c *common) Parameters() sbcon.Parameters { return c.params } // Virtuous implements the ConflictGraph interface func (c *common) Virtuous() ids.Set { return c.virtuous } @@ -79,3 +81,26 @@ func (c *common) Finalized() bool { numPreferences) return numPreferences == 0 } + +// rejector implements Blockable +type rejector struct { + g Consensus + deps ids.Set + errs *wrappers.Errs + rejected bool // true if the tx has been rejected + txID ids.ID +} + +func (r *rejector) Dependencies() ids.Set { return r.deps } + +func (r *rejector) Fulfill(ids.ID) { + if r.rejected || r.errs.Errored() { + return + } + r.rejected = true + r.errs.Add(r.g.reject(r.txID)) +} + +func (*rejector) Abandon(ids.ID) {} + +func (*rejector) Update() {} diff --git a/snow/consensus/snowstorm/consensus.go b/snow/consensus/snowstorm/consensus.go index 7f05c34bf845..bca3894b7fc5 100644 --- a/snow/consensus/snowstorm/consensus.go +++ b/snow/consensus/snowstorm/consensus.go @@ -8,7 +8,8 @@ import ( "github.com/ava-labs/gecko/ids" "github.com/ava-labs/gecko/snow" - "github.com/ava-labs/gecko/snow/consensus/snowball" + + sbcon "github.com/ava-labs/gecko/snow/consensus/snowball" ) // Consensus is a snowball instance deciding between an unbounded number of @@ -18,10 +19,10 @@ type Consensus interface { fmt.Stringer // Takes in the context, alpha, betaVirtuous, and betaRogue - Initialize(*snow.Context, snowball.Parameters) + Initialize(*snow.Context, sbcon.Parameters) error // Returns the parameters that describe this snowstorm instance - Parameters() snowball.Parameters + Parameters() sbcon.Parameters // Returns true if transaction is virtuous. // That is, no transaction has been added that conflicts with @@ -58,4 +59,7 @@ type Consensus interface { // possible that after returning finalized, a new decision may be added such // that this instance is no longer finalized. Finalized() bool + + // Reject all the provided txs and remove them from the graph + reject(txIDs ...ids.ID) error } diff --git a/snow/consensus/snowstorm/consensus_test.go b/snow/consensus/snowstorm/consensus_test.go index f24dece42d7d..478289ec517f 100644 --- a/snow/consensus/snowstorm/consensus_test.go +++ b/snow/consensus/snowstorm/consensus_test.go @@ -8,11 +8,13 @@ import ( "testing" "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/assert" "github.com/ava-labs/gecko/ids" "github.com/ava-labs/gecko/snow" "github.com/ava-labs/gecko/snow/choices" - "github.com/ava-labs/gecko/snow/consensus/snowball" + + sbcon "github.com/ava-labs/gecko/snow/consensus/snowball" ) var ( @@ -36,6 +38,7 @@ var ( ErrorOnAcceptedTest, ErrorOnRejectingLowerConfidenceConflictTest, ErrorOnRejectingHigherConfidenceConflictTest, + UTXOCleanupTest, } Red, Green, Blue, Alpha *TestTx @@ -87,9 +90,13 @@ func MetricsTest(t *testing.T, factory Factory) { Setup() { - params := snowball.Parameters{ - Metrics: prometheus.NewRegistry(), - K: 2, Alpha: 2, BetaVirtuous: 1, BetaRogue: 2, + params := sbcon.Parameters{ + Metrics: prometheus.NewRegistry(), + K: 2, + Alpha: 2, + BetaVirtuous: 1, + BetaRogue: 2, + ConcurrentRepolls: 1, } params.Metrics.Register(prometheus.NewCounter(prometheus.CounterOpts{ Name: "tx_processing", @@ -98,9 +105,13 @@ func MetricsTest(t *testing.T, factory Factory) { graph.Initialize(snow.DefaultContextTest(), params) } { - params := snowball.Parameters{ - Metrics: prometheus.NewRegistry(), - K: 2, Alpha: 2, BetaVirtuous: 1, BetaRogue: 2, + params := sbcon.Parameters{ + Metrics: prometheus.NewRegistry(), + K: 2, + Alpha: 2, + BetaVirtuous: 1, + BetaRogue: 2, + ConcurrentRepolls: 1, } params.Metrics.Register(prometheus.NewCounter(prometheus.CounterOpts{ Name: "tx_accepted", @@ -109,9 +120,13 @@ func MetricsTest(t *testing.T, factory Factory) { graph.Initialize(snow.DefaultContextTest(), params) } { - params := snowball.Parameters{ - Metrics: prometheus.NewRegistry(), - K: 2, Alpha: 2, BetaVirtuous: 1, BetaRogue: 2, + params := sbcon.Parameters{ + Metrics: prometheus.NewRegistry(), + K: 2, + Alpha: 2, + BetaVirtuous: 1, + BetaRogue: 2, + ConcurrentRepolls: 1, } params.Metrics.Register(prometheus.NewCounter(prometheus.CounterOpts{ Name: "tx_rejected", @@ -126,9 +141,13 @@ func ParamsTest(t *testing.T, factory Factory) { graph := factory.New() - params := snowball.Parameters{ - Metrics: prometheus.NewRegistry(), - K: 2, Alpha: 2, BetaVirtuous: 1, BetaRogue: 2, + params := sbcon.Parameters{ + Metrics: prometheus.NewRegistry(), + K: 2, + Alpha: 2, + BetaVirtuous: 1, + BetaRogue: 2, + ConcurrentRepolls: 1, } graph.Initialize(snow.DefaultContextTest(), params) @@ -148,9 +167,13 @@ func IssuedTest(t *testing.T, factory Factory) { graph := factory.New() - params := snowball.Parameters{ - Metrics: prometheus.NewRegistry(), - K: 2, Alpha: 2, BetaVirtuous: 1, BetaRogue: 1, + params := sbcon.Parameters{ + Metrics: prometheus.NewRegistry(), + K: 2, + Alpha: 2, + BetaVirtuous: 1, + BetaRogue: 1, + ConcurrentRepolls: 1, } graph.Initialize(snow.DefaultContextTest(), params) @@ -174,9 +197,13 @@ func LeftoverInputTest(t *testing.T, factory Factory) { graph := factory.New() - params := snowball.Parameters{ - Metrics: prometheus.NewRegistry(), - K: 2, Alpha: 2, BetaVirtuous: 1, BetaRogue: 1, + params := sbcon.Parameters{ + Metrics: prometheus.NewRegistry(), + K: 2, + Alpha: 2, + BetaVirtuous: 1, + BetaRogue: 1, + ConcurrentRepolls: 1, } graph.Initialize(snow.DefaultContextTest(), params) @@ -213,9 +240,13 @@ func LowerConfidenceTest(t *testing.T, factory Factory) { graph := factory.New() - params := snowball.Parameters{ - Metrics: prometheus.NewRegistry(), - K: 2, Alpha: 2, BetaVirtuous: 1, BetaRogue: 1, + params := sbcon.Parameters{ + Metrics: prometheus.NewRegistry(), + K: 2, + Alpha: 2, + BetaVirtuous: 1, + BetaRogue: 1, + ConcurrentRepolls: 1, } graph.Initialize(snow.DefaultContextTest(), params) @@ -252,9 +283,13 @@ func MiddleConfidenceTest(t *testing.T, factory Factory) { graph := factory.New() - params := snowball.Parameters{ - Metrics: prometheus.NewRegistry(), - K: 2, Alpha: 2, BetaVirtuous: 1, BetaRogue: 1, + params := sbcon.Parameters{ + Metrics: prometheus.NewRegistry(), + K: 2, + Alpha: 2, + BetaVirtuous: 1, + BetaRogue: 1, + ConcurrentRepolls: 1, } graph.Initialize(snow.DefaultContextTest(), params) @@ -295,9 +330,13 @@ func IndependentTest(t *testing.T, factory Factory) { graph := factory.New() - params := snowball.Parameters{ - Metrics: prometheus.NewRegistry(), - K: 2, Alpha: 2, BetaVirtuous: 2, BetaRogue: 2, + params := sbcon.Parameters{ + Metrics: prometheus.NewRegistry(), + K: 2, + Alpha: 2, + BetaVirtuous: 2, + BetaRogue: 2, + ConcurrentRepolls: 1, } graph.Initialize(snow.DefaultContextTest(), params) @@ -343,9 +382,13 @@ func VirtuousTest(t *testing.T, factory Factory) { graph := factory.New() - params := snowball.Parameters{ - Metrics: prometheus.NewRegistry(), - K: 2, Alpha: 2, BetaVirtuous: 1, BetaRogue: 1, + params := sbcon.Parameters{ + Metrics: prometheus.NewRegistry(), + K: 2, + Alpha: 2, + BetaVirtuous: 1, + BetaRogue: 1, + ConcurrentRepolls: 1, } graph.Initialize(snow.DefaultContextTest(), params) @@ -381,9 +424,13 @@ func IsVirtuousTest(t *testing.T, factory Factory) { graph := factory.New() - params := snowball.Parameters{ - Metrics: prometheus.NewRegistry(), - K: 2, Alpha: 2, BetaVirtuous: 1, BetaRogue: 1, + params := sbcon.Parameters{ + Metrics: prometheus.NewRegistry(), + K: 2, + Alpha: 2, + BetaVirtuous: 1, + BetaRogue: 1, + ConcurrentRepolls: 1, } graph.Initialize(snow.DefaultContextTest(), params) @@ -421,9 +468,13 @@ func QuiesceTest(t *testing.T, factory Factory) { graph := factory.New() - params := snowball.Parameters{ - Metrics: prometheus.NewRegistry(), - K: 2, Alpha: 2, BetaVirtuous: 1, BetaRogue: 1, + params := sbcon.Parameters{ + Metrics: prometheus.NewRegistry(), + K: 2, + Alpha: 2, + BetaVirtuous: 1, + BetaRogue: 1, + ConcurrentRepolls: 1, } graph.Initialize(snow.DefaultContextTest(), params) @@ -454,9 +505,13 @@ func AcceptingDependencyTest(t *testing.T, factory Factory) { } purple.InputIDsV.Add(ids.Empty.Prefix(8)) - params := snowball.Parameters{ - Metrics: prometheus.NewRegistry(), - K: 1, Alpha: 1, BetaVirtuous: 1, BetaRogue: 2, + params := sbcon.Parameters{ + Metrics: prometheus.NewRegistry(), + K: 1, + Alpha: 1, + BetaVirtuous: 1, + BetaRogue: 2, + ConcurrentRepolls: 1, } graph.Initialize(snow.DefaultContextTest(), params) @@ -545,9 +600,13 @@ func RejectingDependencyTest(t *testing.T, factory Factory) { } purple.InputIDsV.Add(ids.Empty.Prefix(8)) - params := snowball.Parameters{ - Metrics: prometheus.NewRegistry(), - K: 1, Alpha: 1, BetaVirtuous: 1, BetaRogue: 2, + params := sbcon.Parameters{ + Metrics: prometheus.NewRegistry(), + K: 1, + Alpha: 1, + BetaVirtuous: 1, + BetaRogue: 2, + ConcurrentRepolls: 1, } graph.Initialize(snow.DefaultContextTest(), params) @@ -618,9 +677,13 @@ func VacuouslyAcceptedTest(t *testing.T, factory Factory) { StatusV: choices.Processing, }} - params := snowball.Parameters{ - Metrics: prometheus.NewRegistry(), - K: 1, Alpha: 1, BetaVirtuous: 1, BetaRogue: 2, + params := sbcon.Parameters{ + Metrics: prometheus.NewRegistry(), + K: 1, + Alpha: 1, + BetaVirtuous: 1, + BetaRogue: 2, + ConcurrentRepolls: 1, } graph.Initialize(snow.DefaultContextTest(), params) @@ -638,9 +701,13 @@ func ConflictsTest(t *testing.T, factory Factory) { graph := factory.New() - params := snowball.Parameters{ - Metrics: prometheus.NewRegistry(), - K: 1, Alpha: 1, BetaVirtuous: 1, BetaRogue: 2, + params := sbcon.Parameters{ + Metrics: prometheus.NewRegistry(), + K: 1, + Alpha: 1, + BetaVirtuous: 1, + BetaRogue: 2, + ConcurrentRepolls: 1, } graph.Initialize(snow.DefaultContextTest(), params) @@ -688,9 +755,13 @@ func VirtuousDependsOnRogueTest(t *testing.T, factory Factory) { graph := factory.New() - params := snowball.Parameters{ - Metrics: prometheus.NewRegistry(), - K: 1, Alpha: 1, BetaVirtuous: 1, BetaRogue: 2, + params := sbcon.Parameters{ + Metrics: prometheus.NewRegistry(), + K: 1, + Alpha: 1, + BetaVirtuous: 1, + BetaRogue: 2, + ConcurrentRepolls: 1, } graph.Initialize(snow.DefaultContextTest(), params) @@ -753,9 +824,13 @@ func ErrorOnVacuouslyAcceptedTest(t *testing.T, factory Factory) { StatusV: choices.Processing, }} - params := snowball.Parameters{ - Metrics: prometheus.NewRegistry(), - K: 1, Alpha: 1, BetaVirtuous: 1, BetaRogue: 2, + params := sbcon.Parameters{ + Metrics: prometheus.NewRegistry(), + K: 1, + Alpha: 1, + BetaVirtuous: 1, + BetaRogue: 2, + ConcurrentRepolls: 1, } graph.Initialize(snow.DefaultContextTest(), params) @@ -776,9 +851,13 @@ func ErrorOnAcceptedTest(t *testing.T, factory Factory) { }} purple.InputIDsV.Add(ids.Empty.Prefix(4)) - params := snowball.Parameters{ - Metrics: prometheus.NewRegistry(), - K: 1, Alpha: 1, BetaVirtuous: 1, BetaRogue: 2, + params := sbcon.Parameters{ + Metrics: prometheus.NewRegistry(), + K: 1, + Alpha: 1, + BetaVirtuous: 1, + BetaRogue: 2, + ConcurrentRepolls: 1, } graph.Initialize(snow.DefaultContextTest(), params) @@ -813,9 +892,13 @@ func ErrorOnRejectingLowerConfidenceConflictTest(t *testing.T, factory Factory) }} pink.InputIDsV.Add(X) - params := snowball.Parameters{ - Metrics: prometheus.NewRegistry(), - K: 1, Alpha: 1, BetaVirtuous: 1, BetaRogue: 1, + params := sbcon.Parameters{ + Metrics: prometheus.NewRegistry(), + K: 1, + Alpha: 1, + BetaVirtuous: 1, + BetaRogue: 1, + ConcurrentRepolls: 1, } graph.Initialize(snow.DefaultContextTest(), params) @@ -852,9 +935,13 @@ func ErrorOnRejectingHigherConfidenceConflictTest(t *testing.T, factory Factory) }} pink.InputIDsV.Add(X) - params := snowball.Parameters{ - Metrics: prometheus.NewRegistry(), - K: 1, Alpha: 1, BetaVirtuous: 1, BetaRogue: 1, + params := sbcon.Parameters{ + Metrics: prometheus.NewRegistry(), + K: 1, + Alpha: 1, + BetaVirtuous: 1, + BetaRogue: 1, + ConcurrentRepolls: 1, } graph.Initialize(snow.DefaultContextTest(), params) @@ -871,14 +958,62 @@ func ErrorOnRejectingHigherConfidenceConflictTest(t *testing.T, factory Factory) } } +func UTXOCleanupTest(t *testing.T, factory Factory) { + Setup() + + graph := factory.New() + + params := sbcon.Parameters{ + Metrics: prometheus.NewRegistry(), + K: 1, + Alpha: 1, + BetaVirtuous: 1, + BetaRogue: 2, + ConcurrentRepolls: 1, + } + err := graph.Initialize(snow.DefaultContextTest(), params) + assert.NoError(t, err) + + err = graph.Add(Red) + assert.NoError(t, err) + + err = graph.Add(Green) + assert.NoError(t, err) + + redVotes := ids.Bag{} + redVotes.Add(Red.ID()) + _, err = graph.RecordPoll(redVotes) + assert.NoError(t, err) + + _, err = graph.RecordPoll(redVotes) + assert.NoError(t, err) + + assert.Equal(t, choices.Accepted, Red.Status()) + assert.Equal(t, choices.Rejected, Green.Status()) + + err = graph.Add(Blue) + assert.NoError(t, err) + + blueVotes := ids.Bag{} + blueVotes.Add(Blue.ID()) + _, err = graph.RecordPoll(blueVotes) + assert.NoError(t, err) + + assert.Equal(t, choices.Accepted, Blue.Status()) +} + func StringTest(t *testing.T, factory Factory, prefix string) { Setup() graph := factory.New() - params := snowball.Parameters{ - Metrics: prometheus.NewRegistry(), - K: 2, Alpha: 2, BetaVirtuous: 1, BetaRogue: 2, + params := sbcon.Parameters{ + Metrics: prometheus.NewRegistry(), + K: 2, + Alpha: 2, + BetaVirtuous: 1, + BetaRogue: 2, + ConcurrentRepolls: 1, } graph.Initialize(snow.DefaultContextTest(), params) @@ -910,10 +1045,10 @@ func StringTest(t *testing.T, factory Factory, prefix string) { { expected := prefix + "(\n" + - " Choice[0] = ID: LUC1cmcxnfNR9LdkACS2ccGKLEK7SYqB4gLLTycQfg1koyfSq Confidence: 1 Bias: 1\n" + - " Choice[1] = ID: TtF4d2QWbk5vzQGTEPrN48x6vwgAoAmKQ9cbp79inpQmcRKES Confidence: 0 Bias: 0\n" + - " Choice[2] = ID: Zda4gsqTjRaX6XVZekVNi3ovMFPHDRQiGbzYuAb7Nwqy1rGBc Confidence: 0 Bias: 0\n" + - " Choice[3] = ID: 2mcwQKiD8VEspmMJpL1dc7okQQ5dDVAWeCBZ7FWBFAbxpv3t7w Confidence: 1 Bias: 1\n" + + " Choice[0] = ID: LUC1cmcxnfNR9LdkACS2ccGKLEK7SYqB4gLLTycQfg1koyfSq SB(NumSuccessfulPolls = 1, Confidence = 1)\n" + + " Choice[1] = ID: TtF4d2QWbk5vzQGTEPrN48x6vwgAoAmKQ9cbp79inpQmcRKES SB(NumSuccessfulPolls = 0, Confidence = 0)\n" + + " Choice[2] = ID: Zda4gsqTjRaX6XVZekVNi3ovMFPHDRQiGbzYuAb7Nwqy1rGBc SB(NumSuccessfulPolls = 0, Confidence = 0)\n" + + " Choice[3] = ID: 2mcwQKiD8VEspmMJpL1dc7okQQ5dDVAWeCBZ7FWBFAbxpv3t7w SB(NumSuccessfulPolls = 1, Confidence = 1)\n" + ")" if str := graph.String(); str != expected { t.Fatalf("Expected %s, got %s", expected, str) @@ -940,10 +1075,10 @@ func StringTest(t *testing.T, factory Factory, prefix string) { { expected := prefix + "(\n" + - " Choice[0] = ID: LUC1cmcxnfNR9LdkACS2ccGKLEK7SYqB4gLLTycQfg1koyfSq Confidence: 0 Bias: 1\n" + - " Choice[1] = ID: TtF4d2QWbk5vzQGTEPrN48x6vwgAoAmKQ9cbp79inpQmcRKES Confidence: 1 Bias: 1\n" + - " Choice[2] = ID: Zda4gsqTjRaX6XVZekVNi3ovMFPHDRQiGbzYuAb7Nwqy1rGBc Confidence: 1 Bias: 1\n" + - " Choice[3] = ID: 2mcwQKiD8VEspmMJpL1dc7okQQ5dDVAWeCBZ7FWBFAbxpv3t7w Confidence: 0 Bias: 1\n" + + " Choice[0] = ID: LUC1cmcxnfNR9LdkACS2ccGKLEK7SYqB4gLLTycQfg1koyfSq SB(NumSuccessfulPolls = 1, Confidence = 0)\n" + + " Choice[1] = ID: TtF4d2QWbk5vzQGTEPrN48x6vwgAoAmKQ9cbp79inpQmcRKES SB(NumSuccessfulPolls = 1, Confidence = 1)\n" + + " Choice[2] = ID: Zda4gsqTjRaX6XVZekVNi3ovMFPHDRQiGbzYuAb7Nwqy1rGBc SB(NumSuccessfulPolls = 1, Confidence = 1)\n" + + " Choice[3] = ID: 2mcwQKiD8VEspmMJpL1dc7okQQ5dDVAWeCBZ7FWBFAbxpv3t7w SB(NumSuccessfulPolls = 1, Confidence = 0)\n" + ")" if str := graph.String(); str != expected { t.Fatalf("Expected %s, got %s", expected, str) @@ -967,10 +1102,10 @@ func StringTest(t *testing.T, factory Factory, prefix string) { { expected := prefix + "(\n" + - " Choice[0] = ID: LUC1cmcxnfNR9LdkACS2ccGKLEK7SYqB4gLLTycQfg1koyfSq Confidence: 0 Bias: 1\n" + - " Choice[1] = ID: TtF4d2QWbk5vzQGTEPrN48x6vwgAoAmKQ9cbp79inpQmcRKES Confidence: 0 Bias: 1\n" + - " Choice[2] = ID: Zda4gsqTjRaX6XVZekVNi3ovMFPHDRQiGbzYuAb7Nwqy1rGBc Confidence: 0 Bias: 1\n" + - " Choice[3] = ID: 2mcwQKiD8VEspmMJpL1dc7okQQ5dDVAWeCBZ7FWBFAbxpv3t7w Confidence: 0 Bias: 1\n" + + " Choice[0] = ID: LUC1cmcxnfNR9LdkACS2ccGKLEK7SYqB4gLLTycQfg1koyfSq SB(NumSuccessfulPolls = 1, Confidence = 0)\n" + + " Choice[1] = ID: TtF4d2QWbk5vzQGTEPrN48x6vwgAoAmKQ9cbp79inpQmcRKES SB(NumSuccessfulPolls = 1, Confidence = 0)\n" + + " Choice[2] = ID: Zda4gsqTjRaX6XVZekVNi3ovMFPHDRQiGbzYuAb7Nwqy1rGBc SB(NumSuccessfulPolls = 1, Confidence = 0)\n" + + " Choice[3] = ID: 2mcwQKiD8VEspmMJpL1dc7okQQ5dDVAWeCBZ7FWBFAbxpv3t7w SB(NumSuccessfulPolls = 1, Confidence = 0)\n" + ")" if str := graph.String(); str != expected { t.Fatalf("Expected %s, got %s", expected, str) @@ -991,10 +1126,10 @@ func StringTest(t *testing.T, factory Factory, prefix string) { { expected := prefix + "(\n" + - " Choice[0] = ID: LUC1cmcxnfNR9LdkACS2ccGKLEK7SYqB4gLLTycQfg1koyfSq Confidence: 0 Bias: 1\n" + - " Choice[1] = ID: TtF4d2QWbk5vzQGTEPrN48x6vwgAoAmKQ9cbp79inpQmcRKES Confidence: 1 Bias: 2\n" + - " Choice[2] = ID: Zda4gsqTjRaX6XVZekVNi3ovMFPHDRQiGbzYuAb7Nwqy1rGBc Confidence: 1 Bias: 2\n" + - " Choice[3] = ID: 2mcwQKiD8VEspmMJpL1dc7okQQ5dDVAWeCBZ7FWBFAbxpv3t7w Confidence: 0 Bias: 1\n" + + " Choice[0] = ID: LUC1cmcxnfNR9LdkACS2ccGKLEK7SYqB4gLLTycQfg1koyfSq SB(NumSuccessfulPolls = 1, Confidence = 0)\n" + + " Choice[1] = ID: TtF4d2QWbk5vzQGTEPrN48x6vwgAoAmKQ9cbp79inpQmcRKES SB(NumSuccessfulPolls = 2, Confidence = 1)\n" + + " Choice[2] = ID: Zda4gsqTjRaX6XVZekVNi3ovMFPHDRQiGbzYuAb7Nwqy1rGBc SB(NumSuccessfulPolls = 2, Confidence = 1)\n" + + " Choice[3] = ID: 2mcwQKiD8VEspmMJpL1dc7okQQ5dDVAWeCBZ7FWBFAbxpv3t7w SB(NumSuccessfulPolls = 1, Confidence = 0)\n" + ")" if str := graph.String(); str != expected { t.Fatalf("Expected %s, got %s", expected, str) diff --git a/snow/consensus/snowstorm/directed.go b/snow/consensus/snowstorm/directed.go index e33640f71462..95b5dc136258 100644 --- a/snow/consensus/snowstorm/directed.go +++ b/snow/consensus/snowstorm/directed.go @@ -11,8 +11,10 @@ import ( "github.com/ava-labs/gecko/ids" "github.com/ava-labs/gecko/snow" - "github.com/ava-labs/gecko/snow/consensus/snowball" + "github.com/ava-labs/gecko/snow/choices" "github.com/ava-labs/gecko/utils/formatting" + + sbcon "github.com/ava-labs/gecko/snow/consensus/snowball" ) // DirectedFactory implements Factory by returning a directed struct @@ -36,184 +38,263 @@ type Directed struct { } type directedTx struct { - bias, confidence, lastVote int - rogue bool + snowball + + // pendingAccept identifies if this transaction has been marked as accepted + // once its transitive dependencies have also been accepted + pendingAccept bool + + // ins is the set of txIDs that this tx conflicts with that are less + // preferred than this tx + ins ids.Set - pendingAccept, accepted bool - ins, outs ids.Set + // outs is the set of txIDs that this tx conflicts with that are more + // preferred than this tx + outs ids.Set + // tx is the actual transaction this node represents tx Tx } // Initialize implements the Consensus interface -func (dg *Directed) Initialize(ctx *snow.Context, params snowball.Parameters) { - dg.common.Initialize(ctx, params) - - dg.utxos = make(map[[32]byte]ids.Set) +func (dg *Directed) Initialize( + ctx *snow.Context, + params sbcon.Parameters, +) error { dg.txs = make(map[[32]byte]*directedTx) + dg.utxos = make(map[[32]byte]ids.Set) + + return dg.common.Initialize(ctx, params) } // IsVirtuous implements the Consensus interface func (dg *Directed) IsVirtuous(tx Tx) bool { - id := tx.ID() - if node, exists := dg.txs[id.Key()]; exists { + txID := tx.ID() + // If the tx is currently processing, we should just return if was + // registered as rogue or not. + if node, exists := dg.txs[txID.Key()]; exists { return !node.rogue } + + // The tx isn't processing, so we need to check to see if it conflicts with + // any of the other txs that are currently processing. for _, input := range tx.InputIDs().List() { if _, exists := dg.utxos[input.Key()]; exists { + // A currently processing tx names the same input as the provided + // tx, so the provided tx would be rogue. return false } } + + // This tx is virtuous as far as this consensus instance knows. return true } // Conflicts implements the Consensus interface func (dg *Directed) Conflicts(tx Tx) ids.Set { - id := tx.ID() conflicts := ids.Set{} - - if node, exists := dg.txs[id.Key()]; exists { + if node, exists := dg.txs[tx.ID().Key()]; exists { + // If the tx is currently processing, the conflicting txs are just the + // union of the inbound conflicts and the outbound conflicts. conflicts.Union(node.ins) conflicts.Union(node.outs) } else { + // If the tx isn't currently processing, the conflicting txs are the + // union of all the txs that spend an input that this tx spends. for _, input := range tx.InputIDs().List() { if spends, exists := dg.utxos[input.Key()]; exists { conflicts.Union(spends) } } - conflicts.Remove(id) } - return conflicts } // Add implements the Consensus interface func (dg *Directed) Add(tx Tx) error { if dg.Issued(tx) { - return nil // Already inserted + // If the tx was previously inserted, it shouldn't be re-inserted. + return nil } txID := tx.ID() bytes := tx.Bytes() + // Notify the IPC socket that this tx has been issued. dg.ctx.DecisionDispatcher.Issue(dg.ctx.ChainID, txID, bytes) + + // Notify the metrics that this transaction was just issued. + dg.metrics.Issued(txID) + inputs := tx.InputIDs() - // If there are no inputs, Tx is vacuously accepted + + // If this tx doesn't have any inputs, it's impossible for there to be any + // conflicting transactions. Therefore, this transaction is treated as + // vacuously accepted. if inputs.Len() == 0 { + // Accept is called before notifying the IPC so that acceptances that + // cause fatal errors aren't sent to an IPC peer. if err := tx.Accept(); err != nil { return err } + + // Notify the IPC socket that this tx has been accepted. dg.ctx.DecisionDispatcher.Accept(dg.ctx.ChainID, txID, bytes) - dg.metrics.Issued(txID) + + // Notify the metrics that this transaction was just accepted. dg.metrics.Accepted(txID) return nil } txNode := &directedTx{tx: tx} - // For each UTXO input to Tx: - // * Get all transactions that consume that UTXO - // * Add edges from Tx to those transactions in the conflict graph - // * Mark those transactions as rogue + // For each UTXO consumed by the tx: + // * Add edges between this tx and txs that consume this UTXO + // * Mark this tx as attempting to consume this UTXO for _, inputID := range inputs.List() { inputKey := inputID.Key() - spends := dg.utxos[inputKey] // Transactions spending this UTXO - // Add edges to conflict graph - txNode.outs.Union(spends) + // Get the set of txs that are currently processing that also consume + // this UTXO + spenders := dg.utxos[inputKey] + + // Add all the txs that spend this UTXO to this txs conflicts. These + // conflicting txs must be preferred over this tx. We know this because + // this tx currently has a bias of 0 and the tie goes to the tx whose + // bias was updated first. + txNode.outs.Union(spenders) - // Mark transactions conflicting with Tx as rogue - for _, conflictID := range spends.List() { + // Update txs conflicting with tx to account for its issuance + for _, conflictID := range spenders.List() { conflictKey := conflictID.Key() + + // Get the node that contains this conflicting tx conflict := dg.txs[conflictKey] + // This conflicting tx can't be virtuous anymore. So, we attempt to + // remove it from all of the virtuous sets. dg.virtuous.Remove(conflictID) dg.virtuousVoting.Remove(conflictID) + // This tx should be set to rogue if it wasn't rogue before. conflict.rogue = true - conflict.ins.Add(txID) - dg.txs[conflictKey] = conflict + // This conflicting tx is preferred over the tx being inserted, as + // described above. So we add the conflict to the inbound set. + conflict.ins.Add(txID) } - // Add Tx to list of transactions consuming UTXO whose ID is id - spends.Add(txID) - dg.utxos[inputKey] = spends + + // Add this tx to list of txs consuming the current UTXO + spenders.Add(txID) + + // Because this isn't a pointer, we should re-map the set. + dg.utxos[inputKey] = spenders } - txNode.rogue = txNode.outs.Len() != 0 // Mark this transaction as rogue if it has conflicts - // Add the node representing Tx to the node set - dg.txs[txID.Key()] = txNode + // Mark this transaction as rogue if had any conflicts registered above + txNode.rogue = txNode.outs.Len() != 0 + if !txNode.rogue { - // I'm not rogue + // If this tx is currently virtuous, add it to the virtuous sets dg.virtuous.Add(txID) dg.virtuousVoting.Add(txID) - // If I'm not rogue, I must be preferred + // If a tx is virtuous, it must be preferred. dg.preferences.Add(txID) } - dg.metrics.Issued(txID) - // Tx can be accepted only if the transactions it depends on are also accepted - // If any transactions that Tx depends on are rejected, reject Tx - toReject := &directedRejector{ - dg: dg, - txNode: txNode, + // Add this tx to the set of currently processing txs + dg.txs[txID.Key()] = txNode + + // If a tx that this tx depends on is rejected, this tx should also be + // rejected. + toReject := &rejector{ + g: dg, + errs: &dg.errs, + txID: txID, } + + // Register all of this txs dependencies as possibilities to reject this tx. for _, dependency := range tx.Dependencies() { - if !dependency.Status().Decided() { + if dependency.Status() != choices.Accepted { + // If the dependency isn't accepted, then it must be processing. So, + // this tx should be rejected if any of these processing txs are + // rejected. Note that the dependencies can't already be rejected, + // because it is assumped that this tx is currently considered + // valid. toReject.deps.Add(dependency.ID()) } } + + // Register these dependencies dg.pendingReject.Register(toReject) - return dg.errs.Err + + // Registering the rejector can't result in an error, so we can safely + // return nil here. + return nil } // Issued implements the Consensus interface func (dg *Directed) Issued(tx Tx) bool { + // If the tx is either Accepted or Rejected, then it must have been issued + // previously. if tx.Status().Decided() { return true } + + // If the tx is currently processing, then it must have been issued. _, ok := dg.txs[tx.ID().Key()] return ok } // RecordPoll implements the Consensus interface func (dg *Directed) RecordPoll(votes ids.Bag) (bool, error) { + // Increase the vote ID. This is only updated here and is used to reset the + // confidence values of transactions lazily. dg.currentVote++ + + // This flag tracks if the Avalanche instance needs to recompute its + // frontiers. Frontiers only need to be recalculated if preferences change + // or if a tx was accepted. changed := false + // We only want to iterate over txs that received alpha votes votes.SetThreshold(dg.params.Alpha) - threshold := votes.Threshold() // Each element is ID of transaction preferred by >= Alpha poll respondents - for _, toInc := range threshold.List() { - incKey := toInc.Key() - txNode, exist := dg.txs[incKey] + // Get the set of IDs that meet this alpha threshold + metThreshold := votes.Threshold() + for _, txID := range metThreshold.List() { + txKey := txID.Key() + + // Get the node this tx represents + txNode, exist := dg.txs[txKey] if !exist { - // Votes for decided consumers are ignored + // This tx may have already been accepted because of tx + // dependencies. If this is the case, we can just drop the vote. continue } - if txNode.lastVote+1 != dg.currentVote { - txNode.confidence = 0 - } - txNode.lastVote = dg.currentVote + txNode.RecordSuccessfulPoll(dg.currentVote) - dg.ctx.Log.Verbo("Increasing (bias, confidence) of %s from (%d, %d) to (%d, %d)", - toInc, txNode.bias, txNode.confidence, txNode.bias+1, txNode.confidence+1) - - txNode.bias++ - txNode.confidence++ + dg.ctx.Log.Verbo("Updated TxID=%s to have consensus state=%s", + txID, &txNode.snowball) + // If the tx should be accepted, then we should defer its acceptance + // until its dependencies are decided. If this tx was already marked to + // be accepted, we shouldn't register it again. if !txNode.pendingAccept && - ((!txNode.rogue && txNode.confidence >= dg.params.BetaVirtuous) || - txNode.confidence >= dg.params.BetaRogue) { + txNode.Finalized(dg.params.BetaVirtuous, dg.params.BetaRogue) { dg.deferAcceptance(txNode) if dg.errs.Errored() { return changed, dg.errs.Err } } - if !txNode.accepted { + + if txNode.tx.Status() != choices.Accepted { + // If this tx wasn't accepted, then this instance is only changed if + // preferences changed. changed = dg.redirectEdges(txNode) || changed } else { + // By accepting a tx, the state of this instance has changed. changed = true } } @@ -225,35 +306,33 @@ func (dg *Directed) String() string { for _, tx := range dg.txs { nodes = append(nodes, tx) } + // Sort the nodes so that the string representation is canonical sortTxNodes(nodes) sb := strings.Builder{} - sb.WriteString("DG(") format := fmt.Sprintf( - "\n Choice[%s] = ID: %%50s Confidence: %s Bias: %%d", - formatting.IntFormat(len(dg.txs)-1), - formatting.IntFormat(dg.params.BetaRogue-1)) - + "\n Choice[%s] = ID: %%50s %%s", + formatting.IntFormat(len(dg.txs)-1)) for i, txNode := range nodes { - confidence := txNode.confidence - if txNode.lastVote != dg.currentVote { - confidence = 0 - } sb.WriteString(fmt.Sprintf(format, - i, txNode.tx.ID(), confidence, txNode.bias)) + i, txNode.tx.ID(), txNode.snowball.CurrentString(dg.currentVote))) } if len(nodes) > 0 { sb.WriteString("\n") } sb.WriteString(")") - return sb.String() } +// deferAcceptance attempts to mark this tx once all its dependencies are +// accepted. If all the dependencies are already accepted, this function will +// immediately accept the tx. func (dg *Directed) deferAcceptance(txNode *directedTx) { + // Mark that this tx is pending acceptance so this function won't be called + // again txNode.pendingAccept = true toAccept := &directedAccepter{ @@ -261,40 +340,84 @@ func (dg *Directed) deferAcceptance(txNode *directedTx) { txNode: txNode, } for _, dependency := range txNode.tx.Dependencies() { - if !dependency.Status().Decided() { + if dependency.Status() != choices.Accepted { + // If the dependency isn't accepted, then it must be processing. + // This tx should be accepted after this tx is accepted. toAccept.deps.Add(dependency.ID()) } } + // This tx is no longer being voted on, so we remove it from the voting set. + // This ensures that virtuous txs built on top of rogue txs don't force the + // node to treat the rogue tx as virtuous. dg.virtuousVoting.Remove(txNode.tx.ID()) dg.pendingAccept.Register(toAccept) } -func (dg *Directed) reject(ids ...ids.ID) error { - for _, conflict := range ids { - conflictKey := conflict.Key() - conf := dg.txs[conflictKey] +// reject all the named txIDs and remove them from the graph +func (dg *Directed) reject(conflictIDs ...ids.ID) error { + for _, conflictID := range conflictIDs { + conflictKey := conflictID.Key() + conflict := dg.txs[conflictKey] + + // This tx is no longer an option for consuming the UTXOs from its + // inputs, so we should remove their reference to this tx. + for _, inputID := range conflict.tx.InputIDs().List() { + inputKey := inputID.Key() + txIDs, exists := dg.utxos[inputKey] + if !exists { + // This UTXO may no longer exist because it was removed due to + // the acceptance of a tx. If that is the case, there is nothing + // left to remove from memory. + continue + } + txIDs.Remove(conflictID) + if txIDs.Len() == 0 { + // If this tx was the last tx consuming this UTXO, we should + // prune the UTXO from memory entirely. + delete(dg.utxos, inputKey) + } else { + // If this UTXO still has txs consuming it, then we should make + // sure this update is written back to the UTXOs map. + dg.utxos[inputKey] = txIDs + } + } + + // We are rejecting the tx, so we should remove it from the graph delete(dg.txs, conflictKey) - dg.preferences.Remove(conflict) + // While it's statistically unlikely that something being rejected is + // preferred, it is handled for completion. + dg.preferences.Remove(conflictID) // remove the edge between this node and all its neighbors - dg.removeConflict(conflict, conf.ins.List()...) - dg.removeConflict(conflict, conf.outs.List()...) + dg.removeConflict(conflictID, conflict.ins.List()...) + dg.removeConflict(conflictID, conflict.outs.List()...) - // Mark it as rejected - if err := conf.tx.Reject(); err != nil { + // Reject is called before notifying the IPC so that rejections that + // cause fatal errors aren't sent to an IPC peer. + if err := conflict.tx.Reject(); err != nil { return err } - dg.ctx.DecisionDispatcher.Reject(dg.ctx.ChainID, conf.tx.ID(), conf.tx.Bytes()) - dg.metrics.Rejected(conflict) - dg.pendingAccept.Abandon(conflict) - dg.pendingReject.Fulfill(conflict) + // Notify the IPC that the tx was rejected + dg.ctx.DecisionDispatcher.Reject(dg.ctx.ChainID, conflict.tx.ID(), conflict.tx.Bytes()) + + // Update the metrics to account for this transaction's rejection + dg.metrics.Rejected(conflictID) + + // If there is a tx that was accepted pending on this tx, the ancestor + // tx can't be accepted. + dg.pendingAccept.Abandon(conflictID) + // If there is a tx that was issued pending on this tx, the ancestor tx + // must be rejected. + dg.pendingReject.Fulfill(conflictID) } return nil } +// redirectEdges attempts to turn outbound edges into inbound edges if the +// preferences have changed func (dg *Directed) redirectEdges(tx *directedTx) bool { changed := false for _, conflictID := range tx.outs.List() { @@ -303,47 +426,51 @@ func (dg *Directed) redirectEdges(tx *directedTx) bool { return changed } -// Set the confidence of all conflicts to 0 -// Change the direction of edges if needed +// Change the direction of this edge if needed. Returns true if the direction +// was switched. func (dg *Directed) redirectEdge(txNode *directedTx, conflictID ids.ID) bool { - nodeID := txNode.tx.ID() conflict := dg.txs[conflictID.Key()] - if txNode.bias <= conflict.bias { + if txNode.numSuccessfulPolls <= conflict.numSuccessfulPolls { return false } - // TODO: why is this confidence reset here? It should already be reset - // implicitly by the lack of a timestamp increase. - conflict.confidence = 0 + // Because this tx has a higher preference than the conflicting tx, we must + // ensure that the edge is directed towards this tx. + nodeID := txNode.tx.ID() - // Change the edge direction + // Change the edge direction according to the conflict tx conflict.ins.Remove(nodeID) conflict.outs.Add(nodeID) - dg.preferences.Remove(conflictID) // This consumer now has an out edge + dg.preferences.Remove(conflictID) // This conflict has an outbound edge + // Change the edge direction according to this tx txNode.ins.Add(conflictID) txNode.outs.Remove(conflictID) if txNode.outs.Len() == 0 { - // If I don't have out edges, I'm preferred + // If this tx doesn't have any outbound edges, it's preferred dg.preferences.Add(nodeID) } return true } -func (dg *Directed) removeConflict(id ids.ID, ids ...ids.ID) { - for _, neighborID := range ids { +func (dg *Directed) removeConflict(txID ids.ID, neighborIDs ...ids.ID) { + for _, neighborID := range neighborIDs { neighborKey := neighborID.Key() - // If the neighbor doesn't exist, they may have already been rejected - if neighbor, exists := dg.txs[neighborKey]; exists { - neighbor.ins.Remove(id) - neighbor.outs.Remove(id) - - if neighbor.outs.Len() == 0 { - // Make sure to mark the neighbor as preferred if needed - dg.preferences.Add(neighborID) - } + neighbor, exists := dg.txs[neighborKey] + if !exists { + // If the neighbor doesn't exist, they may have already been + // rejected, so this mapping can be skipped. + continue + } - dg.txs[neighborKey] = neighbor + // Remove any edge to this tx. + neighbor.ins.Remove(txID) + neighbor.outs.Remove(txID) + + if neighbor.outs.Len() == 0 { + // If this tx should now be preferred, make sure its status is + // updated. + dg.preferences.Add(neighborID) } } } @@ -365,67 +492,61 @@ func (a *directedAccepter) Fulfill(id ids.ID) { func (a *directedAccepter) Abandon(id ids.ID) { a.rejected = true } func (a *directedAccepter) Update() { - // If I was rejected or I am still waiting on dependencies to finish do - // nothing. + // If I was rejected or I am still waiting on dependencies to finish or an + // error has occurred, I shouldn't do anything. if a.rejected || a.deps.Len() != 0 || a.dg.errs.Errored() { return } - id := a.txNode.tx.ID() - delete(a.dg.txs, id.Key()) + txID := a.txNode.tx.ID() + // We are accepting the tx, so we should remove the node from the graph. + delete(a.dg.txs, txID.Key()) + // This tx is consuming all the UTXOs from its inputs, so we can prune them + // all from memory for _, inputID := range a.txNode.tx.InputIDs().List() { delete(a.dg.utxos, inputID.Key()) } - a.dg.virtuous.Remove(id) - a.dg.preferences.Remove(id) - // Reject the conflicts + // This tx is now accepted, so it shouldn't be part of the virtuous set or + // the preferred set. Its status as Accepted implies these descriptions. + a.dg.virtuous.Remove(txID) + a.dg.preferences.Remove(txID) + + // Reject all the txs that conflicted with this tx. if err := a.dg.reject(a.txNode.ins.List()...); err != nil { a.dg.errs.Add(err) return } - // Should normally be empty + // While it is typically true that a tx this is being accepted is preferred, + // it is possible for this to not be the case. So this is handled for + // completeness. if err := a.dg.reject(a.txNode.outs.List()...); err != nil { a.dg.errs.Add(err) return } - // Mark it as accepted + // Accept is called before notifying the IPC so that acceptances that cause + // fatal errors aren't sent to an IPC peer. if err := a.txNode.tx.Accept(); err != nil { a.dg.errs.Add(err) return } - a.txNode.accepted = true - a.dg.ctx.DecisionDispatcher.Accept(a.dg.ctx.ChainID, id, a.txNode.tx.Bytes()) - a.dg.metrics.Accepted(id) - a.dg.pendingAccept.Fulfill(id) - a.dg.pendingReject.Abandon(id) -} + // Notify the IPC socket that this tx has been accepted. + a.dg.ctx.DecisionDispatcher.Accept(a.dg.ctx.ChainID, txID, a.txNode.tx.Bytes()) -// directedRejector implements Blockable -type directedRejector struct { - dg *Directed - deps ids.Set - rejected bool // true if the transaction has been rejected - txNode *directedTx -} - -func (r *directedRejector) Dependencies() ids.Set { return r.deps } + // Update the metrics to account for this transaction's acceptance + a.dg.metrics.Accepted(txID) -func (r *directedRejector) Fulfill(id ids.ID) { - if r.rejected || r.dg.errs.Errored() { - return - } - r.rejected = true - r.dg.errs.Add(r.dg.reject(r.txNode.tx.ID())) + // If there is a tx that was accepted pending on this tx, the ancestor + // should be notified that it doesn't need to block on this tx anymore. + a.dg.pendingAccept.Fulfill(txID) + // If there is a tx that was issued pending on this tx, the ancestor tx + // doesn't need to be rejected because of this tx. + a.dg.pendingReject.Abandon(txID) } -func (*directedRejector) Abandon(id ids.ID) {} - -func (*directedRejector) Update() {} - type sortTxNodeData []*directedTx func (tnd sortTxNodeData) Less(i, j int) bool { diff --git a/snow/consensus/snowstorm/equality_test.go b/snow/consensus/snowstorm/equality_test.go index 8298c48329e1..383504bbdc78 100644 --- a/snow/consensus/snowstorm/equality_test.go +++ b/snow/consensus/snowstorm/equality_test.go @@ -9,7 +9,7 @@ import ( "github.com/prometheus/client_golang/prometheus" - "github.com/ava-labs/gecko/snow/consensus/snowball" + sbcon "github.com/ava-labs/gecko/snow/consensus/snowball" ) func TestConflictGraphEquality(t *testing.T) { @@ -19,7 +19,7 @@ func TestConflictGraphEquality(t *testing.T) { colorsPerConsumer := 2 maxInputConflicts := 2 numNodes := 100 - params := snowball.Parameters{ + params := sbcon.Parameters{ Metrics: prometheus.NewRegistry(), K: 20, Alpha: 11, diff --git a/snow/consensus/snowstorm/input.go b/snow/consensus/snowstorm/input.go index 0c191618a19f..6f3251c3afbf 100644 --- a/snow/consensus/snowstorm/input.go +++ b/snow/consensus/snowstorm/input.go @@ -11,8 +11,9 @@ import ( "github.com/ava-labs/gecko/ids" "github.com/ava-labs/gecko/snow" - "github.com/ava-labs/gecko/snow/consensus/snowball" "github.com/ava-labs/gecko/utils/formatting" + + sbcon "github.com/ava-labs/gecko/snow/consensus/snowball" ) // InputFactory implements Factory by returning an input struct @@ -53,11 +54,11 @@ type inputUtxo struct { } // Initialize implements the ConflictGraph interface -func (ig *Input) Initialize(ctx *snow.Context, params snowball.Parameters) { - ig.common.Initialize(ctx, params) - +func (ig *Input) Initialize(ctx *snow.Context, params sbcon.Parameters) error { ig.txs = make(map[[32]byte]inputTx) ig.utxos = make(map[[32]byte]inputUtxo) + + return ig.common.Initialize(ctx, params) } // IsVirtuous implements the ConflictGraph interface @@ -128,9 +129,10 @@ func (ig *Input) Add(tx Tx) error { } ig.metrics.Issued(txID) - toReject := &inputRejector{ - ig: ig, - tn: cn, + toReject := &rejector{ + g: ig, + errs: &ig.errs, + txID: txID, } for _, dependency := range tx.Dependencies() { @@ -379,14 +381,14 @@ func (ig *Input) String() string { confidence = 0 break } - - if input.confidence < confidence { - confidence = input.confidence - } if !id.Equals(input.color) { confidence = 0 break } + + if input.confidence < confidence { + confidence = input.confidence + } } nodes = append(nodes, tempNode{ @@ -402,12 +404,11 @@ func (ig *Input) String() string { sb.WriteString("IG(") format := fmt.Sprintf( - "\n Choice[%s] = ID: %%50s Confidence: %s Bias: %%d", - formatting.IntFormat(len(nodes)-1), - formatting.IntFormat(ig.params.BetaRogue-1)) + "\n Choice[%s] = ID: %%50s %%s", + formatting.IntFormat(len(nodes)-1)) for i, cn := range nodes { - sb.WriteString(fmt.Sprintf(format, i, cn.id, cn.confidence, cn.bias)) + sb.WriteString(fmt.Sprintf(format, i, cn.id, &cn)) } if len(nodes) > 0 { @@ -474,33 +475,18 @@ func (a *inputAccepter) Update() { a.ig.pendingReject.Abandon(id) } -// inputRejector implements Blockable -type inputRejector struct { - ig *Input - deps ids.Set - rejected bool // true if the transaction represented by fn has been rejected - tn inputTx -} - -func (r *inputRejector) Dependencies() ids.Set { return r.deps } - -func (r *inputRejector) Fulfill(id ids.ID) { - if r.rejected || r.ig.errs.Errored() { - return - } - r.rejected = true - r.ig.errs.Add(r.ig.reject(r.tn.tx.ID())) -} - -func (*inputRejector) Abandon(id ids.ID) {} - -func (*inputRejector) Update() {} - type tempNode struct { id ids.ID bias, confidence int } +func (tn *tempNode) String() string { + return fmt.Sprintf( + "SB(NumSuccessfulPolls = %d, Confidence = %d)", + tn.bias, + tn.confidence) +} + type sortTempNodeData []tempNode func (tnd sortTempNodeData) Less(i, j int) bool { diff --git a/snow/consensus/snowstorm/metrics.go b/snow/consensus/snowstorm/metrics.go index 22add0082443..36a5332c43f3 100644 --- a/snow/consensus/snowstorm/metrics.go +++ b/snow/consensus/snowstorm/metrics.go @@ -4,20 +4,34 @@ package snowstorm import ( - "fmt" "time" "github.com/prometheus/client_golang/prometheus" "github.com/ava-labs/gecko/ids" "github.com/ava-labs/gecko/utils/timer" + "github.com/ava-labs/gecko/utils/wrappers" ) type metrics struct { - numProcessing prometheus.Gauge - latAccepted, latRejected prometheus.Histogram + // numProcessing keeps track of the number of transactions currently + // processing in a snowstorm instance + numProcessing prometheus.Gauge - clock timer.Clock + // accepted tracks the number of milliseconds that a transaction was + // processing before being accepted + accepted prometheus.Histogram + + // rejected tracks the number of milliseconds that a transaction was + // processing before being rejected + rejected prometheus.Histogram + + // clock gives access to the current wall clock time + clock timer.Clock + + // processing keeps track of the time that each transaction was issued into + // the snowstorm instance. This is used to calculate the amount of time to + // accept or reject the transaction processing map[[32]byte]time.Time } @@ -25,44 +39,43 @@ type metrics struct { func (m *metrics) Initialize(namespace string, registerer prometheus.Registerer) error { m.processing = make(map[[32]byte]time.Time) - m.numProcessing = prometheus.NewGauge( - prometheus.GaugeOpts{ - Namespace: namespace, - Name: "tx_processing", - Help: "Number of processing transactions", - }) - m.latAccepted = prometheus.NewHistogram( - prometheus.HistogramOpts{ - Namespace: namespace, - Name: "tx_accepted", - Help: "Latency of accepting from the time the transaction was issued in milliseconds", - Buckets: timer.MillisecondsBuckets, - }) - m.latRejected = prometheus.NewHistogram( - prometheus.HistogramOpts{ - Namespace: namespace, - Name: "tx_rejected", - Help: "Latency of rejecting from the time the transaction was issued in milliseconds", - Buckets: timer.MillisecondsBuckets, - }) - - if err := registerer.Register(m.numProcessing); err != nil { - return fmt.Errorf("Failed to register tx_processing statistics due to %s", err) - } - if err := registerer.Register(m.latAccepted); err != nil { - return fmt.Errorf("Failed to register tx_accepted statistics due to %s", err) - } - if err := registerer.Register(m.latRejected); err != nil { - return fmt.Errorf("Failed to register tx_rejected statistics due to %s", err) - } - return nil + m.numProcessing = prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "tx_processing", + Help: "Number of processing transactions", + }) + m.accepted = prometheus.NewHistogram(prometheus.HistogramOpts{ + Namespace: namespace, + Name: "tx_accepted", + Help: "Time spent processing before being accepted in milliseconds", + Buckets: timer.MillisecondsBuckets, + }) + m.rejected = prometheus.NewHistogram(prometheus.HistogramOpts{ + Namespace: namespace, + Name: "tx_rejected", + Help: "Time spent processing before being rejected in milliseconds", + Buckets: timer.MillisecondsBuckets, + }) + + errs := wrappers.Errs{} + errs.Add( + registerer.Register(m.numProcessing), + registerer.Register(m.accepted), + registerer.Register(m.rejected), + ) + return errs.Err } +// Issued marks that a transaction with the provided ID was added to the +// snowstorm consensus instance. It is assumed that either Accept or Reject will +// be called with this same ID in the future. func (m *metrics) Issued(id ids.ID) { m.processing[id.Key()] = m.clock.Time() m.numProcessing.Inc() } +// Accepted marks that a transaction with the provided ID was accepted. It is +// assumed that Issued was previously called with this ID. func (m *metrics) Accepted(id ids.ID) { key := id.Key() start := m.processing[key] @@ -70,10 +83,12 @@ func (m *metrics) Accepted(id ids.ID) { delete(m.processing, key) - m.latAccepted.Observe(float64(end.Sub(start).Milliseconds())) + m.accepted.Observe(float64(end.Sub(start).Milliseconds())) m.numProcessing.Dec() } +// Rejected marks that a transaction with the provided ID was rejected. It is +// assumed that Issued was previously called with this ID. func (m *metrics) Rejected(id ids.ID) { key := id.Key() start := m.processing[key] @@ -81,6 +96,6 @@ func (m *metrics) Rejected(id ids.ID) { delete(m.processing, key) - m.latRejected.Observe(float64(end.Sub(start).Milliseconds())) + m.rejected.Observe(float64(end.Sub(start).Milliseconds())) m.numProcessing.Dec() } diff --git a/snow/consensus/snowstorm/network_test.go b/snow/consensus/snowstorm/network_test.go index f60cbe34f9e4..f1e3d77ab38a 100644 --- a/snow/consensus/snowstorm/network_test.go +++ b/snow/consensus/snowstorm/network_test.go @@ -9,12 +9,13 @@ import ( "github.com/ava-labs/gecko/ids" "github.com/ava-labs/gecko/snow" "github.com/ava-labs/gecko/snow/choices" - "github.com/ava-labs/gecko/snow/consensus/snowball" "github.com/ava-labs/gecko/utils/sampler" + + sbcon "github.com/ava-labs/gecko/snow/consensus/snowball" ) type Network struct { - params snowball.Parameters + params sbcon.Parameters consumers []*TestTx nodeTxs []map[[32]byte]*TestTx nodes, running []Consensus @@ -31,7 +32,7 @@ func (n *Network) shuffleConsumers() { n.consumers = consumers } -func (n *Network) Initialize(params snowball.Parameters, numColors, colorsPerConsumer, maxInputConflicts int) { +func (n *Network) Initialize(params sbcon.Parameters, numColors, colorsPerConsumer, maxInputConflicts int) { n.params = params idCount := uint64(0) diff --git a/snow/consensus/snowstorm/snowball.go b/snow/consensus/snowstorm/snowball.go new file mode 100644 index 000000000000..8037ed5842a9 --- /dev/null +++ b/snow/consensus/snowstorm/snowball.go @@ -0,0 +1,67 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package snowstorm + +import ( + "fmt" +) + +type snowball struct { + // numSuccessfulPolls is the number of times this choice was the successful + // result of a network poll + numSuccessfulPolls int + + // confidence is the number of consecutive times this choice was the + // successful result of a network poll as of [lastVote] + confidence int + + // lastVote is the last poll number that this choice was included in a + // successful network poll + lastVote int + + // rogue identifies if there is a known conflict with this choice + rogue bool +} + +func (sb *snowball) RecordSuccessfulPoll(currentVote int) { + // If this choice wasn't voted for during the last poll, the confidence + // should have been reset during the last poll. So, we reset it now. + if sb.lastVote+1 != currentVote { + sb.confidence = 0 + } + + // This choice was voted for in this poll. Mark it as such. + sb.lastVote = currentVote + + // An affirmative vote increases both the snowball and snowflake counters. + sb.numSuccessfulPolls++ + sb.confidence++ +} + +func (sb *snowball) Finalized(betaVirtuous, betaRogue int) bool { + // This choice is finalized if the snowflake counter is at least + // [betaRogue]. If there are no known conflicts with this operation, it can + // be accepted with a snowflake counter of at least [betaVirtuous]. + return (!sb.rogue && sb.confidence >= betaVirtuous) || + sb.confidence >= betaRogue +} + +func (sb *snowball) CurrentString(currentVote int) string { + confidence := sb.confidence + if sb.lastVote != currentVote { + confidence = 0 + } + return fmt.Sprintf( + "SB(NumSuccessfulPolls = %d, Confidence = %d)", + sb.numSuccessfulPolls, + confidence) +} + +func (sb *snowball) String() string { + return fmt.Sprintf( + "SB(NumSuccessfulPolls = %d, Confidence = %d, As of %d)", + sb.numSuccessfulPolls, + sb.confidence, + sb.lastVote) +} diff --git a/snow/consensus/snowstorm/test_tx_test.go b/snow/consensus/snowstorm/test_tx_test.go deleted file mode 100644 index 7f34a97c3db8..000000000000 --- a/snow/consensus/snowstorm/test_tx_test.go +++ /dev/null @@ -1,24 +0,0 @@ -// (c) 2019-2020, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package snowstorm - -import ( - "testing" -) - -func TestTxVerify(t *testing.T) { - Setup() - - if err := Red.Verify(); err != nil { - t.Fatal(err) - } -} - -func TestTxBytes(t *testing.T) { - Setup() - - if Red.Bytes() == nil { - t.Fatalf("Expected non-nil bytes") - } -} diff --git a/snow/engine/avalanche/state/unique_vertex_test.go b/snow/engine/avalanche/state/unique_vertex_test.go index 7cc309c84526..de31d56023ae 100644 --- a/snow/engine/avalanche/state/unique_vertex_test.go +++ b/snow/engine/avalanche/state/unique_vertex_test.go @@ -80,7 +80,7 @@ func TestUniqueVertexCacheHit(t *testing.T) { serializer: s, } if err := uVtx.setVertex(vtx); err != nil { - t.Fatalf("Failed to set vertex due to: %w", err) + t.Fatalf("Failed to set vertex due to: %s", err) } newUVtx := &uniqueVertex{ diff --git a/snow/engine/avalanche/state/vertex_test.go b/snow/engine/avalanche/state/vertex_test.go index 58c4c7666c63..0bdd9aa244f7 100644 --- a/snow/engine/avalanche/state/vertex_test.go +++ b/snow/engine/avalanche/state/vertex_test.go @@ -31,7 +31,7 @@ func TestVertexVerify(t *testing.T) { } if err := validVertex.Verify(); err != nil { - t.Fatalf("Valid vertex failed verification due to: %w", err) + t.Fatalf("Valid vertex failed verification due to: %s", err) } nonUniqueParentsVtx := &innerVertex{ diff --git a/snow/engine/avalanche/transitive.go b/snow/engine/avalanche/transitive.go index 7a8d39cd1af1..b1d8692be660 100644 --- a/snow/engine/avalanche/transitive.go +++ b/snow/engine/avalanche/transitive.go @@ -95,10 +95,9 @@ func (t *Transitive) finishBootstrapping() error { t.Ctx.Log.Error("vertex %s failed to be loaded from the frontier with %s", vtxID, err) } } - t.Consensus.Initialize(t.Ctx, t.Params, frontier) t.Ctx.Log.Info("bootstrapping finished with %d vertices in the accepted frontier", len(frontier)) - return nil + return t.Consensus.Initialize(t.Ctx, t.Params, frontier) } // Gossip implements the Engine interface diff --git a/snow/engine/avalanche/transitive_test.go b/snow/engine/avalanche/transitive_test.go index 24ffc448e551..8a6c90f42431 100644 --- a/snow/engine/avalanche/transitive_test.go +++ b/snow/engine/avalanche/transitive_test.go @@ -2984,6 +2984,7 @@ func TestEngineAggressivePolling(t *testing.T) { config := DefaultConfig() config.Params.ConcurrentRepolls = 3 + config.Params.BetaRogue = 3 vdr := validators.GenerateRandomValidator(1) @@ -3034,8 +3035,12 @@ func TestEngineAggressivePolling(t *testing.T) { } te := &Transitive{} - te.Initialize(config) - te.finishBootstrapping() + if err := te.Initialize(config); err != nil { + t.Fatal(err) + } + if err := te.finishBootstrapping(); err != nil { + t.Fatal(err) + } te.Ctx.Bootstrapped() parsed := new(bool) @@ -3072,7 +3077,7 @@ func TestEngineAggressivePolling(t *testing.T) { t.Fatalf("should have issued one push query") } if *numPullQueries != 2 { - t.Fatalf("should have issued one pull query") + t.Fatalf("should have issued two pull queries") } }