diff --git a/pkg/protocol/chain.go b/pkg/protocol/chain.go index 161f89dda..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, @@ -201,6 +205,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), @@ -208,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, @@ -233,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 de75800aa..78a24bf29 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), @@ -266,6 +273,8 @@ func (c *Commitment) initDerivedProperties() (shutdown func()) { }), ) }), + + c.IsSolid.InheritFrom(parent.IsSolid), ) }), diff --git a/pkg/protocol/engine/accounts/accountsledger/manager.go b/pkg/protocol/engine/accounts/accountsledger/manager.go index 7416434d5..59f5e8c7e 100644 --- a/pkg/protocol/engine/accounts/accountsledger/manager.go +++ b/pkg/protocol/engine/accounts/accountsledger/manager.go @@ -314,7 +314,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. diff --git a/pkg/protocol/engine/blocks/block.go b/pkg/protocol/engine/blocks/block.go index ae2c1b206..e27463b59 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" @@ -11,10 +12,20 @@ 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 { + // 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 missing bool missingBlockID iotago.BlockID @@ -76,6 +87,10 @@ func (r *rootBlock) String() string { func newEmptyBlock() *Block { return &Block{ + ParentsBooked: reactive.NewEvent(), + PayloadDependenciesAvailable: reactive.NewEvent(), + SignedTransactionMetadata: reactive.NewVariable[mempool.SignedTransactionMetadata](), + witnesses: ds.NewSet[account.SeatIndex](), spenderIDs: ds.NewSet[iotago.TransactionID](), payloadSpenderIDs: ds.NewSet[iotago.TransactionID](), @@ -112,6 +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.ParentsBooked.Set(true) + b.PayloadDependenciesAvailable.Set(true) b.solid.Init(true) b.booked.Init(true) b.weightPropagated.Init(true) @@ -663,3 +680,30 @@ func (b *Block) ModelBlock() *model.Block { func (b *Block) WorkScore() iotago.WorkScore { return b.workScore } + +func (b *Block) WaitForPayloadDependencies(dependencies ds.Set[mempool.StateMetadata]) { + if dependencies == nil || dependencies.Size() == 0 { + b.PayloadDependenciesAvailable.Trigger() + + return + } + + var unreferencedOutputCount atomic.Int32 + unreferencedOutputCount.Store(int32(dependencies.Size())) + + dependencies.Range(func(dependency mempool.StateMetadata) { + dependencyReady := false + + dependency.OnAccepted(func() { + dependency.OnInclusionSlotUpdated(func(_ iotago.SlotIndex, inclusionSlot iotago.SlotIndex) { + if !dependencyReady && inclusionSlot <= b.ID().Slot() { + dependencyReady = true + + if unreferencedOutputCount.Add(-1) == 0 { + b.PayloadDependenciesAvailable.Trigger() + } + } + }) + }) + }) +} diff --git a/pkg/protocol/engine/booker/inmemorybooker/booker.go b/pkg/protocol/engine/booker/inmemorybooker/booker.go index af5e66aef..042f5b751 100644 --- a/pkg/protocol/engine/booker/inmemorybooker/booker.go +++ b/pkg/protocol/engine/booker/inmemorybooker/booker.go @@ -96,15 +96,14 @@ 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 block %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() { @@ -137,6 +136,12 @@ 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 payloadDependencies, directlyReferencedPayloadDependencies ds.Set[mempool.StateMetadata] + if signedTransactionMetadata := block.SignedTransactionMetadata.Get(); signedTransactionMetadata != nil && signedTransactionMetadata.SignaturesInvalid() == nil && !signedTransactionMetadata.TransactionMetadata().IsInvalid() { + payloadDependencies = signedTransactionMetadata.TransactionMetadata().Inputs() + directlyReferencedPayloadDependencies = ds.NewSet[mempool.StateMetadata]() + } + var unbookedParentsCount atomic.Int32 unbookedParentsCount.Store(int32(len(block.Parents()))) @@ -149,13 +154,15 @@ func (b *Booker) setupBlock(block *blocks.Block) { } parentBlock.Booked().OnUpdateOnce(func(_ bool, _ bool) { - 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")) - } + if directlyReferencedPayloadDependencies != nil { + if parentTransactionMetadata := parentBlock.SignedTransactionMetadata.Get(); parentTransactionMetadata != nil { + directlyReferencedPayloadDependencies.AddAll(parentTransactionMetadata.TransactionMetadata().Outputs()) } } + + if unbookedParentsCount.Add(-1) == 0 { + block.ParentsBooked.Trigger() + } }) parentBlock.Invalid().OnUpdateOnce(func(_ bool, _ bool) { @@ -164,6 +171,22 @@ func (b *Booker) setupBlock(block *blocks.Block) { } }) }) + + block.ParentsBooked.OnTrigger(func() { + if directlyReferencedPayloadDependencies != nil { + payloadDependencies.DeleteAll(directlyReferencedPayloadDependencies) + } + + block.WaitForPayloadDependencies(payloadDependencies) + }) + + 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")) + } + } + }) } func (b *Booker) book(block *blocks.Block) error { 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/signed_transaction_metadata.go b/pkg/protocol/engine/mempool/signed_transaction_metadata.go index ca6606b7c..cedbc6433 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(callback func(err error)) (unsubscribe func()) + SignaturesInvalid() error + TransactionMetadata() TransactionMetadata Attachments() []iotago.BlockID diff --git a/pkg/protocol/engine/mempool/state.go b/pkg/protocol/engine/mempool/state.go index bfdc6311a..e8c9957ba 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. + 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 44c898800..dd41cb473 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] @@ -16,5 +18,9 @@ type StateMetadata interface { OnAcceptedSpenderUpdated(callback func(spender TransactionMetadata)) + InclusionSlot() iotago.SlotIndex + + OnInclusionSlotUpdated(callback 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 c87aee4c3..5c3163b93 100644 --- a/pkg/protocol/engine/mempool/v1/mempool.go +++ b/pkg/protocol/engine/mempool/v1/mempool.go @@ -466,9 +466,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/signed_transaction_metadata.go b/pkg/protocol/engine/mempool/v1/signed_transaction_metadata.go index 40f073c9b..939ad71fc 100644 --- a/pkg/protocol/engine/mempool/v1/signed_transaction_metadata.go +++ b/pkg/protocol/engine/mempool/v1/signed_transaction_metadata.go @@ -50,6 +50,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 b7a08020f..f275afe43 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 @@ -20,6 +21,7 @@ type StateMetadata struct { doubleSpent *promise.Event spendAccepted reactive.Variable[*TransactionMetadata] spendCommitted reactive.Variable[*TransactionMetadata] + inclusionSlot reactive.Variable[*iotago.SlotIndex] allSpendersRemoved *event.Event spenderIDs reactive.DerivedSet[iotago.TransactionID] @@ -29,12 +31,14 @@ 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(), spendAccepted: reactive.NewVariable[*TransactionMetadata](), spendCommitted: reactive.NewVariable[*TransactionMetadata](), + inclusionSlot: reactive.NewVariable[*iotago.SlotIndex](), allSpendersRemoved: event.New(), spenderIDs: reactive.NewDerivedSet[iotago.TransactionID](), @@ -51,6 +55,16 @@ func (s *StateMetadata) setup(optSource ...*TransactionMetadata) *StateMetadata s.spenderIDs.InheritFrom(source.spenderIDs) + 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.OnAccepted(func() { s.accepted.Set(true) }) source.OnRejected(func() { s.rejected.Trigger() }) source.OnCommittedSlotUpdated(lo.Void(s.committedSlot.Set)) @@ -59,6 +73,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 } @@ -113,6 +135,23 @@ func (s *StateMetadata) HasNoSpenders() bool { return atomic.LoadUint64(&s.spenderCount) == 0 } +func (s *StateMetadata) InclusionSlot() iotago.SlotIndex { + return *s.inclusionSlot.Get() +} + +func (s *StateMetadata) OnInclusionSlotUpdated(callback func(prevID iotago.SlotIndex, newID iotago.SlotIndex)) { + s.inclusionSlot.OnUpdate(func(oldValue *iotago.SlotIndex, newValue *iotago.SlotIndex) { + switch { + case oldValue == nil: + callback(iotago.SlotIndex(0), *newValue) + case newValue == nil: + callback(*oldValue, iotago.SlotIndex(0)) + default: + callback(*oldValue, *newValue) + } + }) +} + func (s *StateMetadata) increaseSpenderCount() { if spenderCount := atomic.AddUint64(&s.spenderCount, 1); spenderCount == 1 { s.spent.Trigger() 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 { 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/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/booker_test.go b/pkg/tests/booker_test.go index 57f396580..ef54660f6 100644 --- a/pkg/tests/booker_test.go +++ b/pkg/tests/booker_test.go @@ -24,30 +24,47 @@ 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.IssueValidationBlockWithHeaderOptions("block4", node1, mock.WithStrongParents(ts.BlockID("block3"))) - 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) { @@ -213,405 +230,6 @@ func Test_DoubleSpend(t *testing.T) { } } -func Test_MultipleAttachments(t *testing.T) { - 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) { - 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( diff --git a/pkg/tests/loss_of_acceptance_test.go b/pkg/tests/loss_of_acceptance_test.go index aa91469f0..5d9ba530c 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/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" @@ -102,6 +103,127 @@ func TestLossOfAcceptanceFromGenesis(t *testing.T) { } } +func TestEngineSwitchingUponStartupWithLossOfAcceptance(t *testing.T) { + ts := testsuite.NewTestSuite(t, + 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) + node1 := ts.AddValidatorNode("node1") + + nodesP1 := []*mock.Node{node0} + nodesP2 := []*mock.Node{node1} + + 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)) + + 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) + 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()...) + } + 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, 58, 59, 60, 61}, 3, "52.1", nodesP1, true, true) + + 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], nodesP1...) + ts.AssertSybilProtectionOnlineCommittee(seatIndexes[0:1], nodesP2...) + + // Issue in P2 + { + ts.IssueBlocksAtSlots("P2:", []iotago.SlotIndex{53, 54, 55, 56, 57, 58, 59, 60, 61}, 3, "52.1", nodesP2, false, false) + + 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], 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)), + ) + + 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{61}, 1, "P2:61.2", nodesP2, false, false) + ts.IssueBlocksAtSlots("P1:", []iotago.SlotIndex{61}, 1, "P1:61.2", nodesP1, false, false) + + ts.Wait() + + ts.AssertLatestCommitmentSlotIndex(59, ts.Nodes()...) + ts.AssertEqualStoredCommitmentAtIndex(59, ts.Nodes()...) + } +} + func TestLossOfAcceptanceFromSnapshot(t *testing.T) { ts := testsuite.NewTestSuite(t, testsuite.WithProtocolParametersOptions( diff --git a/pkg/testsuite/testsuite.go b/pkg/testsuite/testsuite.go index 87fd477a7..df6d092ec 100644 --- a/pkg/testsuite/testsuite.go +++ b/pkg/testsuite/testsuite.go @@ -67,7 +67,8 @@ type TestSuite struct { snapshotPath string commitmentCheck bool - blocks *shrinkingmap.ShrinkingMap[string, *blocks.Block] + blocks *shrinkingmap.ShrinkingMap[string, *blocks.Block] + attachments *shrinkingmap.ShrinkingMap[string, []*blocks.Block] API iotago.API ProtocolParameterOptions []options.Option[iotago.V3ProtocolParameters] @@ -98,6 +99,7 @@ func NewTestSuite(t *testing.T, opts ...options.Option[TestSuite]) *TestSuite { 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 b66eb687a..0cb508d18 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, client) } +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)) + } 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, _ 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, error) { @@ -166,14 +204,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.addReferenceToDependencies(issuingOptionsCopy[node.Name], inputTxName) t.assertParentsCommitmentExistFromBlockOptions(issuingOptionsCopy[node.Name], node.Client) t.assertParentsExistFromBlockOptions(issuingOptionsCopy[node.Name], node.Client) 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) }, }, } 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