From cbe8163bdbca8ceb2aaa8db8cefc1b008fd8ac4e Mon Sep 17 00:00:00 2001 From: muXxer Date: Sun, 26 Jul 2020 18:09:03 +0200 Subject: [PATCH] Adaptive heaviest branch tipsel (#567) * Move transaction root index funcs to dag package * Add new coordinator config options * Show checkpoints as milestones in the visualizer * First working draft of the adaptive heaviest branch tipselection * Feed the dog * Coordinator code cleanup * Fixed race condition in Coordinator * Add checks if URTS plugin is enabled * Fix nextMilestoneSignal getting lost sometimes * Add itemList struct * Replace enforceTips with minRequiredTips * Removed maxTipsCount criteria * Code cleanup * Simplify below max depth check * Move context.WithDeadline to SelectTips * Inline randomTip * Add config option for heaviestBranchSelectionDeadline * Change default values * Remove latestTip * - Fix whiteflag address mutations not correctly mutating if a balance changes multiple times inside the same confirmation Co-authored-by: Alexander Sporn --- config.json | 10 +- config_comnet.json | 10 +- config_devnet.json | 10 +- pkg/config/coordinator_config.go | 22 +- pkg/dag/transaction_root_snapshot_indexes.go | 145 +++++++ pkg/model/coordinator/coordinator.go | 155 ++++--- pkg/model/coordinator/events.go | 2 +- pkg/model/coordinator/state.go | 2 +- pkg/model/mselection/heaviest.go | 433 +++++++++---------- pkg/tipselect/urts.go | 138 +----- pkg/whiteflag/white_flag.go | 2 +- plugins/coordinator/plugin.go | 188 ++++++-- plugins/dashboard/tipsel.go | 6 + plugins/dashboard/visualizer.go | 33 +- plugins/spammer/plugin.go | 12 + plugins/tangle/tangle_processor.go | 8 +- plugins/urts/plugin.go | 7 +- plugins/webapi/gtta.go | 8 + 18 files changed, 711 insertions(+), 480 deletions(-) create mode 100644 pkg/dag/transaction_root_snapshot_indexes.go diff --git a/config.json b/config.json index 63c7d4f0c..4b136a8ab 100644 --- a/config.json +++ b/config.json @@ -81,7 +81,15 @@ "stateFilePath": "coordinator.state", "merkleTreeFilePath": "coordinator.tree", "intervalSeconds": 60, - "checkpointTransactions": 5 + "checkpoints": { + "maxTrackedTails": 10000 + }, + "tipsel": { + "minHeaviestBranchUnconfirmedTransactionsThreshold": 20, + "maxHeaviestBranchTipsPerCheckpoint": 10, + "randomTipsPerCheckpoint": 2, + "heaviestBranchSelectionDeadlineMilliseconds": 100 + } }, "network": { "preferIPv6": false, diff --git a/config_comnet.json b/config_comnet.json index 07a5e6851..365129241 100644 --- a/config_comnet.json +++ b/config_comnet.json @@ -75,7 +75,15 @@ "stateFilePath": "coordinator.state", "merkleTreeFilePath": "coordinator.tree", "intervalSeconds": 60, - "checkpointTransactions": 5 + "checkpoints": { + "maxTrackedTails": 10000 + }, + "tipsel": { + "minHeaviestBranchUnconfirmedTransactionsThreshold": 20, + "maxHeaviestBranchTipsPerCheckpoint": 10, + "randomTipsPerCheckpoint": 2, + "heaviestBranchSelectionDeadlineMilliseconds": 100 + } }, "network": { "preferIPv6": false, diff --git a/config_devnet.json b/config_devnet.json index d46fe4aef..08134f2d1 100644 --- a/config_devnet.json +++ b/config_devnet.json @@ -77,7 +77,15 @@ "stateFilePath": "coordinator.state", "merkleTreeFilePath": "coordinator.tree", "intervalSeconds": 60, - "checkpointTransactions": 5 + "checkpoints": { + "maxTrackedTails": 10000 + }, + "tipsel": { + "minHeaviestBranchUnconfirmedTransactionsThreshold": 20, + "maxHeaviestBranchTipsPerCheckpoint": 10, + "randomTipsPerCheckpoint": 2, + "heaviestBranchSelectionDeadlineMilliseconds": 100 + } }, "network": { "preferIPv6": false, diff --git a/pkg/config/coordinator_config.go b/pkg/config/coordinator_config.go index dd9e07686..5a888c771 100644 --- a/pkg/config/coordinator_config.go +++ b/pkg/config/coordinator_config.go @@ -20,10 +20,22 @@ const ( CfgCoordinatorMerkleTreeFilePath = "coordinator.merkleTreeFilePath" // the interval milestones are issued CfgCoordinatorIntervalSeconds = "coordinator.intervalSeconds" - // the amount of checkpoints issued between two milestones - CfgCoordinatorCheckpointTransactions = "coordinator.checkpointTransactions" // the hash function the coordinator will use to calculate milestone merkle tree hash (see RFC-0012) CfgCoordinatorMilestoneMerkleTreeHashFunc = "coordinator.milestoneMerkleTreeHashFunc" + // the maximum amount of known bundle tails for milestone tipselection + // if this limit is exceeded, a new checkpoint is issued + CfgCoordinatorCheckpointsMaxTrackedTails = "coordinator.checkpoints.maxTrackedTransactions" + // the minimum threshold of unconfirmed transactions in the heaviest branch for milestone tipselection + // if the value falls below that threshold, no more heaviest branch tips are picked + CfgCoordinatorTipselectMinHeaviestBranchUnconfirmedTransactionsThreshold = "coordinator.tipsel.minHeaviestBranchUnconfirmedTransactionsThreshold" + // the maximum amount of checkpoint transactions with heaviest branch tips that are picked + // if the heaviest branch is not below "UnconfirmedTransactionsThreshold" before + CfgCoordinatorTipselectMaxHeaviestBranchTipsPerCheckpoint = "coordinator.tipsel.maxHeaviestBranchTipsPerCheckpoint" + // the amount of checkpoint transactions with random tips that are picked if a checkpoint is issued and at least + // one heaviest branch tip was found, otherwise no random tips will be picked + CfgCoordinatorTipselectRandomTipsPerCheckpoint = "coordinator.tipsel.randomTipsPerCheckpoint" + // the maximum duration to select the heaviest branch tips in milliseconds + CfgCoordinatorTipselectHeaviestBranchSelectionDeadlineMilliseconds = "coordinator.tipsel.heaviestBranchSelectionDeadlineMilliseconds" ) func init() { @@ -35,6 +47,10 @@ func init() { flag.String(CfgCoordinatorStateFilePath, "coordinator.state", "the path to the state file of the coordinator") flag.String(CfgCoordinatorMerkleTreeFilePath, "coordinator.tree", "the path to the Merkle tree of the coordinator") flag.Int(CfgCoordinatorIntervalSeconds, 60, "the interval milestones are issued") - flag.Int(CfgCoordinatorCheckpointTransactions, 5, "the amount of checkpoints issued between two milestones") flag.String(CfgCoordinatorMilestoneMerkleTreeHashFunc, "BLAKE2b-512", "the hash function the coordinator will use to calculate milestone merkle tree hash (see RFC-0012)") + flag.Int(CfgCoordinatorCheckpointsMaxTrackedTails, 10000, "maximum amount of known bundle tails for milestone tipselection") + flag.Int(CfgCoordinatorTipselectMinHeaviestBranchUnconfirmedTransactionsThreshold, 20, "minimum threshold of unconfirmed transactions in the heaviest branch") + flag.Int(CfgCoordinatorTipselectMaxHeaviestBranchTipsPerCheckpoint, 10, "maximum amount of checkpoint transactions with heaviest branch tips") + flag.Int(CfgCoordinatorTipselectRandomTipsPerCheckpoint, 2, "amount of checkpoint transactions with random tips") + flag.Int(CfgCoordinatorTipselectHeaviestBranchSelectionDeadlineMilliseconds, 100, "the maximum duration to select the heaviest branch tips in milliseconds") } diff --git a/pkg/dag/transaction_root_snapshot_indexes.go b/pkg/dag/transaction_root_snapshot_indexes.go new file mode 100644 index 000000000..bf6250f95 --- /dev/null +++ b/pkg/dag/transaction_root_snapshot_indexes.go @@ -0,0 +1,145 @@ +package dag + +import ( + "bytes" + "fmt" + + "github.com/gohornet/hornet/pkg/model/hornet" + "github.com/gohornet/hornet/pkg/model/milestone" + "github.com/gohornet/hornet/pkg/model/tangle" +) + +// UpdateOutdatedRootSnapshotIndexes updates the transaction root snapshot indexes of the given transactions. +// the "outdatedTransactions" should be ordered from latest to oldest to avoid recursion. +func UpdateOutdatedRootSnapshotIndexes(outdatedTransactions hornet.Hashes, lsmi milestone.Index) { + for i := len(outdatedTransactions) - 1; i >= 0; i-- { + outdatedTxHash := outdatedTransactions[i] + + cachedTx := tangle.GetCachedTransactionOrNil(outdatedTxHash) + if cachedTx == nil { + panic(tangle.ErrTransactionNotFound) + } + GetTransactionRootSnapshotIndexes(cachedTx, lsmi) + } +} + +// GetTransactionRootSnapshotIndexes searches the transaction root snapshot indexes for a given transaction. +func GetTransactionRootSnapshotIndexes(cachedTx *tangle.CachedTransaction, lsmi milestone.Index) (youngestTxRootSnapshotIndex milestone.Index, oldestTxRootSnapshotIndex milestone.Index) { + defer cachedTx.Release(true) // tx -1 + + // if the tx already contains recent (calculation index matches LSMI) + // information about yrtsi and ortsi, return that info + yrtsi, ortsi, rtsci := cachedTx.GetMetadata().GetRootSnapshotIndexes() + if rtsci == lsmi { + return yrtsi, ortsi + } + + snapshotInfo := tangle.GetSnapshotInfo() + + youngestTxRootSnapshotIndex = 0 + oldestTxRootSnapshotIndex = 0 + + updateIndexes := func(yrtsi milestone.Index, ortsi milestone.Index) { + if (youngestTxRootSnapshotIndex == 0) || (youngestTxRootSnapshotIndex < yrtsi) { + youngestTxRootSnapshotIndex = yrtsi + } + if (oldestTxRootSnapshotIndex == 0) || (oldestTxRootSnapshotIndex > ortsi) { + oldestTxRootSnapshotIndex = ortsi + } + } + + // collect all approvees in the cone that are not confirmed, + // are no solid entry points and have no recent calculation index + var outdatedTransactions hornet.Hashes + + startTxHash := cachedTx.GetMetadata().GetTxHash() + + // traverse the approvees of this transaction to calculate the root snapshot indexes for this transaction. + // this walk will also collect all outdated transactions in the same cone, to update them afterwards. + if err := TraverseApprovees(cachedTx.GetMetadata().GetTxHash(), + // traversal stops if no more transactions pass the given condition + func(cachedTx *tangle.CachedTransaction) (bool, error) { // tx +1 + defer cachedTx.Release(true) // tx -1 + + // first check if the tx was confirmed => update yrtsi and ortsi with the confirmation index + if confirmed, at := cachedTx.GetMetadata().GetConfirmed(); confirmed { + updateIndexes(at, at) + return false, nil + } + + if bytes.Equal(startTxHash, cachedTx.GetTransaction().GetTxHash()) { + // skip the start transaction, so it doesn't get added to the outdatedTransactions + return true, nil + } + + // if the tx was not confirmed yet, but already contains recent (calculation index matches LSMI) information + // about yrtsi and ortsi, propagate that info + yrtsi, ortsi, rtsci := cachedTx.GetMetadata().GetRootSnapshotIndexes() + if rtsci == lsmi { + updateIndexes(yrtsi, ortsi) + return false, nil + } + + outdatedTransactions = append(outdatedTransactions, cachedTx.GetTransaction().GetTxHash()) + + return true, nil + }, + // consumer + func(cachedTx *tangle.CachedTransaction) error { // tx +1 + defer cachedTx.Release(true) // tx -1 + return nil + }, + // called on missing approvees + func(approveeHash hornet.Hash) error { + return fmt.Errorf("missing approvee %v", approveeHash.Trytes()) + }, + // called on solid entry points + func(txHash hornet.Hash) { + updateIndexes(snapshotInfo.EntryPointIndex, snapshotInfo.EntryPointIndex) + }, true, false, nil); err != nil { + panic(err) + } + + // update the outdated root snapshot indexes of all transactions in the cone in order from oldest txs to latest. + // this is an efficient way to update the whole cone, because updating from oldest to latest will not be recursive. + UpdateOutdatedRootSnapshotIndexes(outdatedTransactions, lsmi) + + // set the new transaction root snapshot indexes in the metadata of the transaction + cachedTx.GetMetadata().SetRootSnapshotIndexes(youngestTxRootSnapshotIndex, oldestTxRootSnapshotIndex, lsmi) + + return youngestTxRootSnapshotIndex, oldestTxRootSnapshotIndex +} + +// UpdateTransactionRootSnapshotIndexes updates the transaction root snapshot +// indexes of the future cone of all given transactions. +// all the transactions of the newly confirmed cone already have updated transaction root snapshot indexes. +// we have to walk the future cone, and update the past cone of all transactions that reference an old cone. +// as a special property, invocations of the yielded function share the same 'already traversed' set to circumvent +// walking the future cone of the same transactions multiple times. +func UpdateTransactionRootSnapshotIndexes(txHashes hornet.Hashes, lsmi milestone.Index) { + traversed := map[string]struct{}{} + + // we update all transactions in order from oldest to latest + for _, txHash := range txHashes { + + if err := TraverseApprovers(txHash, + // traversal stops if no more transactions pass the given condition + func(cachedTx *tangle.CachedTransaction) (bool, error) { // tx +1 + defer cachedTx.Release(true) // tx -1 + _, previouslyTraversed := traversed[string(cachedTx.GetTransaction().GetTxHash())] + return !previouslyTraversed, nil + }, + // consumer + func(cachedTx *tangle.CachedTransaction) error { // tx +1 + defer cachedTx.Release(true) // tx -1 + traversed[string(cachedTx.GetTransaction().GetTxHash())] = struct{}{} + + // updates the transaction root snapshot indexes of the outdated past cone for this transaction + GetTransactionRootSnapshotIndexes(cachedTx.Retain(), lsmi) // tx pass +1 + + return nil + }, true, nil); err != nil { + panic(err) + } + } +} diff --git a/pkg/model/coordinator/coordinator.go b/pkg/model/coordinator/coordinator.go index 76709c6cf..ee43fb060 100644 --- a/pkg/model/coordinator/coordinator.go +++ b/pkg/model/coordinator/coordinator.go @@ -31,18 +31,18 @@ type Bundle = []*transaction.Transaction // SendBundleFunc is a function which sends a bundle to the network. type SendBundleFunc = func(b Bundle) error -// TipSelectionFunc is a function which performs a tipselection and returns two tips. -type TipSelectionFunc = func() (hornet.Hash, error) +// CheckpointTipSelectionFunc is a function which performs a tipselection and returns several tips for a checkpoint. +type CheckpointTipSelectionFunc = func(minRequiredTips int) (hornet.Hashes, error) var ( // ErrNetworkBootstrapped is returned when the flag for bootstrap network was given, but a state file already exists. ErrNetworkBootstrapped = errors.New("network already bootstrapped") ) -// coordinatorEvents are the events issued by the coordinator. -type coordinatorEvents struct { +// CoordinatorEvents are the events issued by the coordinator. +type CoordinatorEvents struct { // Fired when a checkpoint transaction is issued. - IssuedCheckpoint *events.Event + IssuedCheckpointTransaction *events.Event // Fired when a milestone is issued. IssuedMilestone *events.Event } @@ -58,24 +58,24 @@ type Coordinator struct { minWeightMagnitude int stateFilePath string milestoneIntervalSec int - checkpointTransactions int powFunc pow.ProofOfWorkFunc - tipselFunc TipSelectionFunc + checkpointTipselFunc CheckpointTipSelectionFunc sendBundleFunc SendBundleFunc milestoneMerkleHashFunc crypto.Hash // internal state state *State merkleTree *merkle.MerkleTree - lastCheckpointCount int - lastCheckpointHash *hornet.Hash + lastCheckpointIndex int + lastCheckpointHash hornet.Hash + lastMilestoneHash hornet.Hash bootstrapped bool // events of the coordinator - Events *coordinatorEvents + Events *CoordinatorEvents } -// Maps the passed name to one of the supported crypto.Hash hashing functions. +// MilestoneMerkleTreeHashFuncWithName maps the passed name to one of the supported crypto.Hash hashing functions. // Also verifies that the available function is available or else panics. func MilestoneMerkleTreeHashFuncWithName(name string) crypto.Hash { //TODO: golang 1.15 will include a String() method to get the string from the crypto.Hash, so we could iterate over them instead @@ -100,7 +100,7 @@ func MilestoneMerkleTreeHashFuncWithName(name string) crypto.Hash { } // New creates a new coordinator instance. -func New(seed trinary.Hash, securityLvl consts.SecurityLevel, merkleTreeDepth int, minWeightMagnitude int, stateFilePath string, milestoneIntervalSec int, checkpointTransactions int, powFunc pow.ProofOfWorkFunc, tipselFunc TipSelectionFunc, sendBundleFunc SendBundleFunc, milestoneMerkleHashFunc crypto.Hash) *Coordinator { +func New(seed trinary.Hash, securityLvl consts.SecurityLevel, merkleTreeDepth int, minWeightMagnitude int, stateFilePath string, milestoneIntervalSec int, powFunc pow.ProofOfWorkFunc, checkpointTipselFunc CheckpointTipSelectionFunc, sendBundleFunc SendBundleFunc, milestoneMerkleHashFunc crypto.Hash) *Coordinator { result := &Coordinator{ seed: seed, securityLvl: securityLvl, @@ -108,14 +108,13 @@ func New(seed trinary.Hash, securityLvl consts.SecurityLevel, merkleTreeDepth in minWeightMagnitude: minWeightMagnitude, stateFilePath: stateFilePath, milestoneIntervalSec: milestoneIntervalSec, - checkpointTransactions: checkpointTransactions, powFunc: powFunc, - tipselFunc: tipselFunc, + checkpointTipselFunc: checkpointTipselFunc, sendBundleFunc: sendBundleFunc, milestoneMerkleHashFunc: milestoneMerkleHashFunc, - Events: &coordinatorEvents{ - IssuedCheckpoint: events.NewEvent(CheckpointCaller), - IssuedMilestone: events.NewEvent(MilestoneCaller), + Events: &CoordinatorEvents{ + IssuedCheckpointTransaction: events.NewEvent(CheckpointCaller), + IssuedMilestone: events.NewEvent(MilestoneCaller), }, } @@ -186,7 +185,7 @@ func (coo *Coordinator) InitState(bootstrap bool, startIndex milestone.Index) er state.LatestMilestoneTransactions = hornet.Hashes{hornet.NullHashBytes} coo.state = state - coo.lastCheckpointHash = &(coo.state.LatestMilestoneHash) + coo.lastCheckpointHash = coo.state.LatestMilestoneHash coo.bootstrapped = false return nil } @@ -210,35 +209,53 @@ func (coo *Coordinator) InitState(bootstrap bool, startIndex milestone.Index) er } cachedBndl.Release() - coo.lastCheckpointHash = &(coo.state.LatestMilestoneHash) + coo.lastCheckpointHash = coo.state.LatestMilestoneHash + coo.lastMilestoneHash = coo.state.LatestMilestoneHash coo.bootstrapped = true return nil } -// issueCheckpoint sends a secret checkpoint transaction to the network. -// we do that to prevent parasite chain attacks. -// only honest tipselection will reference our checkpoints, so the milestone will reference honest tips. -func (coo *Coordinator) issueCheckpoint() error { +// issueCheckpointWithoutLocking tries to create and send a "checkpoint" to the network. +// a checkpoint can contain multiple chained transactions to reference big parts of the unconfirmed cone. +// this is done to keep the confirmation rate as high as possible, even if there is an attack ongoing. +// new checkpoints always reference the last checkpoint or the last milestone if it is the first checkpoint after a new milestone. +func (coo *Coordinator) issueCheckpointWithoutLocking(minRequiredTips int) error { - tip, err := coo.tipselFunc() - if err != nil { - return err + if !tangle.IsNodeSynced() { + return tangle.ErrNodeNotSynced } - b, err := createCheckpoint(tip, *coo.lastCheckpointHash, coo.minWeightMagnitude, coo.powFunc) + tips, err := coo.checkpointTipselFunc(minRequiredTips) if err != nil { return err } - if err := coo.sendBundleFunc(b); err != nil { - return err + var lastCheckpointHash hornet.Hash + if coo.lastCheckpointIndex == 0 { + // reference the last milestone + lastCheckpointHash = coo.lastMilestoneHash + } else { + // reference the last checkpoint + lastCheckpointHash = coo.lastCheckpointHash } - coo.lastCheckpointCount++ - lastCheckpointHash := hornet.HashFromHashTrytes(b[0].Hash) - coo.lastCheckpointHash = &lastCheckpointHash + for i, tip := range tips { + b, err := createCheckpoint(tip, lastCheckpointHash, coo.minWeightMagnitude, coo.powFunc) + if err != nil { + return err + } + + if err := coo.sendBundleFunc(b); err != nil { + return err + } - coo.Events.IssuedCheckpoint.Trigger(coo.lastCheckpointCount, coo.checkpointTransactions, lastCheckpointHash, tip) + lastCheckpointHash = hornet.HashFromHashTrytes(b[0].Hash) + + coo.Events.IssuedCheckpointTransaction.Trigger(coo.lastCheckpointIndex, i, len(tips), lastCheckpointHash) + } + + coo.lastCheckpointIndex++ + coo.lastCheckpointHash = lastCheckpointHash return nil } @@ -269,11 +286,11 @@ func (coo *Coordinator) createAndSendMilestone(trunkHash hornet.Hash, branchHash tailTx := b[0] // reset checkpoint count - coo.lastCheckpointCount = 0 + coo.lastCheckpointIndex = 0 - // always reference the last milestone directly to speed up syncing (or indirectly via checkpoints) + // always reference the last milestone directly to speed up syncing latestMilestoneHash := hornet.HashFromHashTrytes(tailTx.Hash) - coo.lastCheckpointHash = &latestMilestoneHash + coo.lastMilestoneHash = latestMilestoneHash coo.state.LatestMilestoneHash = latestMilestoneHash coo.state.LatestMilestoneIndex = newMilestoneIndex @@ -289,10 +306,9 @@ func (coo *Coordinator) createAndSendMilestone(trunkHash hornet.Hash, branchHash return nil } -// IssueNextCheckpointOrMilestone creates the next checkpoint or milestone. -// if the network was not bootstrapped yet, it creates the first milestone. -// Returns non-critical and critical errors. -func (coo *Coordinator) IssueNextCheckpointOrMilestone() (error, error) { +// Bootstrap creates the first milestone, if the network was not bootstrapped yet. +// Returns critical errors. +func (coo *Coordinator) Bootstrap() error { coo.milestoneLock.Lock() defer coo.milestoneLock.Unlock() @@ -301,29 +317,54 @@ func (coo *Coordinator) IssueNextCheckpointOrMilestone() (error, error) { // create first milestone to bootstrap the network if err := coo.createAndSendMilestone(hornet.Hash(hornet.NullHashBytes), hornet.Hash(hornet.NullHashBytes), coo.state.LatestMilestoneIndex); err != nil { // creating milestone failed => critical error - return nil, err + return err } coo.bootstrapped = true - return nil, nil } - if coo.lastCheckpointCount < coo.checkpointTransactions { - // issue a checkpoint - if err := coo.issueCheckpoint(); err != nil { - // issuing checkpoint failed => not critical - return err, nil - } - return nil, nil + return nil +} + +// IssueCheckpoint tries to create and send a "checkpoint" to the network. +// a checkpoint can contain multiple chained transactions to reference big parts of the unconfirmed cone. +// this is done to keep the confirmation rate as high as possible, even if there is an attack ongoing. +// new checkpoints always reference the last checkpoint or the last milestone if it is the first checkpoint after a new milestone. +func (coo *Coordinator) IssueCheckpoint() error { + + coo.milestoneLock.Lock() + defer coo.milestoneLock.Unlock() + + return coo.issueCheckpointWithoutLocking(0) +} + +// IssueMilestone creates the next milestone. +// a new checkpoint is created right in front of the milestone to raise confirmation rate. +// Returns non-critical and critical errors. +func (coo *Coordinator) IssueMilestone() (error, error) { + + coo.milestoneLock.Lock() + defer coo.milestoneLock.Unlock() + + if !tangle.IsNodeSynced() { + // return a non-critical error to not kill the database + return tangle.ErrNodeNotSynced, nil } - // issue new milestone - tip, err := coo.tipselFunc() - if err != nil { - // tipselection failed => not critical - return err, nil + lastCheckpointHash := coo.lastCheckpointHash + + // issue a new checkpoint right in front of the milestone + if err := coo.issueCheckpointWithoutLocking(1); err != nil { + // creating checkpoint failed => not critical + if coo.lastCheckpointIndex == 0 { + // no checkpoint created => use the last milestone hash + lastCheckpointHash = coo.lastMilestoneHash + } + } else { + // use the new checkpoint hash + lastCheckpointHash = coo.lastCheckpointHash } - if err := coo.createAndSendMilestone(tip, *coo.lastCheckpointHash, coo.state.LatestMilestoneIndex+1); err != nil { + if err := coo.createAndSendMilestone(coo.lastMilestoneHash, lastCheckpointHash, coo.state.LatestMilestoneIndex+1); err != nil { // creating milestone failed => critical error return nil, err } @@ -331,9 +372,9 @@ func (coo *Coordinator) IssueNextCheckpointOrMilestone() (error, error) { return nil, nil } -// GetInterval returns the interval milestones or checkpoints should be issued. +// GetInterval returns the interval milestones should be issued. func (coo *Coordinator) GetInterval() time.Duration { - return time.Second * time.Duration(coo.milestoneIntervalSec) / time.Duration(coo.checkpointTransactions+1) + return time.Second * time.Duration(coo.milestoneIntervalSec) } // State returns the current state of the coordinator. diff --git a/pkg/model/coordinator/events.go b/pkg/model/coordinator/events.go index 80160b43a..edbaed999 100644 --- a/pkg/model/coordinator/events.go +++ b/pkg/model/coordinator/events.go @@ -7,7 +7,7 @@ import ( // CheckpointCaller is used to signal issued checkpoints. func CheckpointCaller(handler interface{}, params ...interface{}) { - handler.(func(index int, lastIndex int, txHash hornet.Hash, tipHash hornet.Hash))(params[0].(int), params[1].(int), params[2].(hornet.Hash), params[3].(hornet.Hash)) + handler.(func(checkpointIndex int, tipIndex int, tipsTotal int, txHash hornet.Hash))(params[0].(int), params[1].(int), params[2].(int), params[3].(hornet.Hash)) } // MilestoneCaller is used to signal issued milestones. diff --git a/pkg/model/coordinator/state.go b/pkg/model/coordinator/state.go index 79599e554..a42950358 100644 --- a/pkg/model/coordinator/state.go +++ b/pkg/model/coordinator/state.go @@ -49,7 +49,7 @@ func (cs *State) MarshalBinary() (data []byte, err error) { return data, nil } -// Unmarshal parses the binary encoded representation of the coordinator state. +// UnmarshalBinary parses the binary encoded representation of the coordinator state. func (cs *State) UnmarshalBinary(data []byte) error { /* diff --git a/pkg/model/mselection/heaviest.go b/pkg/model/mselection/heaviest.go index 0a7f75b37..2595c4728 100644 --- a/pkg/model/mselection/heaviest.go +++ b/pkg/model/mselection/heaviest.go @@ -1,11 +1,9 @@ package mselection import ( - "bytes" "container/list" "context" "errors" - "fmt" "sync" "time" @@ -20,307 +18,269 @@ import ( "github.com/gohornet/hornet/pkg/utils" ) -// Errors during milestone selection -var ( - ErrWrongReference = errors.New("reference does not match root") +const ( + belowMaxDepth milestone.Index = 15 +) +var ( // ErrNoTipsAvailable is returned when no tips are available in the node. ErrNoTipsAvailable = errors.New("no tips available") ) -const ( - belowMaxDepth milestone.Index = 15 -) - // HeaviestSelector implements the heaviest branch selection strategy. type HeaviestSelector struct { sync.Mutex - approvers map[trinary.Hash]*item - tips *list.List + minHeaviestBranchUnconfirmedTransactionsThreshold int + maxHeaviestBranchTipsPerCheckpoint int + randomTipsPerCheckpoint int + heaviestBranchSelectionDeadline time.Duration + + trackedTails map[string]*bundleTail // map of all tracked bundle transaction tails + tips *list.List // list of available tips +} + +type bundleTail struct { + hash hornet.Hash // hash of the corresponding tail transaction + tip *list.Element // pointer to the element in the tip list + refs *bitset.BitSet // BitSet of all the referenced transactions +} + +type bundleTailList struct { + tails map[string]*bundleTail +} + +// Len returns the length of the inner tails slice. +func (il *bundleTailList) Len() int { + return len(il.tails) } -type item struct { - hash hornet.Hash // hash of the corresponding transaction - tip *list.Element // pointer to the element in the tip list - index uint // index of the transaction in the approvers list - refs *bitset.BitSet // BitSet of all the referenced transactions +// randomTip selects a random tip item from the bundleTailList. +func (il *bundleTailList) randomTip() (*bundleTail, error) { + if len(il.tails) == 0 { + return nil, ErrNoTipsAvailable + } + + randomTailIndex := utils.RandomInsecure(0, len(il.tails)-1) + + for _, tip := range il.tails { + randomTailIndex-- + + // if randomTailIndex reaches zero or below, we return the given tip + if randomTailIndex <= 0 { + return tip, nil + } + } + + return nil, ErrNoTipsAvailable +} + +// referenceTip removes the tip and set all bits of all referenced +// transactions of the tip in all existing tips to zero. +// this way we can track which parts of the cone would already be referenced by this tip, and +// correctly calculate the weight of the remaining tips. +func (il *bundleTailList) referenceTip(tip *bundleTail) { + + il.removeTip(tip) + + // set all bits of all referenced transactions in all existing tips to zero + for _, otherTip := range il.tails { + otherTip.refs.InPlaceDifference(tip.refs) + } +} + +// removeTip removes the tip from the map. +func (il *bundleTailList) removeTip(tip *bundleTail) { + delete(il.tails, string(tip.hash)) } // New creates a new HeaviestSelector instance. -func New() *HeaviestSelector { - s := &HeaviestSelector{} - s.Reset() +func New(minHeaviestBranchUnconfirmedTransactionsThreshold int, maxHeaviestBranchTipsPerCheckpoint int, randomTipsPerCheckpoint int, heaviestBranchSelectionDeadline time.Duration) *HeaviestSelector { + s := &HeaviestSelector{ + minHeaviestBranchUnconfirmedTransactionsThreshold: minHeaviestBranchUnconfirmedTransactionsThreshold, + maxHeaviestBranchTipsPerCheckpoint: maxHeaviestBranchTipsPerCheckpoint, + randomTipsPerCheckpoint: randomTipsPerCheckpoint, + heaviestBranchSelectionDeadline: heaviestBranchSelectionDeadline, + } + s.reset() return s } -// Reset resets the approvers and tips list of s. -func (s *HeaviestSelector) Reset() { +// reset resets the tracked transactions map and tips list of s. +func (s *HeaviestSelector) reset() { s.Lock() defer s.Unlock() // create an empty map - s.approvers = make(map[trinary.Hash]*item) + s.trackedTails = make(map[trinary.Hash]*bundleTail) // create an empty list s.tips = list.New() } -// ResetCone set all bits of all referenced transactions of the tip in all existing tips to zero. -func (s *HeaviestSelector) ResetCone(tipHash hornet.Hash) error { - s.Lock() - defer s.Unlock() +// selectTip selects a tip to be used for the next checkpoint. +// it returns a tip, confirming the most transactions in the future cone, +// and the amount of referenced transactions of this tip, that were not referenced by previously chosen tips. +func (s *HeaviestSelector) selectTip(tipsList *bundleTailList) (*bundleTail, uint, error) { - choosenTip, exists := s.approvers[string(tipHash)] - if !exists { - return ErrWrongReference + if tipsList.Len() == 0 { + return nil, 0, ErrNoTipsAvailable } - // remove the used tip from the tips list - s.removeTip(choosenTip) + var best = struct { + tips []*bundleTail + count uint + }{ + tips: []*bundleTail{}, + count: 0, + } - // set all bits of all referenced transactions in all existing tips to zero - for e := s.tips.Front(); e != nil; e = e.Next() { - e.Value.(*item).refs.InPlaceDifference(choosenTip.refs) + // loop through all tips and find the one with the most referenced transactions + for _, tip := range tipsList.tails { + c := tip.refs.Count() + if c > best.count { + // tip with heavier branch found + best.tips = []*bundleTail{ + tip, + } + best.count = c + } else if c == best.count { + // add the tip to the slice of currently best tips + best.tips = append(best.tips, tip) + } } - return nil + if len(best.tips) == 0 { + return nil, 0, ErrNoTipsAvailable + } + + // select a random tip from the provided slice of tips. + selected := best.tips[utils.RandomInsecure(0, len(best.tips)-1)] + + return selected, best.count, nil } -// selectTip selects a tip to be used for the next milestone. -// It returns a tip, confirming the most transactions in the future cone. -// The selection can be cancelled anytime via the provided context. In this case, it returns the current best solution. -// selectTip be called concurrently with other HeaviestSelector methods. However, it only considers the tips -// that were present at the beginning of the selectTip call. -// TODO: add a proper interface for ms selection that is used by the coordinator -func (s *HeaviestSelector) selectTip(ctx context.Context) (hornet.Hash, error) { - // copy the tips to release the lock at allow faster iteration - tips := s.tipItems() +// SelectTips tries to collect tips that confirm the most recent transactions since the last reset of the selector. +// best tips are determined by counting the referenced transactions (heaviest branches) and by "removing" the +// transactions of the referenced cone of the already choosen tips in the bitsets of the available tips. +// only tips are considered that were present at the beginning of the SelectTips call, +// to prevent attackers from creating heavier branches while we are searching the best tips. +// "maxHeaviestBranchTipsPerCheckpoint" is the amount of tips that are collected if +// the current best tip is not below "UnconfirmedTransactionsThreshold" before. +// a minimum amount of selected tips can be enforced, even if none of the heaviest branches matches the +// "minHeaviestBranchUnconfirmedTransactionsThreshold" criteria. +// if at least one heaviest branch tip was found, "randomTipsPerCheckpoint" random tips are added +// to add some additional randomness to prevent parasite chain attacks. +// the selection is cancelled after a fixed deadline. in this case, it returns the current collected tips. +func (s *HeaviestSelector) SelectTips(minRequiredTips int) (hornet.Hashes, error) { + + // create a working list with the current tips to release the lock to allow faster iteration + // and to get a frozen view of the tangle, so an attacker can't + // create heavier branches while we are searching the best tips + // caution: the tips are not copied, do not mutate! + tipsList := s.tipsToList() // tips could be empty after a reset - if len(tips) == 0 { + if tipsList.Len() == 0 { return nil, ErrNoTipsAvailable } - lastTip := tips[len(tips)-1] + var result hornet.Hashes - var best = struct { - tips hornet.Hashes - count uint - }{ - tips: hornet.Hashes{lastTip.hash}, - count: lastTip.refs.Count(), - } + // run the tip selection for at most 0.1s to keep the view on the tangle recent; this should be plenty + ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(s.heaviestBranchSelectionDeadline)) + defer cancel() - // loop through all tips and find the one with the most referenced transactions - for _, tip := range tips { - // when the context has been cancelled, return the current best with an error + deadlineExceeded := false + + for i := 0; i < s.maxHeaviestBranchTipsPerCheckpoint; i++ { + // when the context has been cancelled, stop collecting heaviest branch tips select { case <-ctx.Done(): - return randomTip(best.tips), ctx.Err() + deadlineExceeded = true default: } - c := tip.refs.Count() - if c > best.count { - best.tips = hornet.Hashes{tip.hash} - best.count = c - } else if c == best.count { - best.tips = append(best.tips, tip.hash) + tip, count, err := s.selectTip(tipsList) + if err != nil { + break } + + if (len(result) > minRequiredTips) && ((count < uint(s.minHeaviestBranchUnconfirmedTransactionsThreshold)) || deadlineExceeded) { + // minimum amount of tips reached and the heaviest tips do not confirm enough transactions or the deadline was exceeded + // => no need to collect more + break + } + + tipsList.referenceTip(tip) + result = append(result, tip.hash) } - // TODO: is it really to select a random tip? Maybe prefer the older (or younger) tip instead - return randomTip(best.tips), nil -} + if len(result) == 0 { + return nil, ErrNoTipsAvailable + } -// SelectTip selects a tip to be used for the next milestone. -func (s *HeaviestSelector) SelectTip() (hornet.Hash, error) { + // also pick random tips if at least one heaviest branch tip was found + for i := 0; i < s.randomTipsPerCheckpoint; i++ { + item, err := tipsList.randomTip() + if err != nil { + break + } - if !tangle.IsNodeSynced() { - return nil, tangle.ErrNodeNotSynced + tipsList.referenceTip(item) + result = append(result, item.hash) } - // run the tip selection for at most 0.1s to keep the view on the tangle recent; this should be plenty - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(100*time.Millisecond)) - defer cancel() - return s.selectTip(ctx) + // reset the whole HeaviestSelector if valid tips were found + s.reset() + + return result, nil } // OnNewSolidBundle adds a new bundle to be processed by s. // The bundle must be solid and OnNewSolidBundle must be called in the order of solidification. // We also have to check if the bundle is below max depth. -func (s *HeaviestSelector) OnNewSolidBundle(bndl *tangle.Bundle) { +func (s *HeaviestSelector) OnNewSolidBundle(bndl *tangle.Bundle) (trackedTailsCount int) { s.Lock() defer s.Unlock() // filter duplicate transaction - if _, contains := s.approvers[string(bndl.GetTailHash())]; contains { + if _, contains := s.trackedTails[string(bndl.GetTailHash())]; contains { return } - trunkHash := bndl.GetTrunk(true) - branchHash := bndl.GetBranch(true) - - trunkItem := s.approvers[string(trunkHash)] - branchItem := s.approvers[string(branchHash)] - - approveeHashes := make(map[string]struct{}) - if trunkItem == nil { - approveeHashes[string(trunkHash)] = struct{}{} - } - - if branchItem == nil { - approveeHashes[string(branchHash)] = struct{}{} - } - - // we have to check the below max depth criteria for approvees that do not reference our future cone. - // if all the unknown approvees do not fail the below max depth criteria, the tip is valid - if len(approveeHashes) > 0 { - lsmi := tangle.GetSolidMilestoneIndex() - - for approveeHash := range approveeHashes { - var approveeORTSI milestone.Index - - if tangle.SolidEntryPointsContain(hornet.Hash(approveeHash)) { - // if the approvee is an solid entry point, use the EntryPointIndex as ORTSI - approveeORTSI = tangle.GetSnapshotInfo().EntryPointIndex - } else { - cachedApproveeTx := tangle.GetCachedTransactionOrNil(hornet.Hash(approveeHash)) // tx +1 - if cachedApproveeTx == nil { - panic(fmt.Sprintf("transaction not found: %v", hornet.Hash(approveeHash).Trytes())) - } - - _, approveeORTSI = s.getTransactionRootSnapshotIndexes(cachedApproveeTx.Retain(), lsmi) // tx +1 - cachedApproveeTx.Release(true) - } - - // if the confirmationIdx to LSMI delta of the approvee is equal or greater belowMaxDepth, the tip is invalid. - // "equal" is important because the next milestone would reference this transaction. - if lsmi-approveeORTSI >= belowMaxDepth { - return - } - } + // we ignore bundles that are below max depth. + if isBelowMaxDepth(bndl.GetTail()) { + return s.GetTrackedTailsCount() } - // TODO: when len(s.approvers) gets too large trigger a checkpoint to prevent drastic performance hits + trunkItem := s.trackedTails[string(bndl.GetTrunk(true))] + branchItem := s.trackedTails[string(bndl.GetBranch(true))] // compute the referenced transactions - idx := uint(len(s.approvers)) - it := &item{hash: bndl.GetTailHash(), index: idx, refs: bitset.New(idx + 1).Set(idx)} + // all the known approvers in the HeaviestSelector are represented by a unique bit in a bitset. + // if a new approver is added, we expand the bitset by 1 bit and store the Union of the bitsets + // of trunk and branch for this approver, to know which parts of the cone are referenced by this approver. + idx := uint(len(s.trackedTails)) + it := &bundleTail{hash: bndl.GetTailHash(), refs: bitset.New(idx + 1).Set(idx)} if trunkItem != nil { it.refs.InPlaceUnion(trunkItem.refs) } if branchItem != nil { it.refs.InPlaceUnion(branchItem.refs) } - s.approvers[string(it.hash)] = it + s.trackedTails[string(it.hash)] = it // update tips s.removeTip(trunkItem) s.removeTip(branchItem) it.tip = s.tips.PushBack(it) -} - -func (s *HeaviestSelector) updateOutdatedRootSnapshotIndexes(outdatedTransactions hornet.Hashes, lsmi milestone.Index) { - for i := len(outdatedTransactions) - 1; i >= 0; i-- { - outdatedTxHash := outdatedTransactions[i] - - cachedTx := tangle.GetCachedTransactionOrNil(outdatedTxHash) - if cachedTx == nil { - panic(tangle.ErrTransactionNotFound) - } - s.getTransactionRootSnapshotIndexes(cachedTx, lsmi) - } -} - -// getTransactionRootSnapshotIndexes searches the root snapshot indexes for a given transaction. -func (s *HeaviestSelector) getTransactionRootSnapshotIndexes(cachedTx *tangle.CachedTransaction, lsmi milestone.Index) (youngestTxRootSnapshotIndex milestone.Index, oldestTxRootSnapshotIndex milestone.Index) { - defer cachedTx.Release(true) // tx -1 - - // if the tx already contains recent (calculation index matches LSMI) - // information about yrtsi and ortsi, return that info - yrtsi, ortsi, rtsci := cachedTx.GetMetadata().GetRootSnapshotIndexes() - if rtsci == lsmi { - return yrtsi, ortsi - } - - snapshotInfo := tangle.GetSnapshotInfo() - - youngestTxRootSnapshotIndex = 0 - oldestTxRootSnapshotIndex = 0 - - updateIndexes := func(yrtsi milestone.Index, ortsi milestone.Index) { - if (youngestTxRootSnapshotIndex == 0) || (youngestTxRootSnapshotIndex < yrtsi) { - youngestTxRootSnapshotIndex = yrtsi - } - if (oldestTxRootSnapshotIndex == 0) || (oldestTxRootSnapshotIndex > ortsi) { - oldestTxRootSnapshotIndex = ortsi - } - } - - // collect all approvees in the cone that are not confirmed, - // are no solid entry points and have no recent calculation index - var outdatedTransactions hornet.Hashes - - startTxHash := cachedTx.GetMetadata().GetTxHash() - - // traverse the approvees of this transaction to calculate the root snapshot indexes for this transaction. - // this walk will also collect all outdated transactions in the same cone, to update them afterwards. - if err := dag.TraverseApprovees(cachedTx.GetMetadata().GetTxHash(), - // traversal stops if no more transactions pass the given condition - func(cachedTx *tangle.CachedTransaction) (bool, error) { // tx +1 - defer cachedTx.Release(true) // tx -1 - - // first check if the tx was confirmed => update yrtsi and ortsi with the confirmation index - if confirmed, at := cachedTx.GetMetadata().GetConfirmed(); confirmed { - updateIndexes(at, at) - return false, nil - } - if bytes.Equal(startTxHash, cachedTx.GetTransaction().GetTxHash()) { - // skip the start transaction, so it doesn't get added to the outdatedTransactions - return true, nil - } - - // if the tx was not confirmed yet, but already contains recent (calculation index matches LSMI) information - // about yrtsi and ortsi, propagate that info - yrtsi, ortsi, rtsci := cachedTx.GetMetadata().GetRootSnapshotIndexes() - if rtsci == lsmi { - updateIndexes(yrtsi, ortsi) - return false, nil - } - - outdatedTransactions = append(outdatedTransactions, cachedTx.GetTransaction().GetTxHash()) - - return true, nil - }, - // consumer - func(cachedTx *tangle.CachedTransaction) error { // tx +1 - defer cachedTx.Release(true) // tx -1 - return nil - }, - // called on missing approvees - func(approveeHash hornet.Hash) error { - return fmt.Errorf("missing approvee %v", approveeHash.Trytes()) - }, - // called on solid entry points - func(txHash hornet.Hash) { - updateIndexes(snapshotInfo.EntryPointIndex, snapshotInfo.EntryPointIndex) - }, true, false, nil); err != nil { - panic(err) - } - - // update the outdated root snapshot indexes of all transactions in the cone in order from oldest txs to latest - s.updateOutdatedRootSnapshotIndexes(outdatedTransactions, lsmi) - - // set the new transaction root snapshot indexes in the metadata of the transaction - cachedTx.GetMetadata().SetRootSnapshotIndexes(youngestTxRootSnapshotIndex, oldestTxRootSnapshotIndex, lsmi) - - return youngestTxRootSnapshotIndex, oldestTxRootSnapshotIndex + return s.GetTrackedTailsCount() } -func (s *HeaviestSelector) removeTip(it *item) { +// removeTip removes the tip item from s. +func (s *HeaviestSelector) removeTip(it *bundleTail) { if it == nil || it.tip == nil { return } @@ -328,22 +288,33 @@ func (s *HeaviestSelector) removeTip(it *item) { it.tip = nil } -// tipItems returns a copy of the items corresponding to tips. -func (s *HeaviestSelector) tipItems() []*item { +// tipsToList returns a new list containing the current tips. +func (s *HeaviestSelector) tipsToList() *bundleTailList { s.Lock() defer s.Unlock() - result := make([]*item, 0, s.tips.Len()) + result := make(map[string]*bundleTail) for e := s.tips.Front(); e != nil; e = e.Next() { - result = append(result, e.Value.(*item)) + tip := e.Value.(*bundleTail) + result[string(tip.hash)] = tip } - return result + return &bundleTailList{tails: result} } -// randomTip selects a random tip from the provided slice of tips. -func randomTip(tips hornet.Hashes) hornet.Hash { - if len(tips) == 0 { - panic("empty tips") - } - return tips[utils.RandomInsecure(0, len(tips)-1)] +// GetTrackedTailsCount returns the amount of known bundle tails. +func (s *HeaviestSelector) GetTrackedTailsCount() (trackedTails int) { + return len(s.trackedTails) +} + +// isBelowMaxDepth checks the below max depth criteria for the given tail transaction. +func isBelowMaxDepth(cachedTailTx *tangle.CachedTransaction) bool { + defer cachedTailTx.Release(true) + + lsmi := tangle.GetSolidMilestoneIndex() + + _, ortsi := dag.GetTransactionRootSnapshotIndexes(cachedTailTx.Retain(), lsmi) // tx +1 + + // if the ORTSI to LSMI delta of the tail transaction is equal or greater belowMaxDepth, the tip is invalid. + // "equal" is important because the next milestone would reference this transaction. + return lsmi-ortsi >= belowMaxDepth } diff --git a/pkg/tipselect/urts.go b/pkg/tipselect/urts.go index f93e274c0..c831b93df 100644 --- a/pkg/tipselect/urts.go +++ b/pkg/tipselect/urts.go @@ -140,7 +140,7 @@ func (ts *TipSelector) AddTip(tailTxHash hornet.Hash) { score := ts.calculateScore(tailTxHash, lsmi) if score == ScoreLazy { // do not add lazy tips. - // lazy tips should also not remove other tips from the pool. + // lazy tips should also not remove other tips from the pool, otherwise the tip pool will run empty. return } @@ -293,104 +293,6 @@ func (ts *TipSelector) SelectTips() (hornet.Hashes, error) { return tips, nil } -func (ts *TipSelector) updateOutdatedRootSnapshotIndexes(outdatedTransactions hornet.Hashes, lsmi milestone.Index) { - for i := len(outdatedTransactions) - 1; i >= 0; i-- { - outdatedTxHash := outdatedTransactions[i] - - cachedTx := tangle.GetCachedTransactionOrNil(outdatedTxHash) - if cachedTx == nil { - panic(tangle.ErrTransactionNotFound) - } - ts.getTransactionRootSnapshotIndexes(cachedTx, lsmi) - } -} - -// getTransactionRootSnapshotIndexes searches the root snapshot indexes for a given transaction. -func (ts *TipSelector) getTransactionRootSnapshotIndexes(cachedTx *tangle.CachedTransaction, lsmi milestone.Index) (youngestTxRootSnapshotIndex milestone.Index, oldestTxRootSnapshotIndex milestone.Index) { - defer cachedTx.Release(true) // tx -1 - - // if the tx already contains recent (calculation index matches LSMI) - // information about yrtsi and ortsi, return that info - yrtsi, ortsi, rtsci := cachedTx.GetMetadata().GetRootSnapshotIndexes() - if rtsci == lsmi { - return yrtsi, ortsi - } - - snapshotInfo := tangle.GetSnapshotInfo() - - youngestTxRootSnapshotIndex = 0 - oldestTxRootSnapshotIndex = 0 - - updateIndexes := func(yrtsi milestone.Index, ortsi milestone.Index) { - if (youngestTxRootSnapshotIndex == 0) || (youngestTxRootSnapshotIndex < yrtsi) { - youngestTxRootSnapshotIndex = yrtsi - } - if (oldestTxRootSnapshotIndex == 0) || (oldestTxRootSnapshotIndex > ortsi) { - oldestTxRootSnapshotIndex = ortsi - } - } - - // collect all approvees in the cone that are not confirmed, - // are no solid entry points and have no recent calculation index - var outdatedTransactions hornet.Hashes - - startTxHash := cachedTx.GetMetadata().GetTxHash() - - // traverse the approvees of this transaction to calculate the root snapshot indexes for this transaction. - // this walk will also collect all outdated transactions in the same cone, to update them afterwards. - if err := dag.TraverseApprovees(cachedTx.GetMetadata().GetTxHash(), - // traversal stops if no more transactions pass the given condition - func(cachedTx *tangle.CachedTransaction) (bool, error) { // tx +1 - defer cachedTx.Release(true) // tx -1 - - // first check if the tx was confirmed => update yrtsi and ortsi with the confirmation index - if confirmed, at := cachedTx.GetMetadata().GetConfirmed(); confirmed { - updateIndexes(at, at) - return false, nil - } - - if bytes.Equal(startTxHash, cachedTx.GetTransaction().GetTxHash()) { - // skip the start transaction, so it doesn't get added to the outdatedTransactions - return true, nil - } - - // if the tx was not confirmed yet, but already contains recent (calculation index matches LSMI) information - // about yrtsi and ortsi, propagate that info - yrtsi, ortsi, rtsci := cachedTx.GetMetadata().GetRootSnapshotIndexes() - if rtsci == lsmi { - updateIndexes(yrtsi, ortsi) - return false, nil - } - - outdatedTransactions = append(outdatedTransactions, cachedTx.GetTransaction().GetTxHash()) - - return true, nil - }, - // consumer - func(cachedTx *tangle.CachedTransaction) error { // tx +1 - defer cachedTx.Release(true) // tx -1 - return nil - }, - // called on missing approvees - func(approveeHash hornet.Hash) error { - return fmt.Errorf("missing approvee %v", approveeHash.Trytes()) - }, - // called on solid entry points - func(txHash hornet.Hash) { - updateIndexes(snapshotInfo.EntryPointIndex, snapshotInfo.EntryPointIndex) - }, true, false, nil); err != nil { - panic(err) - } - - // update the outdated root snapshot indexes of all transactions in the cone in order from oldest txs to latest - ts.updateOutdatedRootSnapshotIndexes(outdatedTransactions, lsmi) - - // set the new transaction root snapshot indexes in the metadata of the transaction - cachedTx.GetMetadata().SetRootSnapshotIndexes(youngestTxRootSnapshotIndex, oldestTxRootSnapshotIndex, lsmi) - - return youngestTxRootSnapshotIndex, oldestTxRootSnapshotIndex -} - // calculateScore calculates the tip selection score of this transaction func (ts *TipSelector) calculateScore(txHash hornet.Hash, lsmi milestone.Index) Score { cachedTx := tangle.GetCachedTransactionOrNil(txHash) // tx +1 @@ -399,7 +301,7 @@ func (ts *TipSelector) calculateScore(txHash hornet.Hash, lsmi milestone.Index) } defer cachedTx.Release(true) - ytrsi, ortsi := ts.getTransactionRootSnapshotIndexes(cachedTx.Retain(), lsmi) // tx +1 + ytrsi, ortsi := dag.GetTransactionRootSnapshotIndexes(cachedTx.Retain(), lsmi) // tx +1 // if the LSMI to YTRSI delta is over MaxDeltaTxYoungestRootSnapshotIndexToLSMI, then the tip is lazy if (lsmi - ytrsi) > ts.maxDeltaTxYoungestRootSnapshotIndexToLSMI { @@ -430,7 +332,7 @@ func (ts *TipSelector) calculateScore(txHash hornet.Hash, lsmi milestone.Index) panic(fmt.Sprintf("transaction not found: %v", hornet.Hash(approveeHash).Trytes())) } - _, approveeORTSI = ts.getTransactionRootSnapshotIndexes(cachedApproveeTx.Retain(), lsmi) // tx +1 + _, approveeORTSI = dag.GetTransactionRootSnapshotIndexes(cachedApproveeTx.Retain(), lsmi) // tx +1 cachedApproveeTx.Release(true) } @@ -452,37 +354,3 @@ func (ts *TipSelector) calculateScore(txHash hornet.Hash, lsmi milestone.Index) return ScoreNonLazy } - -// UpdateTransactionRootSnapshotIndexes updates the transaction root snapshot -// indexes of the future cone of all given transactions. -// all the transactions of the newly confirmed cone already have updated transaction root snapshot indexes. -// we have to walk the future cone, and update the past cone of all transactions that reference an old cone. -// as a special property, invocations of the yielded function share the same 'already traversed' set to circumvent -// walking the future cone of the same transactions multiple times. -func (ts *TipSelector) UpdateTransactionRootSnapshotIndexes(txHashes hornet.Hashes, lsmi milestone.Index) { - traversed := map[string]struct{}{} - - // we update all transactions in order from oldest to latest - for _, txHash := range txHashes { - - if err := dag.TraverseApprovers(txHash, - // traversal stops if no more transactions pass the given condition - func(cachedTx *tangle.CachedTransaction) (bool, error) { // tx +1 - defer cachedTx.Release(true) // tx -1 - _, previouslyTraversed := traversed[string(cachedTx.GetTransaction().GetTxHash())] - return !previouslyTraversed, nil - }, - // consumer - func(cachedTx *tangle.CachedTransaction) error { // tx +1 - defer cachedTx.Release(true) // tx -1 - traversed[string(cachedTx.GetTransaction().GetTxHash())] = struct{}{} - - // updates the transaction root snapshot indexes of the outdated past cone for this transaction - ts.getTransactionRootSnapshotIndexes(cachedTx.Retain(), lsmi) // tx pass +1 - - return nil - }, true, nil); err != nil { - panic(err) - } - } -} diff --git a/pkg/whiteflag/white_flag.go b/pkg/whiteflag/white_flag.go index afc548c9a..e193938f4 100644 --- a/pkg/whiteflag/white_flag.go +++ b/pkg/whiteflag/white_flag.go @@ -290,7 +290,7 @@ func ProcessStack(stack *list.List, wfConf *Confirmation, visited map[string]str // incorporate the mutations in accordance with the previous mutations for addr, mutation := range validMutations { - wfConf.AddressMutations[addr] = mutation + wfConf.AddressMutations[addr] = wfConf.AddressMutations[addr] + mutation } return nil diff --git a/plugins/coordinator/plugin.go b/plugins/coordinator/plugin.go index e3ce6ecae..a3a47648e 100644 --- a/plugins/coordinator/plugin.go +++ b/plugins/coordinator/plugin.go @@ -2,6 +2,7 @@ package coordinator import ( "sync" + "time" "github.com/spf13/pflag" @@ -16,14 +17,16 @@ import ( "github.com/iotaledger/iota.go/transaction" "github.com/gohornet/hornet/pkg/config" + "github.com/gohornet/hornet/pkg/dag" "github.com/gohornet/hornet/pkg/model/coordinator" "github.com/gohornet/hornet/pkg/model/hornet" "github.com/gohornet/hornet/pkg/model/milestone" "github.com/gohornet/hornet/pkg/model/mselection" "github.com/gohornet/hornet/pkg/model/tangle" "github.com/gohornet/hornet/pkg/shutdown" + "github.com/gohornet/hornet/pkg/whiteflag" "github.com/gohornet/hornet/plugins/gossip" - tanglePlugin "github.com/gohornet/hornet/plugins/tangle" + tangleplugin "github.com/gohornet/hornet/plugins/tangle" ) func init() { @@ -38,18 +41,26 @@ var ( bootstrap = pflag.Bool("cooBootstrap", false, "bootstrap the network") startIndex = pflag.Uint32("cooStartIndex", 0, "index of the first milestone at bootstrap") + maxTrackedTails int + + nextCheckpointSignal chan struct{} + nextMilestoneSignal chan struct{} + coo *coordinator.Coordinator selector *mselection.HeaviestSelector + + // Closures + onBundleSolid *events.Closure + onMilestoneConfirmed *events.Closure + onIssuedCheckpointTransaction *events.Closure + onIssuedMilestone *events.Closure ) func configure(plugin *node.Plugin) { log = logger.NewLogger(plugin.Name) // set the node as synced at startup, so the coo plugin can select tips - tanglePlugin.SetUpdateSyncedAtStartup(true) - - // use the heaviest pair tip selection for the milestones - selector = mselection.New() + tangleplugin.SetUpdateSyncedAtStartup(true) var err error coo, err = initCoordinator(*bootstrap, *startIndex) @@ -57,15 +68,7 @@ func configure(plugin *node.Plugin) { log.Panic(err) } - coo.Events.IssuedCheckpoint.Attach(events.NewClosure(func(index int, lastIndex int, txHash hornet.Hash, tipHash hornet.Hash) { - log.Infof("checkpoint issued (%d/%d): %v", index, lastIndex, txHash.Trytes()) - selector.ResetCone(tipHash) - })) - - coo.Events.IssuedMilestone.Attach(events.NewClosure(func(index milestone.Index, tailTxHash hornet.Hash) { - log.Infof("milestone issued (%d): %v", index, tailTxHash.Trytes()) - selector.Reset() - })) + configureEvents() } func initCoordinator(bootstrap bool, startIndex uint32) (*coordinator.Coordinator, error) { @@ -75,8 +78,24 @@ func initCoordinator(bootstrap bool, startIndex uint32) (*coordinator.Coordinato return nil, err } + // use the heaviest branch tip selection for the milestones + selector = mselection.New( + config.NodeConfig.GetInt(config.CfgCoordinatorTipselectMinHeaviestBranchUnconfirmedTransactionsThreshold), + config.NodeConfig.GetInt(config.CfgCoordinatorTipselectMaxHeaviestBranchTipsPerCheckpoint), + config.NodeConfig.GetInt(config.CfgCoordinatorTipselectRandomTipsPerCheckpoint), + time.Duration(config.NodeConfig.GetInt(config.CfgCoordinatorTipselectHeaviestBranchSelectionDeadlineMilliseconds))*time.Millisecond, + ) + _, powFunc := pow.GetFastestProofOfWorkImpl() + nextCheckpointSignal = make(chan struct{}) + + // must be a buffered channel, otherwise signal gets + // lost if checkpoint is generated at the same time + nextMilestoneSignal = make(chan struct{}, 1) + + maxTrackedTails = config.NodeConfig.GetInt(config.CfgCoordinatorCheckpointsMaxTrackedTails) + coo := coordinator.New( seed, consts.SecurityLevel(config.NodeConfig.GetInt(config.CfgCoordinatorSecurityLevel)), @@ -84,9 +103,8 @@ func initCoordinator(bootstrap bool, startIndex uint32) (*coordinator.Coordinato config.NodeConfig.GetInt(config.CfgCoordinatorMWM), config.NodeConfig.GetString(config.CfgCoordinatorStateFilePath), config.NodeConfig.GetInt(config.CfgCoordinatorIntervalSeconds), - config.NodeConfig.GetInt(config.CfgCoordinatorCheckpointTransactions), powFunc, - selector.SelectTip, + selector.SelectTips, sendBundle, coordinator.MilestoneMerkleTreeHashFuncWithName(config.NodeConfig.GetString(config.CfgCoordinatorMilestoneMerkleTreeHashFunc)), ) @@ -99,42 +117,68 @@ func initCoordinator(bootstrap bool, startIndex uint32) (*coordinator.Coordinato return nil, err } - // initialize the selector - selector.Reset() - return coo, nil } func run(plugin *node.Plugin) { - // pass all new solid bundles to the selector - onBundleSolid := events.NewClosure(func(cachedBundle *tangle.CachedBundle) { - cachedBundle.ConsumeBundle(func(bndl *tangle.Bundle) { // bundle -1 - if bndl.IsInvalidPastCone() || !bndl.IsValid() || !bndl.ValidStrictSemantics() { - // ignore invalid bundles or semantically invalid bundles or bundles with invalid past cone - return + // create a background worker that signals to issue new milestones + daemon.BackgroundWorker("Coordinator[MilestoneTicker]", func(shutdownSignal <-chan struct{}) { + + timeutil.Ticker(func() { + // issue next milestone + select { + case nextMilestoneSignal <- struct{}{}: + default: + // do not block if already another signal is waiting } + }, coo.GetInterval(), shutdownSignal) - selector.OnNewSolidBundle(bndl) - }) - }) + }, shutdown.PriorityCoordinator) // create a background worker that issues milestones daemon.BackgroundWorker("Coordinator", func(shutdownSignal <-chan struct{}) { - tanglePlugin.Events.BundleSolid.Attach(onBundleSolid) - defer tanglePlugin.Events.BundleSolid.Detach(onBundleSolid) + attachEvents() - // TODO: add some random jitter to make the ms intervals not predictable - timeutil.Ticker(func() { - err, criticalErr := coo.IssueNextCheckpointOrMilestone() - if criticalErr != nil { - log.Panic(criticalErr) - } - if err != nil { - log.Warn(err) + // bootstrap the network if not done yet + if criticalErr := coo.Bootstrap(); criticalErr != nil { + log.Panic(criticalErr) + } + + coordinatorLoop: + for { + select { + case <-nextCheckpointSignal: + // check the thresholds again, because a new milestone could have been issued in the meantime + if trackedTailsCount := selector.GetTrackedTailsCount(); trackedTailsCount < maxTrackedTails { + continue + } + + // issue a checkpoint + if err := coo.IssueCheckpoint(); err != nil { + // issuing checkpoint failed => not critical + if err != mselection.ErrNoTipsAvailable { + log.Warn(err) + } + } + + case <-nextMilestoneSignal: + err, criticalErr := coo.IssueMilestone() + if criticalErr != nil { + log.Panic(criticalErr) + } + if err != nil { + log.Warn(err) + } + + case <-shutdownSignal: + break coordinatorLoop } - }, coo.GetInterval(), shutdownSignal) + } + + detachEvents() }, shutdown.PriorityCoordinator) + } func sendBundle(b coordinator.Bundle) error { @@ -162,8 +206,8 @@ func sendBundle(b coordinator.Bundle) error { } }) - tanglePlugin.Events.ProcessedTransaction.Attach(processedTxEvent) - defer tanglePlugin.Events.ProcessedTransaction.Detach(processedTxEvent) + tangleplugin.Events.ProcessedTransaction.Attach(processedTxEvent) + defer tangleplugin.Events.ProcessedTransaction.Detach(processedTxEvent) for _, t := range b { tx := t // assign to new variable, otherwise it would be overwritten by the loop before processed @@ -178,3 +222,65 @@ func sendBundle(b coordinator.Bundle) error { return nil } + +// GetEvents returns the events of the coordinator +func GetEvents() *coordinator.CoordinatorEvents { + if coo == nil { + return nil + } + return coo.Events +} + +func configureEvents() { + // pass all new solid bundles to the selector + onBundleSolid = events.NewClosure(func(cachedBundle *tangle.CachedBundle) { + cachedBundle.ConsumeBundle(func(bndl *tangle.Bundle) { // bundle -1 + + if bndl.IsInvalidPastCone() || !bndl.IsValid() || !bndl.ValidStrictSemantics() { + // ignore invalid bundles or semantically invalid bundles or bundles with invalid past cone + return + } + + if trackedTailsCount := selector.OnNewSolidBundle(bndl); trackedTailsCount >= maxTrackedTails { + log.Debugf("Coordinator Tipselector: trackedTailsCount: %d", trackedTailsCount) + + // issue next checkpoint + select { + case nextCheckpointSignal <- struct{}{}: + default: + // do not block if already another signal is waiting + } + } + }) + }) + + onMilestoneConfirmed = events.NewClosure(func(confirmation *whiteflag.Confirmation) { + ts := time.Now() + + // propagate new transaction root snapshot indexes to the future cone for URTS + dag.UpdateTransactionRootSnapshotIndexes(confirmation.TailsReferenced, confirmation.MilestoneIndex) + + log.Debugf("UpdateTransactionRootSnapshotIndexes finished, took: %v", time.Since(ts).Truncate(time.Millisecond)) + }) + + onIssuedCheckpointTransaction = events.NewClosure(func(checkpointIndex int, tipIndex int, tipsTotal int, txHash hornet.Hash) { + log.Infof("checkpoint (%d) transaction issued (%d/%d): %v", checkpointIndex+1, tipIndex+1, tipsTotal, txHash.Trytes()) + }) + + onIssuedMilestone = events.NewClosure(func(index milestone.Index, tailTxHash hornet.Hash) { + log.Infof("milestone issued (%d): %v", index, tailTxHash.Trytes()) + }) +} + +func attachEvents() { + tangleplugin.Events.BundleSolid.Attach(onBundleSolid) + tangleplugin.Events.MilestoneConfirmed.Attach(onMilestoneConfirmed) + coo.Events.IssuedCheckpointTransaction.Attach(onIssuedCheckpointTransaction) + coo.Events.IssuedMilestone.Attach(onIssuedMilestone) +} + +func detachEvents() { + tangleplugin.Events.BundleSolid.Detach(onBundleSolid) + tangleplugin.Events.MilestoneConfirmed.Detach(onMilestoneConfirmed) + coo.Events.IssuedMilestone.Detach(onIssuedMilestone) +} diff --git a/plugins/dashboard/tipsel.go b/plugins/dashboard/tipsel.go index dc94f5536..eb13ef481 100644 --- a/plugins/dashboard/tipsel.go +++ b/plugins/dashboard/tipsel.go @@ -3,6 +3,7 @@ package dashboard import ( "github.com/iotaledger/hive.go/daemon" "github.com/iotaledger/hive.go/events" + "github.com/iotaledger/hive.go/node" "github.com/iotaledger/hive.go/workerpool" "github.com/gohornet/hornet/pkg/model/milestone" @@ -32,6 +33,11 @@ func configureTipSelMetric() { func runTipSelMetricWorker() { + // check if URTS plugin is enabled + if node.IsSkipped(urts.PLUGIN) { + return + } + notifyTipSelPerformed := events.NewClosure(func(metrics *tipselect.TipSelStats) { tipSelMetricWorkerPool.TrySubmit(metrics) }) diff --git a/plugins/dashboard/visualizer.go b/plugins/dashboard/visualizer.go index f8421d91a..9aeae8741 100644 --- a/plugins/dashboard/visualizer.go +++ b/plugins/dashboard/visualizer.go @@ -3,6 +3,7 @@ package dashboard import ( "github.com/iotaledger/hive.go/daemon" "github.com/iotaledger/hive.go/events" + "github.com/iotaledger/hive.go/node" "github.com/iotaledger/hive.go/workerpool" "github.com/gohornet/hornet/pkg/model/hornet" @@ -12,6 +13,7 @@ import ( "github.com/gohornet/hornet/pkg/shutdown" "github.com/gohornet/hornet/pkg/tipselect" "github.com/gohornet/hornet/pkg/whiteflag" + coordinatorPlugin "github.com/gohornet/hornet/plugins/coordinator" "github.com/gohornet/hornet/plugins/tangle" "github.com/gohornet/hornet/plugins/urts" ) @@ -121,6 +123,21 @@ func runVisualizer() { }) }) + // show checkpoints as milestones in the coordinator node + notifyIssuedCheckpointTransaction := events.NewClosure(func(checkpointIndex int, tipIndex int, tipsTotal int, txHash hornet.Hash) { + if !tanglemodel.IsNodeSyncedWithThreshold() { + return + } + + visualizerWorkerPool.TrySubmit( + &msg{ + Type: MsgTypeMilestoneInfo, + Data: &metainfo{ + ID: txHash.Trytes()[:VisualizerIdLength], + }, + }, false) + }) + notifyMilestoneConfirmedInfo := events.NewClosure(func(confirmation *whiteflag.Confirmation) { if !tanglemodel.IsNodeSyncedWithThreshold() { return @@ -178,12 +195,20 @@ func runVisualizer() { defer tangle.Events.TransactionSolid.Detach(notifySolidInfo) tangle.Events.ReceivedNewMilestone.Attach(notifyMilestoneInfo) defer tangle.Events.ReceivedNewMilestone.Detach(notifyMilestoneInfo) + if cooEvents := coordinatorPlugin.GetEvents(); cooEvents != nil { + cooEvents.IssuedCheckpointTransaction.Attach(notifyIssuedCheckpointTransaction) + defer cooEvents.IssuedCheckpointTransaction.Detach(notifyIssuedCheckpointTransaction) + } tangle.Events.MilestoneConfirmed.Attach(notifyMilestoneConfirmedInfo) defer tangle.Events.MilestoneConfirmed.Detach(notifyMilestoneConfirmedInfo) - urts.TipSelector.Events.TipAdded.Attach(notifyTipAdded) - defer urts.TipSelector.Events.TipAdded.Detach(notifyTipAdded) - urts.TipSelector.Events.TipRemoved.Attach(notifyTipRemoved) - defer urts.TipSelector.Events.TipRemoved.Detach(notifyTipRemoved) + + // check if URTS plugin is enabled + if !node.IsSkipped(urts.PLUGIN) { + urts.TipSelector.Events.TipAdded.Attach(notifyTipAdded) + defer urts.TipSelector.Events.TipAdded.Detach(notifyTipAdded) + urts.TipSelector.Events.TipRemoved.Attach(notifyTipRemoved) + defer urts.TipSelector.Events.TipRemoved.Detach(notifyTipRemoved) + } visualizerWorkerPool.Start() <-shutdownSignal diff --git a/plugins/spammer/plugin.go b/plugins/spammer/plugin.go index b9caa5135..4c9aa74f0 100644 --- a/plugins/spammer/plugin.go +++ b/plugins/spammer/plugin.go @@ -18,6 +18,7 @@ import ( "github.com/gohornet/hornet/pkg/shutdown" "github.com/gohornet/hornet/pkg/utils" "github.com/gohornet/hornet/plugins/coordinator" + "github.com/gohornet/hornet/plugins/urts" ) var ( @@ -42,6 +43,12 @@ var ( func configure(plugin *node.Plugin) { log = logger.NewLogger(plugin.Name) + // do not enable the spammer if URTS is disabled + if node.IsSkipped(urts.PLUGIN) { + plugin.Status = node.Disabled + return + } + txAddress = trinary.MustPad(config.NodeConfig.GetString(config.CfgSpammerAddress), consts.AddressTrinarySize/3)[:consts.AddressTrinarySize/3] message = config.NodeConfig.GetString(config.CfgSpammerMessage) tagSubstring = trinary.MustPad(config.NodeConfig.GetString(config.CfgSpammerTag), consts.TagTrinarySize/3)[:consts.TagTrinarySize/3] @@ -110,6 +117,11 @@ func configure(plugin *node.Plugin) { func run(_ *node.Plugin) { + // do not enable the spammer if URTS is disabled + if node.IsSkipped(urts.PLUGIN) { + return + } + // create a background worker that "measures" the spammer averages values every second daemon.BackgroundWorker("Spammer Metrics Updater", func(shutdownSignal <-chan struct{}) { timeutil.Ticker(measureSpammerMetrics, 1*time.Second, shutdownSignal) diff --git a/plugins/tangle/tangle_processor.go b/plugins/tangle/tangle_processor.go index 3a35d6041..efab55e77 100644 --- a/plugins/tangle/tangle_processor.go +++ b/plugins/tangle/tangle_processor.go @@ -131,8 +131,6 @@ func processIncomingTx(incomingTx *hornet.Transaction, request *rqueue.Request, // Release shouldn't be forced, to cache the latest transactions defer cachedTx.Release(!isNodeSyncedWithThreshold) // tx -1 - Events.ProcessedTransaction.Trigger(incomingTx.GetTxHash()) - if !alreadyAdded { metrics.SharedServerMetrics.NewTransactions.Inc() @@ -161,6 +159,12 @@ func processIncomingTx(incomingTx *hornet.Transaction, request *rqueue.Request, Events.ReceivedKnownTransaction.Trigger(cachedTx) } + // "ProcessedTransaction" event has to be fired after "ReceivedNewTransaction" event, + // otherwise there is a race condition in the coordinator plugin that tries to "ComputeMerkleTreeRootHash" + // with the transactions it issued itself because the transactions may be not solid yet and therefore their bundles + // are not created yet. + Events.ProcessedTransaction.Trigger(incomingTx.GetTxHash()) + if request != nil { // mark the received request as processed gossip.RequestQueue().Processed(incomingTx.GetTxHash()) diff --git a/plugins/urts/plugin.go b/plugins/urts/plugin.go index 0067e0594..40be5f485 100644 --- a/plugins/urts/plugin.go +++ b/plugins/urts/plugin.go @@ -9,6 +9,7 @@ import ( "github.com/iotaledger/hive.go/node" "github.com/gohornet/hornet/pkg/config" + "github.com/gohornet/hornet/pkg/dag" "github.com/gohornet/hornet/pkg/model/tangle" "github.com/gohornet/hornet/pkg/shutdown" "github.com/gohornet/hornet/pkg/tipselect" @@ -71,8 +72,12 @@ func configureEvents() { }) onMilestoneConfirmed = events.NewClosure(func(confirmation *whiteflag.Confirmation) { + ts := time.Now() + // propagate new transaction root snapshot indexes to the future cone for URTS - TipSelector.UpdateTransactionRootSnapshotIndexes(confirmation.TailsReferenced, confirmation.MilestoneIndex) + dag.UpdateTransactionRootSnapshotIndexes(confirmation.TailsReferenced, confirmation.MilestoneIndex) + + log.Debugf("UpdateTransactionRootSnapshotIndexes finished, took: %v", time.Since(ts).Truncate(time.Millisecond)) }) } diff --git a/plugins/webapi/gtta.go b/plugins/webapi/gtta.go index ef8fe977c..53cc46ec5 100644 --- a/plugins/webapi/gtta.go +++ b/plugins/webapi/gtta.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/gin-gonic/gin" + "github.com/iotaledger/hive.go/node" "github.com/gohornet/hornet/pkg/model/tangle" "github.com/gohornet/hornet/pkg/tipselect" @@ -18,6 +19,13 @@ func init() { func getTransactionsToApprove(i interface{}, c *gin.Context, _ <-chan struct{}) { e := ErrorReturn{} + // do not reply if URTS is disabled + if node.IsSkipped(urts.PLUGIN) { + e.Error = "tipselection plugin disabled in this node" + c.JSON(http.StatusServiceUnavailable, e) + return + } + tips, err := urts.TipSelector.SelectTips() if err != nil { if err == tangle.ErrNodeNotSynced || err == tipselect.ErrNoTipsAvailable {