From a30f90535b8654c835258430ef67621e173cbcf8 Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Wed, 13 Mar 2024 16:58:57 +0100 Subject: [PATCH 01/40] Fix: Orphanage problem --- pkg/protocol/engine/blocks/block.go | 41 +++++++++++++++++ .../engine/booker/inmemorybooker/booker.go | 46 +++++++++++++------ .../mempool/signed_transaction_metadata.go | 2 + pkg/protocol/engine/mempool/state_metadata.go | 4 ++ .../mempool/v1/signed_transaction_metadata.go | 4 ++ .../engine/mempool/v1/state_metadata.go | 33 ++++++++----- 6 files changed, 104 insertions(+), 26 deletions(-) diff --git a/pkg/protocol/engine/blocks/block.go b/pkg/protocol/engine/blocks/block.go index 0d204e660..b81274c37 100644 --- a/pkg/protocol/engine/blocks/block.go +++ b/pkg/protocol/engine/blocks/block.go @@ -2,6 +2,7 @@ package blocks import ( "fmt" + "sync/atomic" "time" "github.com/iotaledger/hive.go/ds" @@ -12,10 +13,15 @@ import ( "github.com/iotaledger/hive.go/stringify" "github.com/iotaledger/iota-core/pkg/core/account" "github.com/iotaledger/iota-core/pkg/model" + "github.com/iotaledger/iota-core/pkg/protocol/engine/mempool" iotago "github.com/iotaledger/iota.go/v4" ) type Block struct { + AllParentsBooked reactive.Event + AllDependenciesReady reactive.Event + SignedTransactionMetadata reactive.Variable[mempool.SignedTransactionMetadata] + // BlockDAG block missing bool missingBlockID iotago.BlockID @@ -77,6 +83,10 @@ func (r *rootBlock) String() string { // NewBlock creates a new Block with the given options. func NewBlock(modelBlock *model.Block) *Block { return &Block{ + AllParentsBooked: reactive.NewEvent(), + AllDependenciesReady: reactive.NewEvent(), + SignedTransactionMetadata: reactive.NewVariable[mempool.SignedTransactionMetadata](), + witnesses: ds.NewSet[account.SeatIndex](), spenderIDs: ds.NewSet[iotago.TransactionID](), payloadSpenderIDs: ds.NewSet[iotago.TransactionID](), @@ -95,6 +105,10 @@ func NewBlock(modelBlock *model.Block) *Block { func NewRootBlock(blockID iotago.BlockID, commitmentID iotago.CommitmentID, issuingTime time.Time) *Block { b := &Block{ + AllParentsBooked: reactive.NewEvent(), + AllDependenciesReady: reactive.NewEvent(), + SignedTransactionMetadata: reactive.NewVariable[mempool.SignedTransactionMetadata](), + witnesses: ds.NewSet[account.SeatIndex](), spenderIDs: ds.NewSet[iotago.TransactionID](), payloadSpenderIDs: ds.NewSet[iotago.TransactionID](), @@ -117,6 +131,8 @@ func NewRootBlock(blockID iotago.BlockID, commitmentID iotago.CommitmentID, issu } // This should be true since we commit and evict on acceptance. + b.AllParentsBooked.Set(true) + b.AllDependenciesReady.Set(true) b.solid.Set(true) b.booked.Set(true) b.weightPropagated.Set(true) @@ -128,6 +144,10 @@ func NewRootBlock(blockID iotago.BlockID, commitmentID iotago.CommitmentID, issu func NewMissingBlock(blockID iotago.BlockID) *Block { return &Block{ + AllParentsBooked: reactive.NewEvent(), + AllDependenciesReady: reactive.NewEvent(), + SignedTransactionMetadata: reactive.NewVariable[mempool.SignedTransactionMetadata](), + missing: true, missingBlockID: blockID, witnesses: ds.NewSet[account.SeatIndex](), @@ -690,3 +710,24 @@ func (b *Block) ModelBlock() *model.Block { func (b *Block) WorkScore() iotago.WorkScore { return b.workScore } + +func (b *Block) WaitForUnreferencedOutputs(unreferencedOutputs ds.Set[mempool.StateMetadata]) { + var unreferencedOutputCount atomic.Int32 + unreferencedOutputCount.Store(int32(unreferencedOutputs.Size())) + + unreferencedOutputs.Range(func(unreferencedOutput mempool.StateMetadata) { + dependencyReady := false + + unreferencedOutput.OnAccepted(func() { + unreferencedOutput.OnEarliestIncludedAttachmentUpdated(func(_, earliestIncludedAttachment iotago.BlockID) { + if !dependencyReady && earliestIncludedAttachment.Slot() <= b.ID().Slot() { + dependencyReady = true + + if unreferencedOutputCount.Add(-1) == 0 { + b.AllDependenciesReady.Trigger() + } + } + }) + }) + }) +} diff --git a/pkg/protocol/engine/booker/inmemorybooker/booker.go b/pkg/protocol/engine/booker/inmemorybooker/booker.go index 888fe5a34..a47747f53 100644 --- a/pkg/protocol/engine/booker/inmemorybooker/booker.go +++ b/pkg/protocol/engine/booker/inmemorybooker/booker.go @@ -94,26 +94,19 @@ func (b *Booker) Init(ledger ledger.Ledger, loadBlockFromStorage func(iotago.Blo // Queue checks if payload is solid and then sets up the block to react to its parents. func (b *Booker) Queue(block *blocks.Block) error { signedTransactionMetadata, containsTransaction := b.ledger.AttachTransaction(block) - if !containsTransaction { b.setupBlock(block) - return nil - } - if signedTransactionMetadata == nil { + return nil + } else if signedTransactionMetadata == nil { return ierrors.Errorf("transaction in %s was not attached", block.ID()) } + block.SignedTransactionMetadata.Set(signedTransactionMetadata) // Based on the assumption that we always fork and the UTXO and Tangle past cones are always fully known. signedTransactionMetadata.OnSignaturesValid(func() { transactionMetadata := signedTransactionMetadata.TransactionMetadata() - if orphanedSlot, isOrphaned := transactionMetadata.OrphanedSlot(); isOrphaned && orphanedSlot <= block.SlotCommitmentID().Slot() { - block.SetInvalid() - - return - } - transactionMetadata.OnBooked(func() { block.SetPayloadSpenderIDs(transactionMetadata.SpenderIDs()) b.setupBlock(block) @@ -135,6 +128,8 @@ func (b *Booker) Queue(block *blocks.Block) error { func (b *Booker) Reset() { /* nothing to reset but comply with interface */ } func (b *Booker) setupBlock(block *blocks.Block) { + referencedOutputs := ds.NewSet[mempool.StateMetadata]() + var unbookedParentsCount atomic.Int32 unbookedParentsCount.Store(int32(len(block.Parents()))) @@ -147,12 +142,12 @@ func (b *Booker) setupBlock(block *blocks.Block) { } parentBlock.Booked().OnUpdateOnce(func(_ bool, _ bool) { + if parentTransactionMetadata := parentBlock.SignedTransactionMetadata.Get(); parentTransactionMetadata != nil { + referencedOutputs.AddAll(parentTransactionMetadata.TransactionMetadata().Outputs()) + } + if unbookedParentsCount.Add(-1) == 0 { - if err := b.book(block); err != nil { - if block.SetInvalid() { - b.events.BlockInvalid.Trigger(block, ierrors.Wrap(err, "failed to book block")) - } - } + block.AllParentsBooked.Trigger() } }) @@ -162,6 +157,27 @@ func (b *Booker) setupBlock(block *blocks.Block) { } }) }) + + block.AllParentsBooked.OnTrigger(func() { + if signedTransactionMetadata := block.SignedTransactionMetadata.Get(); signedTransactionMetadata == nil || signedTransactionMetadata.SignaturesInvalid() != nil { + block.AllDependenciesReady.Trigger() + } else { + + block.ForEachParent(func(parent iotago.Parent) { + + }) + + block.WaitForUnreferencedOutputs(signedTransactionMetadata.TransactionMetadata().Inputs().DeleteAll(referencedOutputs)) + } + }) + + block.AllDependenciesReady.OnTrigger(func() { + if err := b.book(block); err != nil { + if block.SetInvalid() { + b.events.BlockInvalid.Trigger(block, ierrors.Wrap(err, "failed to book block")) + } + } + }) } func (b *Booker) book(block *blocks.Block) error { diff --git a/pkg/protocol/engine/mempool/signed_transaction_metadata.go b/pkg/protocol/engine/mempool/signed_transaction_metadata.go index 0bf7c3be8..2055bc0f1 100644 --- a/pkg/protocol/engine/mempool/signed_transaction_metadata.go +++ b/pkg/protocol/engine/mempool/signed_transaction_metadata.go @@ -11,6 +11,8 @@ type SignedTransactionMetadata interface { OnSignaturesInvalid(func(err error)) (unsubscribe func()) + SignaturesInvalid() error + TransactionMetadata() TransactionMetadata Attachments() []iotago.BlockID diff --git a/pkg/protocol/engine/mempool/state_metadata.go b/pkg/protocol/engine/mempool/state_metadata.go index 44c898800..2b6f5e085 100644 --- a/pkg/protocol/engine/mempool/state_metadata.go +++ b/pkg/protocol/engine/mempool/state_metadata.go @@ -16,5 +16,9 @@ type StateMetadata interface { OnAcceptedSpenderUpdated(callback func(spender TransactionMetadata)) + EarliestIncludedAttachment() iotago.BlockID + + OnEarliestIncludedAttachmentUpdated(func(prevID, newID iotago.BlockID)) + inclusionFlags } diff --git a/pkg/protocol/engine/mempool/v1/signed_transaction_metadata.go b/pkg/protocol/engine/mempool/v1/signed_transaction_metadata.go index 72e94b1c7..c5bcdd8c6 100644 --- a/pkg/protocol/engine/mempool/v1/signed_transaction_metadata.go +++ b/pkg/protocol/engine/mempool/v1/signed_transaction_metadata.go @@ -54,6 +54,10 @@ func (s *SignedTransactionMetadata) OnSignaturesInvalid(callback func(error)) (u }) } +func (s *SignedTransactionMetadata) SignaturesInvalid() error { + return s.signaturesInvalid.Get() +} + func (s *SignedTransactionMetadata) OnSignaturesValid(callback func()) (unsubscribe func()) { return s.signaturesValid.OnTrigger(callback) } diff --git a/pkg/protocol/engine/mempool/v1/state_metadata.go b/pkg/protocol/engine/mempool/v1/state_metadata.go index 1c9b1bd0f..9fd67ffa4 100644 --- a/pkg/protocol/engine/mempool/v1/state_metadata.go +++ b/pkg/protocol/engine/mempool/v1/state_metadata.go @@ -15,12 +15,13 @@ type StateMetadata struct { state mempool.State // lifecycle - spenderCount uint64 - spent *promise.Event - doubleSpent *promise.Event - spendAccepted reactive.Variable[*TransactionMetadata] - spendCommitted reactive.Variable[*TransactionMetadata] - allSpendersRemoved *event.Event + spenderCount uint64 + spent *promise.Event + doubleSpent *promise.Event + spendAccepted reactive.Variable[*TransactionMetadata] + spendCommitted reactive.Variable[*TransactionMetadata] + earliestIncludedValidAttachment reactive.Variable[iotago.BlockID] + allSpendersRemoved *event.Event spenderIDs reactive.DerivedSet[iotago.TransactionID] @@ -31,11 +32,12 @@ func NewStateMetadata(state mempool.State, optSource ...*TransactionMetadata) *S return (&StateMetadata{ state: state, - spent: promise.NewEvent(), - doubleSpent: promise.NewEvent(), - spendAccepted: reactive.NewVariable[*TransactionMetadata](), - spendCommitted: reactive.NewVariable[*TransactionMetadata](), - allSpendersRemoved: event.New(), + spent: promise.NewEvent(), + doubleSpent: promise.NewEvent(), + spendAccepted: reactive.NewVariable[*TransactionMetadata](), + spendCommitted: reactive.NewVariable[*TransactionMetadata](), + earliestIncludedValidAttachment: reactive.NewVariable[iotago.BlockID](), + allSpendersRemoved: event.New(), spenderIDs: reactive.NewDerivedSet[iotago.TransactionID](), @@ -50,6 +52,7 @@ func (s *StateMetadata) setup(optSource ...*TransactionMetadata) *StateMetadata source := optSource[0] s.spenderIDs.InheritFrom(source.spenderIDs) + s.earliestIncludedValidAttachment.InheritFrom(source.earliestIncludedValidAttachment) source.OnPending(func() { s.accepted.Set(false) }) source.OnAccepted(func() { s.accepted.Set(true) }) @@ -114,6 +117,14 @@ func (s *StateMetadata) HasNoSpenders() bool { return atomic.LoadUint64(&s.spenderCount) == 0 } +func (s *StateMetadata) EarliestIncludedAttachment() iotago.BlockID { + return s.earliestIncludedValidAttachment.Get() +} + +func (s *StateMetadata) OnEarliestIncludedAttachmentUpdated(callback func(prevID iotago.BlockID, newID iotago.BlockID)) { + s.earliestIncludedValidAttachment.OnUpdate(callback) +} + func (s *StateMetadata) increaseSpenderCount() { if spenderCount := atomic.AddUint64(&s.spenderCount, 1); spenderCount == 1 { s.spent.Trigger() From c70f79d5b1a9a6f9a40ac0c2d58f522d720e4a97 Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Wed, 13 Mar 2024 20:03:31 +0100 Subject: [PATCH 02/40] Refactor: refactored code --- .../engine/booker/inmemorybooker/booker.go | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/pkg/protocol/engine/booker/inmemorybooker/booker.go b/pkg/protocol/engine/booker/inmemorybooker/booker.go index a47747f53..8d374890c 100644 --- a/pkg/protocol/engine/booker/inmemorybooker/booker.go +++ b/pkg/protocol/engine/booker/inmemorybooker/booker.go @@ -1,6 +1,7 @@ package inmemorybooker import ( + "fmt" "sync/atomic" "github.com/iotaledger/hive.go/ds" @@ -128,7 +129,13 @@ func (b *Booker) Queue(block *blocks.Block) error { func (b *Booker) Reset() { /* nothing to reset but comply with interface */ } func (b *Booker) setupBlock(block *blocks.Block) { - referencedOutputs := ds.NewSet[mempool.StateMetadata]() + signedTransactionMetadata := block.SignedTransactionMetadata.Get() + waitForDependencies := signedTransactionMetadata != nil && signedTransactionMetadata.SignaturesInvalid() == nil && !signedTransactionMetadata.TransactionMetadata().IsInvalid() + + var referencedOutputs ds.Set[mempool.StateMetadata] + if waitForDependencies { + referencedOutputs = ds.NewSet[mempool.StateMetadata]() + } var unbookedParentsCount atomic.Int32 unbookedParentsCount.Store(int32(len(block.Parents()))) @@ -142,8 +149,10 @@ func (b *Booker) setupBlock(block *blocks.Block) { } parentBlock.Booked().OnUpdateOnce(func(_ bool, _ bool) { - if parentTransactionMetadata := parentBlock.SignedTransactionMetadata.Get(); parentTransactionMetadata != nil { - referencedOutputs.AddAll(parentTransactionMetadata.TransactionMetadata().Outputs()) + if waitForDependencies { + if parentTransactionMetadata := parentBlock.SignedTransactionMetadata.Get(); parentTransactionMetadata != nil { + referencedOutputs.AddAll(parentTransactionMetadata.TransactionMetadata().Outputs()) + } } if unbookedParentsCount.Add(-1) == 0 { @@ -159,15 +168,12 @@ func (b *Booker) setupBlock(block *blocks.Block) { }) block.AllParentsBooked.OnTrigger(func() { - if signedTransactionMetadata := block.SignedTransactionMetadata.Get(); signedTransactionMetadata == nil || signedTransactionMetadata.SignaturesInvalid() != nil { - block.AllDependenciesReady.Trigger() - } else { - - block.ForEachParent(func(parent iotago.Parent) { - - }) + if waitForDependencies { + fmt.Println("WAITING FOR DEPENDENCIES OF", signedTransactionMetadata.ID(), "IN", block.ID(), "TO BECOME READY") block.WaitForUnreferencedOutputs(signedTransactionMetadata.TransactionMetadata().Inputs().DeleteAll(referencedOutputs)) + } else { + block.AllDependenciesReady.Trigger() } }) From 46cb2d47c4eb903d7b785772f643655d277b960e Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Wed, 13 Mar 2024 23:45:50 +0100 Subject: [PATCH 03/40] Feat: finished solidification rules --- pkg/protocol/engine/blocks/block.go | 10 +++- .../engine/booker/inmemorybooker/booker.go | 21 +++----- pkg/protocol/engine/ledger/tests/state.go | 6 +++ pkg/protocol/engine/mempool/state.go | 7 +++ pkg/protocol/engine/mempool/state_metadata.go | 4 +- pkg/protocol/engine/mempool/v1/mempool.go | 3 ++ .../engine/mempool/v1/state_metadata.go | 53 ++++++++++++------- 7 files changed, 69 insertions(+), 35 deletions(-) diff --git a/pkg/protocol/engine/blocks/block.go b/pkg/protocol/engine/blocks/block.go index b81274c37..bdafb5cb7 100644 --- a/pkg/protocol/engine/blocks/block.go +++ b/pkg/protocol/engine/blocks/block.go @@ -712,6 +712,12 @@ func (b *Block) WorkScore() iotago.WorkScore { } func (b *Block) WaitForUnreferencedOutputs(unreferencedOutputs ds.Set[mempool.StateMetadata]) { + if unreferencedOutputs == nil || unreferencedOutputs.Size() == 0 { + b.AllDependenciesReady.Trigger() + + return + } + var unreferencedOutputCount atomic.Int32 unreferencedOutputCount.Store(int32(unreferencedOutputs.Size())) @@ -719,8 +725,8 @@ func (b *Block) WaitForUnreferencedOutputs(unreferencedOutputs ds.Set[mempool.St dependencyReady := false unreferencedOutput.OnAccepted(func() { - unreferencedOutput.OnEarliestIncludedAttachmentUpdated(func(_, earliestIncludedAttachment iotago.BlockID) { - if !dependencyReady && earliestIncludedAttachment.Slot() <= b.ID().Slot() { + unreferencedOutput.OnInclusionSlotUpdated(func(_ iotago.SlotIndex, inclusionSlot iotago.SlotIndex) { + if !dependencyReady && inclusionSlot <= b.ID().Slot() { dependencyReady = true if unreferencedOutputCount.Add(-1) == 0 { diff --git a/pkg/protocol/engine/booker/inmemorybooker/booker.go b/pkg/protocol/engine/booker/inmemorybooker/booker.go index 8d374890c..a3ece4bb2 100644 --- a/pkg/protocol/engine/booker/inmemorybooker/booker.go +++ b/pkg/protocol/engine/booker/inmemorybooker/booker.go @@ -1,7 +1,6 @@ package inmemorybooker import ( - "fmt" "sync/atomic" "github.com/iotaledger/hive.go/ds" @@ -129,11 +128,9 @@ func (b *Booker) Queue(block *blocks.Block) error { func (b *Booker) Reset() { /* nothing to reset but comply with interface */ } func (b *Booker) setupBlock(block *blocks.Block) { - signedTransactionMetadata := block.SignedTransactionMetadata.Get() - waitForDependencies := signedTransactionMetadata != nil && signedTransactionMetadata.SignaturesInvalid() == nil && !signedTransactionMetadata.TransactionMetadata().IsInvalid() - - var referencedOutputs ds.Set[mempool.StateMetadata] - if waitForDependencies { + var referencedOutputs, unreferencedOutputs ds.Set[mempool.StateMetadata] + if signedTransactionMetadata := block.SignedTransactionMetadata.Get(); signedTransactionMetadata != nil && signedTransactionMetadata.SignaturesInvalid() == nil && !signedTransactionMetadata.TransactionMetadata().IsInvalid() { + unreferencedOutputs = signedTransactionMetadata.TransactionMetadata().Outputs() referencedOutputs = ds.NewSet[mempool.StateMetadata]() } @@ -149,7 +146,7 @@ func (b *Booker) setupBlock(block *blocks.Block) { } parentBlock.Booked().OnUpdateOnce(func(_ bool, _ bool) { - if waitForDependencies { + if referencedOutputs != nil { if parentTransactionMetadata := parentBlock.SignedTransactionMetadata.Get(); parentTransactionMetadata != nil { referencedOutputs.AddAll(parentTransactionMetadata.TransactionMetadata().Outputs()) } @@ -168,13 +165,11 @@ func (b *Booker) setupBlock(block *blocks.Block) { }) block.AllParentsBooked.OnTrigger(func() { - if waitForDependencies { - fmt.Println("WAITING FOR DEPENDENCIES OF", signedTransactionMetadata.ID(), "IN", block.ID(), "TO BECOME READY") - - block.WaitForUnreferencedOutputs(signedTransactionMetadata.TransactionMetadata().Inputs().DeleteAll(referencedOutputs)) - } else { - block.AllDependenciesReady.Trigger() + if unreferencedOutputs != nil { + unreferencedOutputs.DeleteAll(referencedOutputs) } + + block.WaitForUnreferencedOutputs(unreferencedOutputs) }) block.AllDependenciesReady.OnTrigger(func() { diff --git a/pkg/protocol/engine/ledger/tests/state.go b/pkg/protocol/engine/ledger/tests/state.go index 19b5a0fdd..fccfffd99 100644 --- a/pkg/protocol/engine/ledger/tests/state.go +++ b/pkg/protocol/engine/ledger/tests/state.go @@ -10,6 +10,7 @@ type MockedState struct { id iotago.OutputID output *MockedOutput creationSlot iotago.SlotIndex + slotBooked iotago.SlotIndex } func NewMockedState(transactionID iotago.TransactionID, index uint16) *MockedState { @@ -17,6 +18,7 @@ func NewMockedState(transactionID iotago.TransactionID, index uint16) *MockedSta id: iotago.OutputIDFromTransactionIDAndIndex(transactionID, index), output: &MockedOutput{}, creationSlot: iotago.SlotIndex(0), + slotBooked: iotago.SlotIndex(0), } } @@ -44,6 +46,10 @@ func (m *MockedState) SlotCreated() iotago.SlotIndex { return m.creationSlot } +func (m *MockedState) SlotBooked() iotago.SlotIndex { + return m.slotBooked +} + func (m *MockedState) String() string { return "MockedOutput(" + m.id.ToHex() + ")" } diff --git a/pkg/protocol/engine/mempool/state.go b/pkg/protocol/engine/mempool/state.go index bfdc6311a..00b51d93c 100644 --- a/pkg/protocol/engine/mempool/state.go +++ b/pkg/protocol/engine/mempool/state.go @@ -15,6 +15,9 @@ type State interface { // Whether the state is read only. IsReadOnly() bool + + // SlotBooked returns the slot index of the state if it is booked, otherwise -1. + SlotBooked() iotago.SlotIndex } // A thin wrapper around a resolved commitment. @@ -34,6 +37,10 @@ func (s CommitmentInputState) IsReadOnly() bool { return true } +func (s CommitmentInputState) SlotBooked() iotago.SlotIndex { + return s.Commitment.Slot +} + func CommitmentInputStateFromCommitment(commitment *iotago.Commitment) CommitmentInputState { return CommitmentInputState{ Commitment: commitment, diff --git a/pkg/protocol/engine/mempool/state_metadata.go b/pkg/protocol/engine/mempool/state_metadata.go index 2b6f5e085..2c82ae746 100644 --- a/pkg/protocol/engine/mempool/state_metadata.go +++ b/pkg/protocol/engine/mempool/state_metadata.go @@ -16,9 +16,9 @@ type StateMetadata interface { OnAcceptedSpenderUpdated(callback func(spender TransactionMetadata)) - EarliestIncludedAttachment() iotago.BlockID + InclusionSlot() iotago.SlotIndex - OnEarliestIncludedAttachmentUpdated(func(prevID, newID iotago.BlockID)) + OnInclusionSlotUpdated(func(prevSlot iotago.SlotIndex, newSlot iotago.SlotIndex)) inclusionFlags } diff --git a/pkg/protocol/engine/mempool/v1/mempool.go b/pkg/protocol/engine/mempool/v1/mempool.go index 0620d5524..1fab9fb8e 100644 --- a/pkg/protocol/engine/mempool/v1/mempool.go +++ b/pkg/protocol/engine/mempool/v1/mempool.go @@ -469,9 +469,12 @@ func (m *MemPool[VoteRank]) requestState(stateRef mempool.StateReference, waitIf request := m.resolveState(stateRef) request.OnSuccess(func(state mempool.State) { + inclusionSlot := state.SlotBooked() + // The output was resolved from the ledger, meaning it was actually persisted as it was accepted and // committed: otherwise we would have found it in cache or the request would have never resolved. stateMetadata := NewStateMetadata(state) + stateMetadata.inclusionSlot.Set(&inclusionSlot) stateMetadata.accepted.Set(true) p.Resolve(stateMetadata) diff --git a/pkg/protocol/engine/mempool/v1/state_metadata.go b/pkg/protocol/engine/mempool/v1/state_metadata.go index 9fd67ffa4..3084f2f34 100644 --- a/pkg/protocol/engine/mempool/v1/state_metadata.go +++ b/pkg/protocol/engine/mempool/v1/state_metadata.go @@ -15,13 +15,13 @@ type StateMetadata struct { state mempool.State // lifecycle - spenderCount uint64 - spent *promise.Event - doubleSpent *promise.Event - spendAccepted reactive.Variable[*TransactionMetadata] - spendCommitted reactive.Variable[*TransactionMetadata] - earliestIncludedValidAttachment reactive.Variable[iotago.BlockID] - allSpendersRemoved *event.Event + spenderCount uint64 + spent *promise.Event + doubleSpent *promise.Event + spendAccepted reactive.Variable[*TransactionMetadata] + spendCommitted reactive.Variable[*TransactionMetadata] + inclusionSlot reactive.Variable[*iotago.SlotIndex] + allSpendersRemoved *event.Event spenderIDs reactive.DerivedSet[iotago.TransactionID] @@ -32,12 +32,12 @@ func NewStateMetadata(state mempool.State, optSource ...*TransactionMetadata) *S return (&StateMetadata{ state: state, - spent: promise.NewEvent(), - doubleSpent: promise.NewEvent(), - spendAccepted: reactive.NewVariable[*TransactionMetadata](), - spendCommitted: reactive.NewVariable[*TransactionMetadata](), - earliestIncludedValidAttachment: reactive.NewVariable[iotago.BlockID](), - allSpendersRemoved: event.New(), + spent: promise.NewEvent(), + doubleSpent: promise.NewEvent(), + spendAccepted: reactive.NewVariable[*TransactionMetadata](), + spendCommitted: reactive.NewVariable[*TransactionMetadata](), + inclusionSlot: reactive.NewVariable[*iotago.SlotIndex](), + allSpendersRemoved: event.New(), spenderIDs: reactive.NewDerivedSet[iotago.TransactionID](), @@ -52,7 +52,16 @@ func (s *StateMetadata) setup(optSource ...*TransactionMetadata) *StateMetadata source := optSource[0] s.spenderIDs.InheritFrom(source.spenderIDs) - s.earliestIncludedValidAttachment.InheritFrom(source.earliestIncludedValidAttachment) + + source.earliestIncludedValidAttachment.OnUpdate(func(_, newValue iotago.BlockID) { + s.inclusionSlot.Compute(func(currentValue *iotago.SlotIndex) *iotago.SlotIndex { + if newSlot := newValue.Slot(); currentValue == nil || newSlot < *currentValue { + return &newSlot + } + + return currentValue + }) + }) source.OnPending(func() { s.accepted.Set(false) }) source.OnAccepted(func() { s.accepted.Set(true) }) @@ -117,12 +126,20 @@ func (s *StateMetadata) HasNoSpenders() bool { return atomic.LoadUint64(&s.spenderCount) == 0 } -func (s *StateMetadata) EarliestIncludedAttachment() iotago.BlockID { - return s.earliestIncludedValidAttachment.Get() +func (s *StateMetadata) InclusionSlot() iotago.SlotIndex { + return *s.inclusionSlot.Get() } -func (s *StateMetadata) OnEarliestIncludedAttachmentUpdated(callback func(prevID iotago.BlockID, newID iotago.BlockID)) { - s.earliestIncludedValidAttachment.OnUpdate(callback) +func (s *StateMetadata) OnInclusionSlotUpdated(callback func(prevID iotago.SlotIndex, newID iotago.SlotIndex)) { + s.inclusionSlot.OnUpdate(func(oldValue *iotago.SlotIndex, newValue *iotago.SlotIndex) { + if oldValue == nil { + callback(iotago.SlotIndex(0), *newValue) + } else if newValue == nil { + callback(*oldValue, iotago.SlotIndex(0)) + } else { + callback(*oldValue, *newValue) + } + }) } func (s *StateMetadata) increaseSpenderCount() { From 5e118194077efd29d688585695fb888a765ab68e Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Thu, 14 Mar 2024 00:30:43 +0100 Subject: [PATCH 04/40] Fix: fixed bug in logic --- pkg/protocol/engine/blocks/block.go | 12 ++++++------ .../engine/booker/inmemorybooker/booker.go | 16 ++++++++-------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pkg/protocol/engine/blocks/block.go b/pkg/protocol/engine/blocks/block.go index bdafb5cb7..ce259f6e8 100644 --- a/pkg/protocol/engine/blocks/block.go +++ b/pkg/protocol/engine/blocks/block.go @@ -711,21 +711,21 @@ func (b *Block) WorkScore() iotago.WorkScore { return b.workScore } -func (b *Block) WaitForUnreferencedOutputs(unreferencedOutputs ds.Set[mempool.StateMetadata]) { - if unreferencedOutputs == nil || unreferencedOutputs.Size() == 0 { +func (b *Block) WaitForUTXODependencies(dependencies ds.Set[mempool.StateMetadata]) { + if dependencies == nil || dependencies.Size() == 0 { b.AllDependenciesReady.Trigger() return } var unreferencedOutputCount atomic.Int32 - unreferencedOutputCount.Store(int32(unreferencedOutputs.Size())) + unreferencedOutputCount.Store(int32(dependencies.Size())) - unreferencedOutputs.Range(func(unreferencedOutput mempool.StateMetadata) { + dependencies.Range(func(dependency mempool.StateMetadata) { dependencyReady := false - unreferencedOutput.OnAccepted(func() { - unreferencedOutput.OnInclusionSlotUpdated(func(_ iotago.SlotIndex, inclusionSlot iotago.SlotIndex) { + dependency.OnAccepted(func() { + dependency.OnInclusionSlotUpdated(func(_ iotago.SlotIndex, inclusionSlot iotago.SlotIndex) { if !dependencyReady && inclusionSlot <= b.ID().Slot() { dependencyReady = true diff --git a/pkg/protocol/engine/booker/inmemorybooker/booker.go b/pkg/protocol/engine/booker/inmemorybooker/booker.go index a3ece4bb2..35c72a9e2 100644 --- a/pkg/protocol/engine/booker/inmemorybooker/booker.go +++ b/pkg/protocol/engine/booker/inmemorybooker/booker.go @@ -128,10 +128,10 @@ func (b *Booker) Queue(block *blocks.Block) error { func (b *Booker) Reset() { /* nothing to reset but comply with interface */ } func (b *Booker) setupBlock(block *blocks.Block) { - var referencedOutputs, unreferencedOutputs ds.Set[mempool.StateMetadata] + var directlyReferencedUTXODependencies, utxoDependencies ds.Set[mempool.StateMetadata] if signedTransactionMetadata := block.SignedTransactionMetadata.Get(); signedTransactionMetadata != nil && signedTransactionMetadata.SignaturesInvalid() == nil && !signedTransactionMetadata.TransactionMetadata().IsInvalid() { - unreferencedOutputs = signedTransactionMetadata.TransactionMetadata().Outputs() - referencedOutputs = ds.NewSet[mempool.StateMetadata]() + utxoDependencies = signedTransactionMetadata.TransactionMetadata().Inputs() + directlyReferencedUTXODependencies = ds.NewSet[mempool.StateMetadata]() } var unbookedParentsCount atomic.Int32 @@ -146,9 +146,9 @@ func (b *Booker) setupBlock(block *blocks.Block) { } parentBlock.Booked().OnUpdateOnce(func(_ bool, _ bool) { - if referencedOutputs != nil { + if directlyReferencedUTXODependencies != nil { if parentTransactionMetadata := parentBlock.SignedTransactionMetadata.Get(); parentTransactionMetadata != nil { - referencedOutputs.AddAll(parentTransactionMetadata.TransactionMetadata().Outputs()) + directlyReferencedUTXODependencies.AddAll(parentTransactionMetadata.TransactionMetadata().Outputs()) } } @@ -165,11 +165,11 @@ func (b *Booker) setupBlock(block *blocks.Block) { }) block.AllParentsBooked.OnTrigger(func() { - if unreferencedOutputs != nil { - unreferencedOutputs.DeleteAll(referencedOutputs) + if directlyReferencedUTXODependencies != nil { + utxoDependencies.DeleteAll(directlyReferencedUTXODependencies) } - block.WaitForUnreferencedOutputs(unreferencedOutputs) + block.WaitForUTXODependencies(utxoDependencies) }) block.AllDependenciesReady.OnTrigger(func() { From 484cbd115aedfe524f41842c4155f085b52987b4 Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Thu, 14 Mar 2024 02:09:35 +0100 Subject: [PATCH 05/40] Refactor: rename --- pkg/protocol/engine/booker/inmemorybooker/booker.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/protocol/engine/booker/inmemorybooker/booker.go b/pkg/protocol/engine/booker/inmemorybooker/booker.go index 35c72a9e2..476e4078e 100644 --- a/pkg/protocol/engine/booker/inmemorybooker/booker.go +++ b/pkg/protocol/engine/booker/inmemorybooker/booker.go @@ -128,7 +128,7 @@ func (b *Booker) Queue(block *blocks.Block) error { func (b *Booker) Reset() { /* nothing to reset but comply with interface */ } func (b *Booker) setupBlock(block *blocks.Block) { - var directlyReferencedUTXODependencies, utxoDependencies ds.Set[mempool.StateMetadata] + var utxoDependencies, directlyReferencedUTXODependencies ds.Set[mempool.StateMetadata] if signedTransactionMetadata := block.SignedTransactionMetadata.Get(); signedTransactionMetadata != nil && signedTransactionMetadata.SignaturesInvalid() == nil && !signedTransactionMetadata.TransactionMetadata().IsInvalid() { utxoDependencies = signedTransactionMetadata.TransactionMetadata().Inputs() directlyReferencedUTXODependencies = ds.NewSet[mempool.StateMetadata]() From b495259e3c54b4dd01bb19076fbeab15b5c0deed Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Fri, 15 Mar 2024 15:20:24 +0100 Subject: [PATCH 06/40] Feat: added new dependency reference to tests --- pkg/testsuite/testsuite.go | 2 ++ pkg/testsuite/testsuite_issue_blocks.go | 46 +++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/pkg/testsuite/testsuite.go b/pkg/testsuite/testsuite.go index 1b7c6b735..45dcff239 100644 --- a/pkg/testsuite/testsuite.go +++ b/pkg/testsuite/testsuite.go @@ -66,6 +66,7 @@ type TestSuite struct { snapshotPath string blocks *shrinkingmap.ShrinkingMap[string, *blocks.Block] + attachments *shrinkingmap.ShrinkingMap[string, []*blocks.Block] API iotago.API ProtocolParameterOptions []options.Option[iotago.V3ProtocolParameters] @@ -94,6 +95,7 @@ func NewTestSuite(testingT *testing.T, opts ...options.Option[TestSuite]) *TestS nodes: orderedmap.New[string, *mock.Node](), wallets: orderedmap.New[string, *mock.Wallet](), blocks: shrinkingmap.New[string, *blocks.Block](), + attachments: shrinkingmap.New[string, []*blocks.Block](), automaticTransactionIssuingCounters: *shrinkingmap.New[string, int](), optsWaitFor: durationFromEnvOrDefault(5*time.Second, "CI_UNIT_TESTS_WAIT_FOR"), diff --git a/pkg/testsuite/testsuite_issue_blocks.go b/pkg/testsuite/testsuite_issue_blocks.go index 80482c244..1c135e3ed 100644 --- a/pkg/testsuite/testsuite_issue_blocks.go +++ b/pkg/testsuite/testsuite_issue_blocks.go @@ -3,6 +3,7 @@ package testsuite import ( "context" "fmt" + "slices" "time" "github.com/stretchr/testify/require" @@ -34,6 +35,33 @@ func (t *TestSuite) assertParentsExistFromBlockOptions(blockOpts []options.Optio t.AssertBlocksExist(t.Blocks(lo.Map(parents, func(id iotago.BlockID) string { return id.Alias() })...), true, node) } +func (t *TestSuite) referenceDependencies(blockOpts []options.Option[mock.BlockHeaderParams], inputTxName string, blockName string) []options.Option[mock.BlockHeaderParams] { + if inputTxName != "Genesis" { + if attachments, exists := t.attachments.Get(inputTxName); !exists { + panic(fmt.Sprintf("input transaction %s does not have an attachment", inputTxName)) + } else if params := options.Apply(&mock.BlockHeaderParams{}, blockOpts); !t.blockReferencesDependency(params, attachments[0].ID()) { + blockOpts = append(blockOpts, mock.WithWeakParents(append(params.References[iotago.WeakParentType], attachments[0].ID())...)) + } + } + + return blockOpts +} + +func (t *TestSuite) blockReferencesDependency(params *mock.BlockHeaderParams, attachment iotago.BlockID) bool { + parents := slices.Concat(lo.Values(params.References)...) + if slices.Contains(parents, attachment) || (params.SlotCommitment != nil && attachment.Slot() <= params.SlotCommitment.Slot) { + return true + } + + for _, block := range t.Blocks(lo.Map(parents, iotago.BlockID.Alias)...) { + if block.SlotCommitmentID().Slot() >= attachment.Slot() { + return true + } + } + + return false +} + func (t *TestSuite) limitParentsCountInBlockOptions(blockOpts []options.Option[mock.BlockHeaderParams], maxCount int) []options.Option[mock.BlockHeaderParams] { params := options.Apply(&mock.BlockHeaderParams{}, blockOpts) if len(params.References[iotago.StrongParentType]) > maxCount { @@ -59,6 +87,16 @@ func (t *TestSuite) RegisterBlock(blockName string, block *blocks.Block) { func (t *TestSuite) registerBlock(blockName string, block *blocks.Block) { t.blocks.Set(blockName, block) block.ID().RegisterAlias(blockName) + + if tx, hasTransaction := block.SignedTransaction(); hasTransaction { + t.attachments.Compute(lo.Return1(tx.Transaction.ID()).Alias(), func(currentValue []*blocks.Block, exists bool) []*blocks.Block { + if currentValue == nil { + currentValue = make([]*blocks.Block, 0) + } + + return append(currentValue, block) + }) + } } func (t *TestSuite) IssueValidationBlockWithHeaderOptions(blockName string, node *mock.Node, blockHeaderOpts ...options.Option[mock.BlockHeaderParams]) *blocks.Block { @@ -161,14 +199,16 @@ func (t *TestSuite) issueBlockRow(prefix string, row int, parentsPrefix string, txCount := t.automaticTransactionIssuingCounters.Compute(node.Partition, func(currentValue int, exists bool) int { return currentValue + 1 }) - inputName := fmt.Sprintf("automaticSpent-%d:0", txCount-1) - txName := fmt.Sprintf("automaticSpent-%d", txCount) + inputTxName := fmt.Sprintf("automaticSpent-%d", txCount-1) if txCount == 1 { - inputName = "Genesis:0" + inputTxName = "Genesis" } + inputName := inputTxName + ":0" + txName := fmt.Sprintf("automaticSpent-%d", txCount) tx := t.DefaultWallet().CreateBasicOutputsEquallyFromInput(txName, 1, inputName) issuingOptionsCopy[node.Name] = t.limitParentsCountInBlockOptions(issuingOptionsCopy[node.Name], iotago.BasicBlockMaxParents) + issuingOptionsCopy[node.Name] = t.referenceDependencies(issuingOptionsCopy[node.Name], inputTxName, blockName) t.assertParentsCommitmentExistFromBlockOptions(issuingOptionsCopy[node.Name], node) t.assertParentsExistFromBlockOptions(issuingOptionsCopy[node.Name], node) From 1bd7a4717f7355c7f60976e98583ffe2286cb8a7 Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Fri, 15 Mar 2024 15:26:33 +0100 Subject: [PATCH 07/40] Feat: uncomment tests which are no longer relevant --- pkg/tests/booker_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/tests/booker_test.go b/pkg/tests/booker_test.go index 0da032c35..325bdd1ac 100644 --- a/pkg/tests/booker_test.go +++ b/pkg/tests/booker_test.go @@ -16,6 +16,8 @@ import ( ) func Test_IssuingTransactionsOutOfOrder(t *testing.T) { + t.Skip("This test is currently disabled because it is not yet clear how to handle this case.") + ts := testsuite.NewTestSuite(t) defer ts.Shutdown() @@ -212,6 +214,8 @@ func Test_DoubleSpend(t *testing.T) { } func Test_MultipleAttachments(t *testing.T) { + t.Skip("This test is currently disabled because it is not yet clear how to handle this case.") + ts := testsuite.NewTestSuite(t) defer ts.Shutdown() @@ -320,6 +324,8 @@ func Test_MultipleAttachments(t *testing.T) { } func Test_SpendRejectedCommittedRace(t *testing.T) { + t.Skip("This test is currently disabled because it is not yet clear how to handle this case.") + ts := testsuite.NewTestSuite(t, testsuite.WithProtocolParametersOptions( iotago.WithTimeProviderOptions( From 0fc0325a9a63408d5737d7eadd7eb6766444e19c Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Fri, 15 Mar 2024 16:59:20 +0100 Subject: [PATCH 08/40] Fix: addressed linter issues --- pkg/testsuite/testsuite_issue_blocks.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/testsuite/testsuite_issue_blocks.go b/pkg/testsuite/testsuite_issue_blocks.go index 1c135e3ed..aa3df6a19 100644 --- a/pkg/testsuite/testsuite_issue_blocks.go +++ b/pkg/testsuite/testsuite_issue_blocks.go @@ -35,7 +35,7 @@ func (t *TestSuite) assertParentsExistFromBlockOptions(blockOpts []options.Optio t.AssertBlocksExist(t.Blocks(lo.Map(parents, func(id iotago.BlockID) string { return id.Alias() })...), true, node) } -func (t *TestSuite) referenceDependencies(blockOpts []options.Option[mock.BlockHeaderParams], inputTxName string, blockName string) []options.Option[mock.BlockHeaderParams] { +func (t *TestSuite) addReferenceToDependencies(blockOpts []options.Option[mock.BlockHeaderParams], inputTxName string) []options.Option[mock.BlockHeaderParams] { if inputTxName != "Genesis" { if attachments, exists := t.attachments.Get(inputTxName); !exists { panic(fmt.Sprintf("input transaction %s does not have an attachment", inputTxName)) @@ -89,7 +89,7 @@ func (t *TestSuite) registerBlock(blockName string, block *blocks.Block) { block.ID().RegisterAlias(blockName) if tx, hasTransaction := block.SignedTransaction(); hasTransaction { - t.attachments.Compute(lo.Return1(tx.Transaction.ID()).Alias(), func(currentValue []*blocks.Block, exists bool) []*blocks.Block { + t.attachments.Compute(lo.Return1(tx.Transaction.ID()).Alias(), func(currentValue []*blocks.Block, _ bool) []*blocks.Block { if currentValue == nil { currentValue = make([]*blocks.Block, 0) } @@ -208,7 +208,7 @@ func (t *TestSuite) issueBlockRow(prefix string, row int, parentsPrefix string, tx := t.DefaultWallet().CreateBasicOutputsEquallyFromInput(txName, 1, inputName) issuingOptionsCopy[node.Name] = t.limitParentsCountInBlockOptions(issuingOptionsCopy[node.Name], iotago.BasicBlockMaxParents) - issuingOptionsCopy[node.Name] = t.referenceDependencies(issuingOptionsCopy[node.Name], inputTxName, blockName) + issuingOptionsCopy[node.Name] = t.addReferenceToDependencies(issuingOptionsCopy[node.Name], inputTxName) t.assertParentsCommitmentExistFromBlockOptions(issuingOptionsCopy[node.Name], node) t.assertParentsExistFromBlockOptions(issuingOptionsCopy[node.Name], node) From 31f262be2b38a192687756a544e67c50a90ae550 Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Fri, 15 Mar 2024 18:40:21 +0100 Subject: [PATCH 09/40] Fix: re-enabled test --- pkg/tests/booker_test.go | 52 +++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/pkg/tests/booker_test.go b/pkg/tests/booker_test.go index 325bdd1ac..bf79f9425 100644 --- a/pkg/tests/booker_test.go +++ b/pkg/tests/booker_test.go @@ -16,8 +16,6 @@ import ( ) func Test_IssuingTransactionsOutOfOrder(t *testing.T) { - t.Skip("This test is currently disabled because it is not yet clear how to handle this case.") - ts := testsuite.NewTestSuite(t) defer ts.Shutdown() @@ -26,30 +24,46 @@ func Test_IssuingTransactionsOutOfOrder(t *testing.T) { ts.Run(true, map[string][]options.Option[protocol.Protocol]{}) tx1 := wallet.CreateBasicOutputsEquallyFromInput("tx1", 1, "Genesis:0") - tx2 := wallet.CreateBasicOutputsEquallyFromInput("tx2", 1, "tx1:0") - ts.IssueBasicBlockWithOptions("block1", wallet, tx2) + // issue block1 that contains an unsolid transaction + { + ts.IssueBasicBlockWithOptions("block1", wallet, tx2) - ts.AssertTransactionsExist(wallet.Transactions("tx2"), true, node1) - ts.AssertTransactionsExist(wallet.Transactions("tx1"), false, node1) + ts.AssertTransactionsExist(wallet.Transactions("tx2"), true, node1) + ts.AssertTransactionsExist(wallet.Transactions("tx1"), false, node1) + ts.AssertTransactionsInCacheBooked(wallet.Transactions("tx2"), false, node1) + // make sure that the block is not booked + } - ts.AssertTransactionsInCacheBooked(wallet.Transactions("tx2"), false, node1) - // make sure that the block is not booked + // issue block2 that makes block1 solid (but not booked yet) + { + ts.IssueBasicBlockWithOptions("block2", wallet, tx1) - ts.IssueBasicBlockWithOptions("block2", wallet, tx1) + ts.AssertTransactionsExist(wallet.Transactions("tx1", "tx2"), true, node1) + ts.AssertTransactionsInCacheBooked(wallet.Transactions("tx1", "tx2"), true, node1) + ts.AssertBlocksInCacheBooked(ts.Blocks("block2"), true, node1) + ts.AssertBlocksInCacheBooked(ts.Blocks("block1"), false, node1) + ts.AssertBlocksInCacheConflicts(map[*blocks.Block][]string{ + ts.Block("block2"): {"tx1"}, + }, node1) + ts.AssertTransactionInCacheConflicts(map[*iotago.Transaction][]string{ + wallet.Transaction("tx2"): {"tx2"}, + wallet.Transaction("tx1"): {"tx1"}, + }, node1) + } - ts.AssertTransactionsExist(wallet.Transactions("tx1", "tx2"), true, node1) - ts.AssertTransactionsInCacheBooked(wallet.Transactions("tx1", "tx2"), true, node1) - ts.AssertBlocksInCacheConflicts(map[*blocks.Block][]string{ - ts.Block("block1"): {"tx2"}, - ts.Block("block2"): {"tx1"}, - }, node1) + // confirm 2nd block so block1 gets booked + { + ts.IssueValidationBlockWithHeaderOptions("block3", node1, mock.WithStrongParents(ts.BlockID("block2"))) - ts.AssertTransactionInCacheConflicts(map[*iotago.Transaction][]string{ - wallet.Transaction("tx2"): {"tx2"}, - wallet.Transaction("tx1"): {"tx1"}, - }, node1) + ts.AssertBlocksInCacheBooked(ts.Blocks("block1", "block2", "block3"), true, node1) + ts.AssertBlocksInCacheConflicts(map[*blocks.Block][]string{ + ts.Block("block1"): {"tx2"}, + ts.Block("block2"): {"tx1"}, + ts.Block("block3"): {"tx1"}, + }, node1) + } } func Test_WeightPropagation(t *testing.T) { From 8f7092eade47548370cc4fd45b8c364fb9226f48 Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Thu, 21 Mar 2024 00:24:14 +0100 Subject: [PATCH 10/40] Feat: WIP WIP --- pkg/protocol/engine/mempool/state_metadata.go | 2 + .../engine/mempool/v1/state_metadata.go | 14 +++- .../engine/tipselection/v1/tip_selection.go | 79 ++++++++++++++++++- 3 files changed, 89 insertions(+), 6 deletions(-) diff --git a/pkg/protocol/engine/mempool/state_metadata.go b/pkg/protocol/engine/mempool/state_metadata.go index 2c82ae746..a1c144d11 100644 --- a/pkg/protocol/engine/mempool/state_metadata.go +++ b/pkg/protocol/engine/mempool/state_metadata.go @@ -6,6 +6,8 @@ import ( ) type StateMetadata interface { + CreatingTransaction() TransactionMetadata + State() State SpenderIDs() reactive.Set[iotago.TransactionID] diff --git a/pkg/protocol/engine/mempool/v1/state_metadata.go b/pkg/protocol/engine/mempool/v1/state_metadata.go index 3084f2f34..9d25847fa 100644 --- a/pkg/protocol/engine/mempool/v1/state_metadata.go +++ b/pkg/protocol/engine/mempool/v1/state_metadata.go @@ -12,7 +12,8 @@ import ( ) type StateMetadata struct { - state mempool.State + state mempool.State + source *TransactionMetadata // lifecycle spenderCount uint64 @@ -30,7 +31,8 @@ type StateMetadata struct { func NewStateMetadata(state mempool.State, optSource ...*TransactionMetadata) *StateMetadata { return (&StateMetadata{ - state: state, + state: state, + source: lo.First(optSource), spent: promise.NewEvent(), doubleSpent: promise.NewEvent(), @@ -72,6 +74,14 @@ func (s *StateMetadata) setup(optSource ...*TransactionMetadata) *StateMetadata return s } +func (s *StateMetadata) CreatingTransaction() mempool.TransactionMetadata { + if s.source == nil { + return nil + } + + return s.source +} + func (s *StateMetadata) State() mempool.State { return s.state } diff --git a/pkg/protocol/engine/tipselection/v1/tip_selection.go b/pkg/protocol/engine/tipselection/v1/tip_selection.go index e9a71d1f4..5a2c4dedd 100644 --- a/pkg/protocol/engine/tipselection/v1/tip_selection.go +++ b/pkg/protocol/engine/tipselection/v1/tip_selection.go @@ -23,6 +23,8 @@ import ( // TipSelection is a component that is used to abstract away the tip selection strategy, used to issue new blocks. type TipSelection struct { + memPool mempool.MemPool[ledger.BlockVoteRank] + // tipManager is the TipManager that is used to access the tip related metadata. tipManager tipmanager.TipManager @@ -101,11 +103,80 @@ func (t *TipSelection) Construct(tipManager tipmanager.TipManager, spendDAG spen return module.InitSimpleLifecycle(t) } +func (t *TipSelection) unacceptedInputs(payload iotago.Payload) (inputs []mempool.StateMetadata, err error) { + if payload.PayloadType() != iotago.PayloadSignedTransaction { + return nil, nil + } + + signedTransaction, isSignedTransaction := payload.(*iotago.SignedTransaction) + if !isSignedTransaction { + return nil, ierrors.New("failed to cast payload to signed transaction") + } + + inputReferences, inputReferencesErr := t.memPool.VM().Inputs(signedTransaction.Transaction) + if inputReferencesErr != nil { + return nil, ierrors.Wrap(inputReferencesErr, "failed to retrieve input references") + } + + for _, inputReference := range inputReferences { + stateMetadata, stateMetadataErr := t.memPool.StateMetadata(inputReference) + if stateMetadataErr != nil { + return nil, ierrors.Wrap(stateMetadataErr, "failed to retrieve state metadata") + } + + if !stateMetadata.IsAccepted() { + inputs = append(inputs, stateMetadata) + } + } + + return inputs, nil +} + +func (t *TipSelection) payloadDependenciesToReference(payload iotago.Payload) (dependencies ds.Set[mempool.TransactionMetadata], err error) { + unacceptedInputs, unacceptedInputsErr := t.unacceptedInputs(payload) + if unacceptedInputsErr != nil { + return nil, ierrors.Wrap(unacceptedInputsErr, "failed to retrieve unaccepted inputs") + } + + dependencies = ds.NewSet[mempool.TransactionMetadata]() + for _, unacceptedInput := range unacceptedInputs { + creatingTransaction := unacceptedInput.CreatingTransaction() + if creatingTransaction == nil { + return nil, ierrors.New("unable to reference non-accepted input - creating transaction not found") + } + + dependencies.Add(creatingTransaction) + } + + return dependencies, nil +} + // SelectTips selects the tips that should be used as references for a new block. -func (t *TipSelection) SelectTips(amount int) (references model.ParentReferences) { +func (t *TipSelection) SelectTips(amount int, optPayload ...iotago.Payload) (references model.ParentReferences) { + if len(optPayload) != 0 { + dependenciesToReference, err := t.payloadDependenciesToReference(optPayload[0]) + if err != nil { + panic(err) + } + + blocksToWeaklyReference := ds.NewSet[iotago.BlockID]() + err = dependenciesToReference.ForEach(func(transactionToReference mempool.TransactionMetadata) error { + validAttachments := transactionToReference.ValidAttachments() + if len(validAttachments) == 0 { + return ierrors.Errorf("transaction %s has no valid attachments", transactionToReference.ID()) + } + + // TODO: FIND LAST VALID ATTACHMENT TO REFERENCE AND CHECK FOR ORPHANAGE / BELOW TSC INSTEAD OF JUST TAKING + // THE FIRST ONE + blocksToWeaklyReference.Add(validAttachments[0]) + + return nil + }) + } + references = make(model.ParentReferences) strongParents := ds.NewSet[iotago.BlockID]() - shallowLikesParents := ds.NewSet[iotago.BlockID]() + shallowLikedParents := ds.NewSet[iotago.BlockID]() _ = t.spendDAG.ReadConsistent(func(_ spenddag.ReadLockedSpendDAG[iotago.TransactionID, mempool.StateID, ledger.BlockVoteRank]) error { previousLikedInsteadConflicts := ds.NewSet[iotago.TransactionID]() @@ -117,7 +188,7 @@ func (t *TipSelection) SelectTips(amount int) (references model.ParentReferences references[iotago.StrongParentType] = append(references[iotago.StrongParentType], tip.ID()) references[iotago.ShallowLikeParentType] = append(references[iotago.ShallowLikeParentType], addedLikedInsteadReferences...) - shallowLikesParents.AddAll(ds.NewSet(addedLikedInsteadReferences...)) + shallowLikedParents.AddAll(ds.NewSet(addedLikedInsteadReferences...)) strongParents.Add(tip.ID()) previousLikedInsteadConflicts = updatedLikedInsteadConflicts @@ -136,7 +207,7 @@ func (t *TipSelection) SelectTips(amount int) (references model.ParentReferences t.collectReferences(func(tip tipmanager.TipMetadata) { if !t.isValidWeakTip(tip.Block()) { tip.TipPool().Set(tipmanager.DroppedTipPool) - } else if !shallowLikesParents.Has(tip.ID()) { + } else if !shallowLikedParents.Has(tip.ID()) { references[iotago.WeakParentType] = append(references[iotago.WeakParentType], tip.ID()) } }, func() int { From 262bb1edf073f8375f97d4cfc23b2bc93780fb3f Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Thu, 21 Mar 2024 08:52:08 +0100 Subject: [PATCH 11/40] Fix: fixed interface --- pkg/protocol/engine/tipselection/tipselection.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/protocol/engine/tipselection/tipselection.go b/pkg/protocol/engine/tipselection/tipselection.go index 59b76431c..222ac8933 100644 --- a/pkg/protocol/engine/tipselection/tipselection.go +++ b/pkg/protocol/engine/tipselection/tipselection.go @@ -5,12 +5,13 @@ import ( "github.com/iotaledger/hive.go/runtime/module" "github.com/iotaledger/iota-core/pkg/model" + iotago "github.com/iotaledger/iota.go/v4" ) // TipSelection is a component that is used to abstract away the tip selection strategy, used to issuing new blocks. type TipSelection interface { // SelectTips selects the tips that should be used as references for a new block. - SelectTips(count int) (references model.ParentReferences) + SelectTips(count int, optPayload ...iotago.Payload) (references model.ParentReferences) // SetAcceptanceTime updates the acceptance time of the TipSelection. SetAcceptanceTime(acceptanceTime time.Time) (previousTime time.Time) @@ -18,6 +19,6 @@ type TipSelection interface { // Reset resets the component to a clean state as if it was created at the last commitment. Reset() - // Interface embeds the required methods of the module.Module. + // Module embeds the module capabilities. module.Module } From 1d696eeff98f2b8da8deded661651bc6524dea09 Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Thu, 21 Mar 2024 09:09:08 +0100 Subject: [PATCH 12/40] Fix: addressed linter issues --- pkg/protocol/engine/mempool/state_metadata.go | 2 +- pkg/protocol/engine/mempool/v1/state_metadata.go | 7 ++++--- pkg/protocol/engine/tipselection/v1/tip_selection.go | 6 ++++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/pkg/protocol/engine/mempool/state_metadata.go b/pkg/protocol/engine/mempool/state_metadata.go index a1c144d11..dd41cb473 100644 --- a/pkg/protocol/engine/mempool/state_metadata.go +++ b/pkg/protocol/engine/mempool/state_metadata.go @@ -20,7 +20,7 @@ type StateMetadata interface { InclusionSlot() iotago.SlotIndex - OnInclusionSlotUpdated(func(prevSlot iotago.SlotIndex, newSlot iotago.SlotIndex)) + OnInclusionSlotUpdated(callback func(prevSlot iotago.SlotIndex, newSlot iotago.SlotIndex)) inclusionFlags } diff --git a/pkg/protocol/engine/mempool/v1/state_metadata.go b/pkg/protocol/engine/mempool/v1/state_metadata.go index f31ffad33..f275afe43 100644 --- a/pkg/protocol/engine/mempool/v1/state_metadata.go +++ b/pkg/protocol/engine/mempool/v1/state_metadata.go @@ -141,11 +141,12 @@ func (s *StateMetadata) InclusionSlot() iotago.SlotIndex { func (s *StateMetadata) OnInclusionSlotUpdated(callback func(prevID iotago.SlotIndex, newID iotago.SlotIndex)) { s.inclusionSlot.OnUpdate(func(oldValue *iotago.SlotIndex, newValue *iotago.SlotIndex) { - if oldValue == nil { + switch { + case oldValue == nil: callback(iotago.SlotIndex(0), *newValue) - } else if newValue == nil { + case newValue == nil: callback(*oldValue, iotago.SlotIndex(0)) - } else { + default: callback(*oldValue, *newValue) } }) diff --git a/pkg/protocol/engine/tipselection/v1/tip_selection.go b/pkg/protocol/engine/tipselection/v1/tip_selection.go index 5a2c4dedd..21779f5fb 100644 --- a/pkg/protocol/engine/tipselection/v1/tip_selection.go +++ b/pkg/protocol/engine/tipselection/v1/tip_selection.go @@ -160,7 +160,7 @@ func (t *TipSelection) SelectTips(amount int, optPayload ...iotago.Payload) (ref } blocksToWeaklyReference := ds.NewSet[iotago.BlockID]() - err = dependenciesToReference.ForEach(func(transactionToReference mempool.TransactionMetadata) error { + if err = dependenciesToReference.ForEach(func(transactionToReference mempool.TransactionMetadata) error { validAttachments := transactionToReference.ValidAttachments() if len(validAttachments) == 0 { return ierrors.Errorf("transaction %s has no valid attachments", transactionToReference.ID()) @@ -171,7 +171,9 @@ func (t *TipSelection) SelectTips(amount int, optPayload ...iotago.Payload) (ref blocksToWeaklyReference.Add(validAttachments[0]) return nil - }) + }); err != nil { + panic(err) + } } references = make(model.ParentReferences) From bc6584b8014d51eaa93737d36bcaab423711ef43 Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Fri, 22 Mar 2024 01:09:23 +0100 Subject: [PATCH 13/40] Feat: WIP WIP (going to bed) --- .../engine/tipselection/v1/payload_utils.go | 118 ++++++++++++++++++ .../engine/tipselection/v1/tip_selection.go | 76 ++--------- 2 files changed, 130 insertions(+), 64 deletions(-) create mode 100644 pkg/protocol/engine/tipselection/v1/payload_utils.go diff --git a/pkg/protocol/engine/tipselection/v1/payload_utils.go b/pkg/protocol/engine/tipselection/v1/payload_utils.go new file mode 100644 index 000000000..9c59e27cb --- /dev/null +++ b/pkg/protocol/engine/tipselection/v1/payload_utils.go @@ -0,0 +1,118 @@ +package tipselectionv1 + +import ( + "github.com/iotaledger/hive.go/ds" + "github.com/iotaledger/hive.go/ierrors" + "github.com/iotaledger/iota-core/pkg/protocol/engine/blocks" + "github.com/iotaledger/iota-core/pkg/protocol/engine/ledger" + "github.com/iotaledger/iota-core/pkg/protocol/engine/mempool" + iotago "github.com/iotaledger/iota.go/v4" +) + +// PayloadUtils is a utility component that provides helper functions for dealing payloads. +type PayloadUtils struct { + memPool mempool.MemPool[ledger.BlockVoteRank] + blocks *blocks.Blocks +} + +// NewPayloadUtils creates a new PayloadUtils instance. +func NewPayloadUtils(memPool mempool.MemPool[ledger.BlockVoteRank], blocks *blocks.Blocks) *PayloadUtils { + return &PayloadUtils{ + memPool: memPool, + blocks: blocks, + } +} + +// UnacceptedInputs returns the unaccepted inputs of a payload. +func (t *PayloadUtils) UnacceptedInputs(payload iotago.Payload) (unacceptedInputs []mempool.StateMetadata, err error) { + if payload.PayloadType() != iotago.PayloadSignedTransaction { + return nil, nil + } + + signedTransaction, isSignedTransaction := payload.(*iotago.SignedTransaction) + if !isSignedTransaction { + return nil, ierrors.New("failed to cast payload to signed transaction") + } + + inputReferences, inputReferencesErr := t.memPool.VM().Inputs(signedTransaction.Transaction) + if inputReferencesErr != nil { + return nil, ierrors.Wrap(inputReferencesErr, "failed to retrieve input references") + } + + for _, inputReference := range inputReferences { + stateMetadata, stateMetadataErr := t.memPool.StateMetadata(inputReference) + if stateMetadataErr != nil { + return nil, ierrors.Wrap(stateMetadataErr, "failed to retrieve state metadata") + } + + if !stateMetadata.IsAccepted() { + unacceptedInputs = append(unacceptedInputs, stateMetadata) + } + } + + return unacceptedInputs, nil +} + +// UnacceptedTransactionDependencies returns the unaccepted transaction dependencies of a payload. +func (t *PayloadUtils) UnacceptedTransactionDependencies(payload iotago.Payload) (dependencies ds.Set[mempool.TransactionMetadata], err error) { + unacceptedInputs, unacceptedInputsErr := t.UnacceptedInputs(payload) + if unacceptedInputsErr != nil { + return nil, ierrors.Wrap(unacceptedInputsErr, "failed to retrieve unaccepted inputs") + } + + dependencies = ds.NewSet[mempool.TransactionMetadata]() + for _, unacceptedInput := range unacceptedInputs { + creatingTransaction := unacceptedInput.CreatingTransaction() + if creatingTransaction == nil { + return nil, ierrors.New("unable to reference non-accepted input - creating transaction not found") + } + + dependencies.Add(creatingTransaction) + } + + return dependencies, nil +} + +// LatestValidAttachment returns the latest valid attachment of a transaction. +func (t *PayloadUtils) LatestValidAttachment(transaction mempool.TransactionMetadata) (latestValidAttachment iotago.BlockID, err error) { + validAttachments := transaction.ValidAttachments() + if len(validAttachments) == 0 { + return iotago.EmptyBlockID, ierrors.Errorf("transaction %s has no valid attachments", transaction.ID()) + } + + var latestValidAttachmentBlock *blocks.Block + isLaterAttachment := func(block *blocks.Block) bool { + switch { + case latestValidAttachmentBlock == nil: + return true + case block.ID().Slot() > latestValidAttachmentBlock.ID().Slot(): + return true + case block.ID().Slot() == latestValidAttachmentBlock.ID().Slot(): + return block.ProtocolBlock().Header.IssuingTime.After(latestValidAttachmentBlock.ProtocolBlock().Header.IssuingTime) + default: + return false + } + } + + for _, validAttachment := range validAttachments { + if block, exists := t.blocks.Block(validAttachment); exists && isLaterAttachment(block) { + latestValidAttachmentBlock = block + } + } + + return latestValidAttachmentBlock.ID(), nil +} + +// LatestValidAttachments returns the latest valid attachments of a set of transactions. +func (t *PayloadUtils) LatestValidAttachments(transactions ds.Set[mempool.TransactionMetadata]) (latestValidAttachments ds.Set[iotago.BlockID], err error) { + latestValidAttachments = ds.NewSet[iotago.BlockID]() + + return latestValidAttachments, transactions.ForEach(func(transaction mempool.TransactionMetadata) error { + latestValidAttachment, latestValidAttachmentErr := t.LatestValidAttachment(transaction) + if latestValidAttachmentErr == nil { + latestValidAttachments.Add(latestValidAttachment) + } + + return latestValidAttachmentErr + }) +} diff --git a/pkg/protocol/engine/tipselection/v1/tip_selection.go b/pkg/protocol/engine/tipselection/v1/tip_selection.go index 21779f5fb..1647d4a53 100644 --- a/pkg/protocol/engine/tipselection/v1/tip_selection.go +++ b/pkg/protocol/engine/tipselection/v1/tip_selection.go @@ -23,7 +23,7 @@ import ( // TipSelection is a component that is used to abstract away the tip selection strategy, used to issue new blocks. type TipSelection struct { - memPool mempool.MemPool[ledger.BlockVoteRank] + payloadUtils *PayloadUtils // tipManager is the TipManager that is used to access the tip related metadata. tipManager tipmanager.TipManager @@ -103,80 +103,28 @@ func (t *TipSelection) Construct(tipManager tipmanager.TipManager, spendDAG spen return module.InitSimpleLifecycle(t) } -func (t *TipSelection) unacceptedInputs(payload iotago.Payload) (inputs []mempool.StateMetadata, err error) { - if payload.PayloadType() != iotago.PayloadSignedTransaction { - return nil, nil - } - - signedTransaction, isSignedTransaction := payload.(*iotago.SignedTransaction) - if !isSignedTransaction { - return nil, ierrors.New("failed to cast payload to signed transaction") - } - - inputReferences, inputReferencesErr := t.memPool.VM().Inputs(signedTransaction.Transaction) - if inputReferencesErr != nil { - return nil, ierrors.Wrap(inputReferencesErr, "failed to retrieve input references") - } - - for _, inputReference := range inputReferences { - stateMetadata, stateMetadataErr := t.memPool.StateMetadata(inputReference) - if stateMetadataErr != nil { - return nil, ierrors.Wrap(stateMetadataErr, "failed to retrieve state metadata") - } - - if !stateMetadata.IsAccepted() { - inputs = append(inputs, stateMetadata) - } - } - - return inputs, nil -} - -func (t *TipSelection) payloadDependenciesToReference(payload iotago.Payload) (dependencies ds.Set[mempool.TransactionMetadata], err error) { - unacceptedInputs, unacceptedInputsErr := t.unacceptedInputs(payload) - if unacceptedInputsErr != nil { - return nil, ierrors.Wrap(unacceptedInputsErr, "failed to retrieve unaccepted inputs") - } - - dependencies = ds.NewSet[mempool.TransactionMetadata]() - for _, unacceptedInput := range unacceptedInputs { - creatingTransaction := unacceptedInput.CreatingTransaction() - if creatingTransaction == nil { - return nil, ierrors.New("unable to reference non-accepted input - creating transaction not found") - } - - dependencies.Add(creatingTransaction) - } - - return dependencies, nil -} - // SelectTips selects the tips that should be used as references for a new block. func (t *TipSelection) SelectTips(amount int, optPayload ...iotago.Payload) (references model.ParentReferences) { + references = make(model.ParentReferences) + if len(optPayload) != 0 { - dependenciesToReference, err := t.payloadDependenciesToReference(optPayload[0]) + dependenciesToReference, err := t.payloadUtils.UnacceptedTransactionDependencies(optPayload[0]) if err != nil { panic(err) } - blocksToWeaklyReference := ds.NewSet[iotago.BlockID]() - if err = dependenciesToReference.ForEach(func(transactionToReference mempool.TransactionMetadata) error { - validAttachments := transactionToReference.ValidAttachments() - if len(validAttachments) == 0 { - return ierrors.Errorf("transaction %s has no valid attachments", transactionToReference.ID()) - } - - // TODO: FIND LAST VALID ATTACHMENT TO REFERENCE AND CHECK FOR ORPHANAGE / BELOW TSC INSTEAD OF JUST TAKING - // THE FIRST ONE - blocksToWeaklyReference.Add(validAttachments[0]) - - return nil - }); err != nil { + latestValidAttachments, err := t.payloadUtils.LatestValidAttachments(dependenciesToReference) + if err != nil { panic(err) } + + if latestValidAttachments.Size() > t.optMaxWeakReferences { + panic("payload requires too many weak references") + } + + references[iotago.WeakParentType] = latestValidAttachments.ToSlice() } - references = make(model.ParentReferences) strongParents := ds.NewSet[iotago.BlockID]() shallowLikedParents := ds.NewSet[iotago.BlockID]() _ = t.spendDAG.ReadConsistent(func(_ spenddag.ReadLockedSpendDAG[iotago.TransactionID, mempool.StateID, ledger.BlockVoteRank]) error { From 3a040230dc944dcf4bf3c284d6aec566901d0866 Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Fri, 22 Mar 2024 14:10:23 +0100 Subject: [PATCH 14/40] Feat: WIP WIP --- components/inx/server_issuance.go | 6 +++++- .../engine/tipselection/tipselection.go | 2 +- .../engine/tipselection/v1/tip_selection.go | 19 +++++++++---------- pkg/requesthandler/blocks.go | 6 ++++-- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/components/inx/server_issuance.go b/components/inx/server_issuance.go index 4f1433db9..fbc951cd3 100644 --- a/components/inx/server_issuance.go +++ b/components/inx/server_issuance.go @@ -3,6 +3,7 @@ package inx import ( "context" + "github.com/iotaledger/hive.go/ierrors" "github.com/iotaledger/hive.go/lo" "github.com/iotaledger/hive.go/serializer/v2/serix" inx "github.com/iotaledger/inx/go" @@ -11,7 +12,10 @@ import ( ) func (s *Server) RequestTips(_ context.Context, req *inx.TipsRequest) (*inx.TipsResponse, error) { - references := deps.Protocol.Engines.Main.Get().TipSelection.SelectTips(int(req.GetCount())) + references, err := deps.Protocol.Engines.Main.Get().TipSelection.SelectTips(int(req.GetCount())) + if err != nil { + return nil, ierrors.Wrap(err, "failed to select tips") + } return &inx.TipsResponse{ StrongTips: inx.NewBlockIds(references[iotago.StrongParentType]), diff --git a/pkg/protocol/engine/tipselection/tipselection.go b/pkg/protocol/engine/tipselection/tipselection.go index 222ac8933..1cd797d33 100644 --- a/pkg/protocol/engine/tipselection/tipselection.go +++ b/pkg/protocol/engine/tipselection/tipselection.go @@ -11,7 +11,7 @@ import ( // TipSelection is a component that is used to abstract away the tip selection strategy, used to issuing new blocks. type TipSelection interface { // SelectTips selects the tips that should be used as references for a new block. - SelectTips(count int, optPayload ...iotago.Payload) (references model.ParentReferences) + SelectTips(count int, optPayload ...iotago.Payload) (references model.ParentReferences, err error) // SetAcceptanceTime updates the acceptance time of the TipSelection. SetAcceptanceTime(acceptanceTime time.Time) (previousTime time.Time) diff --git a/pkg/protocol/engine/tipselection/v1/tip_selection.go b/pkg/protocol/engine/tipselection/v1/tip_selection.go index 1647d4a53..94339c331 100644 --- a/pkg/protocol/engine/tipselection/v1/tip_selection.go +++ b/pkg/protocol/engine/tipselection/v1/tip_selection.go @@ -104,22 +104,21 @@ func (t *TipSelection) Construct(tipManager tipmanager.TipManager, spendDAG spen } // SelectTips selects the tips that should be used as references for a new block. -func (t *TipSelection) SelectTips(amount int, optPayload ...iotago.Payload) (references model.ParentReferences) { +func (t *TipSelection) SelectTips(amount int, optPayload ...iotago.Payload) (references model.ParentReferences, err error) { references = make(model.ParentReferences) - if len(optPayload) != 0 { - dependenciesToReference, err := t.payloadUtils.UnacceptedTransactionDependencies(optPayload[0]) - if err != nil { - panic(err) + dependenciesToReference, dependenciesErr := t.payloadUtils.UnacceptedTransactionDependencies(optPayload[0]) + if dependenciesErr != nil { + return nil, ierrors.Wrap(dependenciesErr, "failed to retrieve unaccepted transaction dependencies") } - latestValidAttachments, err := t.payloadUtils.LatestValidAttachments(dependenciesToReference) - if err != nil { - panic(err) + latestValidAttachments, latestValidAttachmentsErr := t.payloadUtils.LatestValidAttachments(dependenciesToReference) + if latestValidAttachmentsErr != nil { + return nil, ierrors.Wrap(latestValidAttachmentsErr, "failed to retrieve latest valid attachments") } if latestValidAttachments.Size() > t.optMaxWeakReferences { - panic("payload requires too many weak references") + return nil, ierrors.New("payload requires too many weak references") } references[iotago.WeakParentType] = latestValidAttachments.ToSlice() @@ -167,7 +166,7 @@ func (t *TipSelection) SelectTips(amount int, optPayload ...iotago.Payload) (ref return nil }) - return references + return references, nil } // SetAcceptanceTime updates the acceptance time of the TipSelection. diff --git a/pkg/requesthandler/blocks.go b/pkg/requesthandler/blocks.go index 3f80278e1..d6ad79c1b 100644 --- a/pkg/requesthandler/blocks.go +++ b/pkg/requesthandler/blocks.go @@ -51,8 +51,10 @@ func (r *RequestHandler) BlockWithMetadataFromBlockID(blockID iotago.BlockID) (* } func (r *RequestHandler) BlockIssuance() (*api.IssuanceBlockHeaderResponse, error) { - references := r.protocol.Engines.Main.Get().TipSelection.SelectTips(iotago.BasicBlockMaxParents) - if len(references[iotago.StrongParentType]) == 0 { + references, err := r.protocol.Engines.Main.Get().TipSelection.SelectTips(iotago.BasicBlockMaxParents) + if err != nil { + return nil, ierrors.Wrap(err, "failed to select tips") + } else if len(references[iotago.StrongParentType]) == 0 { return nil, ierrors.WithMessage(echo.ErrServiceUnavailable, "no strong parents available") } From ca8c9aa0eff0f6b4154400261b25330665e6aff7 Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Mon, 15 Apr 2024 14:33:37 +0200 Subject: [PATCH 15/40] Fix: fixed test --- pkg/tests/booker_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/tests/booker_test.go b/pkg/tests/booker_test.go index c8c7b1a82..8f4d54273 100644 --- a/pkg/tests/booker_test.go +++ b/pkg/tests/booker_test.go @@ -56,6 +56,7 @@ func Test_IssuingTransactionsOutOfOrder(t *testing.T) { // confirm 2nd block so block1 gets booked { ts.IssueValidationBlockWithHeaderOptions("block3", node1, mock.WithStrongParents(ts.BlockID("block2"))) + ts.IssueValidationBlockWithHeaderOptions("block4", node1, mock.WithStrongParents(ts.BlockID("block3"))) ts.AssertBlocksInCacheBooked(ts.Blocks("block1", "block2", "block3"), true, node1) ts.AssertBlocksInCacheConflicts(map[*blocks.Block][]string{ From dbc8b0d819b763926496d4d48aa5c10aac12e8e5 Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Mon, 15 Apr 2024 15:39:50 +0200 Subject: [PATCH 16/40] Refactor: reverted some changes --- components/inx/server_issuance.go | 6 +---- .../engine/booker/inmemorybooker/booker.go | 6 +++++ .../engine/tipselection/tipselection.go | 3 +-- .../engine/tipselection/v1/tip_selection.go | 22 ++----------------- pkg/requesthandler/blocks.go | 6 ++--- 5 files changed, 12 insertions(+), 31 deletions(-) diff --git a/components/inx/server_issuance.go b/components/inx/server_issuance.go index fbc951cd3..4f1433db9 100644 --- a/components/inx/server_issuance.go +++ b/components/inx/server_issuance.go @@ -3,7 +3,6 @@ package inx import ( "context" - "github.com/iotaledger/hive.go/ierrors" "github.com/iotaledger/hive.go/lo" "github.com/iotaledger/hive.go/serializer/v2/serix" inx "github.com/iotaledger/inx/go" @@ -12,10 +11,7 @@ import ( ) func (s *Server) RequestTips(_ context.Context, req *inx.TipsRequest) (*inx.TipsResponse, error) { - references, err := deps.Protocol.Engines.Main.Get().TipSelection.SelectTips(int(req.GetCount())) - if err != nil { - return nil, ierrors.Wrap(err, "failed to select tips") - } + references := deps.Protocol.Engines.Main.Get().TipSelection.SelectTips(int(req.GetCount())) return &inx.TipsResponse{ StrongTips: inx.NewBlockIds(references[iotago.StrongParentType]), diff --git a/pkg/protocol/engine/booker/inmemorybooker/booker.go b/pkg/protocol/engine/booker/inmemorybooker/booker.go index 78f44e719..34c1e03dd 100644 --- a/pkg/protocol/engine/booker/inmemorybooker/booker.go +++ b/pkg/protocol/engine/booker/inmemorybooker/booker.go @@ -109,6 +109,12 @@ func (b *Booker) Queue(block *blocks.Block) error { signedTransactionMetadata.OnSignaturesValid(func() { transactionMetadata := signedTransactionMetadata.TransactionMetadata() + if orphanedSlot, isOrphaned := transactionMetadata.OrphanedSlot(); isOrphaned && orphanedSlot <= block.SlotCommitmentID().Slot() { + block.SetInvalid() + + return + } + transactionMetadata.OnBooked(func() { block.SetPayloadSpenderIDs(transactionMetadata.SpenderIDs()) b.setupBlock(block) diff --git a/pkg/protocol/engine/tipselection/tipselection.go b/pkg/protocol/engine/tipselection/tipselection.go index 1cd797d33..be0c172ed 100644 --- a/pkg/protocol/engine/tipselection/tipselection.go +++ b/pkg/protocol/engine/tipselection/tipselection.go @@ -5,13 +5,12 @@ import ( "github.com/iotaledger/hive.go/runtime/module" "github.com/iotaledger/iota-core/pkg/model" - iotago "github.com/iotaledger/iota.go/v4" ) // TipSelection is a component that is used to abstract away the tip selection strategy, used to issuing new blocks. type TipSelection interface { // SelectTips selects the tips that should be used as references for a new block. - SelectTips(count int, optPayload ...iotago.Payload) (references model.ParentReferences, err error) + SelectTips(count int) (references model.ParentReferences) // SetAcceptanceTime updates the acceptance time of the TipSelection. SetAcceptanceTime(acceptanceTime time.Time) (previousTime time.Time) diff --git a/pkg/protocol/engine/tipselection/v1/tip_selection.go b/pkg/protocol/engine/tipselection/v1/tip_selection.go index 8ea7329ff..d329b2fda 100644 --- a/pkg/protocol/engine/tipselection/v1/tip_selection.go +++ b/pkg/protocol/engine/tipselection/v1/tip_selection.go @@ -110,26 +110,8 @@ func (t *TipSelection) Construct(tipManager tipmanager.TipManager, spendDAG spen } // SelectTips selects the tips that should be used as references for a new block. -func (t *TipSelection) SelectTips(amount int, optPayload ...iotago.Payload) (references model.ParentReferences, err error) { +func (t *TipSelection) SelectTips(amount int) (references model.ParentReferences) { references = make(model.ParentReferences) - if len(optPayload) != 0 { - dependenciesToReference, dependenciesErr := t.payloadUtils.UnacceptedTransactionDependencies(optPayload[0]) - if dependenciesErr != nil { - return nil, ierrors.Wrap(dependenciesErr, "failed to retrieve unaccepted transaction dependencies") - } - - latestValidAttachments, latestValidAttachmentsErr := t.payloadUtils.LatestValidAttachments(dependenciesToReference) - if latestValidAttachmentsErr != nil { - return nil, ierrors.Wrap(latestValidAttachmentsErr, "failed to retrieve latest valid attachments") - } - - if latestValidAttachments.Size() > t.optMaxWeakReferences { - return nil, ierrors.New("payload requires too many weak references") - } - - references[iotago.WeakParentType] = latestValidAttachments.ToSlice() - } - strongParents := ds.NewSet[iotago.BlockID]() shallowLikedParents := ds.NewSet[iotago.BlockID]() _ = t.spendDAG.ReadConsistent(func(_ spenddag.ReadLockedSpendDAG[iotago.TransactionID, mempool.StateID, ledger.BlockVoteRank]) error { @@ -172,7 +154,7 @@ func (t *TipSelection) SelectTips(amount int, optPayload ...iotago.Payload) (ref return nil }) - return references, nil + return references } // SetAcceptanceTime updates the acceptance time of the TipSelection. diff --git a/pkg/requesthandler/blocks.go b/pkg/requesthandler/blocks.go index be53d8abc..b80d4e1f1 100644 --- a/pkg/requesthandler/blocks.go +++ b/pkg/requesthandler/blocks.go @@ -61,10 +61,8 @@ func (r *RequestHandler) BlockWithMetadataFromBlockID(blockID iotago.BlockID) (* } func (r *RequestHandler) BlockIssuance() (*api.IssuanceBlockHeaderResponse, error) { - references, err := r.protocol.Engines.Main.Get().TipSelection.SelectTips(iotago.BasicBlockMaxParents) - if err != nil { - return nil, ierrors.Wrap(err, "failed to select tips") - } else if len(references[iotago.StrongParentType]) == 0 { + references := r.protocol.Engines.Main.Get().TipSelection.SelectTips(iotago.BasicBlockMaxParents) + if len(references[iotago.StrongParentType]) == 0 { return nil, ierrors.WithMessage(echo.ErrServiceUnavailable, "no strong parents available") } From a527c42c2bed848ecf25e10af57ba200ef1aa927 Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Mon, 15 Apr 2024 15:42:42 +0200 Subject: [PATCH 17/40] Refactor: removed PayloadUtils --- .../engine/tipselection/v1/payload_utils.go | 118 ------------------ .../engine/tipselection/v1/tip_selection.go | 2 - 2 files changed, 120 deletions(-) delete mode 100644 pkg/protocol/engine/tipselection/v1/payload_utils.go diff --git a/pkg/protocol/engine/tipselection/v1/payload_utils.go b/pkg/protocol/engine/tipselection/v1/payload_utils.go deleted file mode 100644 index 9c59e27cb..000000000 --- a/pkg/protocol/engine/tipselection/v1/payload_utils.go +++ /dev/null @@ -1,118 +0,0 @@ -package tipselectionv1 - -import ( - "github.com/iotaledger/hive.go/ds" - "github.com/iotaledger/hive.go/ierrors" - "github.com/iotaledger/iota-core/pkg/protocol/engine/blocks" - "github.com/iotaledger/iota-core/pkg/protocol/engine/ledger" - "github.com/iotaledger/iota-core/pkg/protocol/engine/mempool" - iotago "github.com/iotaledger/iota.go/v4" -) - -// PayloadUtils is a utility component that provides helper functions for dealing payloads. -type PayloadUtils struct { - memPool mempool.MemPool[ledger.BlockVoteRank] - blocks *blocks.Blocks -} - -// NewPayloadUtils creates a new PayloadUtils instance. -func NewPayloadUtils(memPool mempool.MemPool[ledger.BlockVoteRank], blocks *blocks.Blocks) *PayloadUtils { - return &PayloadUtils{ - memPool: memPool, - blocks: blocks, - } -} - -// UnacceptedInputs returns the unaccepted inputs of a payload. -func (t *PayloadUtils) UnacceptedInputs(payload iotago.Payload) (unacceptedInputs []mempool.StateMetadata, err error) { - if payload.PayloadType() != iotago.PayloadSignedTransaction { - return nil, nil - } - - signedTransaction, isSignedTransaction := payload.(*iotago.SignedTransaction) - if !isSignedTransaction { - return nil, ierrors.New("failed to cast payload to signed transaction") - } - - inputReferences, inputReferencesErr := t.memPool.VM().Inputs(signedTransaction.Transaction) - if inputReferencesErr != nil { - return nil, ierrors.Wrap(inputReferencesErr, "failed to retrieve input references") - } - - for _, inputReference := range inputReferences { - stateMetadata, stateMetadataErr := t.memPool.StateMetadata(inputReference) - if stateMetadataErr != nil { - return nil, ierrors.Wrap(stateMetadataErr, "failed to retrieve state metadata") - } - - if !stateMetadata.IsAccepted() { - unacceptedInputs = append(unacceptedInputs, stateMetadata) - } - } - - return unacceptedInputs, nil -} - -// UnacceptedTransactionDependencies returns the unaccepted transaction dependencies of a payload. -func (t *PayloadUtils) UnacceptedTransactionDependencies(payload iotago.Payload) (dependencies ds.Set[mempool.TransactionMetadata], err error) { - unacceptedInputs, unacceptedInputsErr := t.UnacceptedInputs(payload) - if unacceptedInputsErr != nil { - return nil, ierrors.Wrap(unacceptedInputsErr, "failed to retrieve unaccepted inputs") - } - - dependencies = ds.NewSet[mempool.TransactionMetadata]() - for _, unacceptedInput := range unacceptedInputs { - creatingTransaction := unacceptedInput.CreatingTransaction() - if creatingTransaction == nil { - return nil, ierrors.New("unable to reference non-accepted input - creating transaction not found") - } - - dependencies.Add(creatingTransaction) - } - - return dependencies, nil -} - -// LatestValidAttachment returns the latest valid attachment of a transaction. -func (t *PayloadUtils) LatestValidAttachment(transaction mempool.TransactionMetadata) (latestValidAttachment iotago.BlockID, err error) { - validAttachments := transaction.ValidAttachments() - if len(validAttachments) == 0 { - return iotago.EmptyBlockID, ierrors.Errorf("transaction %s has no valid attachments", transaction.ID()) - } - - var latestValidAttachmentBlock *blocks.Block - isLaterAttachment := func(block *blocks.Block) bool { - switch { - case latestValidAttachmentBlock == nil: - return true - case block.ID().Slot() > latestValidAttachmentBlock.ID().Slot(): - return true - case block.ID().Slot() == latestValidAttachmentBlock.ID().Slot(): - return block.ProtocolBlock().Header.IssuingTime.After(latestValidAttachmentBlock.ProtocolBlock().Header.IssuingTime) - default: - return false - } - } - - for _, validAttachment := range validAttachments { - if block, exists := t.blocks.Block(validAttachment); exists && isLaterAttachment(block) { - latestValidAttachmentBlock = block - } - } - - return latestValidAttachmentBlock.ID(), nil -} - -// LatestValidAttachments returns the latest valid attachments of a set of transactions. -func (t *PayloadUtils) LatestValidAttachments(transactions ds.Set[mempool.TransactionMetadata]) (latestValidAttachments ds.Set[iotago.BlockID], err error) { - latestValidAttachments = ds.NewSet[iotago.BlockID]() - - return latestValidAttachments, transactions.ForEach(func(transaction mempool.TransactionMetadata) error { - latestValidAttachment, latestValidAttachmentErr := t.LatestValidAttachment(transaction) - if latestValidAttachmentErr == nil { - latestValidAttachments.Add(latestValidAttachment) - } - - return latestValidAttachmentErr - }) -} diff --git a/pkg/protocol/engine/tipselection/v1/tip_selection.go b/pkg/protocol/engine/tipselection/v1/tip_selection.go index d329b2fda..cc298dcec 100644 --- a/pkg/protocol/engine/tipselection/v1/tip_selection.go +++ b/pkg/protocol/engine/tipselection/v1/tip_selection.go @@ -23,8 +23,6 @@ import ( // TipSelection is a component that is used to abstract away the tip selection strategy, used to issue new blocks. type TipSelection struct { - payloadUtils *PayloadUtils - // tipManager is the TipManager that is used to access the tip related metadata. tipManager tipmanager.TipManager From a573a63b5aa375f179af7cdc7ef82005191c0c2e Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Mon, 15 Apr 2024 15:45:28 +0200 Subject: [PATCH 18/40] Refactor: fixed minor things --- pkg/protocol/engine/mempool/state.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/protocol/engine/mempool/state.go b/pkg/protocol/engine/mempool/state.go index 00b51d93c..e8c9957ba 100644 --- a/pkg/protocol/engine/mempool/state.go +++ b/pkg/protocol/engine/mempool/state.go @@ -16,7 +16,7 @@ type State interface { // Whether the state is read only. IsReadOnly() bool - // SlotBooked returns the slot index of the state if it is booked, otherwise -1. + // SlotBooked returns the slot index of the state if it is booked. SlotBooked() iotago.SlotIndex } From c2b4ad2e081146d7611f213d1bee8c7f40226182 Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Mon, 15 Apr 2024 15:51:16 +0200 Subject: [PATCH 19/40] Refactor: minimized changes --- pkg/protocol/engine/tipselection/v1/tip_selection.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/protocol/engine/tipselection/v1/tip_selection.go b/pkg/protocol/engine/tipselection/v1/tip_selection.go index cc298dcec..2b41bf709 100644 --- a/pkg/protocol/engine/tipselection/v1/tip_selection.go +++ b/pkg/protocol/engine/tipselection/v1/tip_selection.go @@ -111,7 +111,7 @@ func (t *TipSelection) Construct(tipManager tipmanager.TipManager, spendDAG spen func (t *TipSelection) SelectTips(amount int) (references model.ParentReferences) { references = make(model.ParentReferences) strongParents := ds.NewSet[iotago.BlockID]() - shallowLikedParents := ds.NewSet[iotago.BlockID]() + shallowLikesParents := ds.NewSet[iotago.BlockID]() _ = t.spendDAG.ReadConsistent(func(_ spenddag.ReadLockedSpendDAG[iotago.TransactionID, mempool.StateID, ledger.BlockVoteRank]) error { previousLikedInsteadConflicts := ds.NewSet[iotago.TransactionID]() @@ -123,7 +123,7 @@ func (t *TipSelection) SelectTips(amount int) (references model.ParentReferences references[iotago.StrongParentType] = append(references[iotago.StrongParentType], tip.ID()) references[iotago.ShallowLikeParentType] = append(references[iotago.ShallowLikeParentType], addedLikedInsteadReferences...) - shallowLikedParents.AddAll(ds.NewSet(addedLikedInsteadReferences...)) + shallowLikesParents.AddAll(ds.NewSet(addedLikedInsteadReferences...)) strongParents.Add(tip.ID()) previousLikedInsteadConflicts = updatedLikedInsteadConflicts @@ -142,7 +142,7 @@ func (t *TipSelection) SelectTips(amount int) (references model.ParentReferences t.collectReferences(func(tip tipmanager.TipMetadata) { if !t.isValidWeakTip(tip.Block()) { tip.TipPool().Set(tipmanager.DroppedTipPool) - } else if !shallowLikedParents.Has(tip.ID()) { + } else if !shallowLikesParents.Has(tip.ID()) { references[iotago.WeakParentType] = append(references[iotago.WeakParentType], tip.ID()) } }, func() int { From fb9bd39744aae04f5e5b18df36a047d15155931f Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Mon, 15 Apr 2024 15:51:53 +0200 Subject: [PATCH 20/40] Refactor: minimize changes --- pkg/protocol/engine/tipselection/tipselection.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/protocol/engine/tipselection/tipselection.go b/pkg/protocol/engine/tipselection/tipselection.go index be0c172ed..59b76431c 100644 --- a/pkg/protocol/engine/tipselection/tipselection.go +++ b/pkg/protocol/engine/tipselection/tipselection.go @@ -18,6 +18,6 @@ type TipSelection interface { // Reset resets the component to a clean state as if it was created at the last commitment. Reset() - // Module embeds the module capabilities. + // Interface embeds the required methods of the module.Module. module.Module } From 48f4fd14d13f9eff3e8ff1fd189813d7708d45ff Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Mon, 15 Apr 2024 18:09:54 +0200 Subject: [PATCH 21/40] Feat: renamed variables --- pkg/protocol/engine/blocks/block.go | 25 +++++++++++-------- .../engine/booker/inmemorybooker/booker.go | 22 ++++++++-------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/pkg/protocol/engine/blocks/block.go b/pkg/protocol/engine/blocks/block.go index 312d3db3c..e27463b59 100644 --- a/pkg/protocol/engine/blocks/block.go +++ b/pkg/protocol/engine/blocks/block.go @@ -17,8 +17,13 @@ import ( ) type Block struct { - AllParentsBooked reactive.Event - AllDependenciesReady reactive.Event + // ParentsBooked is triggered when all parents of the block are booked. + ParentsBooked reactive.Event + + // PayloadDependenciesAvailable is triggered when the dependencies of the block's payload are available. + PayloadDependenciesAvailable reactive.Event + + // SignedTransactionMetadata contains the signed transaction metadata of the block. SignedTransactionMetadata reactive.Variable[mempool.SignedTransactionMetadata] // BlockDAG block @@ -82,9 +87,9 @@ func (r *rootBlock) String() string { func newEmptyBlock() *Block { return &Block{ - AllParentsBooked: reactive.NewEvent(), - AllDependenciesReady: reactive.NewEvent(), - SignedTransactionMetadata: reactive.NewVariable[mempool.SignedTransactionMetadata](), + ParentsBooked: reactive.NewEvent(), + PayloadDependenciesAvailable: reactive.NewEvent(), + SignedTransactionMetadata: reactive.NewVariable[mempool.SignedTransactionMetadata](), witnesses: ds.NewSet[account.SeatIndex](), spenderIDs: ds.NewSet[iotago.TransactionID](), @@ -122,8 +127,8 @@ func NewRootBlock(blockID iotago.BlockID, commitmentID iotago.CommitmentID, issu b.scheduled = true // This should be true since we commit and evict on acceptance. - b.AllParentsBooked.Set(true) - b.AllDependenciesReady.Set(true) + b.ParentsBooked.Set(true) + b.PayloadDependenciesAvailable.Set(true) b.solid.Init(true) b.booked.Init(true) b.weightPropagated.Init(true) @@ -676,9 +681,9 @@ func (b *Block) WorkScore() iotago.WorkScore { return b.workScore } -func (b *Block) WaitForUTXODependencies(dependencies ds.Set[mempool.StateMetadata]) { +func (b *Block) WaitForPayloadDependencies(dependencies ds.Set[mempool.StateMetadata]) { if dependencies == nil || dependencies.Size() == 0 { - b.AllDependenciesReady.Trigger() + b.PayloadDependenciesAvailable.Trigger() return } @@ -695,7 +700,7 @@ func (b *Block) WaitForUTXODependencies(dependencies ds.Set[mempool.StateMetadat dependencyReady = true if unreferencedOutputCount.Add(-1) == 0 { - b.AllDependenciesReady.Trigger() + b.PayloadDependenciesAvailable.Trigger() } } }) diff --git a/pkg/protocol/engine/booker/inmemorybooker/booker.go b/pkg/protocol/engine/booker/inmemorybooker/booker.go index 34c1e03dd..042f5b751 100644 --- a/pkg/protocol/engine/booker/inmemorybooker/booker.go +++ b/pkg/protocol/engine/booker/inmemorybooker/booker.go @@ -136,10 +136,10 @@ func (b *Booker) Queue(block *blocks.Block) error { func (b *Booker) Reset() { /* nothing to reset but comply with interface */ } func (b *Booker) setupBlock(block *blocks.Block) { - var utxoDependencies, directlyReferencedUTXODependencies ds.Set[mempool.StateMetadata] + var payloadDependencies, directlyReferencedPayloadDependencies ds.Set[mempool.StateMetadata] if signedTransactionMetadata := block.SignedTransactionMetadata.Get(); signedTransactionMetadata != nil && signedTransactionMetadata.SignaturesInvalid() == nil && !signedTransactionMetadata.TransactionMetadata().IsInvalid() { - utxoDependencies = signedTransactionMetadata.TransactionMetadata().Inputs() - directlyReferencedUTXODependencies = ds.NewSet[mempool.StateMetadata]() + payloadDependencies = signedTransactionMetadata.TransactionMetadata().Inputs() + directlyReferencedPayloadDependencies = ds.NewSet[mempool.StateMetadata]() } var unbookedParentsCount atomic.Int32 @@ -154,14 +154,14 @@ func (b *Booker) setupBlock(block *blocks.Block) { } parentBlock.Booked().OnUpdateOnce(func(_ bool, _ bool) { - if directlyReferencedUTXODependencies != nil { + if directlyReferencedPayloadDependencies != nil { if parentTransactionMetadata := parentBlock.SignedTransactionMetadata.Get(); parentTransactionMetadata != nil { - directlyReferencedUTXODependencies.AddAll(parentTransactionMetadata.TransactionMetadata().Outputs()) + directlyReferencedPayloadDependencies.AddAll(parentTransactionMetadata.TransactionMetadata().Outputs()) } } if unbookedParentsCount.Add(-1) == 0 { - block.AllParentsBooked.Trigger() + block.ParentsBooked.Trigger() } }) @@ -172,15 +172,15 @@ func (b *Booker) setupBlock(block *blocks.Block) { }) }) - block.AllParentsBooked.OnTrigger(func() { - if directlyReferencedUTXODependencies != nil { - utxoDependencies.DeleteAll(directlyReferencedUTXODependencies) + block.ParentsBooked.OnTrigger(func() { + if directlyReferencedPayloadDependencies != nil { + payloadDependencies.DeleteAll(directlyReferencedPayloadDependencies) } - block.WaitForUTXODependencies(utxoDependencies) + block.WaitForPayloadDependencies(payloadDependencies) }) - block.AllDependenciesReady.OnTrigger(func() { + block.PayloadDependenciesAvailable.OnTrigger(func() { if err := b.book(block); err != nil { if block.SetInvalid() { b.events.BlockInvalid.Trigger(block, ierrors.Wrap(err, "failed to book block")) From e20d126507684092fec9624e4255a432b868fb62 Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Mon, 15 Apr 2024 19:17:38 +0200 Subject: [PATCH 22/40] Refactor: removed unnecessary tests --- pkg/tests/booker_test.go | 403 --------------------------------------- 1 file changed, 403 deletions(-) diff --git a/pkg/tests/booker_test.go b/pkg/tests/booker_test.go index 8f4d54273..ef54660f6 100644 --- a/pkg/tests/booker_test.go +++ b/pkg/tests/booker_test.go @@ -230,409 +230,6 @@ func Test_DoubleSpend(t *testing.T) { } } -func Test_MultipleAttachments(t *testing.T) { - t.Skip("This test is currently disabled because it is not yet clear how to handle this case.") - - ts := testsuite.NewTestSuite(t) - defer ts.Shutdown() - - nodeA := ts.AddValidatorNode("nodeA") - nodeB := ts.AddValidatorNode("nodeB") - wallet := ts.AddDefaultWallet(nodeA) - - ts.Run(true, map[string][]options.Option[protocol.Protocol]{}) - - blocksConflicts := make(map[*blocks.Block][]string) - - // 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") - - ts.IssueBasicBlockWithOptions("A.1", wallet, tx1, mock.WithStrongParents(ts.BlockID("Genesis"))) - ts.IssueValidationBlockWithHeaderOptions("A.1.1", nodeA, mock.WithStrongParents(ts.BlockID("A.1"))) - wallet.SetDefaultClient(nodeB.Client) - ts.IssueBasicBlockWithOptions("B.1", wallet, tx1, mock.WithStrongParents(ts.BlockID("Genesis"))) - ts.IssueValidationBlockWithHeaderOptions("B.1.1", nodeB, mock.WithStrongParents(ts.BlockID("B.1"))) - - nodeA.Wait() - 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.1"): {"tx1"}, - ts.Block("B.2.1"): {"tx1"}, - }), ts.Nodes()...) - ts.AssertTransactionInCacheConflicts(map[*iotago.Transaction][]string{ - wallet.Transaction("tx1"): {"tx1"}, - }, ts.Nodes()...) - ts.AssertSpendersInCacheAcceptanceState([]string{"tx1"}, acceptance.Accepted, ts.Nodes()...) - } - - // Create a transaction that is included and whose conflict is accepted, but whose inputs are not accepted. - { - tx2 := wallet.CreateBasicOutputsEquallyFromInput("tx2", 1, "tx1:1") - - wallet.SetDefaultClient(nodeA.Client) - ts.IssueBasicBlockWithOptions("A.3", wallet, tx2, mock.WithStrongParents(ts.BlockID("Genesis"))) - ts.IssueValidationBlockWithHeaderOptions("A.3.1", nodeA, mock.WithStrongParents(ts.BlockID("A.3"))) - 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.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", "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()...) - ts.AssertTransactionsInCachePending(wallet.Transactions("tx1", "tx2"), true, ts.Nodes()...) - - ts.AssertBlocksInCacheConflicts(lo.MergeMaps(blocksConflicts, map[*blocks.Block][]string{ - ts.Block("A.3"): {"tx2"}, - ts.Block("B.3"): {"tx2"}, - ts.Block("A.4"): {"tx2"}, - 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"}, - wallet.Transaction("tx2"): {"tx2"}, - }, nodeA, nodeB) - ts.AssertSpendersInCacheAcceptanceState([]string{"tx1", "tx2"}, acceptance.Accepted, ts.Nodes()...) - } - - // 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.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.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()...) - ts.AssertTransactionsExist(wallet.Transactions("tx1", "tx2"), true, ts.Nodes()...) - ts.AssertTransactionsInCacheBooked(wallet.Transactions("tx1", "tx2"), true, ts.Nodes()...) - ts.AssertTransactionsInCacheAccepted(wallet.Transactions("tx1", "tx2"), true, ts.Nodes()...) - - ts.AssertBlocksInCacheConflicts(lo.MergeMaps(blocksConflicts, map[*blocks.Block][]string{ - ts.Block("A.6"): {}, - ts.Block("B.5"): {}, - ts.Block("A.7"): {}, - ts.Block("B.6"): {}, - }), ts.Nodes()...) - - ts.AssertTransactionInCacheConflicts(map[*iotago.Transaction][]string{ - wallet.Transaction("tx1"): {"tx1"}, - wallet.Transaction("tx2"): {"tx2"}, - }, nodeA, nodeB) - ts.AssertSpendersInCacheAcceptanceState([]string{"tx1", "tx2"}, acceptance.Accepted, nodeA, nodeB) - } -} - -func Test_SpendRejectedCommittedRace(t *testing.T) { - t.Skip("This test is currently disabled because it is not yet clear how to handle this case.") - - ts := testsuite.NewTestSuite(t, - testsuite.WithProtocolParametersOptions( - iotago.WithTimeProviderOptions( - 0, - testsuite.GenesisTimeWithOffsetBySlots(20, testsuite.DefaultSlotDurationInSeconds), - testsuite.DefaultSlotDurationInSeconds, - testsuite.DefaultSlotsPerEpochExponent, - ), - iotago.WithLivenessOptions( - 15, - 15, - 2, - 5, - testsuite.DefaultEpochNearingThreshold, - ), - ), - ) - defer ts.Shutdown() - - node1 := ts.AddValidatorNode("node1") - node2 := ts.AddValidatorNode("node2") - wallet := ts.AddDefaultWallet(node1) - - ts.Run(true, map[string][]options.Option[protocol.Protocol]{}) - - ts.AssertSybilProtectionCommittee(0, []iotago.AccountID{ - node1.Validator.AccountData.ID, - node2.Validator.AccountData.ID, - }, ts.Nodes()...) - - genesisCommitment := lo.PanicOnErr(node1.Protocol.Engines.Main.Get().Storage.Commitments().Load(0)).Commitment() - - // Create and issue double spends - { - tx1 := wallet.CreateBasicOutputsEquallyFromInput("tx1", 1, "Genesis:0") - tx2 := wallet.CreateBasicOutputsEquallyFromInput("tx2", 1, "Genesis:0") - - wallet.SetDefaultClient(node1.Client) - ts.SetCurrentSlot(1) - ts.IssueBasicBlockWithOptions("block1.1", wallet, tx1, mock.WithSlotCommitment(genesisCommitment), mock.WithStrongParents(ts.BlockID("Genesis"))) - ts.IssueBasicBlockWithOptions("block1.2", wallet, tx2, mock.WithSlotCommitment(genesisCommitment), mock.WithStrongParents(ts.BlockID("Genesis"))) - ts.SetCurrentSlot(2) - ts.IssueValidationBlockWithHeaderOptions("block2.tx1", node1, mock.WithSlotCommitment(genesisCommitment), mock.WithStrongParents(ts.BlockIDs("block1.1")...)) - - ts.AssertTransactionsExist(wallet.Transactions("tx1", "tx2"), true, node1, node2) - ts.AssertTransactionsInCacheBooked(wallet.Transactions("tx1", "tx2"), true, node1, node2) - ts.AssertTransactionsInCachePending(wallet.Transactions("tx1", "tx2"), true, node1, node2) - ts.AssertBlocksInCacheConflicts(map[*blocks.Block][]string{ - ts.Block("block1.1"): {"tx1"}, - ts.Block("block1.2"): {"tx2"}, - ts.Block("block2.tx1"): {"tx1"}, - }, node1, node2) - - ts.AssertTransactionInCacheConflicts(map[*iotago.Transaction][]string{ - wallet.Transaction("tx2"): {"tx2"}, - wallet.Transaction("tx1"): {"tx1"}, - }, node1, node2) - } - - // Issue some more blocks and assert that conflicts are propagated to blocks. - { - ts.IssueValidationBlockWithHeaderOptions("block2.1", node1, mock.WithSlotCommitment(genesisCommitment), mock.WithStrongParents(ts.BlockIDs("block1.1")...)) - ts.IssueValidationBlockWithHeaderOptions("block2.2", node1, mock.WithSlotCommitment(genesisCommitment), mock.WithStrongParents(ts.BlockIDs("block1.2")...)) - - ts.AssertBlocksInCacheConflicts(map[*blocks.Block][]string{ - ts.Block("block2.1"): {"tx1"}, - ts.Block("block2.2"): {"tx2"}, - ts.Block("block2.tx1"): {"tx1"}, - }, node1, node2) - ts.AssertTransactionsInCachePending(wallet.Transactions("tx1", "tx2"), true, node1, node2) - } - - // Issue valid blocks that resolve the conflict. - { - 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"}, - ts.Block("block2.tx1"): {"tx1"}, - }, node1, node2) - ts.AssertTransactionsInCacheAccepted(wallet.Transactions("tx2"), true, node1, node2) - ts.AssertTransactionsInCacheRejected(wallet.Transactions("tx1"), true, node1, node2) - } - - // Advance both nodes at the edge of slot 1 committability - { - ts.IssueBlocksAtSlots("", []iotago.SlotIndex{2, 3, 4}, 1, "block2.4", ts.Nodes("node1", "node2"), false, false) - - ts.AssertNodeState(ts.Nodes(), - testsuite.WithProtocolParameters(ts.API.ProtocolParameters()), - testsuite.WithLatestCommitmentSlotIndex(0), - testsuite.WithEqualStoredCommitmentAtIndex(0), - testsuite.WithEvictedSlot(0), - ) - - ts.SetCurrentSlot(5) - ts.IssueValidationBlockWithHeaderOptions("block5.1", node1, mock.WithSlotCommitment(genesisCommitment), mock.WithStrongParents(ts.BlockIDsWithPrefix("block1.1")...)) - ts.IssueValidationBlockWithHeaderOptions("block5.2", node1, mock.WithSlotCommitment(genesisCommitment), mock.WithStrongParents(ts.BlockIDsWithPrefix("block1.2")...)) - - ts.AssertBlocksInCacheConflicts(map[*blocks.Block][]string{ - ts.Block("block5.1"): {"tx1"}, // on rejected conflict - ts.Block("block5.2"): {}, // accepted merged-to-master - ts.Block("block2.tx1"): {"tx1"}, - }, node1, node2) - - ts.IssueBlocksAtSlots("", []iotago.SlotIndex{5}, 1, "4.0", ts.Nodes("node1"), false, false) - - ts.AssertBlocksExist(ts.BlocksWithPrefix("5.0"), true, ts.ClientsForNodes()...) - } - - partitions := map[string][]*mock.Node{ - "node1": {node1}, - "node2": {node2}, - } - - // Split the nodes into partitions and commit slot 1 only on node2 - { - - ts.SplitIntoPartitions(partitions) - - // Only node2 will commit after issuing this one - ts.IssueBlocksAtSlots("", []iotago.SlotIndex{5}, 1, "5.0", ts.Nodes("node2"), false, false) - - ts.AssertNodeState(ts.Nodes("node1"), - testsuite.WithProtocolParameters(ts.API.ProtocolParameters()), - testsuite.WithLatestCommitmentSlotIndex(0), - testsuite.WithEqualStoredCommitmentAtIndex(0), - testsuite.WithEvictedSlot(0), - ) - - ts.AssertNodeState(ts.Nodes("node2"), - testsuite.WithProtocolParameters(ts.API.ProtocolParameters()), - testsuite.WithLatestCommitmentSlotIndex(1), - testsuite.WithEqualStoredCommitmentAtIndex(1), - testsuite.WithEvictedSlot(1), - ) - } - - commitment1 := lo.PanicOnErr(node2.Protocol.Engines.Main.Get().Storage.Commitments().Load(1)).Commitment() - - // This should be booked on the rejected tx1 conflict - tx4 := wallet.CreateBasicOutputsEquallyFromInput("tx4", 1, "tx1:0") - - // Issue TX3 on top of rejected TX1 and 1 commitment on node2 (committed to slot 1) - { - wallet.SetDefaultClient(node2.Client) - ts.IssueBasicBlockWithOptions("n2-commit1", wallet, tx4, mock.WithSlotCommitment(commitment1)) - - ts.AssertBlocksInCacheConflicts(map[*blocks.Block][]string{ - ts.Block("n2-commit1"): {}, // no conflits inherited as the block is invalid and doesn't get booked. - ts.Block("block2.tx1"): {"tx1"}, - }, node2) - - ts.AssertTransactionsExist(wallet.Transactions("tx1"), true, node2) - ts.AssertTransactionsInCacheRejected(wallet.Transactions("tx4"), true, node2) - ts.AssertTransactionsInCacheBooked(wallet.Transactions("tx4"), true, node2) - - // As the block commits to 1 but spending something orphaned in 1 it should be invalid - ts.AssertBlocksInCacheBooked(ts.Blocks("n2-commit1"), false, node2) - ts.AssertBlocksInCacheInvalid(ts.Blocks("n2-commit1"), true, node2) - } - - // Issue a block on node1 that inherits a pending conflict that has been orphaned on node2 - { - ts.IssueValidationBlockWithHeaderOptions("n1-rejected-genesis", node1, mock.WithSlotCommitment(genesisCommitment), mock.WithStrongParents(ts.BlockIDs("block2.tx1")...)) - - ts.AssertBlocksInCacheBooked(ts.Blocks("n1-rejected-genesis"), true, node1) - ts.AssertBlocksInCacheInvalid(ts.Blocks("n1-rejected-genesis"), false, node1) - - ts.AssertTransactionsInCacheRejected(wallet.Transactions("tx1"), true, node2) - - ts.AssertBlocksInCacheConflicts(map[*blocks.Block][]string{ - ts.Block("block2.tx1"): {"tx1"}, - ts.Block("n1-rejected-genesis"): {"tx1"}, // on rejected conflict - }, node1) - } - - // Issue TX4 on top of rejected TX1 but Genesis commitment on node2 (committed to slot 1) - { - wallet.SetDefaultClient(node2.Client) - ts.IssueBasicBlockWithOptions("n2-genesis", wallet, tx4, mock.WithStrongParents(ts.BlockID("Genesis")), mock.WithSlotCommitment(genesisCommitment)) - - ts.AssertBlocksInCacheConflicts(map[*blocks.Block][]string{ - ts.Block("n2-genesis"): {"tx4"}, // on rejected conflict - }, node2) - - ts.AssertBlocksInCacheBooked(ts.Blocks("n2-genesis"), true, node2) - ts.AssertBlocksInCacheInvalid(ts.Blocks("n2-genesis"), false, node2) - } - - // Issue TX4 on top of rejected TX1 but Genesis commitment on node1 (committed to slot 0) - { - wallet.SetDefaultClient(node1.Client) - ts.IssueBasicBlockWithOptions("n1-genesis", wallet, tx4, mock.WithStrongParents(ts.BlockID("Genesis")), mock.WithSlotCommitment(genesisCommitment)) - - ts.AssertTransactionsExist(wallet.Transactions("tx1"), true, node2) - ts.AssertTransactionsInCacheRejected(wallet.Transactions("tx4"), true, node2) - ts.AssertTransactionsInCacheBooked(wallet.Transactions("tx4"), true, node2) - - ts.AssertBlocksInCacheConflicts(map[*blocks.Block][]string{ - ts.Block("n1-genesis"): {"tx4"}, // on rejected conflict - }, node1) - - ts.AssertBlocksInCacheBooked(ts.Blocks("n1-genesis"), true, node1) - ts.AssertBlocksInCacheInvalid(ts.Blocks("n1-genesis"), false, node1) - } - - ts.MergePartitionsToMain(lo.Keys(partitions)...) - - // Sync up the nodes to he same point and check consistency between them. - { - // Let node1 catch up with commitment 1 - ts.IssueBlocksAtSlots("5.1", []iotago.SlotIndex{5}, 1, "5.0", ts.Nodes("node2"), false, false) - - ts.AssertNodeState(ts.Nodes("node1", "node2"), - testsuite.WithProtocolParameters(ts.API.ProtocolParameters()), - testsuite.WithLatestCommitmentSlotIndex(1), - testsuite.WithEqualStoredCommitmentAtIndex(1), - testsuite.WithEvictedSlot(1), - ) - - // Exchange each-other blocks, ignoring invalidity - wallet.SetDefaultClient(node1.Client) - ts.IssueExistingBlock("n2-genesis", wallet) - ts.IssueExistingBlock("n2-commit1", wallet) - wallet.SetDefaultClient(node2.Client) - ts.IssueExistingBlock("n1-genesis", wallet) - ts.IssueExistingBlock("n1-rejected-genesis", wallet) - - ts.IssueValidationBlockWithHeaderOptions("n1-rejected-commit1", node1, mock.WithSlotCommitment(commitment1), mock.WithStrongParents(ts.BlockIDs("n1-rejected-genesis")...)) - // Needs reissuing on node2 because it is invalid - ts.IssueExistingBlock("n1-rejected-commit1", wallet) - - // The nodes agree on the results of the invalid blocks - ts.AssertBlocksInCacheBooked(ts.Blocks("n2-genesis", "n1-genesis", "n1-rejected-genesis"), true, node1, node2) - ts.AssertBlocksInCacheInvalid(ts.Blocks("n2-genesis", "n1-genesis", "n1-rejected-genesis"), false, node1, node2) - - // This block propagates the orphaned conflict from Tangle - ts.AssertBlocksInCacheBooked(ts.Blocks("n1-rejected-commit1"), true, node1, node2) - ts.AssertBlocksInCacheInvalid(ts.Blocks("n1-rejected-commit1"), false, node1, node2) - - // This block spends an orphaned conflict from its Transaction - ts.AssertBlocksInCacheBooked(ts.Blocks("n2-commit1"), false, node1, node2) - ts.AssertBlocksInCacheInvalid(ts.Blocks("n2-commit1"), true, node1, node2) - - ts.AssertBlocksInCacheConflicts(map[*blocks.Block][]string{ - ts.Block("n1-genesis"): {"tx4"}, // on rejected conflict - ts.Block("n2-genesis"): {"tx4"}, // on rejected conflict - ts.Block("n1-rejected-genesis"): {"tx1"}, // on rejected conflict - ts.Block("n2-commit1"): {}, // invalid block - ts.Block("n1-rejected-commit1"): {}, // merged-to-master - }, node1, node2) - } - - // Commit further and test eviction of transactions - { - ts.AssertTransactionsExist(wallet.Transactions("tx1", "tx2", "tx4"), true, node1, node2) - - ts.IssueBlocksAtSlots("", []iotago.SlotIndex{6, 7, 8, 9, 10}, 5, "5.1", ts.Nodes("node1", "node2"), false, false) - - ts.AssertNodeState(ts.Nodes("node1", "node2"), - testsuite.WithProtocolParameters(ts.API.ProtocolParameters()), - testsuite.WithLatestCommitmentSlotIndex(8), - testsuite.WithEqualStoredCommitmentAtIndex(8), - testsuite.WithEvictedSlot(8), - ) - - ts.AssertTransactionsExist(wallet.Transactions("tx1", "tx2", "tx4"), false, node1, node2) - } -} - func Test_SpendPendingCommittedRace(t *testing.T) { ts := testsuite.NewTestSuite(t, testsuite.WithProtocolParametersOptions( From e05850d42b576fb4f0a251255693e1fc754f9d4a Mon Sep 17 00:00:00 2001 From: jonastheis <4181434+jonastheis@users.noreply.github.com> Date: Tue, 16 Apr 2024 16:39:58 +0800 Subject: [PATCH 23/40] Implement test case to reproduce commitment mismatch for account root --- pkg/tests/loss_of_acceptance_test.go | 64 +++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 11 deletions(-) diff --git a/pkg/tests/loss_of_acceptance_test.go b/pkg/tests/loss_of_acceptance_test.go index aa91469f0..475465f13 100644 --- a/pkg/tests/loss_of_acceptance_test.go +++ b/pkg/tests/loss_of_acceptance_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/iotaledger/hive.go/lo" + "github.com/iotaledger/hive.go/log" "github.com/iotaledger/iota-core/pkg/protocol" "github.com/iotaledger/iota-core/pkg/protocol/engine/blocks" "github.com/iotaledger/iota-core/pkg/testsuite" @@ -17,7 +18,6 @@ import ( func TestLossOfAcceptanceFromGenesis(t *testing.T) { ts := testsuite.NewTestSuite(t, - testsuite.WithWaitFor(15*time.Second), testsuite.WithProtocolParametersOptions( iotago.WithTimeProviderOptions( 0, @@ -39,11 +39,17 @@ func TestLossOfAcceptanceFromGenesis(t *testing.T) { node0 := ts.AddValidatorNode("node0") ts.AddDefaultWallet(node0) - ts.AddValidatorNode("node1") - ts.AddNode("node2") + node1 := ts.AddValidatorNode("node1") + node2 := ts.AddNode("node2") + + nodesP1 := []*mock.Node{node0, node2} + nodesP2 := []*mock.Node{node1} ts.Run(true, nil) + node0.Protocol.SetLogLevel(log.LevelTrace) + node1.Protocol.SetLogLevel(log.LevelTrace) + // Create snapshot to use later. snapshotPath := ts.Directory.Path(fmt.Sprintf("%d_snapshot", time.Now().Unix())) require.NoError(t, node0.Protocol.Engines.Main.Get().WriteSnapshot(snapshotPath)) @@ -68,13 +74,33 @@ func TestLossOfAcceptanceFromGenesis(t *testing.T) { ts.AssertBlocksExist(ts.Blocks("block0"), true, ts.ClientsForNodes()...) } - // Continue issuing on all nodes for a few slots. + ts.SplitIntoPartitions(map[string][]*mock.Node{ + "P1": nodesP1, + "P2": nodesP2, + }) + + // Issue in P1 { - ts.IssueBlocksAtSlots("", []iotago.SlotIndex{53, 54, 55, 56, 57}, 3, "52.1", ts.Nodes(), true, false) + ts.IssueBlocksAtSlots("P1:", []iotago.SlotIndex{53, 54, 55, 56, 57}, 3, "52.1", nodesP1, true, false) - ts.AssertBlocksInCacheAccepted(ts.BlocksWithPrefix("57.0"), true, ts.Nodes()...) - ts.AssertLatestCommitmentSlotIndex(55, ts.Nodes()...) - ts.AssertEqualStoredCommitmentAtIndex(55, ts.Nodes()...) + ts.AssertBlocksInCacheAccepted(ts.BlocksWithPrefix("57.0"), true, nodesP1...) + ts.AssertLatestCommitmentSlotIndex(55, nodesP1...) + ts.AssertEqualStoredCommitmentAtIndex(55, nodesP1...) + + ts.AssertBlocksExist(ts.BlocksWithPrefix("P1"), true, ts.ClientsForNodes(nodesP1...)...) + ts.AssertBlocksExist(ts.BlocksWithPrefix("P1"), false, ts.ClientsForNodes(nodesP2...)...) + } + + // Issue in P2 + { + ts.IssueBlocksAtSlots("P2:", []iotago.SlotIndex{53, 54, 55, 56, 57}, 3, "52.1", nodesP2, true, false) + + ts.AssertBlocksInCacheAccepted(ts.BlocksWithPrefix("57.0"), true, nodesP2...) + ts.AssertLatestCommitmentSlotIndex(55, nodesP2...) + ts.AssertEqualStoredCommitmentAtIndex(55, nodesP2...) + + ts.AssertBlocksExist(ts.BlocksWithPrefix("P2"), false, ts.ClientsForNodes(nodesP1...)...) + ts.AssertBlocksExist(ts.BlocksWithPrefix("P2"), true, ts.ClientsForNodes(nodesP2...)...) } // Start node3 from genesis snapshot. @@ -84,18 +110,34 @@ func TestLossOfAcceptanceFromGenesis(t *testing.T) { protocol.WithSnapshotPath(snapshotPath), protocol.WithBaseDirectory(ts.Directory.PathWithCreate(node3.Name)), ) + // node3.Protocol.SetLogLevel(log.LevelTrace) ts.Wait() } - // Continue issuing on all nodes for a few slots. + ts.MergePartitionsToMain() + fmt.Println("\n=========================\nMerged network partitions\n=========================") + + // Continue issuing on all nodes on top of their chain, respectively. { - ts.IssueBlocksAtSlots("", []iotago.SlotIndex{58, 59}, 3, "57.2", ts.Nodes("node0", "node1", "node2"), true, false) + ts.IssueBlocksAtSlots("P1:", []iotago.SlotIndex{58, 59}, 3, "P1:57.2", nodesP1, true, false) + ts.IssueBlocksAtSlots("P2:", []iotago.SlotIndex{58, 59}, 3, "P2:57.2", nodesP2, true, false) - ts.AssertBlocksInCacheAccepted(ts.BlocksWithPrefix("59.0"), true, ts.Nodes()...) + // ts.AssertBlocksInCacheAccepted(ts.BlocksWithPrefix("59.0"), true, ts.Nodes()...) ts.AssertLatestCommitmentSlotIndex(57, ts.Nodes()...) ts.AssertEqualStoredCommitmentAtIndex(57, ts.Nodes()...) } + return + + // Continue issuing on all nodes for a few slots. + { + ts.IssueBlocksAtSlots("", []iotago.SlotIndex{53, 54, 55, 56, 57}, 3, "52.1", ts.Nodes(), true, false) + + ts.AssertBlocksInCacheAccepted(ts.BlocksWithPrefix("57.0"), true, ts.Nodes()...) + ts.AssertLatestCommitmentSlotIndex(55, ts.Nodes()...) + ts.AssertEqualStoredCommitmentAtIndex(55, ts.Nodes()...) + } + // Check that commitments from 1-49 are empty. for slot := iotago.SlotIndex(1); slot <= 49; slot++ { ts.AssertStorageCommitmentBlocks(slot, nil, ts.Nodes()...) From 58de79556b3c25ef5dfb3f7c4b8b0b57ed0c1282 Mon Sep 17 00:00:00 2001 From: Piotr Macek <4007944+piotrm50@users.noreply.github.com> Date: Tue, 16 Apr 2024 16:09:45 +0200 Subject: [PATCH 24/40] Fix logger --- pkg/protocol/engines.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/protocol/engines.go b/pkg/protocol/engines.go index 153652611..c3be37c89 100644 --- a/pkg/protocol/engines.go +++ b/pkg/protocol/engines.go @@ -105,7 +105,7 @@ func (e *Engines) ForkAtSlot(slot iotago.SlotIndex) (*engine.Engine, error) { evictionState.Initialize(latestCommitment.Slot()) blockCache := blocks.New(evictionState, newStorage.Settings().APIProvider()) - accountsManager := accountsledger.New(module.New(log.NewLogger(log.WithName("ForkedAccountsLedger"))), newStorage.Settings().APIProvider(), blockCache.Block, newStorage.AccountDiffs, newStorage.Accounts()) + accountsManager := accountsledger.New(e.protocol.NewSubModule("ForkedAccountsLedger"), newStorage.Settings().APIProvider(), blockCache.Block, newStorage.AccountDiffs, newStorage.Accounts()) accountsManager.SetLatestCommittedSlot(latestCommitment.Slot()) if err = accountsManager.Rollback(slot); err != nil { From eda06e9bceebac0da530257312ad5ab0b6557a72 Mon Sep 17 00:00:00 2001 From: Piotr Macek <4007944+piotrm50@users.noreply.github.com> Date: Tue, 16 Apr 2024 16:10:20 +0200 Subject: [PATCH 25/40] Commit accounts ledger after rollback to persist the changes --- pkg/protocol/engine/accounts/accountsledger/manager.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/protocol/engine/accounts/accountsledger/manager.go b/pkg/protocol/engine/accounts/accountsledger/manager.go index 7416434d5..2ee8245e1 100644 --- a/pkg/protocol/engine/accounts/accountsledger/manager.go +++ b/pkg/protocol/engine/accounts/accountsledger/manager.go @@ -127,6 +127,11 @@ func (m *Manager) AccountsTreeRoot() iotago.Identifier { m.mutex.RLock() defer m.mutex.RUnlock() + _ = m.accountsTree.Stream(func(accountID iotago.AccountID, accountData *accounts.AccountData) error { + m.LogDebug(">> committing account account", "accountID", accountID, "BIC.VALUE", accountData.Credits.Value, "BIC.UpdateSlot", accountData.Credits.UpdateSlot) + return nil + }) + return m.accountsTree.Root() } @@ -314,7 +319,7 @@ func (m *Manager) Rollback(targetSlot iotago.SlotIndex) error { } } - return nil + return m.accountsTree.Commit() } // AddAccount adds a new account to the Account tree, allotting to it the balance on the given output. From b52b8c64e9d1095d77d0783f8aed2aecf60f21a2 Mon Sep 17 00:00:00 2001 From: Piotr Macek <4007944+piotrm50@users.noreply.github.com> Date: Tue, 16 Apr 2024 16:10:37 +0200 Subject: [PATCH 26/40] Reproduce engine nil pointer and start fixing --- pkg/protocol/attestations.go | 8 +++- pkg/protocol/chain.go | 1 + pkg/protocol/commitment.go | 24 +++++++---- pkg/tests/loss_of_acceptance_test.go | 62 ++++++++++++++-------------- 4 files changed, 55 insertions(+), 40 deletions(-) diff --git a/pkg/protocol/attestations.go b/pkg/protocol/attestations.go index 235f7974b..4f4b7a5bd 100644 --- a/pkg/protocol/attestations.go +++ b/pkg/protocol/attestations.go @@ -1,6 +1,8 @@ package protocol import ( + "fmt" + "github.com/libp2p/go-libp2p/core/peer" "github.com/iotaledger/hive.go/core/eventticker" @@ -129,7 +131,11 @@ func (a *Attestations) setupCommitmentVerifier(chain *Chain) (shutdown func()) { } a.commitmentVerifiers.GetOrCreate(forkingPoint.ID(), func() (commitmentVerifier *CommitmentVerifier) { - commitmentVerifier, err := newCommitmentVerifier(forkingPoint.Chain.Get().LatestEngine(), parentOfForkingPoint.Commitment) + engine := forkingPoint.Chain.Get().LatestEngine() + if engine == nil { + fmt.Println("engine not available ", a.ParentLogger().LogName(), a.LogName()) + } + commitmentVerifier, err := newCommitmentVerifier(engine, parentOfForkingPoint.Commitment) if err != nil { a.LogError("failed to create commitment verifier", "chain", chain.LogName(), "error", err) } diff --git a/pkg/protocol/chain.go b/pkg/protocol/chain.go index 161f89dda..3216866eb 100644 --- a/pkg/protocol/chain.go +++ b/pkg/protocol/chain.go @@ -201,6 +201,7 @@ func (c *Chain) initLogger() (shutdown func()) { c.LatestSyncedSlot.LogUpdates(c, log.LevelTrace, "LatestSyncedSlot"), c.OutOfSyncThreshold.LogUpdates(c, log.LevelTrace, "OutOfSyncThreshold"), c.ForkingPoint.LogUpdates(c, log.LevelTrace, "ForkingPoint", (*Commitment).LogName), + c.ParentChain.LogUpdates(c, log.LevelTrace, "ParentChain", (*Chain).LogName), c.LatestCommitment.LogUpdates(c, log.LevelTrace, "LatestCommitment", (*Commitment).LogName), c.LatestAttestedCommitment.LogUpdates(c, log.LevelTrace, "LatestAttestedCommitment", (*Commitment).LogName), c.LatestProducedCommitment.LogUpdates(c, log.LevelDebug, "LatestProducedCommitment", (*Commitment).LogName), diff --git a/pkg/protocol/commitment.go b/pkg/protocol/commitment.go index de75800aa..93246adcd 100644 --- a/pkg/protocol/commitment.go +++ b/pkg/protocol/commitment.go @@ -60,6 +60,10 @@ type Commitment struct { // IsRoot contains a flag indicating if this Commitment is the root of the Chain. IsRoot reactive.Event + // IsSolid contains a flag indicating if this Commitment is solid (has all the commitments in its past cone until + // the RootCommitment). + IsSolid reactive.Event + // IsAttested contains a flag indicating if we have received attestations for this Commitment. IsAttested reactive.Event @@ -108,6 +112,7 @@ func newCommitment(commitments *Commitments, model *model.Commitment) *Commitmen CumulativeAttestedWeight: reactive.NewVariable[uint64](), CumulativeVerifiedWeight: reactive.NewVariable[uint64](), IsRoot: reactive.NewEvent(), + IsSolid: reactive.NewEvent(), IsAttested: reactive.NewEvent(), IsSynced: reactive.NewEvent(), IsCommittable: reactive.NewEvent(), @@ -219,6 +224,7 @@ func (c *Commitment) initLogger() (shutdown func()) { c.CumulativeAttestedWeight.LogUpdates(c, log.LevelTrace, "CumulativeAttestedWeight"), c.CumulativeVerifiedWeight.LogUpdates(c, log.LevelTrace, "CumulativeVerifiedWeight"), c.IsRoot.LogUpdates(c, log.LevelTrace, "IsRoot"), + c.IsSolid.LogUpdates(c, log.LevelTrace, "IsSolid"), c.IsAttested.LogUpdates(c, log.LevelTrace, "IsAttested"), c.IsSynced.LogUpdates(c, log.LevelTrace, "IsSynced"), c.IsCommittable.LogUpdates(c, log.LevelTrace, "IsCommittable"), @@ -235,6 +241,7 @@ func (c *Commitment) initDerivedProperties() (shutdown func()) { return lo.BatchReverse( // mark commitments that are marked as root as verified c.IsVerified.InheritFrom(c.IsRoot), + c.IsSolid.InheritFrom(c.IsRoot), // mark commitments that are marked as verified as attested and synced c.IsAttested.InheritFrom(c.IsVerified), @@ -252,7 +259,7 @@ func (c *Commitment) initDerivedProperties() (shutdown func()) { return lo.BatchReverse( c.deriveChain(parent), - + c.IsSolid.InheritFrom(parent.IsSolid), c.deriveCumulativeAttestedWeight(parent), c.deriveIsAboveLatestVerifiedCommitment(parent), @@ -294,9 +301,9 @@ func (c *Commitment) registerChild(child *Commitment) { // deriveChain derives the Chain of this Commitment which is either inherited from the parent if we are the main child // or a newly created chain. func (c *Commitment) deriveChain(parent *Commitment) func() { - return c.Chain.DeriveValueFrom(reactive.NewDerivedVariable3(func(currentChain *Chain, isRoot bool, mainChild *Commitment, parentChain *Chain) *Chain { + return c.Chain.DeriveValueFrom(reactive.NewDerivedVariable4(func(currentChain *Chain, isRoot bool, isSolid bool, mainChild *Commitment, parentChain *Chain) *Chain { // do not adjust the chain of the root commitment (it is set from the outside) - if isRoot { + if isRoot || !isSolid { return currentChain } @@ -315,13 +322,14 @@ func (c *Commitment) deriveChain(parent *Commitment) func() { // then we inherit the parent chain and evict the current one. // We will spawn a new one if we ever change back to not being the main child. // Here we basically move commitments to the parent chain. - if currentChain != nil && currentChain != parentChain { - // TODO: refactor it to use a dedicated WorkerPool - go currentChain.IsEvicted.Trigger() - } + + //if currentChain != nil && currentChain != parentChain { + // // TODO: refactor it to use a dedicated WorkerPool + // go currentChain.IsEvicted.Trigger() + //} return parentChain - }, c.IsRoot, parent.MainChild, parent.Chain, c.Chain.Get())) + }, c.IsRoot, c.IsSolid, parent.MainChild, parent.Chain, c.Chain.Get())) } // deriveCumulativeAttestedWeight derives the CumulativeAttestedWeight of this Commitment which is the sum of the diff --git a/pkg/tests/loss_of_acceptance_test.go b/pkg/tests/loss_of_acceptance_test.go index 475465f13..7ad739286 100644 --- a/pkg/tests/loss_of_acceptance_test.go +++ b/pkg/tests/loss_of_acceptance_test.go @@ -9,6 +9,7 @@ import ( "github.com/iotaledger/hive.go/lo" "github.com/iotaledger/hive.go/log" + "github.com/iotaledger/iota-core/pkg/core/account" "github.com/iotaledger/iota-core/pkg/protocol" "github.com/iotaledger/iota-core/pkg/protocol/engine/blocks" "github.com/iotaledger/iota-core/pkg/testsuite" @@ -47,13 +48,19 @@ func TestLossOfAcceptanceFromGenesis(t *testing.T) { ts.Run(true, nil) - node0.Protocol.SetLogLevel(log.LevelTrace) - node1.Protocol.SetLogLevel(log.LevelTrace) + node0.Protocol.SetLogLevel(log.LevelFatal) + node1.Protocol.SetLogLevel(log.LevelFatal) + node2.Protocol.SetLogLevel(log.LevelFatal) // Create snapshot to use later. snapshotPath := ts.Directory.Path(fmt.Sprintf("%d_snapshot", time.Now().Unix())) require.NoError(t, node0.Protocol.Engines.Main.Get().WriteSnapshot(snapshotPath)) + seatIndexes := []account.SeatIndex{ + lo.Return1(lo.Return1(node0.Protocol.Engines.Main.Get().SybilProtection.SeatManager().CommitteeInSlot(1)).GetSeat(node0.Validator.AccountData.ID)), + lo.Return1(lo.Return1(node1.Protocol.Engines.Main.Get().SybilProtection.SeatManager().CommitteeInSlot(1)).GetSeat(node1.Validator.AccountData.ID)), + } + // Revive chain on node0. { ts.SetCurrentSlot(50) @@ -73,44 +80,52 @@ func TestLossOfAcceptanceFromGenesis(t *testing.T) { ts.AssertEqualStoredCommitmentAtIndex(50, ts.Nodes()...) ts.AssertBlocksExist(ts.Blocks("block0"), true, ts.ClientsForNodes()...) } + ts.AssertSybilProtectionOnlineCommittee(seatIndexes[0:1], ts.Nodes()...) ts.SplitIntoPartitions(map[string][]*mock.Node{ "P1": nodesP1, "P2": nodesP2, }) + ts.AssertSybilProtectionOnlineCommittee(seatIndexes[0:1], node0) + ts.AssertSybilProtectionOnlineCommittee(seatIndexes[0:1], node1) + // Issue in P1 { - ts.IssueBlocksAtSlots("P1:", []iotago.SlotIndex{53, 54, 55, 56, 57}, 3, "52.1", nodesP1, true, false) + ts.IssueBlocksAtSlots("P1:", []iotago.SlotIndex{53, 54, 55, 56, 57, 58, 59, 60, 61}, 3, "52.1", nodesP1, true, true) - ts.AssertBlocksInCacheAccepted(ts.BlocksWithPrefix("57.0"), true, nodesP1...) - ts.AssertLatestCommitmentSlotIndex(55, nodesP1...) - ts.AssertEqualStoredCommitmentAtIndex(55, nodesP1...) + ts.AssertBlocksInCacheAccepted(ts.BlocksWithPrefix("61.0"), true, nodesP1...) + ts.AssertLatestCommitmentSlotIndex(59, nodesP1...) + ts.AssertEqualStoredCommitmentAtIndex(59, nodesP1...) ts.AssertBlocksExist(ts.BlocksWithPrefix("P1"), true, ts.ClientsForNodes(nodesP1...)...) ts.AssertBlocksExist(ts.BlocksWithPrefix("P1"), false, ts.ClientsForNodes(nodesP2...)...) } + ts.AssertSybilProtectionOnlineCommittee(seatIndexes[0:1], node0, node2) + ts.AssertSybilProtectionOnlineCommittee(seatIndexes[0:1], node1) // Issue in P2 { - ts.IssueBlocksAtSlots("P2:", []iotago.SlotIndex{53, 54, 55, 56, 57}, 3, "52.1", nodesP2, true, false) + ts.IssueBlocksAtSlots("P2:", []iotago.SlotIndex{53, 54, 55, 56, 57, 58, 59, 60, 61}, 3, "52.1", nodesP2, false, false) - ts.AssertBlocksInCacheAccepted(ts.BlocksWithPrefix("57.0"), true, nodesP2...) - ts.AssertLatestCommitmentSlotIndex(55, nodesP2...) - ts.AssertEqualStoredCommitmentAtIndex(55, nodesP2...) + ts.AssertBlocksInCacheAccepted(ts.BlocksWithPrefix("61.0"), true, nodesP2...) + ts.AssertLatestCommitmentSlotIndex(59, nodesP2...) + ts.AssertEqualStoredCommitmentAtIndex(59, nodesP2...) ts.AssertBlocksExist(ts.BlocksWithPrefix("P2"), false, ts.ClientsForNodes(nodesP1...)...) ts.AssertBlocksExist(ts.BlocksWithPrefix("P2"), true, ts.ClientsForNodes(nodesP2...)...) } + ts.AssertSybilProtectionOnlineCommittee(seatIndexes[0:1], node0, node2) + ts.AssertSybilProtectionOnlineCommittee(seatIndexes[1:2], node1) // Start node3 from genesis snapshot. + node3 := ts.AddNode("node3") { - node3 := ts.AddNode("node3") node3.Initialize(true, protocol.WithSnapshotPath(snapshotPath), protocol.WithBaseDirectory(ts.Directory.PathWithCreate(node3.Name)), ) - // node3.Protocol.SetLogLevel(log.LevelTrace) + node3.Protocol.SetLogLevel(log.LevelTrace) ts.Wait() } @@ -119,28 +134,13 @@ func TestLossOfAcceptanceFromGenesis(t *testing.T) { // Continue issuing on all nodes on top of their chain, respectively. { - ts.IssueBlocksAtSlots("P1:", []iotago.SlotIndex{58, 59}, 3, "P1:57.2", nodesP1, true, false) - ts.IssueBlocksAtSlots("P2:", []iotago.SlotIndex{58, 59}, 3, "P2:57.2", nodesP2, true, false) + ts.IssueBlocksAtSlots("P2:", []iotago.SlotIndex{62}, 1, "P2:61.2", nodesP2, false, false) + ts.IssueBlocksAtSlots("P1:", []iotago.SlotIndex{62}, 1, "P1:61.2", nodesP1, false, false) // ts.AssertBlocksInCacheAccepted(ts.BlocksWithPrefix("59.0"), true, ts.Nodes()...) - ts.AssertLatestCommitmentSlotIndex(57, ts.Nodes()...) - ts.AssertEqualStoredCommitmentAtIndex(57, ts.Nodes()...) - } - - return + ts.AssertLatestCommitmentSlotIndex(59, ts.Nodes()...) - // Continue issuing on all nodes for a few slots. - { - ts.IssueBlocksAtSlots("", []iotago.SlotIndex{53, 54, 55, 56, 57}, 3, "52.1", ts.Nodes(), true, false) - - ts.AssertBlocksInCacheAccepted(ts.BlocksWithPrefix("57.0"), true, ts.Nodes()...) - ts.AssertLatestCommitmentSlotIndex(55, ts.Nodes()...) - ts.AssertEqualStoredCommitmentAtIndex(55, ts.Nodes()...) - } - - // Check that commitments from 1-49 are empty. - for slot := iotago.SlotIndex(1); slot <= 49; slot++ { - ts.AssertStorageCommitmentBlocks(slot, nil, ts.Nodes()...) + ts.AssertEqualStoredCommitmentAtIndex(59, ts.Nodes()...) } } From 2e553ff9af5eff606af6ad44962b5ee2d85a88d2 Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Tue, 16 Apr 2024 22:15:20 +0200 Subject: [PATCH 27/40] Fix: fix nil pointer exception --- pkg/protocol/chain.go | 7 +++++++ pkg/protocol/chains.go | 4 +++- pkg/protocol/commitment.go | 10 ++++++---- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/pkg/protocol/chain.go b/pkg/protocol/chain.go index 3216866eb..24192c724 100644 --- a/pkg/protocol/chain.go +++ b/pkg/protocol/chain.go @@ -57,6 +57,9 @@ type Chain struct { // IsEvicted contains a flag that indicates whether this chain was evicted. IsEvicted reactive.Event + // IsSolid contains a flag that indicates whether this chain is solid (has a continuous connection to the root). + IsSolid reactive.Event + // shouldEvict contains a flag that indicates whether this chain should be evicted. shouldEvict reactive.Event @@ -86,6 +89,7 @@ func newChain(chains *Chains) *Chain { StartEngine: reactive.NewVariable[bool](), Engine: reactive.NewVariable[*engine.Engine](), IsEvicted: reactive.NewEvent(), + IsSolid: reactive.NewEvent(), shouldEvict: reactive.NewEvent(), chains: chains, @@ -209,6 +213,7 @@ func (c *Chain) initLogger() (shutdown func()) { c.StartEngine.LogUpdates(c, log.LevelDebug, "StartEngine"), c.Engine.LogUpdates(c, log.LevelTrace, "Engine", (*engine.Engine).LogName), c.IsEvicted.LogUpdates(c, log.LevelTrace, "IsEvicted"), + c.IsSolid.LogUpdates(c, log.LevelTrace, "IsSolid"), c.shouldEvict.LogUpdates(c, log.LevelTrace, "shouldEvict"), c.Logger.Shutdown, @@ -234,6 +239,8 @@ func (c *Chain) initDerivedProperties() (shutdown func()) { c.deriveShouldEvict(forkingPoint, parentChain), ) }), + + c.IsSolid.InheritFrom(forkingPoint.IsSolid), ) }), ), diff --git a/pkg/protocol/chains.go b/pkg/protocol/chains.go index d50974d46..7bb09169f 100644 --- a/pkg/protocol/chains.go +++ b/pkg/protocol/chains.go @@ -323,7 +323,9 @@ func (c *Chains) initChainSwitching() (shutdown func()) { return lo.BatchReverse( c.HeaviestClaimedCandidate.WithNonEmptyValue(func(heaviestClaimedCandidate *Chain) (shutdown func()) { - return heaviestClaimedCandidate.RequestAttestations.ToggleValue(true) + return heaviestClaimedCandidate.IsSolid.WithNonEmptyValue(func(_ bool) (teardown func()) { + return heaviestClaimedCandidate.RequestAttestations.ToggleValue(true) + }) }), c.HeaviestAttestedCandidate.OnUpdate(func(_ *Chain, heaviestAttestedCandidate *Chain) { diff --git a/pkg/protocol/commitment.go b/pkg/protocol/commitment.go index 93246adcd..446c52493 100644 --- a/pkg/protocol/commitment.go +++ b/pkg/protocol/commitment.go @@ -259,7 +259,7 @@ func (c *Commitment) initDerivedProperties() (shutdown func()) { return lo.BatchReverse( c.deriveChain(parent), - c.IsSolid.InheritFrom(parent.IsSolid), + c.deriveCumulativeAttestedWeight(parent), c.deriveIsAboveLatestVerifiedCommitment(parent), @@ -273,6 +273,8 @@ func (c *Commitment) initDerivedProperties() (shutdown func()) { }), ) }), + + c.IsSolid.InheritFrom(parent.IsSolid), ) }), @@ -301,9 +303,9 @@ func (c *Commitment) registerChild(child *Commitment) { // deriveChain derives the Chain of this Commitment which is either inherited from the parent if we are the main child // or a newly created chain. func (c *Commitment) deriveChain(parent *Commitment) func() { - return c.Chain.DeriveValueFrom(reactive.NewDerivedVariable4(func(currentChain *Chain, isRoot bool, isSolid bool, mainChild *Commitment, parentChain *Chain) *Chain { + return c.Chain.DeriveValueFrom(reactive.NewDerivedVariable3(func(currentChain *Chain, isRoot bool, mainChild *Commitment, parentChain *Chain) *Chain { // do not adjust the chain of the root commitment (it is set from the outside) - if isRoot || !isSolid { + if isRoot { return currentChain } @@ -329,7 +331,7 @@ func (c *Commitment) deriveChain(parent *Commitment) func() { //} return parentChain - }, c.IsRoot, c.IsSolid, parent.MainChild, parent.Chain, c.Chain.Get())) + }, c.IsRoot, parent.MainChild, parent.Chain, c.Chain.Get())) } // deriveCumulativeAttestedWeight derives the CumulativeAttestedWeight of this Commitment which is the sum of the From 9db69b68837b8d89f48f04593fdfa9f191349318 Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Tue, 16 Apr 2024 22:22:43 +0200 Subject: [PATCH 28/40] Refactor: reverted unnecessary changes --- pkg/protocol/attestations.go | 8 +------- pkg/protocol/commitment.go | 9 ++++----- pkg/protocol/engine/accounts/accountsledger/manager.go | 5 ----- 3 files changed, 5 insertions(+), 17 deletions(-) diff --git a/pkg/protocol/attestations.go b/pkg/protocol/attestations.go index 4f4b7a5bd..235f7974b 100644 --- a/pkg/protocol/attestations.go +++ b/pkg/protocol/attestations.go @@ -1,8 +1,6 @@ package protocol import ( - "fmt" - "github.com/libp2p/go-libp2p/core/peer" "github.com/iotaledger/hive.go/core/eventticker" @@ -131,11 +129,7 @@ func (a *Attestations) setupCommitmentVerifier(chain *Chain) (shutdown func()) { } a.commitmentVerifiers.GetOrCreate(forkingPoint.ID(), func() (commitmentVerifier *CommitmentVerifier) { - engine := forkingPoint.Chain.Get().LatestEngine() - if engine == nil { - fmt.Println("engine not available ", a.ParentLogger().LogName(), a.LogName()) - } - commitmentVerifier, err := newCommitmentVerifier(engine, parentOfForkingPoint.Commitment) + commitmentVerifier, err := newCommitmentVerifier(forkingPoint.Chain.Get().LatestEngine(), parentOfForkingPoint.Commitment) if err != nil { a.LogError("failed to create commitment verifier", "chain", chain.LogName(), "error", err) } diff --git a/pkg/protocol/commitment.go b/pkg/protocol/commitment.go index 446c52493..78a24bf29 100644 --- a/pkg/protocol/commitment.go +++ b/pkg/protocol/commitment.go @@ -324,11 +324,10 @@ func (c *Commitment) deriveChain(parent *Commitment) func() { // then we inherit the parent chain and evict the current one. // We will spawn a new one if we ever change back to not being the main child. // Here we basically move commitments to the parent chain. - - //if currentChain != nil && currentChain != parentChain { - // // TODO: refactor it to use a dedicated WorkerPool - // go currentChain.IsEvicted.Trigger() - //} + if currentChain != nil && currentChain != parentChain { + // TODO: refactor it to use a dedicated WorkerPool + go currentChain.IsEvicted.Trigger() + } return parentChain }, c.IsRoot, parent.MainChild, parent.Chain, c.Chain.Get())) diff --git a/pkg/protocol/engine/accounts/accountsledger/manager.go b/pkg/protocol/engine/accounts/accountsledger/manager.go index 2ee8245e1..59f5e8c7e 100644 --- a/pkg/protocol/engine/accounts/accountsledger/manager.go +++ b/pkg/protocol/engine/accounts/accountsledger/manager.go @@ -127,11 +127,6 @@ func (m *Manager) AccountsTreeRoot() iotago.Identifier { m.mutex.RLock() defer m.mutex.RUnlock() - _ = m.accountsTree.Stream(func(accountID iotago.AccountID, accountData *accounts.AccountData) error { - m.LogDebug(">> committing account account", "accountID", accountID, "BIC.VALUE", accountData.Credits.Value, "BIC.UpdateSlot", accountData.Credits.UpdateSlot) - return nil - }) - return m.accountsTree.Root() } From 8206baa52a924e0b9dc84ddf79406b1a1316f6cc Mon Sep 17 00:00:00 2001 From: jonastheis <4181434+jonastheis@users.noreply.github.com> Date: Wed, 17 Apr 2024 12:22:37 +0800 Subject: [PATCH 29/40] moar debug logs to finally find lastAccessedEpoch bug and node being stuck in "deleting" practically endless (ghost) epochs from disk --- pkg/protocol/engines.go | 22 ++++++++ pkg/storage/prunable/epochstore/epoch_kv.go | 4 ++ pkg/storage/prunable/prunable.go | 16 ++++++ pkg/tests/loss_of_acceptance_test.go | 59 +++++++++++++-------- 4 files changed, 78 insertions(+), 23 deletions(-) diff --git a/pkg/protocol/engines.go b/pkg/protocol/engines.go index c3be37c89..7ec714ec9 100644 --- a/pkg/protocol/engines.go +++ b/pkg/protocol/engines.go @@ -81,29 +81,39 @@ func (e *Engines) initLogging() (shutdown func()) { // ForkAtSlot creates a new engine instance that forks from the main engine at the given slot. func (e *Engines) ForkAtSlot(slot iotago.SlotIndex) (*engine.Engine, error) { + e.LogInfo("forking engine at slot", "slot", slot) + newEngineAlias := lo.PanicOnErr(uuid.NewUUID()).String() errorHandler := func(err error) { e.protocol.LogError("engine error", "err", err, "name", newEngineAlias[0:8]) } + e.LogInfo("forking engine at slot 2", "slot", slot) + // copy raw data on disk. newStorage, err := storage.Clone(e, e.Main.Get().Storage, e.directory.Path(newEngineAlias), DatabaseVersion, errorHandler, e.protocol.Options.StorageOptions...) if err != nil { return nil, ierrors.Wrapf(err, "failed to copy storage from active engine instance (%s) to new engine instance (%s)", e.Main.Get().Storage.Directory(), e.directory.Path(newEngineAlias)) } + e.LogInfo("forking engine at slot 3", "slot", slot) + // remove commitments that after forking point. latestCommitment := newStorage.Settings().LatestCommitment() if err = newStorage.Commitments().Rollback(slot, latestCommitment.Slot()); err != nil { return nil, ierrors.Wrap(err, "failed to rollback commitments") } + e.LogInfo("forking engine at slot 4", "slot", slot) + // some components are automatically rolled back by deleting their data on disk (e.g. slot based storage). // some other components need to be rolled back manually, like the UTXO ledger for example. // we need to create temporary components to rollback their permanent state, which will be reflected on disk. evictionState := eviction.NewState(newStorage.Settings(), newStorage.RootBlocks) evictionState.Initialize(latestCommitment.Slot()) + e.LogInfo("forking engine at slot 5", "slot", slot) + blockCache := blocks.New(evictionState, newStorage.Settings().APIProvider()) accountsManager := accountsledger.New(e.protocol.NewSubModule("ForkedAccountsLedger"), newStorage.Settings().APIProvider(), blockCache.Block, newStorage.AccountDiffs, newStorage.Accounts()) @@ -119,19 +129,27 @@ func (e *Engines) ForkAtSlot(slot iotago.SlotIndex) (*engine.Engine, error) { return nil, err } + e.LogInfo("forking engine at slot 6", "slot", slot) + targetCommitment, err := newStorage.Commitments().Load(slot) if err != nil { return nil, ierrors.Wrapf(err, "error while retrieving commitment for target index %d", slot) } + e.LogInfo("forking engine at slot 6.1", "slot", slot) + if err = newStorage.Settings().Rollback(targetCommitment); err != nil { return nil, err } + e.LogInfo("forking engine at slot 6.2", "slot", slot) if err = newStorage.Rollback(slot); err != nil { + e.LogError("failed to rollback storage", "err", err) return nil, err } + e.LogInfo("forking engine at slot 7", "slot", slot) + candidateEngine := e.loadEngineInstanceWithStorage(newEngineAlias, newStorage) // rollback attestations already on created engine instance, because this action modifies the in-memory storage. @@ -139,6 +157,8 @@ func (e *Engines) ForkAtSlot(slot iotago.SlotIndex) (*engine.Engine, error) { return nil, ierrors.Wrap(err, "error while rolling back attestations storage on candidate engine") } + e.LogInfo("forking engine at slot 8", "slot", slot) + return candidateEngine, nil } @@ -274,7 +294,9 @@ func (e *Engines) injectEngineInstances() (shutdown func()) { } else { e.protocol.Network.OnShutdown(func() { newEngine.ShutdownEvent().Trigger() }) + e.LogInfo("injecting engine instance before", "chain", chain.LogName()) chain.Engine.Set(newEngine) + e.LogInfo("injecting engine instance after", "chain", chain.LogName()) } }) }) diff --git a/pkg/storage/prunable/epochstore/epoch_kv.go b/pkg/storage/prunable/epochstore/epoch_kv.go index 1c106b28a..0275afd26 100644 --- a/pkg/storage/prunable/epochstore/epoch_kv.go +++ b/pkg/storage/prunable/epochstore/epoch_kv.go @@ -1,6 +1,8 @@ package epochstore import ( + "fmt" + "github.com/iotaledger/hive.go/ierrors" "github.com/iotaledger/hive.go/kvstore" "github.com/iotaledger/hive.go/lo" @@ -108,10 +110,12 @@ func (e *EpochKVStore) Prune(epoch iotago.EpochIndex, defaultPruningDelay iotago func (e *EpochKVStore) RollbackEpochs(epoch iotago.EpochIndex) (lastPrunedEpoch iotago.EpochIndex, err error) { lastAccessedEpoch, err := e.LastAccessedEpoch() + fmt.Println("lastAccessedEpoch: ", lastAccessedEpoch) if err != nil { return lastAccessedEpoch, ierrors.Wrap(err, "failed to get last accessed epoch") } + fmt.Println("epoch: ", epoch) for epochToPrune := epoch; epochToPrune <= lastAccessedEpoch; epochToPrune++ { if err = e.DeleteEpoch(epochToPrune); err != nil { return epochToPrune, ierrors.Wrapf(err, "error while deleting epoch %d", epochToPrune) diff --git a/pkg/storage/prunable/prunable.go b/pkg/storage/prunable/prunable.go index e90f2b0b8..e186eedb6 100644 --- a/pkg/storage/prunable/prunable.go +++ b/pkg/storage/prunable/prunable.go @@ -1,6 +1,8 @@ package prunable import ( + "fmt" + copydir "github.com/otiai10/copy" "github.com/iotaledger/hive.go/ierrors" @@ -171,34 +173,47 @@ func (p *Prunable) Flush() { } func (p *Prunable) Rollback(targetEpoch iotago.EpochIndex, startPruneRange iotago.SlotIndex, endPruneRange iotago.SlotIndex) error { + fmt.Println("Rollback", targetEpoch, startPruneRange, endPruneRange) + if err := p.prunableSlotStore.PruneSlots(targetEpoch, startPruneRange, endPruneRange); err != nil { return ierrors.Wrapf(err, "failed to prune slots in range [%d, %d] from target epoch %d", startPruneRange, endPruneRange, targetEpoch) } + fmt.Println("Rollback 2", targetEpoch, startPruneRange, endPruneRange) + if err := p.rollbackCommitteesCandidates(targetEpoch, startPruneRange); err != nil { return ierrors.Wrapf(err, "failed to rollback committee candidates to target epoch %d", targetEpoch) } + fmt.Println("Rollback 3", targetEpoch, startPruneRange, endPruneRange) lastPrunedCommitteeEpoch, err := p.rollbackCommitteeEpochs(targetEpoch+1, startPruneRange-1) if err != nil { return ierrors.Wrapf(err, "failed to rollback committee epochs to target epoch %d", targetEpoch) } + fmt.Println("Rollback 4", targetEpoch, startPruneRange, endPruneRange) + lastPrunedPoolStatsEpoch, _, err := p.poolStats.RollbackEpochs(targetEpoch) if err != nil { return ierrors.Wrapf(err, "failed to rollback pool stats epochs to target epoch %d", targetEpoch) } + fmt.Println("Rollback 5", targetEpoch, startPruneRange, endPruneRange) + lastPrunedDecidedUpgradeSignalsEpoch, _, err := p.decidedUpgradeSignals.RollbackEpochs(targetEpoch) if err != nil { return ierrors.Wrapf(err, "failed to rollback decided upgrade signals epochs to target epoch %d", targetEpoch) } + fmt.Println("Rollback 6", targetEpoch, startPruneRange, endPruneRange) + lastPrunedPoolRewardsEpoch, err := p.poolRewards.RollbackEpochs(targetEpoch) if err != nil { return ierrors.Wrapf(err, "failed to rollback pool rewards epochs to target epoch %d", targetEpoch) } + fmt.Println("Rollback 7 ", targetEpoch, startPruneRange, endPruneRange) + for epochToPrune := targetEpoch + 1; epochToPrune <= max( lastPrunedCommitteeEpoch, lastPrunedPoolStatsEpoch, @@ -207,6 +222,7 @@ func (p *Prunable) Rollback(targetEpoch iotago.EpochIndex, startPruneRange iotag ); epochToPrune++ { p.prunableSlotStore.DeleteBucket(epochToPrune) } + fmt.Println("Rollback 8", targetEpoch, startPruneRange, endPruneRange) return nil } diff --git a/pkg/tests/loss_of_acceptance_test.go b/pkg/tests/loss_of_acceptance_test.go index 7ad739286..f3eb950a8 100644 --- a/pkg/tests/loss_of_acceptance_test.go +++ b/pkg/tests/loss_of_acceptance_test.go @@ -2,6 +2,9 @@ package tests import ( "fmt" + "net/http" + _ "net/http" + _ "net/http/pprof" "testing" "time" @@ -18,6 +21,12 @@ import ( ) func TestLossOfAcceptanceFromGenesis(t *testing.T) { + // debug.SetEnabled(true) + + go func() { + fmt.Println(http.ListenAndServe("localhost:6061", nil)) + }() + ts := testsuite.NewTestSuite(t, testsuite.WithProtocolParametersOptions( iotago.WithTimeProviderOptions( @@ -36,21 +45,21 @@ func TestLossOfAcceptanceFromGenesis(t *testing.T) { ), testsuite.WithWaitFor(15*time.Second), ) - defer ts.Shutdown() + // defer ts.Shutdown() node0 := ts.AddValidatorNode("node0") ts.AddDefaultWallet(node0) node1 := ts.AddValidatorNode("node1") - node2 := ts.AddNode("node2") + // node2 := ts.AddNode("node2") - nodesP1 := []*mock.Node{node0, node2} + nodesP1 := []*mock.Node{node0} nodesP2 := []*mock.Node{node1} ts.Run(true, nil) - node0.Protocol.SetLogLevel(log.LevelFatal) - node1.Protocol.SetLogLevel(log.LevelFatal) - node2.Protocol.SetLogLevel(log.LevelFatal) + node0.Protocol.SetLogLevel(log.LevelTrace) + // node1.Protocol.SetLogLevel(log.LevelTrace) + // node2.Protocol.SetLogLevel(log.LevelFatal) // Create snapshot to use later. snapshotPath := ts.Directory.Path(fmt.Sprintf("%d_snapshot", time.Now().Unix())) @@ -101,8 +110,8 @@ func TestLossOfAcceptanceFromGenesis(t *testing.T) { ts.AssertBlocksExist(ts.BlocksWithPrefix("P1"), true, ts.ClientsForNodes(nodesP1...)...) ts.AssertBlocksExist(ts.BlocksWithPrefix("P1"), false, ts.ClientsForNodes(nodesP2...)...) } - ts.AssertSybilProtectionOnlineCommittee(seatIndexes[0:1], node0, node2) - ts.AssertSybilProtectionOnlineCommittee(seatIndexes[0:1], node1) + ts.AssertSybilProtectionOnlineCommittee(seatIndexes[0:1], nodesP1...) + ts.AssertSybilProtectionOnlineCommittee(seatIndexes[0:1], nodesP2...) // Issue in P2 { @@ -115,32 +124,36 @@ func TestLossOfAcceptanceFromGenesis(t *testing.T) { ts.AssertBlocksExist(ts.BlocksWithPrefix("P2"), false, ts.ClientsForNodes(nodesP1...)...) ts.AssertBlocksExist(ts.BlocksWithPrefix("P2"), true, ts.ClientsForNodes(nodesP2...)...) } - ts.AssertSybilProtectionOnlineCommittee(seatIndexes[0:1], node0, node2) - ts.AssertSybilProtectionOnlineCommittee(seatIndexes[1:2], node1) + ts.AssertSybilProtectionOnlineCommittee(seatIndexes[0:1], nodesP1...) + ts.AssertSybilProtectionOnlineCommittee(seatIndexes[1:2], nodesP2...) // Start node3 from genesis snapshot. - node3 := ts.AddNode("node3") - { - node3.Initialize(true, - protocol.WithSnapshotPath(snapshotPath), - protocol.WithBaseDirectory(ts.Directory.PathWithCreate(node3.Name)), - ) - node3.Protocol.SetLogLevel(log.LevelTrace) - ts.Wait() - } - + // node3 := ts.AddNode("node3") + // { + // node3.Initialize(true, + // protocol.WithSnapshotPath(snapshotPath), + // protocol.WithBaseDirectory(ts.Directory.PathWithCreate(node3.Name)), + // ) + // node3.Protocol.SetLogLevel(log.LevelTrace) + // ts.Wait() + // } ts.MergePartitionsToMain() fmt.Println("\n=========================\nMerged network partitions\n=========================") // Continue issuing on all nodes on top of their chain, respectively. { - ts.IssueBlocksAtSlots("P2:", []iotago.SlotIndex{62}, 1, "P2:61.2", nodesP2, false, false) - ts.IssueBlocksAtSlots("P1:", []iotago.SlotIndex{62}, 1, "P1:61.2", nodesP1, false, false) + ts.IssueBlocksAtSlots("P2:", []iotago.SlotIndex{61}, 1, "P2:61.2", nodesP2, false, false) + ts.IssueBlocksAtSlots("P1:", []iotago.SlotIndex{61}, 1, "P1:61.2", nodesP1, false, false) + ts.Wait() + + fmt.Println(">>>>>>> Checking stuff...") // ts.AssertBlocksInCacheAccepted(ts.BlocksWithPrefix("59.0"), true, ts.Nodes()...) ts.AssertLatestCommitmentSlotIndex(59, ts.Nodes()...) - + fmt.Println(">>>>>>> Latest commitment slot index:", 59) + // pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) ts.AssertEqualStoredCommitmentAtIndex(59, ts.Nodes()...) + fmt.Println(">>>>>>> Stored commitment at index 59 is equal for all nodes.") } } From d43a2f1c3507f85658d2641bd8d77f2d1c37bbd7 Mon Sep 17 00:00:00 2001 From: Piotr Macek <4007944+piotrm50@users.noreply.github.com> Date: Wed, 17 Apr 2024 09:59:17 +0200 Subject: [PATCH 30/40] Fix underflow when exporting a snapshot. --- .../sybilprotectionv1/performance/snapshot.go | 17 +++++++++++------ pkg/storage/prunable/epochstore/epoch_kv.go | 5 +---- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/pkg/protocol/sybilprotection/sybilprotectionv1/performance/snapshot.go b/pkg/protocol/sybilprotection/sybilprotectionv1/performance/snapshot.go index d4b19e2a5..6e0ed9bf2 100644 --- a/pkg/protocol/sybilprotection/sybilprotectionv1/performance/snapshot.go +++ b/pkg/protocol/sybilprotection/sybilprotectionv1/performance/snapshot.go @@ -3,6 +3,7 @@ package performance import ( "io" + "github.com/iotaledger/hive.go/core/safemath" "github.com/iotaledger/hive.go/ierrors" "github.com/iotaledger/hive.go/lo" "github.com/iotaledger/hive.go/serializer/v2" @@ -47,12 +48,15 @@ func (t *Tracker) Export(writer io.WriteSeeker, targetSlotIndex iotago.SlotIndex timeProvider := t.apiProvider.APIForSlot(targetSlotIndex).TimeProvider() targetEpoch := timeProvider.EpochFromSlot(targetSlotIndex) - // if the target index is the last slot of the epoch, the epoch was committed - if timeProvider.EpochEnd(targetEpoch) != targetSlotIndex { - targetEpoch-- + // if the target index is the last slot of the epoch, the epoch was committed - unless it's epoch 0 to avoid underflow. + if timeProvider.EpochEnd(targetEpoch) != targetSlotIndex && targetEpoch > 0 { + targetEpoch = lo.PanicOnErr(safemath.SafeSub(targetEpoch, 1)) } - if err := t.exportPerformanceFactor(writer, timeProvider.EpochStart(targetEpoch+1), targetSlotIndex); err != nil { + // If targetEpoch==0 then export performance factors from slot 0 to the targetSlotIndex. + // PoolRewards and PoolStats are empty if epoch 0 was not committed yet, so it's not a problem. + // But PerformanceFactors are exported for the ongoing epoch, so for epoch 0 we must make an exception and not add 1 to the targetEpoch. + if err := t.exportPerformanceFactor(writer, timeProvider.EpochStart(targetEpoch+lo.Cond(targetEpoch == 0, iotago.EpochIndex(0), iotago.EpochIndex(1))), targetSlotIndex); err != nil { return ierrors.Wrap(err, "unable to export performance factor") } @@ -277,8 +281,9 @@ func (t *Tracker) exportPoolRewards(writer io.WriteSeeker, targetEpoch iotago.Ep if err := stream.WriteCollection(writer, serializer.SeriLengthPrefixTypeAsUint32, func() (int, error) { var epochCount int - - for epoch := targetEpoch; epoch > iotago.EpochIndex(lo.Max(0, int(targetEpoch)-daysInYear)); epoch-- { + // Here underflow will not happen because we will stop iterating for epoch 0, because 0 is not greater than zero. + // Use safemath here anyway to avoid hard to trace problems stemming from an accidental underflow. + for epoch := targetEpoch; epoch > iotago.EpochIndex(lo.Max(0, int(targetEpoch)-daysInYear)); epoch = lo.PanicOnErr(safemath.SafeSub(epoch, 1)) { rewardsMap, err := t.rewardsMap(epoch) if err != nil { return 0, ierrors.Wrapf(err, "unable to get rewards tree for epoch %d", epoch) diff --git a/pkg/storage/prunable/epochstore/epoch_kv.go b/pkg/storage/prunable/epochstore/epoch_kv.go index 0275afd26..5b94cbde9 100644 --- a/pkg/storage/prunable/epochstore/epoch_kv.go +++ b/pkg/storage/prunable/epochstore/epoch_kv.go @@ -1,8 +1,6 @@ package epochstore import ( - "fmt" - "github.com/iotaledger/hive.go/ierrors" "github.com/iotaledger/hive.go/kvstore" "github.com/iotaledger/hive.go/lo" @@ -110,12 +108,11 @@ func (e *EpochKVStore) Prune(epoch iotago.EpochIndex, defaultPruningDelay iotago func (e *EpochKVStore) RollbackEpochs(epoch iotago.EpochIndex) (lastPrunedEpoch iotago.EpochIndex, err error) { lastAccessedEpoch, err := e.LastAccessedEpoch() - fmt.Println("lastAccessedEpoch: ", lastAccessedEpoch) + if err != nil { return lastAccessedEpoch, ierrors.Wrap(err, "failed to get last accessed epoch") } - fmt.Println("epoch: ", epoch) for epochToPrune := epoch; epochToPrune <= lastAccessedEpoch; epochToPrune++ { if err = e.DeleteEpoch(epochToPrune); err != nil { return epochToPrune, ierrors.Wrapf(err, "error while deleting epoch %d", epochToPrune) From 8bb7715547b883e215b1b02ab9c504a63efa4d90 Mon Sep 17 00:00:00 2001 From: Piotr Macek <4007944+piotrm50@users.noreply.github.com> Date: Wed, 17 Apr 2024 10:09:10 +0200 Subject: [PATCH 31/40] Disable trace logging --- pkg/tests/loss_of_acceptance_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/tests/loss_of_acceptance_test.go b/pkg/tests/loss_of_acceptance_test.go index f3eb950a8..35c3c2653 100644 --- a/pkg/tests/loss_of_acceptance_test.go +++ b/pkg/tests/loss_of_acceptance_test.go @@ -11,7 +11,6 @@ import ( "github.com/stretchr/testify/require" "github.com/iotaledger/hive.go/lo" - "github.com/iotaledger/hive.go/log" "github.com/iotaledger/iota-core/pkg/core/account" "github.com/iotaledger/iota-core/pkg/protocol" "github.com/iotaledger/iota-core/pkg/protocol/engine/blocks" @@ -57,7 +56,7 @@ func TestLossOfAcceptanceFromGenesis(t *testing.T) { ts.Run(true, nil) - node0.Protocol.SetLogLevel(log.LevelTrace) + //node0.Protocol.SetLogLevel(log.LevelTrace) // node1.Protocol.SetLogLevel(log.LevelTrace) // node2.Protocol.SetLogLevel(log.LevelFatal) From 1ff5de2875655a3756aa76b0c0e8ef6903118a30 Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Wed, 17 Apr 2024 11:46:20 +0200 Subject: [PATCH 32/40] Refactor: cleaned up changes --- pkg/protocol/engines.go | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/pkg/protocol/engines.go b/pkg/protocol/engines.go index 7ec714ec9..87407d8b2 100644 --- a/pkg/protocol/engines.go +++ b/pkg/protocol/engines.go @@ -81,39 +81,29 @@ func (e *Engines) initLogging() (shutdown func()) { // ForkAtSlot creates a new engine instance that forks from the main engine at the given slot. func (e *Engines) ForkAtSlot(slot iotago.SlotIndex) (*engine.Engine, error) { - e.LogInfo("forking engine at slot", "slot", slot) - newEngineAlias := lo.PanicOnErr(uuid.NewUUID()).String() errorHandler := func(err error) { e.protocol.LogError("engine error", "err", err, "name", newEngineAlias[0:8]) } - e.LogInfo("forking engine at slot 2", "slot", slot) - // copy raw data on disk. newStorage, err := storage.Clone(e, e.Main.Get().Storage, e.directory.Path(newEngineAlias), DatabaseVersion, errorHandler, e.protocol.Options.StorageOptions...) if err != nil { return nil, ierrors.Wrapf(err, "failed to copy storage from active engine instance (%s) to new engine instance (%s)", e.Main.Get().Storage.Directory(), e.directory.Path(newEngineAlias)) } - e.LogInfo("forking engine at slot 3", "slot", slot) - // remove commitments that after forking point. latestCommitment := newStorage.Settings().LatestCommitment() if err = newStorage.Commitments().Rollback(slot, latestCommitment.Slot()); err != nil { return nil, ierrors.Wrap(err, "failed to rollback commitments") } - e.LogInfo("forking engine at slot 4", "slot", slot) - // some components are automatically rolled back by deleting their data on disk (e.g. slot based storage). // some other components need to be rolled back manually, like the UTXO ledger for example. // we need to create temporary components to rollback their permanent state, which will be reflected on disk. evictionState := eviction.NewState(newStorage.Settings(), newStorage.RootBlocks) evictionState.Initialize(latestCommitment.Slot()) - e.LogInfo("forking engine at slot 5", "slot", slot) - blockCache := blocks.New(evictionState, newStorage.Settings().APIProvider()) accountsManager := accountsledger.New(e.protocol.NewSubModule("ForkedAccountsLedger"), newStorage.Settings().APIProvider(), blockCache.Block, newStorage.AccountDiffs, newStorage.Accounts()) @@ -129,27 +119,19 @@ func (e *Engines) ForkAtSlot(slot iotago.SlotIndex) (*engine.Engine, error) { return nil, err } - e.LogInfo("forking engine at slot 6", "slot", slot) - targetCommitment, err := newStorage.Commitments().Load(slot) if err != nil { return nil, ierrors.Wrapf(err, "error while retrieving commitment for target index %d", slot) } - e.LogInfo("forking engine at slot 6.1", "slot", slot) - if err = newStorage.Settings().Rollback(targetCommitment); err != nil { return nil, err } - e.LogInfo("forking engine at slot 6.2", "slot", slot) if err = newStorage.Rollback(slot); err != nil { - e.LogError("failed to rollback storage", "err", err) return nil, err } - e.LogInfo("forking engine at slot 7", "slot", slot) - candidateEngine := e.loadEngineInstanceWithStorage(newEngineAlias, newStorage) // rollback attestations already on created engine instance, because this action modifies the in-memory storage. @@ -157,8 +139,6 @@ func (e *Engines) ForkAtSlot(slot iotago.SlotIndex) (*engine.Engine, error) { return nil, ierrors.Wrap(err, "error while rolling back attestations storage on candidate engine") } - e.LogInfo("forking engine at slot 8", "slot", slot) - return candidateEngine, nil } From 66370670ecdde16477541e11858e324fb06abcaa Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Wed, 17 Apr 2024 11:48:14 +0200 Subject: [PATCH 33/40] Refactor: cleaned up more code --- pkg/protocol/engines.go | 2 -- pkg/storage/prunable/epochstore/epoch_kv.go | 1 - pkg/storage/prunable/prunable.go | 16 ---------------- 3 files changed, 19 deletions(-) diff --git a/pkg/protocol/engines.go b/pkg/protocol/engines.go index 87407d8b2..c3be37c89 100644 --- a/pkg/protocol/engines.go +++ b/pkg/protocol/engines.go @@ -274,9 +274,7 @@ func (e *Engines) injectEngineInstances() (shutdown func()) { } else { e.protocol.Network.OnShutdown(func() { newEngine.ShutdownEvent().Trigger() }) - e.LogInfo("injecting engine instance before", "chain", chain.LogName()) chain.Engine.Set(newEngine) - e.LogInfo("injecting engine instance after", "chain", chain.LogName()) } }) }) diff --git a/pkg/storage/prunable/epochstore/epoch_kv.go b/pkg/storage/prunable/epochstore/epoch_kv.go index 5b94cbde9..1c106b28a 100644 --- a/pkg/storage/prunable/epochstore/epoch_kv.go +++ b/pkg/storage/prunable/epochstore/epoch_kv.go @@ -108,7 +108,6 @@ func (e *EpochKVStore) Prune(epoch iotago.EpochIndex, defaultPruningDelay iotago func (e *EpochKVStore) RollbackEpochs(epoch iotago.EpochIndex) (lastPrunedEpoch iotago.EpochIndex, err error) { lastAccessedEpoch, err := e.LastAccessedEpoch() - if err != nil { return lastAccessedEpoch, ierrors.Wrap(err, "failed to get last accessed epoch") } diff --git a/pkg/storage/prunable/prunable.go b/pkg/storage/prunable/prunable.go index e186eedb6..e90f2b0b8 100644 --- a/pkg/storage/prunable/prunable.go +++ b/pkg/storage/prunable/prunable.go @@ -1,8 +1,6 @@ package prunable import ( - "fmt" - copydir "github.com/otiai10/copy" "github.com/iotaledger/hive.go/ierrors" @@ -173,47 +171,34 @@ func (p *Prunable) Flush() { } func (p *Prunable) Rollback(targetEpoch iotago.EpochIndex, startPruneRange iotago.SlotIndex, endPruneRange iotago.SlotIndex) error { - fmt.Println("Rollback", targetEpoch, startPruneRange, endPruneRange) - if err := p.prunableSlotStore.PruneSlots(targetEpoch, startPruneRange, endPruneRange); err != nil { return ierrors.Wrapf(err, "failed to prune slots in range [%d, %d] from target epoch %d", startPruneRange, endPruneRange, targetEpoch) } - fmt.Println("Rollback 2", targetEpoch, startPruneRange, endPruneRange) - if err := p.rollbackCommitteesCandidates(targetEpoch, startPruneRange); err != nil { return ierrors.Wrapf(err, "failed to rollback committee candidates to target epoch %d", targetEpoch) } - fmt.Println("Rollback 3", targetEpoch, startPruneRange, endPruneRange) lastPrunedCommitteeEpoch, err := p.rollbackCommitteeEpochs(targetEpoch+1, startPruneRange-1) if err != nil { return ierrors.Wrapf(err, "failed to rollback committee epochs to target epoch %d", targetEpoch) } - fmt.Println("Rollback 4", targetEpoch, startPruneRange, endPruneRange) - lastPrunedPoolStatsEpoch, _, err := p.poolStats.RollbackEpochs(targetEpoch) if err != nil { return ierrors.Wrapf(err, "failed to rollback pool stats epochs to target epoch %d", targetEpoch) } - fmt.Println("Rollback 5", targetEpoch, startPruneRange, endPruneRange) - lastPrunedDecidedUpgradeSignalsEpoch, _, err := p.decidedUpgradeSignals.RollbackEpochs(targetEpoch) if err != nil { return ierrors.Wrapf(err, "failed to rollback decided upgrade signals epochs to target epoch %d", targetEpoch) } - fmt.Println("Rollback 6", targetEpoch, startPruneRange, endPruneRange) - lastPrunedPoolRewardsEpoch, err := p.poolRewards.RollbackEpochs(targetEpoch) if err != nil { return ierrors.Wrapf(err, "failed to rollback pool rewards epochs to target epoch %d", targetEpoch) } - fmt.Println("Rollback 7 ", targetEpoch, startPruneRange, endPruneRange) - for epochToPrune := targetEpoch + 1; epochToPrune <= max( lastPrunedCommitteeEpoch, lastPrunedPoolStatsEpoch, @@ -222,7 +207,6 @@ func (p *Prunable) Rollback(targetEpoch iotago.EpochIndex, startPruneRange iotag ); epochToPrune++ { p.prunableSlotStore.DeleteBucket(epochToPrune) } - fmt.Println("Rollback 8", targetEpoch, startPruneRange, endPruneRange) return nil } From 7a8d3b13a3ea6d097f610bd177a4d5bdf5ebdefe Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Wed, 17 Apr 2024 11:50:38 +0200 Subject: [PATCH 34/40] Refactor: reverted more changes --- pkg/tests/loss_of_acceptance_test.go | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/pkg/tests/loss_of_acceptance_test.go b/pkg/tests/loss_of_acceptance_test.go index 35c3c2653..bf18e0d0e 100644 --- a/pkg/tests/loss_of_acceptance_test.go +++ b/pkg/tests/loss_of_acceptance_test.go @@ -2,7 +2,6 @@ package tests import ( "fmt" - "net/http" _ "net/http" _ "net/http/pprof" "testing" @@ -20,12 +19,6 @@ import ( ) func TestLossOfAcceptanceFromGenesis(t *testing.T) { - // debug.SetEnabled(true) - - go func() { - fmt.Println(http.ListenAndServe("localhost:6061", nil)) - }() - ts := testsuite.NewTestSuite(t, testsuite.WithProtocolParametersOptions( iotago.WithTimeProviderOptions( @@ -44,22 +37,17 @@ func TestLossOfAcceptanceFromGenesis(t *testing.T) { ), testsuite.WithWaitFor(15*time.Second), ) - // defer ts.Shutdown() + defer ts.Shutdown() node0 := ts.AddValidatorNode("node0") ts.AddDefaultWallet(node0) node1 := ts.AddValidatorNode("node1") - // node2 := ts.AddNode("node2") nodesP1 := []*mock.Node{node0} nodesP2 := []*mock.Node{node1} ts.Run(true, nil) - //node0.Protocol.SetLogLevel(log.LevelTrace) - // node1.Protocol.SetLogLevel(log.LevelTrace) - // node2.Protocol.SetLogLevel(log.LevelFatal) - // Create snapshot to use later. snapshotPath := ts.Directory.Path(fmt.Sprintf("%d_snapshot", time.Now().Unix())) require.NoError(t, node0.Protocol.Engines.Main.Get().WriteSnapshot(snapshotPath)) From 0f2803143f6d8edcdf386846bd9cae5e6c19df89 Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Wed, 17 Apr 2024 11:52:07 +0200 Subject: [PATCH 35/40] Refactor: more cleanup --- pkg/tests/loss_of_acceptance_test.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pkg/tests/loss_of_acceptance_test.go b/pkg/tests/loss_of_acceptance_test.go index bf18e0d0e..d8098de31 100644 --- a/pkg/tests/loss_of_acceptance_test.go +++ b/pkg/tests/loss_of_acceptance_test.go @@ -134,13 +134,8 @@ func TestLossOfAcceptanceFromGenesis(t *testing.T) { ts.Wait() - fmt.Println(">>>>>>> Checking stuff...") - // ts.AssertBlocksInCacheAccepted(ts.BlocksWithPrefix("59.0"), true, ts.Nodes()...) ts.AssertLatestCommitmentSlotIndex(59, ts.Nodes()...) - fmt.Println(">>>>>>> Latest commitment slot index:", 59) - // pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) ts.AssertEqualStoredCommitmentAtIndex(59, ts.Nodes()...) - fmt.Println(">>>>>>> Stored commitment at index 59 is equal for all nodes.") } } From 971c6cd561fe3a2670525a6b7f64eaaccc4d4809 Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Wed, 17 Apr 2024 11:53:24 +0200 Subject: [PATCH 36/40] Refactor: removed unused imports --- pkg/tests/loss_of_acceptance_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/tests/loss_of_acceptance_test.go b/pkg/tests/loss_of_acceptance_test.go index d8098de31..5244ba03e 100644 --- a/pkg/tests/loss_of_acceptance_test.go +++ b/pkg/tests/loss_of_acceptance_test.go @@ -2,8 +2,6 @@ package tests import ( "fmt" - _ "net/http" - _ "net/http/pprof" "testing" "time" From 872232b01f144b833aa9790670101702031a0adf Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Wed, 17 Apr 2024 12:38:44 +0200 Subject: [PATCH 37/40] Fix: fixed deadlock in bucketmanager --- pkg/storage/prunable/bucket_manager.go | 2 +- pkg/storage/prunable/bucketed_kvstore.go | 131 +++++++++++++++++++++++ pkg/tests/loss_of_acceptance_test.go | 18 ++-- 3 files changed, 141 insertions(+), 10 deletions(-) create mode 100644 pkg/storage/prunable/bucketed_kvstore.go diff --git a/pkg/storage/prunable/bucket_manager.go b/pkg/storage/prunable/bucket_manager.go index c557e2903..22218c73d 100644 --- a/pkg/storage/prunable/bucket_manager.go +++ b/pkg/storage/prunable/bucket_manager.go @@ -67,7 +67,7 @@ func (b *BucketManager) Get(epoch iotago.EpochIndex, realm kvstore.Realm) (kvsto return nil, ierrors.WithMessagef(database.ErrEpochPruned, "epoch %d", epoch) } - kv := b.getDBInstance(epoch).KVStore() + kv := newBucketedKVStore(b, b.getDBInstance(epoch).KVStore()) return lo.PanicOnErr(kv.WithExtendedRealm(realm)), nil } diff --git a/pkg/storage/prunable/bucketed_kvstore.go b/pkg/storage/prunable/bucketed_kvstore.go new file mode 100644 index 000000000..3d5d7d1f1 --- /dev/null +++ b/pkg/storage/prunable/bucketed_kvstore.go @@ -0,0 +1,131 @@ +package prunable + +import "github.com/iotaledger/hive.go/kvstore" + +type bucketedKVStore struct { + bucketManager *BucketManager + store kvstore.KVStore +} + +func newBucketedKVStore(bucketManager *BucketManager, store kvstore.KVStore) *bucketedKVStore { + return &bucketedKVStore{ + bucketManager: bucketManager, + store: store, + } +} + +func (b *bucketedKVStore) WithRealm(realm kvstore.Realm) (kvstore.KVStore, error) { + b.rLockBucketManager() + defer b.rUnlockBucketManager() + + s, err := b.store.WithRealm(realm) + if err != nil { + return nil, err + } + + return newBucketedKVStore(b.bucketManager, s), nil +} + +func (b *bucketedKVStore) WithExtendedRealm(realm kvstore.Realm) (kvstore.KVStore, error) { + b.rLockBucketManager() + defer b.rUnlockBucketManager() + + s, err := b.store.WithExtendedRealm(realm) + if err != nil { + return nil, err + } + + return newBucketedKVStore(b.bucketManager, s), nil +} + +func (b *bucketedKVStore) Realm() kvstore.Realm { + b.rLockBucketManager() + defer b.rUnlockBucketManager() + + return b.store.Realm() +} + +func (b *bucketedKVStore) Iterate(prefix kvstore.KeyPrefix, kvConsumerFunc kvstore.IteratorKeyValueConsumerFunc, direction ...kvstore.IterDirection) error { + b.rLockBucketManager() + defer b.rUnlockBucketManager() + + return b.store.Iterate(prefix, kvConsumerFunc, direction...) +} + +func (b *bucketedKVStore) IterateKeys(prefix kvstore.KeyPrefix, consumerFunc kvstore.IteratorKeyConsumerFunc, direction ...kvstore.IterDirection) error { + b.rLockBucketManager() + defer b.rUnlockBucketManager() + + return b.store.IterateKeys(prefix, consumerFunc, direction...) +} + +func (b *bucketedKVStore) Clear() error { + b.rLockBucketManager() + defer b.rUnlockBucketManager() + + return b.store.Clear() +} + +func (b *bucketedKVStore) Get(key kvstore.Key) (value kvstore.Value, err error) { + b.rLockBucketManager() + defer b.rUnlockBucketManager() + + return b.store.Get(key) +} + +func (b *bucketedKVStore) Set(key kvstore.Key, value kvstore.Value) error { + b.rLockBucketManager() + defer b.rUnlockBucketManager() + + return b.store.Set(key, value) +} + +func (b *bucketedKVStore) Has(key kvstore.Key) (bool, error) { + b.rLockBucketManager() + defer b.rUnlockBucketManager() + + return b.store.Has(key) +} + +func (b *bucketedKVStore) Delete(key kvstore.Key) error { + b.rLockBucketManager() + defer b.rUnlockBucketManager() + + return b.store.Delete(key) +} + +func (b *bucketedKVStore) DeletePrefix(prefix kvstore.KeyPrefix) error { + b.rLockBucketManager() + defer b.rUnlockBucketManager() + + return b.store.DeletePrefix(prefix) +} + +func (b *bucketedKVStore) Flush() error { + b.rLockBucketManager() + defer b.rUnlockBucketManager() + + return b.store.Flush() +} + +func (b *bucketedKVStore) Close() error { + b.rLockBucketManager() + defer b.rUnlockBucketManager() + + return b.store.Close() +} + +func (b *bucketedKVStore) Batched() (kvstore.BatchedMutations, error) { + b.rLockBucketManager() + defer b.rUnlockBucketManager() + + return b.store.Batched() +} + +func (b *bucketedKVStore) rLockBucketManager() { + b.bucketManager.mutex.RLock() +} + +func (b *bucketedKVStore) rUnlockBucketManager() { + b.bucketManager.mutex.RUnlock() +} diff --git a/pkg/tests/loss_of_acceptance_test.go b/pkg/tests/loss_of_acceptance_test.go index 5244ba03e..6bbadf2e7 100644 --- a/pkg/tests/loss_of_acceptance_test.go +++ b/pkg/tests/loss_of_acceptance_test.go @@ -113,15 +113,15 @@ func TestLossOfAcceptanceFromGenesis(t *testing.T) { ts.AssertSybilProtectionOnlineCommittee(seatIndexes[1:2], nodesP2...) // Start node3 from genesis snapshot. - // node3 := ts.AddNode("node3") - // { - // node3.Initialize(true, - // protocol.WithSnapshotPath(snapshotPath), - // protocol.WithBaseDirectory(ts.Directory.PathWithCreate(node3.Name)), - // ) - // node3.Protocol.SetLogLevel(log.LevelTrace) - // ts.Wait() - // } + node3 := ts.AddNode("node3") + { + node3.Initialize(true, + protocol.WithSnapshotPath(snapshotPath), + protocol.WithBaseDirectory(ts.Directory.PathWithCreate(node3.Name)), + ) + + ts.Wait() + } ts.MergePartitionsToMain() fmt.Println("\n=========================\nMerged network partitions\n=========================") From 575416a2e1fbcaf881ed65c3e3775dff6af75066 Mon Sep 17 00:00:00 2001 From: Hans Moog <3293976+hmoog@users.noreply.github.com> Date: Wed, 17 Apr 2024 12:42:02 +0200 Subject: [PATCH 38/40] Feat: re-add previous test --- pkg/tests/loss_of_acceptance_test.go | 87 ++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/pkg/tests/loss_of_acceptance_test.go b/pkg/tests/loss_of_acceptance_test.go index 6bbadf2e7..5d9ba530c 100644 --- a/pkg/tests/loss_of_acceptance_test.go +++ b/pkg/tests/loss_of_acceptance_test.go @@ -17,6 +17,93 @@ import ( ) func TestLossOfAcceptanceFromGenesis(t *testing.T) { + ts := testsuite.NewTestSuite(t, + testsuite.WithWaitFor(15*time.Second), + testsuite.WithProtocolParametersOptions( + iotago.WithTimeProviderOptions( + 0, + testsuite.GenesisTimeWithOffsetBySlots(100, testsuite.DefaultSlotDurationInSeconds), + testsuite.DefaultSlotDurationInSeconds, + 3, + ), + iotago.WithLivenessOptions( + 10, + 10, + 2, + 4, + 5, + ), + ), + testsuite.WithWaitFor(15*time.Second), + ) + defer ts.Shutdown() + + node0 := ts.AddValidatorNode("node0") + ts.AddDefaultWallet(node0) + ts.AddValidatorNode("node1") + ts.AddNode("node2") + + ts.Run(true, nil) + + // Create snapshot to use later. + snapshotPath := ts.Directory.Path(fmt.Sprintf("%d_snapshot", time.Now().Unix())) + require.NoError(t, node0.Protocol.Engines.Main.Get().WriteSnapshot(snapshotPath)) + + // Revive chain on node0. + { + ts.SetCurrentSlot(50) + block0 := lo.PanicOnErr(ts.IssueValidationBlockWithHeaderOptions("block0", node0)) + require.EqualValues(t, 48, ts.Block("block0").SlotCommitmentID().Slot()) + // Reviving the chain should select one parent from the last committed slot. + require.Len(t, block0.Parents(), 1) + require.Equal(t, block0.Parents()[0].Alias(), "Genesis") + ts.AssertBlocksExist(ts.Blocks("block0"), true, ts.ClientsForNodes(node0)...) + } + + // Need to issue to slot 52 so that all other nodes can warp sync up to slot 49 and then commit slot 50 themselves. + { + ts.IssueBlocksAtSlots("", []iotago.SlotIndex{51, 52}, 2, "block0", mock.Nodes(node0), true, false) + + ts.AssertLatestCommitmentSlotIndex(50, ts.Nodes()...) + ts.AssertEqualStoredCommitmentAtIndex(50, ts.Nodes()...) + ts.AssertBlocksExist(ts.Blocks("block0"), true, ts.ClientsForNodes()...) + } + + // Continue issuing on all nodes for a few slots. + { + ts.IssueBlocksAtSlots("", []iotago.SlotIndex{53, 54, 55, 56, 57}, 3, "52.1", ts.Nodes(), true, false) + + ts.AssertBlocksInCacheAccepted(ts.BlocksWithPrefix("57.0"), true, ts.Nodes()...) + ts.AssertLatestCommitmentSlotIndex(55, ts.Nodes()...) + ts.AssertEqualStoredCommitmentAtIndex(55, ts.Nodes()...) + } + + // Start node3 from genesis snapshot. + { + node3 := ts.AddNode("node3") + node3.Initialize(true, + protocol.WithSnapshotPath(snapshotPath), + protocol.WithBaseDirectory(ts.Directory.PathWithCreate(node3.Name)), + ) + ts.Wait() + } + + // Continue issuing on all nodes for a few slots. + { + ts.IssueBlocksAtSlots("", []iotago.SlotIndex{58, 59}, 3, "57.2", ts.Nodes("node0", "node1", "node2"), true, false) + + ts.AssertBlocksInCacheAccepted(ts.BlocksWithPrefix("59.0"), true, ts.Nodes()...) + ts.AssertLatestCommitmentSlotIndex(57, ts.Nodes()...) + ts.AssertEqualStoredCommitmentAtIndex(57, ts.Nodes()...) + } + + // Check that commitments from 1-49 are empty. + for slot := iotago.SlotIndex(1); slot <= 49; slot++ { + ts.AssertStorageCommitmentBlocks(slot, nil, ts.Nodes()...) + } +} + +func TestEngineSwitchingUponStartupWithLossOfAcceptance(t *testing.T) { ts := testsuite.NewTestSuite(t, testsuite.WithProtocolParametersOptions( iotago.WithTimeProviderOptions( From 2fc844231ad8375cc8d445f50d7d5f69f33de564 Mon Sep 17 00:00:00 2001 From: Alexander Sporn Date: Wed, 17 Apr 2024 13:25:38 +0200 Subject: [PATCH 39/40] Added custom protocol params to presets_yaml.go --- .../genesis-snapshot/presets/presets_yaml.go | 59 ++++++++++++++----- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/tools/genesis-snapshot/presets/presets_yaml.go b/tools/genesis-snapshot/presets/presets_yaml.go index 0c879c7df..cd38ec268 100644 --- a/tools/genesis-snapshot/presets/presets_yaml.go +++ b/tools/genesis-snapshot/presets/presets_yaml.go @@ -2,6 +2,7 @@ package presets import ( "fmt" + "time" "golang.org/x/crypto/blake2b" @@ -21,18 +22,22 @@ type ValidatorYaml struct { } type BlockIssuerYaml struct { - Name string `yaml:"name"` - PublicKey string `yaml:"publicKey"` + Name string `yaml:"name"` + PublicKey string `yaml:"publicKey"` + BlockIssuanceCredits uint64 `yaml:"blockIssuanceCredits"` } type BasicOutputYaml struct { + Name string `yaml:"name"` Address string `yaml:"address"` Amount uint64 `yaml:"amount"` Mana uint64 `yaml:"mana"` } type ConfigYaml struct { - Name string `yaml:"name"` + NetworkName string `yaml:"networkName"` + Bech32HRP string `yaml:"bech32HRP"` + FilePath string `yaml:"filepath"` Validators []ValidatorYaml `yaml:"validators"` @@ -40,12 +45,32 @@ type ConfigYaml struct { BasicOutputs []BasicOutputYaml `yaml:"basicOutputs"` } +func TestnetProtocolParameters(networkName string, bech32HRP iotago.NetworkPrefix) iotago.ProtocolParameters { + return iotago.NewV3SnapshotProtocolParameters( + iotago.WithNetworkOptions(networkName, bech32HRP), + iotago.WithStorageOptions(100, 1, 100, 1000, 1000, 1000), + iotago.WithWorkScoreOptions(500, 110_000, 7_500, 40_000, 90_000, 50_000, 40_000, 70_000, 5_000, 15_000), + iotago.WithTimeProviderOptions(0, time.Now().Unix(), 10, 13), + iotago.WithLivenessOptions(10, 15, 4, 7, 100), + iotago.WithSupplyOptions(4600000000000000, 63, 1, 17, 32, 21, 70), + iotago.WithCongestionControlOptions(1, 1, 1, 400_000_000, 250_000_000, 50_000_000, 1000, 100), + iotago.WithStakingOptions(3, 10, 10), + iotago.WithVersionSignalingOptions(7, 5, 7), + iotago.WithRewardsOptions(8, 11, 2, 384), + iotago.WithTargetCommitteeSize(16), + iotago.WithChainSwitchingThreshold(3), + ) +} + func GenerateFromYaml(hostsFile string) ([]options.Option[snapshotcreator.Options], error) { var configYaml ConfigYaml if err := ioutils.ReadYAMLFromFile(hostsFile, &configYaml); err != nil { return nil, err } + fmt.Printf("generating protocol parameters for network %s with bech32HRP %s\n", configYaml.NetworkName, configYaml.Bech32HRP) + protocolParams := TestnetProtocolParameters(configYaml.NetworkName, iotago.NetworkPrefix(configYaml.Bech32HRP)) + accounts := make([]snapshotcreator.AccountDetails, 0, len(configYaml.Validators)+len(configYaml.BlockIssuers)) for _, validator := range configYaml.Validators { pubkey := validator.PublicKey @@ -53,39 +78,45 @@ func GenerateFromYaml(hostsFile string) ([]options.Option[snapshotcreator.Option account := snapshotcreator.AccountDetails{ AccountID: blake2b.Sum256(lo.PanicOnErr(hexutil.DecodeHex(pubkey))), Address: iotago.Ed25519AddressFromPubKey(lo.PanicOnErr(hexutil.DecodeHex(pubkey))), - Amount: mock.MinValidatorAccountAmount(ProtocolParamsDocker), + Amount: mock.MinValidatorAccountAmount(protocolParams), IssuerKey: iotago.Ed25519PublicKeyHashBlockIssuerKeyFromPublicKey(ed25519.PublicKey(lo.PanicOnErr(hexutil.DecodeHex(pubkey)))), ExpirySlot: iotago.MaxSlotIndex, - BlockIssuanceCredits: iotago.MaxBlockIssuanceCredits / 4, + BlockIssuanceCredits: 0, StakingEndEpoch: iotago.MaxEpochIndex, FixedCost: 1, - StakedAmount: mock.MinValidatorAccountAmount(ProtocolParamsDocker), - Mana: iotago.Mana(mock.MinValidatorAccountAmount(ProtocolParamsDocker)), + StakedAmount: mock.MinValidatorAccountAmount(protocolParams), + Mana: iotago.Mana(mock.MinValidatorAccountAmount(protocolParams)), } accounts = append(accounts, account) } for _, blockIssuer := range configYaml.BlockIssuers { pubkey := blockIssuer.PublicKey - fmt.Printf("adding blockissueer %s with publicKey %s\n", blockIssuer.Name, pubkey) + fmt.Printf("adding blockissuer %s with publicKey %s\n", blockIssuer.Name, pubkey) account := snapshotcreator.AccountDetails{ AccountID: blake2b.Sum256(lo.PanicOnErr(hexutil.DecodeHex(pubkey))), Address: iotago.Ed25519AddressFromPubKey(lo.PanicOnErr(hexutil.DecodeHex(pubkey))), - Amount: mock.MinValidatorAccountAmount(ProtocolParamsDocker), + Amount: mock.MinValidatorAccountAmount(protocolParams), IssuerKey: iotago.Ed25519PublicKeyHashBlockIssuerKeyFromPublicKey(ed25519.PublicKey(lo.PanicOnErr(hexutil.DecodeHex(pubkey)))), ExpirySlot: iotago.MaxSlotIndex, - BlockIssuanceCredits: iotago.MaxBlockIssuanceCredits / 4, - Mana: iotago.Mana(mock.MinValidatorAccountAmount(ProtocolParamsDocker)), + BlockIssuanceCredits: iotago.BlockIssuanceCredits(blockIssuer.BlockIssuanceCredits), + Mana: iotago.Mana(mock.MinValidatorAccountAmount(protocolParams)), } accounts = append(accounts, account) } basicOutputs := make([]snapshotcreator.BasicOutputDetails, 0, len(configYaml.BasicOutputs)) for _, basicOutput := range configYaml.BasicOutputs { - address := lo.Return2(iotago.ParseBech32(basicOutput.Address)) + hrp, address, err := iotago.ParseBech32(basicOutput.Address) + if err != nil { + panic(err) + } + if protocolParams.Bech32HRP() != hrp { + panic(fmt.Sprintf("address %s has wrong HRP %s, expected %s", address, hrp, protocolParams.Bech32HRP())) + } amount := basicOutput.Amount mana := basicOutput.Mana - fmt.Printf("adding basicOutput for %s with amount %d and mana %d\n", address, amount, mana) + fmt.Printf("adding basicOutput %s for %s with amount %d and mana %d\n", basicOutput.Name, address, amount, mana) basicOutputs = append(basicOutputs, snapshotcreator.BasicOutputDetails{ Address: address, Amount: iotago.BaseToken(amount), @@ -95,7 +126,7 @@ func GenerateFromYaml(hostsFile string) ([]options.Option[snapshotcreator.Option return []options.Option[snapshotcreator.Options]{ snapshotcreator.WithFilePath(configYaml.FilePath), - snapshotcreator.WithProtocolParameters(ProtocolParamsDocker), + snapshotcreator.WithProtocolParameters(protocolParams), snapshotcreator.WithAccounts(accounts...), snapshotcreator.WithBasicOutputs(basicOutputs...), }, nil From bf289019c9fea8042d76101716e51a0e6a78f5c2 Mon Sep 17 00:00:00 2001 From: muXxer Date: Wed, 17 Apr 2024 13:53:20 +0200 Subject: [PATCH 40/40] Fix Test_ManagementAPI_Pruning --- tools/docker-network/tests/api_management_test.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tools/docker-network/tests/api_management_test.go b/tools/docker-network/tests/api_management_test.go index a423df753..837ccae43 100644 --- a/tools/docker-network/tests/api_management_test.go +++ b/tools/docker-network/tests/api_management_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/iotaledger/iota-core/pkg/storage/database" iotago "github.com/iotaledger/iota.go/v4" "github.com/iotaledger/iota.go/v4/api" ) @@ -213,7 +214,7 @@ func Test_ManagementAPI_Pruning(t *testing.T) { info, err := nodeClientV1.Info(getContextWithTimeout(5 * time.Second)) require.NoError(t, err) - currentEpoch := nodeClientV1.CommittedAPI().TimeProvider().EpochFromSlot(info.Status.LatestCommitmentID.Slot()) + currentEpoch := nodeClientV1.CommittedAPI().TimeProvider().EpochFromSlot(info.Status.LatestFinalizedSlot) // await the start slot of the next epoch d.AwaitCommitment(nodeClientV1.CommittedAPI().TimeProvider().EpochStart(currentEpoch + 1)) @@ -258,9 +259,9 @@ func Test_ManagementAPI_Pruning(t *testing.T) { awaitNextEpoch() // prune database by size - pruneDatabaseResponse, err := managementClient.PruneDatabaseBySize(getContextWithTimeout(5*time.Second), "1M") - require.NoError(t, err) - require.NotNil(t, pruneDatabaseResponse) + pruneDatabaseResponse, err := managementClient.PruneDatabaseBySize(getContextWithTimeout(5*time.Second), "5G") + require.ErrorIs(t, err, database.ErrNoPruningNeeded) + require.Nil(t, pruneDatabaseResponse) }, }, }