Skip to content

Commit

Permalink
Merge pull request #887 from iotaledger/feat/spenddag-preacceptance
Browse files Browse the repository at this point in the history
Feat: SpendDAG pre-acceptance
  • Loading branch information
piotrm50 authored Mar 30, 2024
2 parents d5e0427 + a1ae89b commit ec53292
Show file tree
Hide file tree
Showing 10 changed files with 167 additions and 44 deletions.
15 changes: 15 additions & 0 deletions pkg/core/weight/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ type Value struct {
// validatorsWeight is the second tier which tracks weight in a non-cumulative manner (BFT style).
validatorsWeight int64

// attestorsWeight is the third tier which tracks weight in a non-cumulative manner (BFT style).
attestorsWeight int64

// acceptanceState is the final tier which determines the decision of the spender.
acceptanceState acceptance.State
}
Expand Down Expand Up @@ -55,6 +58,18 @@ func (v Value) SetValidatorsWeight(weight int64) Value {
return v
}

// AttestorsWeight returns the weight of the validators.
func (v Value) AttestorsWeight() int64 {
return v.attestorsWeight
}

// SetAttestorsWeight sets the weight of the attestors and returns the new Value.
func (v Value) SetAttestorsWeight(weight int64) Value {
v.attestorsWeight = weight

return v
}

