Skip to content

Commit

Permalink
Merge pull request #673 from iotaledger/feat/validator-tipset
Browse files Browse the repository at this point in the history
Feat: Add validator tip set
  • Loading branch information
piotrm50 authored Feb 2, 2024
2 parents c91cb76 + 3127aee commit aa124fe
Show file tree
Hide file tree
Showing 10 changed files with 432 additions and 55 deletions.
3 changes: 1 addition & 2 deletions pkg/protocol/engine/consensus/blockgadget/gadget_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ func TestBlockGadget(t *testing.T) {
tf.Events.BlockPreConfirmed.Hook(checkOrder(&expectedPreConfirmationOrder, "pre-confirmation"))
tf.Events.BlockConfirmed.Hook(checkOrder(&expectedConfirmationOrder, "confirmation"))

tf.SeatManager.AddRandomAccount("A")
tf.SeatManager.AddRandomAccount("B")
tf.SeatManager.AddRandomAccounts("A", "B")

tf.SeatManager.SetOnline("A")
tf.SeatManager.SetOnline("B")
Expand Down
84 changes: 81 additions & 3 deletions pkg/protocol/engine/tipmanager/tests/testframework.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ import (
"github.com/stretchr/testify/require"

"github.com/iotaledger/hive.go/ds"
"github.com/iotaledger/hive.go/kvstore/mapdb"
"github.com/iotaledger/hive.go/lo"
"github.com/iotaledger/iota-core/pkg/core/account"
"github.com/iotaledger/iota-core/pkg/model"
"github.com/iotaledger/iota-core/pkg/protocol/engine/blocks"
"github.com/iotaledger/iota-core/pkg/protocol/engine/tipmanager"
tipmanagerv1 "github.com/iotaledger/iota-core/pkg/protocol/engine/tipmanager/v1"
"github.com/iotaledger/iota-core/pkg/protocol/sybilprotection/seatmanager/mock"
"github.com/iotaledger/iota-core/pkg/storage/prunable/epochstore"
iotago "github.com/iotaledger/iota.go/v4"
"github.com/iotaledger/iota.go/v4/builder"
"github.com/iotaledger/iota.go/v4/tpkg"
Expand All @@ -24,6 +28,9 @@ type TestFramework struct {
tipMetadataByAlias map[string]tipmanager.TipMetadata
blocksByID map[iotago.BlockID]*blocks.Block
test *testing.T
time time.Time

manualPOA mock.ManualPOA

API iotago.API
}
Expand All @@ -35,27 +42,90 @@ func NewTestFramework(test *testing.T) *TestFramework {
blocksByID: make(map[iotago.BlockID]*blocks.Block),
test: test,
API: tpkg.ZeroCostTestAPI,
time: time.Now(),
manualPOA: *mock.NewManualPOA(iotago.SingleVersionProvider(tpkg.ZeroCostTestAPI),
epochstore.NewStore[*account.SeatedAccounts](
nil,
mapdb.NewMapDB(),
func(index iotago.EpochIndex) iotago.EpochIndex { return index },
(*account.SeatedAccounts).Bytes,
account.SeatedAccountsFromBytes),
),
}

t.blockIDsByAlias["Genesis"] = iotago.EmptyBlockID

t.Instance = tipmanagerv1.New(func(blockID iotago.BlockID) (block *blocks.Block, exists bool) {
block, exists = t.blocksByID[blockID]
return block, exists
})
}, t.manualPOA.CommitteeInSlot)

return t
}

func (t *TestFramework) Validator(alias string) iotago.AccountID {
return t.manualPOA.AccountID(alias)
}

func (t *TestFramework) AddValidators(aliases ...string) {
t.manualPOA.AddRandomAccounts(aliases...)

for _, alias := range aliases {
seat, exists := t.manualPOA.GetSeat(alias)
if !exists {
panic("seat does not exist")
}

t.Instance.AddSeat(seat)
}
}

func (t *TestFramework) AddBlock(alias string) tipmanager.TipMetadata {
t.tipMetadataByAlias[alias] = t.Instance.AddBlock(t.Block(alias))
t.tipMetadataByAlias[alias].ID().RegisterAlias(alias)

return t.tipMetadataByAlias[alias]
}

