Skip to content

Commit

Permalink
Adaptive heaviest branch tipsel (#567)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
muXxer and alexsporn authored Jul 26, 2020
1 parent 7df9729 commit cbe8163
Show file tree
Hide file tree
Showing 18 changed files with 711 additions and 480 deletions.
10 changes: 9 additions & 1 deletion config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 9 additions & 1 deletion config_comnet.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 9 additions & 1 deletion config_devnet.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
22 changes: 19 additions & 3 deletions pkg/config/coordinator_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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")
}
145 changes: 145 additions & 0 deletions pkg/dag/transaction_root_snapshot_indexes.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Loading

0 comments on commit cbe8163

Please sign in to comment.