// AcceptanceState returns the acceptance state of the Value.
func (v Value) AcceptanceState() acceptance.State {
return v.acceptanceState
Expand Down
57 changes: 55 additions & 2 deletions pkg/core/weight/weight.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ type Weight struct {
// Voters is the set of voters contributing to the weight
Voters ds.Set[account.SeatIndex]

// Attestors is the set of attestors contributing to the weight.
Attestors ds.Set[account.SeatIndex]

// value is the current weight Value.
value Value

Expand All @@ -27,8 +30,9 @@ type Weight struct {
// New creates a new Weight instance.
func New() *Weight {
w := &Weight{
Voters: ds.NewSet[account.SeatIndex](),
OnUpdate: event.New1[Value](),
Voters: ds.NewSet[account.SeatIndex](),
Attestors: ds.NewSet[account.SeatIndex](),
OnUpdate: event.New1[Value](),
}

return w
Expand Down Expand Up @@ -103,6 +107,41 @@ func (w *Weight) DeleteVoter(seat account.SeatIndex) *Weight {
return w
}

// AddAttestor adds the given voter to the list of Attestors, updates the weight and returns the Weight (for chaining).
func (w *Weight) AddAttestor(seat account.SeatIndex) *Weight {
if added := w.Attestors.Add(seat); added {
if newValue, valueUpdated := w.updateAttestorsWeight(); valueUpdated {
w.OnUpdate.Trigger(newValue)
}
}

return w
}

// DeleteAttestor removes the given voter from the list of Attestors, updates the weight and returns the Weight (for chaining).
func (w *Weight) DeleteAttestor(seat account.SeatIndex) *Weight {
if deleted := w.Attestors.Delete(seat); deleted {
if newValue, valueUpdated := w.updateAttestorsWeight(); valueUpdated {
w.OnUpdate.Trigger(newValue)
}
}

return w
}

// ResetAttestors removes all voters from the list of Attestors, updates the weight and returns the Weight (for chaining).
func (w *Weight) ResetAttestors() *Weight {
if w.Attestors.Size() >= 1 {
w.Attestors.Clear()

if newValue, valueUpdated := w.updateAttestorsWeight(); valueUpdated {
w.OnUpdate.Trigger(newValue)
}
}

return w
}

func (w *Weight) updateValidatorsWeight() (Value, bool) {
w.mutex.Lock()
defer w.mutex.Unlock()
Expand All @@ -117,6 +156,20 @@ func (w *Weight) updateValidatorsWeight() (Value, bool) {
return w.value, false
}

func (w *Weight) updateAttestorsWeight() (Value, bool) {
w.mutex.Lock()
defer w.mutex.Unlock()

newAttestorsWeight := int64(w.Attestors.Size())
if w.value.AttestorsWeight() != newAttestorsWeight {
w.value = w.value.SetAttestorsWeight(newAttestorsWeight)

return w.value, true
}

return w.value, false
}

// AcceptanceState returns the acceptance state of the weight.
func (w *Weight) AcceptanceState() acceptance.State {
w.mutex.RLock()
Expand Down
6 changes: 3 additions & 3 deletions pkg/protocol/chains.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,15 +213,15 @@ func attachEngineLogs(instance *engine.Engine) func() {
}).Unhook,

events.SeatManager.OnlineCommitteeSeatAdded.Hook(func(seat account.SeatIndex, accountID iotago.AccountID) {
instance.LogTrace("SybilProtection.OnlineCommitteeSeatAdded", "seat", seat, "accountID", accountID)
instance.LogInfo("SybilProtection.OnlineCommitteeSeatAdded", "seat", seat, "accountID", accountID)
}).Unhook,

events.SeatManager.OnlineCommitteeSeatRemoved.Hook(func(seat account.SeatIndex) {
instance.LogTrace("SybilProtection.OnlineCommitteeSeatRemoved", "seat", seat)
instance.LogInfo("SybilProtection.OnlineCommitteeSeatRemoved", "seat", seat)
}).Unhook,

events.SybilProtection.CommitteeSelected.Hook(func(committee *account.SeatedAccounts, epoch iotago.EpochIndex) {
instance.LogTrace("SybilProtection.CommitteeSelected", "epoch", epoch, "committee", committee.IDs())
instance.LogInfo("SybilProtection.CommitteeSelected", "epoch", epoch, "committee", committee.IDs())
}).Unhook,

events.SpendDAG.SpenderCreated.Hook(func(conflictID iotago.TransactionID) {
Expand Down
15 changes: 13 additions & 2 deletions pkg/protocol/engine/mempool/spenddag/spenddagv1/spender.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,13 @@ func NewSpender[SpenderID, ResourceID spenddag.IDType, VoteRank spenddag.VoteRan
c.preferredInstead = c

c.unhookAcceptanceMonitoring = c.Weight.OnUpdate.Hook(func(value weight.Value) {
if value.AcceptanceState().IsPending() && value.ValidatorsWeight() >= c.acceptanceThreshold() {
if threshold := c.acceptanceThreshold(); value.AcceptanceState().IsPending() && value.ValidatorsWeight() >= threshold && value.AttestorsWeight() >= threshold {
c.setAcceptanceState(acceptance.Accepted)
}
}).Unhook

// in case the initial weight is enough to accept the spend, accept it immediately
if threshold := c.acceptanceThreshold(); initialWeight.Value().ValidatorsWeight() >= threshold {
if threshold := c.acceptanceThreshold(); initialWeight.AcceptanceState().IsPending() && initialWeight.Value().ValidatorsWeight() >= threshold && initialWeight.Value().AttestorsWeight() >= threshold {
c.setAcceptanceState(acceptance.Accepted)
}

Expand Down Expand Up @@ -219,6 +219,17 @@ func (c *Spender[SpenderID, ResourceID, VoteRank]) ApplyVote(vote *vote.Vote[Vot
// update the latest vote
c.LatestVotes.Set(vote.Voter, vote)

// track attestors when we reach the acceptance threshold
if c.Weight.Value().ValidatorsWeight() >= c.acceptanceThreshold() {
if vote.IsLiked() {
c.Weight.AddAttestor(vote.Voter)
} else {
c.Weight.DeleteAttestor(vote.Voter)
}
} else {
c.Weight.ResetAttestors()
}

// abort if the vote does not change the opinion of the validator
if exists && latestVote.IsLiked() == vote.IsLiked() {
return
Expand Down
20 changes: 19 additions & 1 deletion pkg/protocol/engine/mempool/spenddag/tests/tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@ func CreateSpendWithoutMembers(t *testing.T, tf *Framework) {
require.NoError(t, tf.CastVotes("nodeID1", 1, "spender1"))
require.NoError(t, tf.CastVotes("nodeID2", 1, "spender1"))
require.NoError(t, tf.CastVotes("nodeID3", 1, "spender1"))

require.NoError(t, tf.CastVotes("nodeID1", 2, "spender1"))
require.NoError(t, tf.CastVotes("nodeID2", 2, "spender1"))
require.NoError(t, tf.CastVotes("nodeID3", 2, "spender1"))
tf.Assert.LikedInstead([]string{"spender1"})
tf.Assert.Accepted("spender1")
}
Expand Down Expand Up @@ -188,6 +190,9 @@ func SpendAcceptance(t *testing.T, tf *Framework) {
require.NoError(t, tf.CastVotes("nodeID2", 1, "spender4"))
require.NoError(t, tf.CastVotes("nodeID3", 1, "spender4"))

require.NoError(t, tf.CastVotes("nodeID1", 2, "spender4"))
require.NoError(t, tf.CastVotes("nodeID2", 2, "spender4"))
require.NoError(t, tf.CastVotes("nodeID3", 2, "spender4"))
tf.Assert.LikedInstead([]string{"spender1"})
tf.Assert.LikedInstead([]string{"spender2"}, "spender1")
tf.Assert.LikedInstead([]string{"spender3"}, "spender4")
Expand Down Expand Up @@ -224,6 +229,10 @@ func CastVotes(t *testing.T, tf *Framework) {
require.NoError(t, tf.CastVotes("nodeID1", 1, "spender2"))
require.NoError(t, tf.CastVotes("nodeID2", 1, "spender2"))
require.NoError(t, tf.CastVotes("nodeID3", 1, "spender2"))
require.NoError(t, tf.CastVotes("nodeID4", 2, "spender2"))
require.NoError(t, tf.CastVotes("nodeID2", 2, "spender2"))
require.NoError(t, tf.CastVotes("nodeID3", 2, "spender2"))

tf.Assert.LikedInstead([]string{"spender1"}, "spender2")

tf.Assert.Accepted("spender2")
Expand Down Expand Up @@ -317,6 +326,9 @@ func CastVotesAcceptance(t *testing.T, tf *Framework) {
require.NoError(t, tf.CastVotes("nodeID1", 1, "spender3"))
require.NoError(t, tf.CastVotes("nodeID2", 1, "spender3"))
require.NoError(t, tf.CastVotes("nodeID3", 1, "spender3"))
require.NoError(t, tf.CastVotes("nodeID4", 2, "spender3"))
require.NoError(t, tf.CastVotes("nodeID2", 2, "spender3"))
require.NoError(t, tf.CastVotes("nodeID3", 2, "spender3"))
tf.Assert.LikedInstead([]string{"spender1"})
tf.Assert.Accepted("spender1")
tf.Assert.Rejected("spender2")
Expand Down Expand Up @@ -410,6 +422,9 @@ func EvictAcceptedSpender(t *testing.T, tf *Framework) {
require.NoError(t, tf.CastVotes("nodeID1", 1, "spender2"))
require.NoError(t, tf.CastVotes("nodeID2", 1, "spender2"))
require.NoError(t, tf.CastVotes("nodeID3", 1, "spender2"))
require.NoError(t, tf.CastVotes("nodeID1", 2, "spender2"))
require.NoError(t, tf.CastVotes("nodeID2", 2, "spender2"))
require.NoError(t, tf.CastVotes("nodeID3", 2, "spender2"))
tf.Assert.LikedInstead([]string{"spender1"}, "spender2")

tf.Assert.Accepted("spender2")
Expand Down Expand Up @@ -478,6 +493,9 @@ func EvictRejectedSpender(t *testing.T, tf *Framework) {
require.NoError(t, tf.CastVotes("nodeID1", 1, "spender2"))
require.NoError(t, tf.CastVotes("nodeID2", 1, "spender2"))
require.NoError(t, tf.CastVotes("nodeID3", 1, "spender2"))
require.NoError(t, tf.CastVotes("nodeID1", 2, "spender2"))
require.NoError(t, tf.CastVotes("nodeID2", 2, "spender2"))
require.NoError(t, tf.CastVotes("nodeID3", 2, "spender2"))
tf.Assert.LikedInstead([]string{"spender1"}, "spender2")

tf.Assert.Rejected("spender1")
Expand Down
1 change: 0 additions & 1 deletion pkg/protocol/engine/mempool/v1/mempool.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,6 @@ func (m *MemPool[VoteRank]) AttachSignedTransaction(signedTransaction mempool.Si

if isNewTransaction {
m.transactionAttached.Trigger(storedSignedTransaction.transactionMetadata)

m.solidifyInputs(storedSignedTransaction.transactionMetadata)
}
}
Expand Down
52 changes: 36 additions & 16 deletions pkg/tests/booker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,8 @@ func Test_DoubleSpend(t *testing.T) {
{
ts.IssueValidationBlockWithHeaderOptions("block6", node2, mock.WithStrongParents(ts.BlockIDs("block3", "block4")...), mock.WithShallowLikeParents(ts.BlockID("block2")))
ts.IssueValidationBlockWithHeaderOptions("block7", node1, mock.WithStrongParents(ts.BlockIDs("block6")...))
ts.IssueValidationBlockWithHeaderOptions("block8", node2, mock.WithStrongParents(ts.BlockIDs("block7")...))
ts.IssueValidationBlockWithHeaderOptions("block9", node1, mock.WithStrongParents(ts.BlockIDs("block8")...))

ts.AssertBlocksInCacheConflicts(map[*blocks.Block][]string{
ts.Block("block6"): {"tx2"},
Expand All @@ -223,7 +225,7 @@ func Test_MultipleAttachments(t *testing.T) {

blocksConflicts := make(map[*blocks.Block][]string)

// Create a transaction and issue it from both nodes, so that the conflict is accepted, but no attachment is included yet.
// Create a transaction and issue it from both nodes, so that the spend is accepted, but no attachment is included yet.
{
tx1 := wallet.CreateBasicOutputsEquallyFromInput("tx1", 2, "Genesis:0")

Expand All @@ -234,17 +236,23 @@ func Test_MultipleAttachments(t *testing.T) {
ts.IssueValidationBlockWithHeaderOptions("B.1.1", nodeB, mock.WithStrongParents(ts.BlockID("B.1")))

nodeA.Wait()
ts.IssueValidationBlockWithHeaderOptions("A.2", nodeA, mock.WithStrongParents(ts.BlockID("B.1.1")))
ts.IssueValidationBlockWithHeaderOptions("B.2", nodeB, mock.WithStrongParents(ts.BlockID("A.1.1")))
ts.IssueValidationBlockWithHeaderOptions("A.2.1", nodeA, mock.WithStrongParents(ts.BlockID("B.1.1")))
ts.IssueValidationBlockWithHeaderOptions("B.2.1", nodeB, mock.WithStrongParents(ts.BlockID("A.1.1")))

// Cast more votes to accept the transaction.
ts.IssueValidationBlockWithHeaderOptions("A.2.2", nodeA, mock.WithStrongParents(ts.BlockID("A.1")))
ts.IssueValidationBlockWithHeaderOptions("B.2.2", nodeB, mock.WithStrongParents(ts.BlockID("B.1")))
ts.IssueValidationBlockWithHeaderOptions("A.3.2", nodeA, mock.WithStrongParents(ts.BlockID("B.2.2")))
ts.IssueValidationBlockWithHeaderOptions("B.3.2", nodeB, mock.WithStrongParents(ts.BlockID("A.2.2")))

ts.AssertBlocksInCachePreAccepted(ts.Blocks("A.1", "B.1"), true, ts.Nodes()...)
ts.AssertBlocksInCacheAccepted(ts.Blocks("A.1", "B.1"), false, ts.Nodes()...)

ts.AssertBlocksInCacheConflicts(lo.MergeMaps(blocksConflicts, map[*blocks.Block][]string{
ts.Block("A.1"): {"tx1"},
ts.Block("B.1"): {"tx1"},
ts.Block("A.2"): {"tx1"},
ts.Block("B.2"): {"tx1"},
ts.Block("A.1"): {"tx1"},
ts.Block("B.1"): {"tx1"},
ts.Block("A.2.1"): {"tx1"},
ts.Block("B.2.1"): {"tx1"},
}), ts.Nodes()...)
ts.AssertTransactionInCacheConflicts(map[*iotago.Transaction][]string{
wallet.Transaction("tx1"): {"tx1"},
Expand All @@ -262,13 +270,19 @@ func Test_MultipleAttachments(t *testing.T) {
ts.IssueValidationBlockWithHeaderOptions("B.3", nodeB, mock.WithStrongParents(ts.BlockID("A.3.1")))
ts.IssueValidationBlockWithHeaderOptions("A.4", nodeA, mock.WithStrongParents(ts.BlockID("B.3")))

// Issue attestor votes.
ts.IssueValidationBlockWithHeaderOptions("B.4", nodeB, mock.WithStrongParents(ts.BlockID("A.3")))
ts.IssueValidationBlockWithHeaderOptions("A.5", nodeA, mock.WithStrongParents(ts.BlockID("B.4")))

ts.AssertBlocksInCachePreAccepted(ts.Blocks("A.3"), true, ts.Nodes()...)

ts.IssueValidationBlockWithHeaderOptions("B.4", nodeB, mock.WithStrongParents(ts.BlockIDs("B.3", "A.4")...))
ts.IssueValidationBlockWithHeaderOptions("A.5", nodeA, mock.WithStrongParents(ts.BlockIDs("B.3", "A.4")...))
ts.IssueValidationBlockWithHeaderOptions("B.5", nodeB, mock.WithStrongParents(ts.BlockIDs("B.3", "A.4")...))
ts.IssueValidationBlockWithHeaderOptions("A.6", nodeA, mock.WithStrongParents(ts.BlockIDs("B.3", "A.4")...))
ts.IssueValidationBlockWithHeaderOptions("B.6", nodeB, mock.WithStrongParents(ts.BlockIDs("B.3", "A.4")...))
ts.IssueValidationBlockWithHeaderOptions("A.7", nodeA, mock.WithStrongParents(ts.BlockIDs("B.3", "A.4")...))

ts.AssertBlocksInCachePreAccepted(ts.Blocks("B.3", "A.4"), true, ts.Nodes()...)
ts.AssertBlocksInCachePreAccepted(ts.Blocks("B.4", "A.5"), false, ts.Nodes()...)
ts.AssertBlocksInCachePreAccepted(ts.Blocks("B.3", "A.4", "B.4"), true, ts.Nodes()...)
ts.AssertBlocksInCachePreAccepted(ts.Blocks("A.5", "B.5", "A.6", "B.6", "A.7"), false, ts.Nodes()...)
ts.AssertBlocksInCacheAccepted(ts.Blocks("A.3"), true, ts.Nodes()...)

ts.AssertTransactionsInCacheBooked(wallet.Transactions("tx1", "tx2"), true, ts.Nodes()...)
Expand All @@ -278,8 +292,12 @@ func Test_MultipleAttachments(t *testing.T) {
ts.Block("A.3"): {"tx2"},
ts.Block("B.3"): {"tx2"},
ts.Block("A.4"): {"tx2"},
ts.Block("A.5"): {},
ts.Block("B.4"): {},
ts.Block("A.5"): {"tx2"},
ts.Block("A.6"): {},
ts.Block("B.4"): {"tx2"},
ts.Block("B.5"): {"tx2"},
ts.Block("A.7"): {},
ts.Block("B.6"): {},
}), ts.Nodes()...)
ts.AssertTransactionInCacheConflicts(map[*iotago.Transaction][]string{
wallet.Transaction("tx1"): {"tx1"},
Expand All @@ -290,13 +308,13 @@ func Test_MultipleAttachments(t *testing.T) {

// Issue a block that includes tx1, and make sure that tx2 is accepted as well as a consequence.
{
ts.IssueValidationBlockWithHeaderOptions("A.6", nodeA, mock.WithStrongParents(ts.BlockIDs("A.2", "B.2")...))
ts.IssueValidationBlockWithHeaderOptions("B.5", nodeB, mock.WithStrongParents(ts.BlockIDs("A.2", "B.2")...))
ts.IssueValidationBlockWithHeaderOptions("A.6", nodeA, mock.WithStrongParents(ts.BlockIDs("A.2.1", "B.2.1")...))
ts.IssueValidationBlockWithHeaderOptions("B.5", nodeB, mock.WithStrongParents(ts.BlockIDs("A.2.1", "B.2.1")...))

ts.IssueValidationBlockWithHeaderOptions("A.7", nodeA, mock.WithStrongParents(ts.BlockIDs("A.6", "B.5")...))
ts.IssueValidationBlockWithHeaderOptions("B.6", nodeB, mock.WithStrongParents(ts.BlockIDs("A.6", "B.5")...))

ts.AssertBlocksInCachePreAccepted(ts.Blocks("A.2", "B.2", "A.6", "B.5"), true, ts.Nodes()...)
ts.AssertBlocksInCachePreAccepted(ts.Blocks("A.2.1", "B.2.1", "A.6", "B.5"), true, ts.Nodes()...)
ts.AssertBlocksInCacheAccepted(ts.Blocks("A.1", "B.1"), true, ts.Nodes()...)

ts.AssertBlocksInCachePreAccepted(ts.Blocks("A.7", "B.6"), false, ts.Nodes()...)
Expand Down Expand Up @@ -396,6 +414,8 @@ func Test_SpendRejectedCommittedRace(t *testing.T) {
{
ts.IssueValidationBlockWithHeaderOptions("block2.3", node2, mock.WithSlotCommitment(genesisCommitment), mock.WithStrongParents(ts.BlockIDs("block2.2")...))
ts.IssueValidationBlockWithHeaderOptions("block2.4", node1, mock.WithSlotCommitment(genesisCommitment), mock.WithStrongParents(ts.BlockIDs("block2.3")...))
ts.IssueValidationBlockWithHeaderOptions("block2.5", node2, mock.WithSlotCommitment(genesisCommitment), mock.WithStrongParents(ts.BlockIDs("block2.4")...))
ts.IssueValidationBlockWithHeaderOptions("block2.6", node1, mock.WithSlotCommitment(genesisCommitment), mock.WithStrongParents(ts.BlockIDs("block2.5")...))

ts.AssertBlocksInCacheConflicts(map[*blocks.Block][]string{
ts.Block("block2.3"): {"tx2"},
Expand Down
Loading

0 comments on commit ec53292

Please sign in to comment.