kaspad/mining/txselection.go
Ori Newman 2174a0a7f2 [NOD-497] Implement GHOSTDAG (#575)
* [NOD-540] Implement reachability (#545)

* [NOD-540] Begin implementing reachability.

* [NOD-540] Finish implementing reachability.

* [NOD-540] Implement TestIsFutureBlock.

* [NOD-540] Implement TestInsertFutureBlock.

* [NOD-540] Add comments.

* [NOD-540] Add comment for interval in blockNode.

* [NOD-540] Updated comments over insertFutureBlock and isFutureBlock.

* [NOD-540] Implement interval splitting methods.

* [NOD-540] Begin implementing tree manipulation in blockNode.

* [NOD-540] Implement countSubtreesUp.

* [NOD-540] Add a comment explaining an impossible condition.

* [NOD-540] Implement applyIntervalDown.

* [NOD-540] Moved the reachability tree stuff into reachability.go.

* [NOD-540] Add some comments.

* [NOD-540] Add more comments, implement isInPast.

* [NOD-540] Fix comments.

* [NOD-540] Implement TestSplitFraction.

* [NOD-540] Implement TestSplitExact.

* [NOD-540] Implement TestSplit.

* [NOD-540] Add comments to structs.

* [NOD-540] Implement TestAddTreeChild.

* [NOD-540] Fix a comment.

* [NOD-540] Rename isInPast to isAncestorOf.

* [NOD-540] Rename futureBlocks to futureCoveringSet.

* [NOD-540] Rename isFutureBlock to isInFuture.

* [NOD-540] move reachabilityInterval to the top of reachability.go.

* [NOD-540] Change "s.t." to "such that" in a comment.

* [NOD-540] Fix indentation.

* [NOD-540] Fix a potential bug involving float inaccuracy.

* [NOD-540] Wrote a more descriptive error message.

* [NOD-540] Fix error messsage.

* [NOD-540] Fix the recursive countSubtreesUp.

* [NOD-540] Rename countSubtreesUp to countSubtrees and applyIntervalDown to propagateInterval.

* [NOD-540] Implement updating reachability for a valid new block.

* [NOD-540] Implement a disk storage for reachability data.

* [NOD-540] Fix not all tree nodes being written to the database.

* [NOD-540] Implement serialization for reachabilityData.

* [NOD-540] Implement some deserialization for reachabilityData.

* [NOD-540] Implement restoring the reachabilityStore on node restart.

* [NOD-540] Made interval and remainingInterval pointers.

* [NOD-540] Rename setTreeInterval to setInterval.

* [NOD-540] Rename reindexTreeIntervals to reindexIntervals and fixed the comment above it.

* [NOD-540] Expand the comment above reindexIntervals.

* [NOD-540] Fix comment above countSubtrees.

* [NOD-540] Fix comment above countSubtrees some more.

* [NOD-540] Fix comment above split.

* [NOD-540] Fix comment above isAncestorOf.

* [NOD-540] Fix comment above reachabilityTreeNode.

* [NOD-540] Fix weird condition in addTreeChild.

* [NOD-540] Rename addTreeChild to addChild.

* [NOD-540] Fix weird condition in splitFraction.

* [NOD-540] Reverse the lines in reachabilityTreeNode.String().

* [NOD-540] Renamed f to fraction and x to size.

* [NOD-540] Fix comment above bisect.

* [NOD-540] Implement rtn.isAncestorOf().

* [NOD-540] Use treeNode isAncestorOf instead of treeInterval isAncestorOf.

* [NOD-540] Use newReachabilityInterval instead of struct initialization.

* [NOD-540] Make reachabilityTreeNode.String() use strings.Join.

* [NOD-540] Use sync.RWMutex instead of locks.PriorityMutex.

* [NOD-540] Rename thisTreeNode to newTreeNode.

* [NOD-540] Rename setTreeNode to addTreeNode.

* [NOD-540] Extracted selectedParentAnticone to a separate function.

* [NOD-540] Rename node to this.

* [NOD-540] Move updateReachability and isAncestorOf from dag.go to reachability.go.

* [NOD-540] Add whitespace after multiline function signatures in reachability.go.

* [NOD-540] Make splitFraction return an error on empty interval.

* [NOD-540] Add a comment about rounding to splitFraction.

* [NOD-540] Replace sneaky tabs with spaces.

* [NOD-540] Rename split to splitExponential.

* [NOD-540] Extract exponentialFractions to a separate function.

* [NOD-540] Rename bisect to findIndex.

* [NOD-540] Add call to reachabilityStore.clearDirtyEntries at the end of saveChangesFromBlock.

* [NOD-540] Explain the dirty hack in reachabilityStore.init().

* [NOD-540] Split the function signature for deserializeReachabilityData to two lines.

* [NOD-540] Add a comment about float precision loss to exponentialFractions.

* [NOD-540] Corrected a comment about float precision loss to exponentialFractions.

* [NOD-540] Fixed a comment about float precision loss to exponentialFractions some more.

* [NOD-540] Added further comments above futureCoveringBlockSet.

* [NOD-540] Rename addTreeNode to setTreeNode.

* [NOD-540] Rename splitExponential to splitWithExponentialBias.

* [NOD-540] Fix object references in reachabilityData deserialization (#563)

* [NOD-540] Fix broken references in deserialization.

* [NOD-540] Fix broken references in futureCoveringSet deserialization. Also add comments.

* [NOD-540] Don't deserialize on the first pass in reachabilityStore.init().

* [NOD-540] Remove redundant assignment to loaded[hash].

* [NOD-540] Use NewHash instead of SetBytes. Rename data to destination.

* [NOD-540] Preallocate futureCoveringSet.

* [NOD-541] Implement GHOSTDAG (#560)

* [NOD-541] Implement GHOSTDAG

* [NOD-541] Replace the old PHANTOM variant with GHOSTDAG

* [NOD-541] Move dag.updateReachability to the top of dag.applyDAGChanges to update reachability before the virtual block is updated

* [NOD-541] Fix blueAnticoneSize

* [NOD-541] Initialize node.bluesAnticoneSizes

* [NOD-541] Fix pastUTXO and applyBlueBlocks blues order

* [NOD-541] Add serialization logic to node.bluesAnticoneSizes

* [NOD-541] Fix GHOSTDAG to not count the new block and the blue candidates anticone, add selected parent to blues, and save to node.bluesAnticoneSizes properly

* [NOD-541] Fix test names in inner strings

* [NOD-541] Writing TestGHOSTDAG

* [NOD-541] In blueAnticoneSize change node->current

* [NOD-541] name ghostdag return values

* [NOD-541] fix ghostdag to return slice

* [NOD-541] Split k-cluster violation rules

* [NOD-541] Add missing space

* [NOD-541] Add comment to ghostdag

* [NOD-541] In selectedParentAnticone rename past->selectedParentPast

* [NOD-541] Fix misrefernces to TestChainUpdates

* [NOD-541] Fix ghostdag comment

* [NOD-541] Make PrepareBlockForTest in blockdag package

* [NOD-541] Make PrepareBlockForTest in blockdag package

* [NOD-541] Assign to selectedParentAnticone[i] instead of appending

* [NOD-541] Remove redundant forceTransactions arguments from PrepareBlockForTEST

* [NOD-541] Add non-selected parents to anticoneHeap

* [NOD-541] add test for ghostdag

* [NOD-541] Add comments

* [NOD-541] Use adjusted time for initializing blockNode

* [NOD-541] Rename isAncestorOf -> isAncestorOfBlueCandidate

* [NOD-541] Remove params from PrepareBlockForTest

* [NOD-541] Fix TestChainHeight

* [NOD-541] Remove recursive lock

* [NOD-541] Fix TestTxIndexConnectBlock

* [NOD-541] Fix TestBlueBlockWindow

* [NOD-541] Put prepareAndProcessBlock in common_test.go

* [NOD-541] Fix TestConfirmations

* [NOD-541] Fix TestAcceptingBlock

* [NOD-541] Fix TestDifficulty

* [NOD-541] Fix TestVirtualBlock

* [NOD-541] Fix TestSelectedPath

* [NOD-541] Fix TestChainUpdates

* [NOD-541] Shorten TestDifficulty test time

* [NOD-541] Make PrepareBlockForTest use minimal valid block time

* [NOD-541] Remove TODO comment

* [NOD-541] Move blockdag related mining functions to mining.go

* [NOD-541] Use NextBlockCoinbaseTransaction instead of NextBlockCoinbaseTransactionNoLock in NextCoinbaseFromAddress

* [NOD-541] Remove useMinimalTime from BlockForMining

* [NOD-541] Make MedianAdjustedTime a *BlockDAG method

* [NOD-541] Fix ghostdag to use anticone slice instead of heap

* [NOD-541] Fix NewBlockTemplate locks

* [NOD-541] Fix ghostdag comments

* [NOD-541] Convert MedianAdjustedTime to NextBlockTime

* [NOD-541] Fix ghostdag comment

* [NOD-541] Fix TestGHOSTDAG comment

* [NOD-541] Add comment before sanity check

* [NOD-541] Explicitly initialize .blues in ghostdag

* [NOD-541] Rename *blockNode.lessThan to *blockNode.less

* [NOD-541] Remove redundant check if block != chainBlock

* [NOD-541] Fix comment

* [NOD-541] Fix comment

* [NOD-497] Add comment; General refactoring

* [NOD-497] General refactoring.

* [NOD-497] Use isAncestor of the tree rather than the node

* [NOD-497] Remove reachability mutex lock as it is redundant (dag lock is held so no need); General refactoring.

* [NOD-497] Update comment

* [NOD-497] Undo test blocktimestamp

* [NOD-497] Update comments; Use BlockNode.less for blockset;

* [NOD-497] Change processBlock to return boolean and not the delay duration (merge conflict)

* [NOD-497] Undo change for bluest to use less; Change blocknode less to use daghash.Less

Co-authored-by: stasatdaglabs <39559713+stasatdaglabs@users.noreply.github.com>
Co-authored-by: Dan Aharoni <dereeno@protonmail.com>
2020-01-08 17:41:22 +02:00

366 lines
12 KiB
Go

package mining
import (
"github.com/kaspanet/kaspad/blockdag"
"github.com/kaspanet/kaspad/util"
"github.com/kaspanet/kaspad/util/random"
"github.com/kaspanet/kaspad/util/subnetworkid"
"math"
"math/rand"
"sort"
)
const (
// alpha is a coefficient that defines how uniform the distribution of
// candidate transactions should be. A smaller alpha makes the distribution
// more uniform. Alpha is used when determining a candidate transaction's
// initial p value.
alpha = 3
// rebalanceThreshold is the percentage of candidate transactions under which
// we don't rebalance. Rebalancing is a heavy operation so we prefer to avoid
// rebalancing very often. On the other hand, if we don't rebalance often enough
// we risk having too many collisions.
// The value is derived from the max probability of collision. That is to say,
// if rebalanceThreshold is 0.95, there's a 1-in-20 chance of collision.
// See selectTxs for further details.
rebalanceThreshold = 0.95
)
type candidateTx struct {
txDesc *TxDesc
txValue float64
txMass uint64
gasLimit uint64
p float64
start float64
end float64
isMarkedForDeletion bool
}
type txsForBlockTemplate struct {
selectedTxs []*util.Tx
txMasses []uint64
txFees []uint64
totalMass uint64
totalFees uint64
}
// selectTxs implements a probabilistic transaction selection algorithm.
// The algorithm, roughly, is as follows:
// 1. We assign a probability to each transaction equal to:
// (candidateTx.Value^alpha) / Σ(tx.Value^alpha)
// Where the sum of the probabilities of all txs is 1.
// 2. We draw a random number in [0,1) and select a transaction accordingly.
// 3. If it's valid, add it to the selectedTxs and remove it from the candidates.
// 4. Continue iterating the above until we have either selected all
// available transactions or ran out of gas/block space.
//
// Note that we make two optimizations here:
// * Draw a number in [0,Σ(tx.Value^alpha)) to avoid normalization
// * Instead of removing a candidate after each iteration, mark it for deletion.
// Once the sum of probabilities of marked transactions is greater than
// rebalanceThreshold percent of the sum of probabilities of all transactions,
// rebalance.
func (g *BlkTmplGenerator) selectTxs(payToAddress util.Address) (*txsForBlockTemplate, error) {
// Fetch the source transactions.
sourceTxs := g.txSource.MiningDescs()
// Create a new txsForBlockTemplate struct, onto which all selectedTxs
// will be appended.
txsForBlockTemplate, err := g.newTxsForBlockTemplate(payToAddress, sourceTxs)
if err != nil {
return nil, err
}
// Collect candidateTxs while excluding txs that will certainly not
// be selected.
candidateTxs := g.collectCandidatesTxs(sourceTxs)
log.Debugf("Considering %d transactions for inclusion to new block",
len(candidateTxs))
// Choose which transactions make it into the block.
g.populateTemplateFromCandidates(candidateTxs, txsForBlockTemplate)
return txsForBlockTemplate, nil
}
// newTxsForBlockTemplate creates a txsForBlockTemplate and initializes it
// with a coinbase transaction.
func (g *BlkTmplGenerator) newTxsForBlockTemplate(payToAddress util.Address, sourceTxs []*TxDesc) (*txsForBlockTemplate, error) {
// Create a new txsForBlockTemplate struct. The struct holds the mass,
// the fees, and number of signature operations for each of the selected
// transactions and adds an entry for the coinbase. This allows the code
// below to simply append details about a transaction as it is selected
// for inclusion in the final block.
txsForBlockTemplate := &txsForBlockTemplate{
selectedTxs: make([]*util.Tx, 0),
txMasses: make([]uint64, 0),
txFees: make([]uint64, 0),
}
extraNonce, err := random.Uint64()
if err != nil {
return nil, err
}
coinbasePayloadExtraData, err := blockdag.CoinbasePayloadExtraData(extraNonce, CoinbaseFlags)
if err != nil {
return nil, err
}
coinbaseTx, err := g.dag.NextCoinbaseFromAddress(payToAddress, coinbasePayloadExtraData)
if err != nil {
return nil, err
}
coinbaseTxMass, err := blockdag.CalcTxMassFromUTXOSet(coinbaseTx, g.dag.UTXOSet())
if err != nil {
return nil, err
}
// Add the coinbase.
txsForBlockTemplate.selectedTxs = append(txsForBlockTemplate.selectedTxs, coinbaseTx)
txsForBlockTemplate.totalMass = coinbaseTxMass
txsForBlockTemplate.totalFees = uint64(0)
txsForBlockTemplate.txMasses = append(txsForBlockTemplate.txMasses, coinbaseTxMass)
txsForBlockTemplate.txFees = append(txsForBlockTemplate.txFees, 0) // For coinbase tx
return txsForBlockTemplate, nil
}
// collectCandidateTxs goes over the sourceTxs and collects only the ones that
// may be included in the next block.
func (g *BlkTmplGenerator) collectCandidatesTxs(sourceTxs []*TxDesc) []*candidateTx {
nextBlockBlueScore := g.dag.VirtualBlueScore()
candidateTxs := make([]*candidateTx, 0, len(sourceTxs))
for _, txDesc := range sourceTxs {
tx := txDesc.Tx
// A block can't contain non-finalized transactions.
if !blockdag.IsFinalizedTransaction(tx, nextBlockBlueScore,
g.timeSource.AdjustedTime()) {
log.Debugf("Skipping non-finalized tx %s", tx.ID())
continue
}
// A block can't contain zero-fee transactions.
if txDesc.Fee == 0 {
log.Warnf("Skipped zero-fee tx %s", tx.ID())
continue
}
txMass, err := blockdag.CalcTxMassFromUTXOSet(tx, g.dag.UTXOSet())
if err != nil {
log.Warnf("Skipping tx %s due to error in "+
"CalcTxMass: %s", tx.ID(), err)
continue
}
gasLimit := uint64(0)
if !tx.MsgTx().SubnetworkID.IsEqual(subnetworkid.SubnetworkIDNative) && !tx.MsgTx().SubnetworkID.IsBuiltIn() {
subnetworkID := tx.MsgTx().SubnetworkID
gasLimit, err = g.dag.SubnetworkStore.GasLimit(&subnetworkID)
if err != nil {
log.Warnf("Skipping tx %s due to error in "+
"GasLimit: %s", tx.ID(), err)
continue
}
}
// Calculate the tx value
txValue, err := g.calcTxValue(tx, txDesc.Fee)
if err != nil {
log.Warnf("Skipping tx %s due to error in "+
"calcTxValue: %s", tx.ID(), err)
continue
}
candidateTxs = append(candidateTxs, &candidateTx{
txDesc: txDesc,
txValue: txValue,
txMass: txMass,
gasLimit: gasLimit,
})
}
// Sort the candidate txs by subnetworkID.
sort.Slice(candidateTxs, func(i, j int) bool {
return subnetworkid.Less(&candidateTxs[i].txDesc.Tx.MsgTx().SubnetworkID,
&candidateTxs[j].txDesc.Tx.MsgTx().SubnetworkID)
})
return candidateTxs
}
// calcTxValue calculates a value to be used in transaction selection.
// The higher the number the more likely it is that the transaction will be
// included in the block.
func (g *BlkTmplGenerator) calcTxValue(tx *util.Tx, fee uint64) (float64, error) {
mass, err := blockdag.CalcTxMassFromUTXOSet(tx, g.dag.UTXOSet())
if err != nil {
return 0, err
}
massLimit := g.policy.BlockMaxMass
msgTx := tx.MsgTx()
if msgTx.SubnetworkID.IsEqual(subnetworkid.SubnetworkIDNative) ||
msgTx.SubnetworkID.IsBuiltIn() {
return float64(fee) / (float64(mass) / float64(massLimit)), nil
}
gas := msgTx.Gas
gasLimit, err := g.dag.SubnetworkStore.GasLimit(&msgTx.SubnetworkID)
if err != nil {
return 0, err
}
return float64(fee) / (float64(mass)/float64(massLimit) + float64(gas)/float64(gasLimit)), nil
}
// populateTemplateFromCandidates loops over the candidate transactions
// and appends the ones that will be included in the next block into
// txsForBlockTemplates.
// See selectTxs for further details.
func (g *BlkTmplGenerator) populateTemplateFromCandidates(candidateTxs []*candidateTx, txsForBlockTemplate *txsForBlockTemplate) {
usedCount, usedP := 0, 0.0
candidateTxs, totalP := rebalanceCandidates(candidateTxs, true)
gasUsageMap := make(map[subnetworkid.SubnetworkID]uint64)
markCandidateTxForDeletion := func(candidateTx *candidateTx) {
candidateTx.isMarkedForDeletion = true
usedCount++
usedP += candidateTx.p
}
for len(candidateTxs)-usedCount > 0 {
// Rebalance the candidates if it's required
if usedP >= rebalanceThreshold*totalP {
candidateTxs, totalP = rebalanceCandidates(candidateTxs, false)
usedCount, usedP = 0, 0.0
// Break if we now ran out of transactions
if len(candidateTxs) == 0 {
break
}
}
// Select a candidate tx at random
r := rand.Float64()
r *= totalP
selectedTx := findTx(candidateTxs, r)
// If isMarkedForDeletion is set, it means we got a collision.
// Ignore and select another Tx.
if selectedTx.isMarkedForDeletion == true {
continue
}
tx := selectedTx.txDesc.Tx
// Enforce maximum transaction mass per block. Also check
// for overflow.
if txsForBlockTemplate.totalMass+selectedTx.txMass < txsForBlockTemplate.totalMass ||
txsForBlockTemplate.totalMass+selectedTx.txMass > g.policy.BlockMaxMass {
log.Tracef("Tx %s would exceed the max block mass. "+
"As such, stopping.", tx.ID())
break
}
// Enforce maximum gas per subnetwork per block. Also check
// for overflow.
if !tx.MsgTx().SubnetworkID.IsEqual(subnetworkid.SubnetworkIDNative) && !tx.MsgTx().SubnetworkID.IsBuiltIn() {
subnetworkID := tx.MsgTx().SubnetworkID
gasUsage, ok := gasUsageMap[subnetworkID]
if !ok {
gasUsage = 0
}
txGas := tx.MsgTx().Gas
if gasUsage+txGas < gasUsage ||
gasUsage+txGas > selectedTx.gasLimit {
log.Tracef("Tx %s would exceed the gas limit in "+
"subnetwork %s. Removing all remaining txs from this "+
"subnetwork.",
tx.MsgTx().TxID(), subnetworkID)
for _, candidateTx := range candidateTxs {
// candidateTxs are ordered by subnetwork, so we can safely assume
// that transactions after subnetworkID will not be relevant.
if subnetworkid.Less(&subnetworkID, &candidateTx.txDesc.Tx.MsgTx().SubnetworkID) {
break
}
if candidateTx.txDesc.Tx.MsgTx().SubnetworkID.IsEqual(&subnetworkID) {
markCandidateTxForDeletion(candidateTx)
}
}
continue
}
gasUsageMap[subnetworkID] = gasUsage + txGas
}
// Add the transaction to the result, increment counters, and
// save the masses, fees, and signature operation counts to the
// result.
txsForBlockTemplate.selectedTxs = append(txsForBlockTemplate.selectedTxs, tx)
txsForBlockTemplate.totalMass += selectedTx.txMass
txsForBlockTemplate.totalFees += selectedTx.txDesc.Fee
txsForBlockTemplate.txMasses = append(txsForBlockTemplate.txMasses, selectedTx.txMass)
txsForBlockTemplate.txFees = append(txsForBlockTemplate.txFees, selectedTx.txDesc.Fee)
log.Tracef("Adding tx %s (feePerKB %.2f)",
tx.ID(), selectedTx.txDesc.FeePerKB)
markCandidateTxForDeletion(selectedTx)
}
}
func rebalanceCandidates(oldCandidateTxs []*candidateTx, isFirstRun bool) (
candidateTxs []*candidateTx, totalP float64) {
totalP = 0.0
candidateTxs = make([]*candidateTx, 0, len(oldCandidateTxs))
for _, candidateTx := range oldCandidateTxs {
if candidateTx.isMarkedForDeletion {
continue
}
candidateTxs = append(candidateTxs, candidateTx)
}
for _, candidateTx := range candidateTxs {
if isFirstRun {
candidateTx.p = math.Pow(candidateTx.txValue, alpha)
}
candidateTx.start = totalP
candidateTx.end = totalP + candidateTx.p
totalP += candidateTx.p
}
return
}
// findTx finds the candidateTx in whose range r falls.
// For example, if we have candidateTxs with starts and ends:
// * tx1: start 0, end 100
// * tx2: start 100, end 105
// * tx3: start 105, end 2000
// And r=102, then findTx will return tx2.
func findTx(candidateTxs []*candidateTx, r float64) *candidateTx {
min := 0
max := len(candidateTxs) - 1
for {
i := (min + max) / 2
candidateTx := candidateTxs[i]
if candidateTx.end < r {
min = i + 1
continue
} else if candidateTx.start > r {
max = i - 1
continue
}
return candidateTx
}
}