Skip to content
This repository has been archived by the owner on Jan 24, 2025. It is now read-only.

Feat: Add validator tip set #673

Merged
merged 14 commits into from
Feb 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Loading