diff --git a/snow/engine/avalanche/transitive.go b/snow/engine/avalanche/transitive.go index 15f659e..a2bd3a1 100644 --- a/snow/engine/avalanche/transitive.go +++ b/snow/engine/avalanche/transitive.go @@ -369,6 +369,7 @@ func (t *Transitive) batch(txs []snowstorm.Tx, force, empty bool) error { issuedTxs := ids.Set{} consumed := ids.Set{} issued := false + orphans := t.Consensus.Orphans() for _, tx := range txs { inputs := tx.InputIDs() overlaps := consumed.Overlaps(inputs) @@ -380,8 +381,10 @@ func (t *Transitive) batch(txs []snowstorm.Tx, force, empty bool) error { overlaps = false } - // Force allows for a conflict to be issued - if txID := tx.ID(); !overlaps && !issuedTxs.Contains(txID) && (force || t.Consensus.IsVirtuous(tx)) && !tx.Status().Decided() { + if txID := tx.ID(); !overlaps && // should never allow conflicting txs in the same vertex + !issuedTxs.Contains(txID) && // shouldn't issue duplicated transactions to the same vertex + (force || t.Consensus.IsVirtuous(tx)) && // force allows for a conflict to be issued + (!t.Consensus.TxIssued(tx) || orphans.Contains(txID)) { // should only reissued orphaned txs batch = append(batch, tx) issuedTxs.Add(txID) consumed.Union(inputs) diff --git a/snow/engine/avalanche/transitive_test.go b/snow/engine/avalanche/transitive_test.go index 31b8af8..90bb3fa 100644 --- a/snow/engine/avalanche/transitive_test.go +++ b/snow/engine/avalanche/transitive_test.go @@ -2975,3 +2975,110 @@ func TestEngineAggressivePolling(t *testing.T) { t.Fatalf("should have issued one pull query") } } + +func TestEngineDuplicatedIssuance(t *testing.T) { + config := DefaultConfig() + config.Params.BatchSize = 1 + config.Params.BetaVirtuous = 5 + config.Params.BetaRogue = 5 + + sender := &common.SenderTest{} + sender.T = t + config.Sender = sender + + sender.Default(true) + sender.CantGetAcceptedFrontier = false + + vdr := validators.GenerateRandomValidator(1) + + vals := validators.NewSet() + config.Validators = vals + + vals.Add(vdr) + + st := &stateTest{t: t} + config.State = st + + st.Default(true) + + vm := &VMTest{} + vm.T = t + config.VM = vm + + vm.Default(true) + + gVtx := &Vtx{ + id: GenerateID(), + status: choices.Accepted, + } + mVtx := &Vtx{ + id: GenerateID(), + status: choices.Accepted, + } + + gTx := &TestTx{ + TestTx: snowstorm.TestTx{ + Identifier: GenerateID(), + Stat: choices.Accepted, + }, + } + + utxos := []ids.ID{GenerateID(), GenerateID()} + + tx := &TestTx{ + TestTx: snowstorm.TestTx{ + Identifier: GenerateID(), + Deps: []snowstorm.Tx{gTx}, + Stat: choices.Processing, + }, + } + tx.Ins.Add(utxos[0]) + + st.edge = func() []ids.ID { return []ids.ID{gVtx.ID(), mVtx.ID()} } + st.getVertex = func(id ids.ID) (avalanche.Vertex, error) { + switch { + case id.Equals(gVtx.ID()): + return gVtx, nil + case id.Equals(mVtx.ID()): + return mVtx, nil + } + t.Fatalf("Unknown vertex") + panic("Should have errored") + } + + te := &Transitive{} + te.Initialize(config) + te.finishBootstrapping() + + lastVtx := new(Vtx) + st.buildVertex = func(_ ids.Set, txs []snowstorm.Tx) (avalanche.Vertex, error) { + consumers := []snowstorm.Tx{} + for _, tx := range txs { + consumers = append(consumers, tx) + } + lastVtx = &Vtx{ + parents: []avalanche.Vertex{gVtx, mVtx}, + id: GenerateID(), + txs: consumers, + status: choices.Processing, + bytes: []byte{1}, + } + return lastVtx, nil + } + + sender.CantPushQuery = false + + vm.PendingTxsF = func() []snowstorm.Tx { return []snowstorm.Tx{tx} } + te.Notify(common.PendingTxs) + + if len(lastVtx.txs) != 1 || !lastVtx.txs[0].ID().Equals(tx.ID()) { + t.Fatalf("Should have issued txs differently") + } + + st.buildVertex = func(ids.Set, []snowstorm.Tx) (avalanche.Vertex, error) { + t.Fatalf("shouldn't have attempted to issue a duplicated tx") + return nil, nil + } + + te.Notify(common.PendingTxs) +}