func (t *TestFramework) CreateBlock(alias string, parents map[iotago.ParentsType][]string, optBlockBuilder ...func(*builder.BasicBlockBuilder)) *blocks.Block {
func (t *TestFramework) CreateBasicBlock(alias string, parents map[iotago.ParentsType][]string, optBlockBuilder ...func(*builder.BasicBlockBuilder)) *blocks.Block {
blockBuilder := builder.NewBasicBlockBuilder(t.API)
blockBuilder.IssuingTime(time.Now())

// Make sure that blocks don't have the same timestamp.
t.time = t.time.Add(1)
blockBuilder.IssuingTime(t.time)

if strongParents, strongParentsExist := parents[iotago.StrongParentType]; strongParentsExist {
blockBuilder.StrongParents(lo.Map(strongParents, t.BlockID))
}
if weakParents, weakParentsExist := parents[iotago.WeakParentType]; weakParentsExist {
blockBuilder.WeakParents(lo.Map(weakParents, t.BlockID))
}
if shallowLikeParents, shallowLikeParentsExist := parents[iotago.ShallowLikeParentType]; shallowLikeParentsExist {
blockBuilder.ShallowLikeParents(lo.Map(shallowLikeParents, t.BlockID))
}

if len(optBlockBuilder) > 0 {
optBlockBuilder[0](blockBuilder)
}

block, err := blockBuilder.Build()
require.NoError(t.test, err)

modelBlock, err := model.BlockFromBlock(block)
require.NoError(t.test, err)

t.blocksByID[modelBlock.ID()] = blocks.NewBlock(modelBlock)
t.blockIDsByAlias[alias] = modelBlock.ID()

return t.blocksByID[modelBlock.ID()]
}

func (t *TestFramework) CreateValidationBlock(alias string, parents map[iotago.ParentsType][]string, optBlockBuilder ...func(blockBuilder *builder.ValidationBlockBuilder)) *blocks.Block {
blockBuilder := builder.NewValidationBlockBuilder(t.API)

// Make sure that blocks don't have the same timestamp.
t.time = t.time.Add(1)
blockBuilder.IssuingTime(t.time)

if strongParents, strongParentsExist := parents[iotago.StrongParentType]; strongParentsExist {
blockBuilder.StrongParents(lo.Map(strongParents, t.BlockID))
Expand Down Expand Up @@ -115,6 +185,14 @@ func (t *TestFramework) RequireStrongTips(aliases ...string) {
require.Equal(t.test, len(aliases), len(t.Instance.StrongTips()), "strongTips size does not match")
}

func (t *TestFramework) RequireValidationTips(aliases ...string) {
for _, alias := range aliases {
require.True(t.test, ds.NewSet(lo.Map(t.Instance.ValidationTips(), tipmanager.TipMetadata.ID)...).Has(t.BlockID(alias)), "validationTips does not contain block '%s'", alias)
}

require.Equal(t.test, len(aliases), len(t.Instance.ValidationTips()), "validationTips size does not match")
}

func (t *TestFramework) RequireLivenessThresholdReached(alias string, expected bool) {
require.Equal(t.test, expected, t.TipMetadata(alias).LivenessThresholdReached().Get())
}
119 changes: 113 additions & 6 deletions pkg/protocol/engine/tipmanager/tests/tipmanager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,20 @@ import (

"github.com/iotaledger/iota-core/pkg/protocol/engine/tipmanager"
iotago "github.com/iotaledger/iota.go/v4"
"github.com/iotaledger/iota.go/v4/builder"
"github.com/iotaledger/iota.go/v4/tpkg"
)

func TestTipManager(t *testing.T) {
tf := NewTestFramework(t)

tf.CreateBlock("Bernd", map[iotago.ParentsType][]string{
tf.CreateBasicBlock("Bernd", map[iotago.ParentsType][]string{
iotago.StrongParentType: {"Genesis"},
})
tf.CreateBlock("Bernd1", map[iotago.ParentsType][]string{
tf.CreateBasicBlock("Bernd1", map[iotago.ParentsType][]string{
iotago.StrongParentType: {"Bernd"},
})
tf.CreateBlock("Bernd1.1", map[iotago.ParentsType][]string{
tf.CreateBasicBlock("Bernd1.1", map[iotago.ParentsType][]string{
iotago.StrongParentType: {"Bernd"},
})

Expand All @@ -33,13 +35,13 @@ func TestTipManager(t *testing.T) {
func Test_Orphanage(t *testing.T) {
tf := NewTestFramework(t)

tf.CreateBlock("A", map[iotago.ParentsType][]string{
tf.CreateBasicBlock("A", map[iotago.ParentsType][]string{
iotago.StrongParentType: {"Genesis"},
})
tf.CreateBlock("B", map[iotago.ParentsType][]string{
tf.CreateBasicBlock("B", map[iotago.ParentsType][]string{
iotago.StrongParentType: {"Genesis"},
})
tf.CreateBlock("C", map[iotago.ParentsType][]string{
tf.CreateBasicBlock("C", map[iotago.ParentsType][]string{
iotago.StrongParentType: {"A", "B"},
})

Expand All @@ -56,3 +58,108 @@ func Test_Orphanage(t *testing.T) {
blockB.LivenessThresholdReached().Trigger()
tf.RequireStrongTips("A")
}

func Test_ValidationTips(t *testing.T) {
tf := NewTestFramework(t)

tf.AddValidators("validatorA", "validatorB")

{
tf.CreateBasicBlock("1", map[iotago.ParentsType][]string{
iotago.StrongParentType: {"Genesis"},
})
tf.CreateBasicBlock("2", map[iotago.ParentsType][]string{
iotago.StrongParentType: {"Genesis"},
})

tf.AddBlock("1").TipPool().Set(tipmanager.StrongTipPool)
tf.AddBlock("2").TipPool().Set(tipmanager.StrongTipPool)

tf.RequireStrongTips("1", "2")
}

// Add validation tip for validatorA.
{
tf.CreateValidationBlock("3", map[iotago.ParentsType][]string{
iotago.StrongParentType: {"2"},
}, func(blockBuilder *builder.ValidationBlockBuilder) {
blockBuilder.Sign(tf.Validator("validatorA"), tpkg.RandEd25519PrivateKey())
})

tf.AddBlock("3").TipPool().Set(tipmanager.StrongTipPool)

tf.RequireValidationTips("3")
tf.RequireStrongTips("1", "3")
}

// Add validation tip for validatorB.
{
tf.CreateValidationBlock("4", map[iotago.ParentsType][]string{
iotago.StrongParentType: {"1"},
}, func(blockBuilder *builder.ValidationBlockBuilder) {
blockBuilder.Sign(tf.Validator("validatorB"), tpkg.RandEd25519PrivateKey())
})

tf.AddBlock("4").TipPool().Set(tipmanager.StrongTipPool)

tf.RequireValidationTips("3", "4")
tf.RequireStrongTips("3", "4")
}

// Add basic blocks in the future cone of the validation tips, referencing both existing validation tips.
{
tf.CreateBasicBlock("5", map[iotago.ParentsType][]string{
iotago.StrongParentType: {"3", "4"},
})

tf.AddBlock("5").TipPool().Set(tipmanager.StrongTipPool)

tf.RequireValidationTips("5")
tf.RequireStrongTips("5")
}

// Add basic blocks in the future cone of the validation tips.
{
tf.CreateBasicBlock("6", map[iotago.ParentsType][]string{
iotago.StrongParentType: {"3"},
})
tf.CreateBasicBlock("7", map[iotago.ParentsType][]string{
iotago.StrongParentType: {"4"},
})

tf.AddBlock("6").TipPool().Set(tipmanager.StrongTipPool)
tf.AddBlock("7").TipPool().Set(tipmanager.StrongTipPool)

tf.RequireValidationTips("5", "6", "7")
tf.RequireStrongTips("5", "6", "7")
}

// A newer validation block replaces the previous validation tip of that validator.
{
tf.CreateValidationBlock("8", map[iotago.ParentsType][]string{
iotago.StrongParentType: {"6"},
}, func(blockBuilder *builder.ValidationBlockBuilder) {
blockBuilder.Sign(tf.Validator("validatorB"), tpkg.RandEd25519PrivateKey())
})

tf.AddBlock("8").TipPool().Set(tipmanager.StrongTipPool)

tf.RequireValidationTips("5", "8")
tf.RequireStrongTips("5", "7", "8")
}

// A newer validation block (with parallel past cone) still becomes a validation tip and overwrites
// the previous validation tip of that validator.
{
tf.CreateValidationBlock("9", map[iotago.ParentsType][]string{
iotago.StrongParentType: {"Genesis"},
}, func(blockBuilder *builder.ValidationBlockBuilder) {
blockBuilder.Sign(tf.Validator("validatorA"), tpkg.RandEd25519PrivateKey())
})

tf.AddBlock("9").TipPool().Set(tipmanager.StrongTipPool)

tf.RequireValidationTips("8", "9")
tf.RequireStrongTips("5", "7", "8", "9")
}
}
10 changes: 10 additions & 0 deletions pkg/protocol/engine/tipmanager/tipmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package tipmanager

import (
"github.com/iotaledger/hive.go/runtime/module"
"github.com/iotaledger/iota-core/pkg/core/account"
"github.com/iotaledger/iota-core/pkg/protocol/engine/blocks"
iotago "github.com/iotaledger/iota.go/v4"
)
Expand All @@ -27,6 +28,15 @@ type TipManager interface {
// OnBlockAdded registers a callback that is triggered whenever a new Block was added to the TipManager.
OnBlockAdded(handler func(block TipMetadata)) (unsubscribe func())

// AddSeat adds a validator seat to the tracking of the TipManager.
AddSeat(seat account.SeatIndex)

// RemoveSeat removes a validator seat from the tracking of the TipManager.
RemoveSeat(seat account.SeatIndex)

// ValidationTips returns the validation tips of the TipManager (with an optional limit).
ValidationTips(optAmount ...int) []TipMetadata

// StrongTips returns the strong tips of the TipManager (with an optional limit).
StrongTips(optAmount ...int) []TipMetadata

Expand Down
10 changes: 9 additions & 1 deletion pkg/protocol/engine/tipmanager/v1/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,29 @@ import (
"github.com/iotaledger/hive.go/runtime/event"
"github.com/iotaledger/hive.go/runtime/module"
"github.com/iotaledger/hive.go/runtime/workerpool"
"github.com/iotaledger/iota-core/pkg/core/account"
"github.com/iotaledger/iota-core/pkg/protocol/engine"
"github.com/iotaledger/iota-core/pkg/protocol/engine/tipmanager"
iotago "github.com/iotaledger/iota.go/v4"
)

// NewProvider creates a new TipManager provider, that can be used to inject the component into an engine.
func NewProvider() module.Provider[*engine.Engine, tipmanager.TipManager] {
return module.Provide(func(e *engine.Engine) tipmanager.TipManager {
t := New(e.BlockCache.Block)
t := New(e.BlockCache.Block, e.SybilProtection.SeatManager().CommitteeInSlot)

e.Constructed.OnTrigger(func() {
tipWorker := e.Workers.CreatePool("AddTip", workerpool.WithWorkerCount(2))

e.Events.Scheduler.BlockScheduled.Hook(lo.Void(t.AddBlock), event.WithWorkerPool(tipWorker))
e.Events.Scheduler.BlockSkipped.Hook(lo.Void(t.AddBlock), event.WithWorkerPool(tipWorker))
e.BlockCache.Evict.Hook(t.Evict)

e.Events.SeatManager.OnlineCommitteeSeatAdded.Hook(func(index account.SeatIndex, _ iotago.AccountID) {
t.AddSeat(index)
})
e.Events.SeatManager.OnlineCommitteeSeatRemoved.Hook(t.RemoveSeat)

e.Events.TipManager.BlockAdded.LinkTo(t.blockAdded)

t.TriggerInitialized()
Expand Down
Loading

0 comments on commit aa124fe

Please sign in to comment.