Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite ConflictDAG #2606

Draft
wants to merge 141 commits into
base: develop
Choose a base branch
from
Draft
Changes from 1 commit
Commits
Show all changes
141 commits
Select commit Hold shift + click to select a range
7c2cb28
Feat: Initial Brainstorm un multi-tiered conflictdag
hmoog Mar 10, 2023
10b10a0
Feat: more brainstorming
hmoog Mar 10, 2023
37109ba
Feat: started implementing ordering
hmoog Mar 11, 2023
83d251e
Feat: WIP WIP
hmoog Mar 14, 2023
bb26ad8
Feat: added unit tests
hmoog Mar 15, 2023
0021cfd
Feat: added more stuff
hmoog Mar 15, 2023
3e34f51
Feat: WIP WIP it works
hmoog Mar 16, 2023
71b62f6
Feat: started finalizing and commenting sub packages
hmoog Mar 16, 2023
5513fe5
Feat: refactored more code
hmoog Mar 16, 2023
9d2bc1a
Feat: more cleanup
hmoog Mar 16, 2023
9094a6e
Refactor: removed unnecessary code
hmoog Mar 16, 2023
9bfceea
Refactor: fixed some outputs in the String methods
hmoog Mar 16, 2023
8ced0b0
Merge branch 'develop' of https://github.com/iotaledger/goshimmer int…
hmoog Mar 17, 2023
3394cbc
Fix: updated constructor of AdvancedSet
hmoog Mar 17, 2023
643d928
Refactor: removed unused code
hmoog Mar 17, 2023
c933c52
Feat: implemented some preferred instead stuff
hmoog Mar 17, 2023
5740e1c
Feat: Added PreferredInstead functionalitz
hmoog Mar 17, 2023
80aeece
Refactor: started cleaning up
hmoog Mar 17, 2023
e042aef
Refactor: cleaned up more code
hmoog Mar 17, 2023
eb04c9c
Refactor: cleaned up code
hmoog Mar 17, 2023
6b80c04
Refactor: started cleanup SortedSet
hmoog Mar 17, 2023
55496d3
Refactor: cleaned up SortedSet
hmoog Mar 18, 2023
3aad4ed
Feat: hooked PreferredInsteadUpdated
hmoog Mar 18, 2023
bbab455
Test new Conflicts WIP
piotrm50 Mar 20, 2023
6b96cf1
Create a worker for updating conflict preference
piotrm50 Mar 21, 2023
222a962
Sorted conflicts somewhat work
piotrm50 Mar 21, 2023
ba18dd0
Test WIP
piotrm50 Mar 21, 2023
fc1aa60
Feat: added shared pendingTasksCounter
hmoog Mar 21, 2023
d7d7813
Feat: fixed bugs?
hmoog Mar 22, 2023
e82ff02
Add event log for debugging
piotrm50 Mar 22, 2023
c315e24
Fix: fixed some bugs
hmoog Mar 22, 2023
1dcc458
Improve unit tests
piotrm50 Mar 22, 2023
6e3148e
Improve test assertion
piotrm50 Mar 22, 2023
608bb25
Fix: fixed linter issue
hmoog Mar 22, 2023
14445bc
Refactor: refactored code
hmoog Mar 22, 2023
cc6980a
Fix: fixed race condition
hmoog Mar 22, 2023
c66b60c
Add parentsPreferredConflicts property
piotrm50 Mar 23, 2023
8f7fcca
Feat: started adding likeInstead references
hmoog Mar 24, 2023
484a33e
Feat: started adding tests for LikedInstead
hmoog Mar 24, 2023
56a4551
Feat: likedinstead seems to work
hmoog Mar 24, 2023
d5e1181
Refactor: refactored code
hmoog Mar 24, 2023
101870c
Fix: fixed linter errors
hmoog Mar 24, 2023
6dbff41
Refactor: fixed linter error
hmoog Mar 24, 2023
03b4b7a
Refactor: cleaned up code
hmoog Mar 25, 2023
13a3b23
Feat: started tying things together in the ConflictDAG
hmoog Mar 25, 2023
195a7a9
Merge branch 'develop' of github.com:iotaledger/goshimmer into feat/r…
hmoog Mar 29, 2023
12437c1
Feat: started adding conflictdag tests
hmoog Mar 29, 2023
0ef7a4c
Feat: extended tests
hmoog Mar 29, 2023
63e66d1
Fix: fixed bug
hmoog Mar 29, 2023
20e2aba
Fix: fixed bug
hmoog Mar 29, 2023
0254d06
Feat: added more tests
hmoog Mar 30, 2023
77785eb
Feat: upgraded hive.go
hmoog Apr 4, 2023
1fadcaf
Feat: cleaned up API
hmoog Apr 4, 2023
a2dad39
Feat: simplified API
hmoog Apr 4, 2023
21ca0f9
Feat: cleaned up API
hmoog Apr 5, 2023
f7af16c
Feat: simplified api
hmoog Apr 5, 2023
65e5c26
Feat: added method to join a conflict to new conflictSets
hmoog Apr 5, 2023
cf5fe5b
Feat: uploaded some changes
hmoog Apr 5, 2023
e85c8c8
Feat: cleaned up types
hmoog Apr 5, 2023
c90601c
Feat: added voting primitives
hmoog Apr 5, 2023
d02da12
Feat: cleaned up code
hmoog Apr 5, 2023
a107dbb
Refactor: cleaned up more code
hmoog Apr 5, 2023
1fffdd5
Feat: cleaned up code
hmoog Apr 6, 2023
bbee28a
Feat: merged more funcs
hmoog Apr 6, 2023
c6dbe6e
Feat: more cleanup
hmoog Apr 6, 2023
87f34c9
Refactor: started restructuring code
hmoog Apr 6, 2023
908cb0b
Refactor: added comments in respect to locking
hmoog Apr 7, 2023
7496958
Refactor: removed unnecessary variable
hmoog Apr 7, 2023
e02482a
Merge branch 'develop' into feat/rewrite-conflictdag-with-voting
hmoog Apr 7, 2023
cce86b2
Feat: go mod tidy
hmoog Apr 7, 2023
f378894
Fix: fixed data race
hmoog Apr 7, 2023
88b6ad5
Feat: cleaned up code
hmoog Apr 10, 2023
9d82204
Feat: started adding vote logic
hmoog Apr 10, 2023
bac12e8
Refactor: refactored walkers and removed utils
hmoog Apr 10, 2023
0d8a269
Feat: WIP (almost done)
hmoog Apr 11, 2023
44a821d
Feat: added tests for acceptance
hmoog Apr 11, 2023
dd73c3f
Feat: extended tests
hmoog Apr 11, 2023
5e223a1
Evaluate results of a test
piotrm50 Apr 11, 2023
e70660d
Feat: cleaned up code
hmoog Apr 12, 2023
8d32368
Clean up stuff
piotrm50 Apr 12, 2023
bc8b174
Move things from conflict package to conflictdag package and improve …
piotrm50 Apr 12, 2023
eb5ad7c
Fix: reverted accidental change from previous commit
hmoog Apr 12, 2023
5a1046e
Implement Dispose methods for Conflict and SortedConflicts
piotrm50 Apr 12, 2023
a311ac3
Feat: started tracking conflictsets in conflicts
hmoog Apr 13, 2023
b51e5ae
Implement eviction of the new ConflictDAG
piotrm50 Apr 13, 2023
80eeda0
Fix tests after refactoring
piotrm50 Apr 13, 2023
6b660de
Feat: started fixing tests
hmoog Apr 13, 2023
af2d817
Merge branch 'feat/rewrite-conflictdag-with-voting' of https://github…
hmoog Apr 13, 2023
b42d725
Fix: fixed tests#
hmoog Apr 13, 2023
8e6222e
Refactor: introduced utility method for getting conflictsets
hmoog Apr 13, 2023
750f35b
Feat: continued work ok eviction
hmoog Apr 14, 2023
6c2cc1d
Fix: fixed bug
hmoog Apr 14, 2023
5806b69
Feat: cleaned up eviction
hmoog Apr 14, 2023
bb9aa0f
Ledger uses new conflict DAG
piotrm50 Apr 14, 2023
1f0ed9b
Refactor: added comment
hmoog Apr 14, 2023
bd3dd69
Merge branch 'feat/rewrite-conflictdag-with-voting' of https://github…
hmoog Apr 14, 2023
b6961e3
Get rid of VirtualVoting component. Make markerbooker use new Conflic…
piotrm50 Apr 14, 2023
d3c2da5
Integrate the new conflictdag with the rest of the components
piotrm50 Apr 14, 2023
1412314
Start fixing compile errors
piotrm50 Apr 14, 2023
e1779ee
Refactor: reverted accidental rename
hmoog Apr 14, 2023
0cf3e83
Refactor: fix more rename bugs
hmoog Apr 14, 2023
e5c8078
Feat: more cleanup
hmoog Apr 14, 2023
bf717e9
Refactor: minimized changes
hmoog Apr 14, 2023
b0d3508
Feat: further minimized diff
hmoog Apr 14, 2023
d86df62
Fix: fixed some bugs
hmoog Apr 14, 2023
66ff2d4
Fix: fixed ReferenceProvider
hmoog Apr 14, 2023
cc057b7
Refactor: refactored code
hmoog Apr 15, 2023
53ec43b
Refactor: refactored code
hmoog Apr 15, 2023
fd9b2b0
Refactor: refactor
hmoog Apr 15, 2023
a2c126a
Refactor: minimized diff
hmoog Apr 15, 2023
eb09e99
Refactor: reverted erroneous rename
hmoog Apr 15, 2023
d0e8a14
Feat: switched ConflictDAG API to use AdvancedSets
hmoog Apr 17, 2023
9a80012
Feat: optimized code to use AdvancedSet everywhere
hmoog Apr 17, 2023
2462094
Feat: fixed interfaces
hmoog Apr 17, 2023
8a34c1d
Continue integrating new conflictdag with the codebase
piotrm50 Apr 17, 2023
6307ff4
Fix last problems
piotrm50 Apr 17, 2023
8d064cf
Fix tips conflicts tracker
piotrm50 Apr 17, 2023
26bdd21
Fix some problems with double spend resolution
piotrm50 Apr 17, 2023
d0e9ccd
Merge branch 'debug/race-conditions' into feat/rewrite-conflictdag-wi…
piotrm50 Apr 17, 2023
9537e80
Merge remote-tracking branch 'origin/debug/race-conditions' into feat…
piotrm50 Apr 17, 2023
162034f
Continue fixing problems after integrating the new conflictdag
piotrm50 Apr 17, 2023
2d8b6a4
Fix some more problems with double spend and guava spam.
piotrm50 Apr 17, 2023
ed31879
Feat: added some missing comments
hmoog Apr 17, 2023
3c9f913
Fix: fixed tests
hmoog Apr 17, 2023
9920479
Feat: Created ConflictDAG interface package
hmoog Apr 18, 2023
fa1d97a
Only remove strong parents when adding a new tip to tip pool
piotrm50 Apr 18, 2023
1cbee1c
Fix casting votes when propagating conflict after fork.
piotrm50 Apr 18, 2023
6ecdf48
Revert accidental change
piotrm50 Apr 18, 2023
2503e80
Feat: implemented generic test framework for conflictdag
hmoog Apr 19, 2023
27312ab
Merge remote-tracking branch 'origin/develop' into feat/rewrite-confl…
piotrm50 Apr 20, 2023
d026284
Fix post merge errors
piotrm50 Apr 20, 2023
aed80c5
Merge remote-tracking branch 'origin/develop' into feat/rewrite-confl…
piotrm50 Apr 20, 2023
d745f0a
Remove some unused metrics that caused problems
piotrm50 Apr 20, 2023
9846cee
Fix nil pointer in the metrics plugin
piotrm50 Apr 26, 2023
db1714c
Always create conflictsets
piotrm50 Apr 27, 2023
c7866a2
Get rid of implicit self-like
piotrm50 Apr 27, 2023
151a66e
Add a parent block back to the tip pool after removing old one.
piotrm50 Apr 27, 2023
7258ae8
Remove printlns for subjectively invalid blocks
piotrm50 Apr 27, 2023
4e167db
Fix imports
piotrm50 Apr 28, 2023
8574b8e
Add non-conflicting conflict to ConflictDAG test suite
karimodm May 11, 2023
749aa3e
Merge branch 'feat/rewrite-conflictdag-with-voting' of github.com:iot…
karimodm May 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Implement eviction of the new ConflictDAG
piotrm50 committed Apr 13, 2023

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
commit b51e5aebd454a4b6289afe273d67f693b41b1170
52 changes: 42 additions & 10 deletions packages/protocol/engine/ledger/mempool/newconflictdag/conflict.go
Original file line number Diff line number Diff line change
@@ -4,6 +4,8 @@ import (
"bytes"
"sync"

"go.uber.org/atomic"

"github.com/iotaledger/goshimmer/packages/protocol/engine/ledger/mempool/newconflictdag/acceptance"
"github.com/iotaledger/goshimmer/packages/protocol/engine/ledger/mempool/newconflictdag/vote"
"github.com/iotaledger/goshimmer/packages/protocol/engine/ledger/mempool/newconflictdag/weight"
@@ -59,6 +61,9 @@ type Conflict[ConflictID, ResourceID IDType, VotePower constraints.Comparable[Vo
// preferredInstead is the preferred instead value of the Conflict.
preferredInstead *Conflict[ConflictID, ResourceID, VotePower]

// evicted
evicted atomic.Bool

// preferredInsteadMutex is used to synchronize access to the preferred instead value of the Conflict.
preferredInsteadMutex sync.RWMutex

@@ -165,6 +170,10 @@ func (c *Conflict[ConflictID, ResourceID, VotePower]) ApplyVote(vote *vote.Vote[

// JoinConflictSets registers the Conflict with the given ConflictSets.
func (c *Conflict[ConflictID, ResourceID, VotePower]) JoinConflictSets(conflictSets ...*ConflictSet[ConflictID, ResourceID, VotePower]) (joinedConflictSets map[ResourceID]*ConflictSet[ConflictID, ResourceID, VotePower]) {
if c.evicted.Load() {
return
}

registerConflictingConflict := func(c, conflict *Conflict[ConflictID, ResourceID, VotePower]) {
c.structureMutex.Lock()
defer c.structureMutex.Unlock()
@@ -260,25 +269,48 @@ func (c *Conflict[ConflictID, ResourceID, VotePower]) LikedInstead() *advancedse
return c.likedInstead.Clone()
}

// Dispose cleans up the sortedConflict.
func (c *Conflict[ConflictID, ResourceID, VotePower]) Dispose() {
// Evict cleans up the sortedConflict.
func (c *Conflict[ConflictID, ResourceID, VotePower]) Evict() []ConflictID {
// TODO: make other methods respect c.evicted flag
c.evicted.Store(true)

// iterate through the children and dispose of them
_ = c.Children.ForEach(func(childConflict *Conflict[ConflictID, ResourceID, VotePower]) (err error) {
if !childConflict.IsPending() {
childConflict.Evict()
}

return nil
})

c.structureMutex.Lock()
defer c.structureMutex.Unlock()

c.ConflictingConflicts.Dispose()
for _, parentConflict := range c.Parents.Slice() {
parentConflict.unregisterChild(c)
}
c.Parents.Clear()

_ = c.Children.ForEach(func(childConflict *Conflict[ConflictID, ResourceID, VotePower]) (err error) {
childConflict.structureMutex.Lock()
defer childConflict.structureMutex.Unlock()
// deattach all events etc.

for _, conflictSet := range c.ConflictSets.Slice() {
conflictSet.Remove(c)
}

if childConflict.Parents.Delete(c) {
c.unregisterChild(childConflict)
c.ConflictSets.Clear()

for _, conflict := range c.ConflictingConflicts.Shutdown() {
if conflict != c {
conflict.ConflictingConflicts.EvictConflict(c.ID)
c.ConflictingConflicts.EvictConflict(conflict.ID)
}
}

return nil
})
c.ConflictingConflicts.EvictConflict(c.ID)

c.acceptanceUnhook()

return nil
}

// Compare compares the Conflict to the given other Conflict.
Original file line number Diff line number Diff line change
@@ -3,6 +3,8 @@ package newconflictdag
import (
"sync"

"go.uber.org/atomic"

"github.com/iotaledger/hive.go/constraints"
"github.com/iotaledger/hive.go/ds/advancedset"
)
@@ -15,6 +17,8 @@ type ConflictSet[ConflictID, ResourceID IDType, VotePower constraints.Comparable
// members is the set of Conflicts that are conflicting over the shared resource.
members *advancedset.AdvancedSet[*Conflict[ConflictID, ResourceID, VotePower]]

allMembersEvicted atomic.Bool

mutex sync.RWMutex
}

@@ -41,17 +45,15 @@ func (c *ConflictSet[ConflictID, ResourceID, VotePower]) Add(addedConflict *Conf
}

// Remove removes a Conflict from the ConflictSet and returns all remaining members of the set.
func (c *ConflictSet[ConflictID, ResourceID, VotePower]) Remove(removedConflict *Conflict[ConflictID, ResourceID, VotePower]) (otherMembers []*Conflict[ConflictID, ResourceID, VotePower]) {
func (c *ConflictSet[ConflictID, ResourceID, VotePower]) Remove(removedConflict *Conflict[ConflictID, ResourceID, VotePower]) (removed bool) {
c.mutex.Lock()
defer c.mutex.Unlock()

if !c.members.Delete(removedConflict) {
return nil
}

if c.members.IsEmpty() {
// TODO: trigger conflict set removal
if removed = !c.members.Delete(removedConflict); removed && c.members.IsEmpty() {
if wasShutdown := c.allMembersEvicted.Swap(true); !wasShutdown {
// TODO: trigger conflict set removal
}
}

return c.members.Slice()
return removed
}
Original file line number Diff line number Diff line change
@@ -61,28 +61,43 @@ func New[ConflictID, ResourceID IDType, VotePower constraints.Comparable[VotePow
}

// CreateConflict creates a new Conflict that is conflicting over the given ResourceIDs and that has the given parents.
func (c *ConflictDAG[ConflictID, ResourceID, VotePower]) CreateConflict(id ConflictID, parentIDs []ConflictID, resourceIDs []ResourceID, initialWeight *weight.Weight) *Conflict[ConflictID, ResourceID, VotePower] {
createdConflict := func() *Conflict[ConflictID, ResourceID, VotePower] {
func (c *ConflictDAG[ConflictID, ResourceID, VotePower]) CreateConflict(id ConflictID, parentIDs []ConflictID, resourceIDs []ResourceID, initialWeight *weight.Weight) error {
createdConflict, err := func() (*Conflict[ConflictID, ResourceID, VotePower], error) {
c.mutex.RLock()
defer c.mutex.RUnlock()

parents := lo.Values(c.Conflicts(parentIDs...))
conflictSets := lo.Values(c.ConflictSets(resourceIDs...))

createdConflict, isNew := c.conflictsByID.GetOrCreate(id, func() *Conflict[ConflictID, ResourceID, VotePower] {
return NewConflict[ConflictID, ResourceID, VotePower](id, parents, conflictSets, initialWeight, c.pendingTasks, c.acceptanceThresholdProvider)
})
conflictSetsMap := make(map[ResourceID]*ConflictSet[ConflictID, ResourceID, VotePower])
for _, resourceID := range resourceIDs {
if initialWeight.AcceptanceState().IsRejected() {
conflictSet, exists := c.conflictSetsByID.Get(resourceID)
if !exists {
return nil, xerrors.Errorf("tried to create a Conflict with evicted Resource: %w", EvictionError)
}

conflictSetsMap[resourceID] = conflictSet
} else {
conflictSetsMap[resourceID] = lo.Return1(c.conflictSetsByID.GetOrCreate(resourceID, func() *ConflictSet[ConflictID, ResourceID, VotePower] {
// TODO: hook to conflictSet event that is triggered when it becomes empty
return NewConflictSet[ConflictID, ResourceID, VotePower](resourceID)
}))
}
}

if createdConflict, isNew := c.conflictsByID.GetOrCreate(id, func() *Conflict[ConflictID, ResourceID, VotePower] {
return NewConflict[ConflictID, ResourceID, VotePower](id, parents, lo.Values(conflictSetsMap), initialWeight, c.pendingTasks, c.acceptanceThresholdProvider)
}); isNew {
return createdConflict, nil

if !isNew {
panic("tried to re-create an already existing conflict")
}

return createdConflict
return nil, xerrors.Errorf("tried to create conflict with %s twice: %w", id, RuntimeError)
}()

c.ConflictCreated.Trigger(createdConflict)

return createdConflict
return err
}

// JoinConflictSets adds the Conflict to the given ConflictSets and returns true if the conflict membership was modified during this operation.
@@ -169,6 +184,7 @@ func (c *ConflictDAG[ConflictID, ResourceID, VotePower]) ConflictSets(resourceID
conflictSets := make(map[ResourceID]*ConflictSet[ConflictID, ResourceID, VotePower])
for _, resourceID := range resourceIDs {
conflictSets[resourceID] = lo.Return1(c.conflictSetsByID.GetOrCreate(resourceID, func() *ConflictSet[ConflictID, ResourceID, VotePower] {
// TODO: hook to conflictSet event that is triggered when it becomes empty
return NewConflictSet[ConflictID, ResourceID, VotePower](resourceID)
}))
}
@@ -198,6 +214,32 @@ func (c *ConflictDAG[ConflictID, ResourceID, VotePower]) CastVotes(vote *vote.Vo
return nil
}

// EvictConflict removes conflict with given ConflictID from ConflictDAG.
func (c *ConflictDAG[ConflictID, ResourceID, VotePower]) EvictConflict(conflictID ConflictID) {
c.mutex.Lock()
defer c.mutex.Unlock()
// TODO: make locking more fine-grained on conflictset level

conflict, exists := c.conflictsByID.Get(conflictID)
if !exists {
return
}

evictedConflictIDs := conflict.Evict()

for _, evictedConflictID := range evictedConflictIDs {
c.conflictsByID.Delete(evictedConflictID)
}

//_ = conflictSets.ForEach(func(conflictSet *ConflictSet[ConflictID, ResourceID, VotePower]) (err error) {
// if conflictSet.members.IsEmpty() {
// c.conflictSetsByID.Delete(conflictSet.ID)
// }
//
// return nil
//})
}

func (c *ConflictDAG[ConflictID, ResourceID, VotePower]) determineVotes(conflictIDs ...ConflictID) (supportedConflicts, revokedConflicts *advancedset.AdvancedSet[*Conflict[ConflictID, ResourceID, VotePower]], err error) {
supportedConflicts = advancedset.New[*Conflict[ConflictID, ResourceID, VotePower]]()
revokedConflicts = advancedset.New[*Conflict[ConflictID, ResourceID, VotePower]]()

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package newconflictdag

import "golang.org/x/xerrors"

var (
EvictionError = xerrors.New("tried to operate on evicted entity")
RuntimeError = xerrors.New("unexpected operation")
)
Original file line number Diff line number Diff line change
@@ -61,7 +61,7 @@ func newSortedConflict[ConflictID, ResourceID IDType, VotePower constraints.Comp
Conflict: conflict,
}

if conflict != set.owner {
if set.owner != nil {
s.onAcceptanceStateUpdatedHook = conflict.AcceptanceStateUpdated.Hook(s.onAcceptanceStateUpdated)
}

@@ -101,14 +101,22 @@ func (s *sortedConflict[ConflictID, ResourceID, VotePower]) IsPreferred() bool {
return s.PreferredInstead() == s.Conflict
}

// Dispose cleans up the sortedConflict.
func (s *sortedConflict[ConflictID, ResourceID, VotePower]) Dispose() {
// Unhook cleans up the sortedConflict.
func (s *sortedConflict[ConflictID, ResourceID, VotePower]) Unhook() {
if s.onAcceptanceStateUpdatedHook != nil {
s.onAcceptanceStateUpdatedHook.Unhook()
s.onAcceptanceStateUpdatedHook = nil
}

s.onWeightUpdatedHook.Unhook()
s.onPreferredUpdatedHook.Unhook()
if s.onWeightUpdatedHook != nil {
s.onWeightUpdatedHook.Unhook()
s.onWeightUpdatedHook = nil
}

if s.onPreferredUpdatedHook != nil {
s.onPreferredUpdatedHook.Unhook()
s.onPreferredUpdatedHook = nil
}
}

func (s *sortedConflict[ConflictID, ResourceID, VotePower]) onAcceptanceStateUpdated(_, newState acceptance.State) {
@@ -158,7 +166,7 @@ func (s *sortedConflict[ConflictID, ResourceID, VotePower]) queuePreferredInstea

if (s.queuedPreferredInstead == nil && s.currentPreferredInstead == conflict) ||
(s.queuedPreferredInstead != nil && s.queuedPreferredInstead == conflict) ||
s.sortedSet.owner == conflict {
s.sortedSet.owner.Conflict == conflict {
return
}

Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@ import (
// SortedConflicts is a set of Conflicts that is sorted by their weight.
type SortedConflicts[ConflictID, ResourceID IDType, VotePower constraints.Comparable[VotePower]] struct {
// owner is the Conflict that owns this SortedConflicts.
owner *Conflict[ConflictID, ResourceID, VotePower]
owner *sortedConflict[ConflictID, ResourceID, VotePower]

// members is a map of ConflictIDs to their corresponding sortedConflict.
members *shrinkingmap.ShrinkingMap[ConflictID, *sortedConflict[ConflictID, ResourceID, VotePower]]
@@ -59,7 +59,6 @@ type SortedConflicts[ConflictID, ResourceID IDType, VotePower constraints.Compar
// NewSortedConflicts creates a new SortedConflicts that is owned by the given Conflict.
func NewSortedConflicts[ConflictID, ResourceID IDType, VotePower constraints.Comparable[VotePower]](owner *Conflict[ConflictID, ResourceID, VotePower], pendingUpdatesCounter *syncutils.Counter) *SortedConflicts[ConflictID, ResourceID, VotePower] {
s := &SortedConflicts[ConflictID, ResourceID, VotePower]{
owner: owner,
members: shrinkingmap.New[ConflictID, *sortedConflict[ConflictID, ResourceID, VotePower]](),
pendingWeightUpdates: shrinkingmap.New[ConflictID, *sortedConflict[ConflictID, ResourceID, VotePower]](),
pendingUpdatesCounter: pendingUpdatesCounter,
@@ -68,11 +67,11 @@ func NewSortedConflicts[ConflictID, ResourceID IDType, VotePower constraints.Com
s.pendingWeightUpdatesSignal = sync.NewCond(&s.pendingWeightUpdatesMutex)
s.pendingPreferredInsteadSignal = sync.NewCond(&s.pendingPreferredInsteadMutex)

newMember := newSortedConflict[ConflictID, ResourceID, VotePower](s, owner)
s.members.Set(owner.ID, newMember)
s.owner = newSortedConflict[ConflictID, ResourceID, VotePower](s, owner)
s.members.Set(owner.ID, s.owner)

s.heaviestMember = newMember
s.heaviestPreferredMember = newMember
s.heaviestMember = s.owner
s.heaviestPreferredMember = s.owner

// TODO: move to WorkerPool so we are consistent with the rest of the codebase
go s.fixMemberPositionWorker()
@@ -86,6 +85,10 @@ func (s *SortedConflicts[ConflictID, ResourceID, VotePower]) Add(conflict *Confl
s.mutex.Lock()
defer s.mutex.Unlock()

if s.isShutdown.Load() {
return false
}

newMember, isNew := s.members.GetOrCreate(conflict.ID, func() *sortedConflict[ConflictID, ResourceID, VotePower] {
return newSortedConflict[ConflictID, ResourceID, VotePower](s, conflict)
})
@@ -138,7 +141,7 @@ func (s *SortedConflicts[ConflictID, ResourceID, VotePower]) ForEach(callback fu
defer s.mutex.RUnlock()

for currentMember := s.heaviestMember; currentMember != nil; currentMember = currentMember.lighterMember {
if !lo.First(optIncludeOwner) && currentMember.Conflict == s.owner {
if !lo.First(optIncludeOwner) && currentMember == s.owner {
continue
}

@@ -150,20 +153,37 @@ func (s *SortedConflicts[ConflictID, ResourceID, VotePower]) ForEach(callback fu
return nil
}

func (s *SortedConflicts[ConflictID, ResourceID, VotePower]) Dispose() {
func (s *SortedConflicts[ConflictID, ResourceID, VotePower]) EvictConflict(id ConflictID) bool {
s.mutex.Lock()
defer s.mutex.Unlock()
s.pendingWeightUpdatesMutex.Lock()
defer s.pendingWeightUpdatesMutex.Unlock()
s.pendingPreferredInsteadMutex.Lock()
defer s.pendingPreferredInsteadMutex.Unlock()

s.members.ForEach(func(conflictID ConflictID, sortedConflict *sortedConflict[ConflictID, ResourceID, VotePower]) bool {
sortedConflict.Dispose()
return true
})
conflict, exists := s.members.Get(id)
if !exists || !s.members.Delete(id) {
return false
}

s.isShutdown.Store(true)
conflict.Unhook()

if conflict.heavierMember != nil {
conflict.heavierMember.lighterMember = conflict.lighterMember
}

if conflict.lighterMember != nil {
conflict.lighterMember.heavierMember = conflict.heavierMember
}

if s.heaviestMember == conflict {
s.heaviestMember = conflict.lighterMember
}

if s.heaviestPreferredMember == conflict {
s.findLowerHeaviestPreferredMember(conflict.lighterMember)
}

conflict.lighterMember = nil
conflict.heavierMember = nil

return true
}

// String returns a human-readable representation of the SortedConflicts.
@@ -240,7 +260,7 @@ func (s *SortedConflicts[ConflictID, ResourceID, VotePower]) fixMemberPosition(m
for currentMember := member.heavierMember; currentMember != nil && currentMember.Compare(member) == weight.Lighter; currentMember = member.heavierMember {
s.swapNeighbors(member, currentMember)

if currentMember == s.heaviestPreferredMember && (preferredConflict == currentMember.Conflict || memberIsPreferred || member.Conflict == s.owner) {
if currentMember == s.heaviestPreferredMember && (preferredConflict == currentMember.Conflict || memberIsPreferred || member == s.owner) {
s.heaviestPreferredMember = member
s.owner.setPreferredInstead(member.Conflict)
}
@@ -250,7 +270,7 @@ func (s *SortedConflicts[ConflictID, ResourceID, VotePower]) fixMemberPosition(m
for currentMember := member.lighterMember; currentMember != nil && currentMember.Compare(member) == weight.Heavier; currentMember = member.lighterMember {
s.swapNeighbors(currentMember, member)

if member == s.heaviestPreferredMember && (currentMember.IsPreferred() || currentMember.PreferredInstead() == member.Conflict || currentMember.Conflict == s.owner) {
if member == s.heaviestPreferredMember && (currentMember.IsPreferred() || currentMember.PreferredInstead() == member.Conflict || currentMember == s.owner) {
s.heaviestPreferredMember = currentMember
s.owner.setPreferredInstead(currentMember.Conflict)
}
@@ -318,15 +338,21 @@ func (s *SortedConflicts[ConflictID, ResourceID, VotePower]) fixHeaviestPreferre
}

if s.heaviestPreferredMember == member {
for currentMember := member; ; currentMember = currentMember.lighterMember {
if currentMember.Conflict == s.owner || currentMember.IsPreferred() || currentMember.PreferredInstead() == member.Conflict {
s.heaviestPreferredMember = currentMember
s.owner.setPreferredInstead(currentMember.Conflict)
s.findLowerHeaviestPreferredMember(member)
}
}

return
}
func (s *SortedConflicts[ConflictID, ResourceID, VotePower]) findLowerHeaviestPreferredMember(member *sortedConflict[ConflictID, ResourceID, VotePower]) {
for currentMember := member; currentMember != nil; currentMember = currentMember.lighterMember {
if currentMember == s.owner || currentMember.IsPreferred() || currentMember.PreferredInstead() == member.Conflict {
s.heaviestPreferredMember = currentMember
s.owner.setPreferredInstead(currentMember.Conflict)

return
}
}

s.heaviestPreferredMember = nil
}

// swapNeighbors swaps the given members in the SortedConflicts.
@@ -347,3 +373,22 @@ func (s *SortedConflicts[ConflictID, ResourceID, VotePower]) swapNeighbors(heavi
s.heaviestMember = heavierMember
}
}

func (s *SortedConflicts[ConflictID, ResourceID, VotePower]) Shutdown() []*Conflict[ConflictID, ResourceID, VotePower] {
s.mutex.Lock()
defer s.mutex.Unlock()

s.isShutdown.Store(true)

s.pendingWeightUpdatesMutex.Lock()
s.pendingWeightUpdates.Clear()
s.pendingWeightUpdatesMutex.Unlock()

s.pendingPreferredInsteadMutex.Lock()
s.pendingPreferredInsteadUpdates.Clear()
s.pendingPreferredInsteadMutex.Unlock()

return lo.Map(s.members.Values(), func(conflict *sortedConflict[ConflictID, ResourceID, VotePower]) *Conflict[ConflictID, ResourceID, VotePower] {
return conflict.Conflict
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package newconflictdag

import (
"fmt"
"strings"
"testing"

"golang.org/x/crypto/blake2b"

"github.com/iotaledger/goshimmer/packages/core/votes"
"github.com/iotaledger/goshimmer/packages/protocol/engine/ledger/mempool/newconflictdag/acceptance"
"github.com/iotaledger/goshimmer/packages/protocol/engine/ledger/mempool/newconflictdag/vote"
"github.com/iotaledger/goshimmer/packages/protocol/engine/ledger/mempool/newconflictdag/weight"
"github.com/iotaledger/goshimmer/packages/protocol/engine/ledger/utxo"
"github.com/iotaledger/goshimmer/packages/protocol/engine/sybilprotection"
"github.com/iotaledger/hive.go/kvstore/mapdb"
"github.com/iotaledger/hive.go/lo"
"github.com/iotaledger/hive.go/runtime/options"
)

type TestFramework struct {
test *testing.T
ConflictDAG *ConflictDAG[TestID, TestID, votes.MockedVotePower]
Weights *sybilprotection.Weights

conflictsByAlias map[string]*Conflict[TestID, TestID, votes.MockedVotePower]
conflictSetsByAlias map[string]*ConflictSet[TestID, TestID, votes.MockedVotePower]
}

// NewTestFramework creates a new instance of the TestFramework with one default output "Genesis" which has to be
// consumed by the first transaction.
func NewTestFramework(test *testing.T, opts ...options.Option[TestFramework]) *TestFramework {
return options.Apply(&TestFramework{
test: test,
conflictsByAlias: make(map[string]*Conflict[TestID, TestID, votes.MockedVotePower]),
conflictSetsByAlias: make(map[string]*ConflictSet[TestID, TestID, votes.MockedVotePower]),
}, opts, func(t *TestFramework) {
if t.Weights == nil {
t.Weights = sybilprotection.NewWeights(mapdb.NewMapDB())
}

if t.ConflictDAG == nil {
t.ConflictDAG = New[TestID, TestID, votes.MockedVotePower](acceptance.ThresholdProvider(t.Weights.TotalWeight))
}
})
}

func (t *TestFramework) CreateConflict(alias string, parentIDs []string, resourceAliases []string, initialWeight *weight.Weight) (*Conflict[TestID, TestID, votes.MockedVotePower], error) {
if err := t.ConflictDAG.CreateConflict(NewTestID(alias), t.ConflictIDs(parentIDs...), t.ConflictSetIDs(resourceAliases...), initialWeight); err != nil {
return nil, err
}

t.conflictsByAlias[alias] = lo.Return1(t.ConflictDAG.conflictsByID.Get(NewTestID(alias)))

for _, resourceAlias := range resourceAliases {
t.conflictSetsByAlias[resourceAlias] = lo.Return1(t.ConflictDAG.conflictSetsByID.Get(NewTestID(resourceAlias)))
}

return t.conflictsByAlias[alias], nil
}

func (t *TestFramework) ConflictIDs(aliases ...string) (conflictIDs []TestID) {
for _, alias := range aliases {
conflictIDs = append(conflictIDs, NewTestID(alias))
}

return conflictIDs
}

func (t *TestFramework) ConflictSetIDs(aliases ...string) (conflictSetIDs []TestID) {
for _, alias := range aliases {
conflictSetIDs = append(conflictSetIDs, NewTestID(alias))
}

return conflictSetIDs
}

func (t *TestFramework) Conflict(alias string) *Conflict[TestID, TestID, votes.MockedVotePower] {
conflict, ok := t.conflictsByAlias[alias]
if !ok {
panic(fmt.Sprintf("Conflict alias %s not registered", alias))
}

return conflict
}

func (t *TestFramework) ConflictSet(alias string) *ConflictSet[TestID, TestID, votes.MockedVotePower] {
conflictSet, ok := t.conflictSetsByAlias[alias]
if !ok {
panic(fmt.Sprintf("ConflictSet alias %s not registered", alias))
}

return conflictSet
}

func (t *TestFramework) Weight() *weight.Weight {
return weight.New(t.Weights)
}

func (t *TestFramework) UpdateConflictParents(conflictAlias string, addedParentID string, removedParentIDs ...string) bool {
return t.ConflictDAG.UpdateConflictParents(NewTestID(conflictAlias), NewTestID(addedParentID), t.ConflictIDs(removedParentIDs...)...)
}

func (t *TestFramework) JoinConflictSets(conflictAlias string, resourceAliases ...string) []*ConflictSet[TestID, TestID, votes.MockedVotePower] {
return lo.Values(t.ConflictDAG.JoinConflictSets(NewTestID(conflictAlias), t.ConflictSetIDs(resourceAliases...)...))
}

func (t *TestFramework) LikedInstead(conflictAliases ...string) []*Conflict[TestID, TestID, votes.MockedVotePower] {
return lo.Values(t.ConflictDAG.LikedInstead(t.ConflictIDs(conflictAliases...)...))
}

func (t *TestFramework) CastVotes(vote *vote.Vote[votes.MockedVotePower], conflictAliases ...string) error {
return t.ConflictDAG.CastVotes(vote, t.ConflictIDs(conflictAliases...)...)
}

func WithWeights(weights *sybilprotection.Weights) options.Option[TestFramework] {
return func(t *TestFramework) {
t.Weights = weights
}
}

type TestID struct {
utxo.TransactionID
}

func NewTestID(alias string) TestID {
hashedAlias := blake2b.Sum256([]byte(alias))

testID := utxo.NewTransactionID(hashedAlias[:])
testID.RegisterAlias(alias)

return TestID{testID}
}

func (id TestID) String() string {
return strings.Replace(id.TransactionID.String(), "TransactionID", "TestID", 1)
}