mirror of
https://github.com/kaspanet/kaspad.git
synced 2025-06-06 14:16:43 +00:00
[NOD-1423] Refactor the miner and mempool (#981)
* Make TransactionOutputEstimatedSerializedSize public * Update the mempool interface * Refactor the mempool to the new design * refactor txselection and blocktemplatebuilder to the new design * Update the mining manager * Update the MiningManager factory * mempool fix requested changed
This commit is contained in:
parent
c59adaa4db
commit
87ad9dfc59
@ -22,7 +22,7 @@ func TransactionEstimatedSerializedSize(tx *externalapi.DomainTransaction) uint6
|
|||||||
|
|
||||||
size += 8 // number of outputs (uint64)
|
size += 8 // number of outputs (uint64)
|
||||||
for _, output := range tx.Outputs {
|
for _, output := range tx.Outputs {
|
||||||
size += transactionOutputEstimatedSerializedSize(output)
|
size += TransactionOutputEstimatedSerializedSize(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
size += 8 // lock time (uint64)
|
size += 8 // lock time (uint64)
|
||||||
@ -54,7 +54,8 @@ func outpointEstimatedSerializedSize() uint64 {
|
|||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
|
|
||||||
func transactionOutputEstimatedSerializedSize(output *externalapi.DomainTransactionOutput) uint64 {
|
// TransactionOutputEstimatedSerializedSize is the same as TransactionEstimatedSerializedSize but for outputs only
|
||||||
|
func TransactionOutputEstimatedSerializedSize(output *externalapi.DomainTransactionOutput) uint64 {
|
||||||
size := uint64(0)
|
size := uint64(0)
|
||||||
size += 8 // value (uint64)
|
size += 8 // value (uint64)
|
||||||
size += 8 // length of script public key (uint64)
|
size += 8 // length of script public key (uint64)
|
||||||
|
@ -3,24 +3,167 @@ package blocktemplatebuilder
|
|||||||
import (
|
import (
|
||||||
"github.com/kaspanet/kaspad/domain/consensus"
|
"github.com/kaspanet/kaspad/domain/consensus"
|
||||||
consensusexternalapi "github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
|
consensusexternalapi "github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
|
||||||
|
"github.com/kaspanet/kaspad/domain/consensus/ruleerrors"
|
||||||
|
"github.com/kaspanet/kaspad/domain/consensus/utils/subnetworks"
|
||||||
miningmanagerapi "github.com/kaspanet/kaspad/domain/miningmanager/model"
|
miningmanagerapi "github.com/kaspanet/kaspad/domain/miningmanager/model"
|
||||||
|
"github.com/kaspanet/kaspad/util"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type candidateTx struct {
|
||||||
|
*consensusexternalapi.DomainTransaction
|
||||||
|
txValue float64
|
||||||
|
gasLimit uint64
|
||||||
|
|
||||||
|
p float64
|
||||||
|
start float64
|
||||||
|
end float64
|
||||||
|
|
||||||
|
isMarkedForDeletion bool
|
||||||
|
}
|
||||||
|
|
||||||
// blockTemplateBuilder creates block templates for a miner to consume
|
// blockTemplateBuilder creates block templates for a miner to consume
|
||||||
type blockTemplateBuilder struct {
|
type blockTemplateBuilder struct {
|
||||||
consensus *consensus.Consensus
|
consensus consensus.Consensus
|
||||||
mempool miningmanagerapi.Mempool
|
mempool miningmanagerapi.Mempool
|
||||||
|
policy policy
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new blockTemplateBuilder
|
// New creates a new blockTemplateBuilder
|
||||||
func New(consensus *consensus.Consensus, mempool miningmanagerapi.Mempool) miningmanagerapi.BlockTemplateBuilder {
|
func New(consensus consensus.Consensus, mempool miningmanagerapi.Mempool, blockMaxMass uint64) miningmanagerapi.BlockTemplateBuilder {
|
||||||
return &blockTemplateBuilder{
|
return &blockTemplateBuilder{
|
||||||
consensus: consensus,
|
consensus: consensus,
|
||||||
mempool: mempool,
|
mempool: mempool,
|
||||||
|
policy: policy{BlockMaxMass: blockMaxMass},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBlockTemplate creates a block template for a miner to consume
|
// GetBlockTemplate creates a block template for a miner to consume
|
||||||
|
// GetBlockTemplate returns a new block template that is ready to be solved
|
||||||
|
// using the transactions from the passed transaction source pool and a coinbase
|
||||||
|
// that either pays to the passed address if it is not nil, or a coinbase that
|
||||||
|
// is redeemable by anyone if the passed address is nil. The nil address
|
||||||
|
// functionality is useful since there are cases such as the getblocktemplate
|
||||||
|
// RPC where external mining software is responsible for creating their own
|
||||||
|
// coinbase which will replace the one generated for the block template. Thus
|
||||||
|
// the need to have configured address can be avoided.
|
||||||
|
//
|
||||||
|
// The transactions selected and included are prioritized according to several
|
||||||
|
// factors. First, each transaction has a priority calculated based on its
|
||||||
|
// value, age of inputs, and size. Transactions which consist of larger
|
||||||
|
// amounts, older inputs, and small sizes have the highest priority. Second, a
|
||||||
|
// fee per kilobyte is calculated for each transaction. Transactions with a
|
||||||
|
// higher fee per kilobyte are preferred. Finally, the block generation related
|
||||||
|
// policy settings are all taken into account.
|
||||||
|
//
|
||||||
|
// Transactions which only spend outputs from other transactions already in the
|
||||||
|
// block DAG are immediately added to a priority queue which either
|
||||||
|
// prioritizes based on the priority (then fee per kilobyte) or the fee per
|
||||||
|
// kilobyte (then priority) depending on whether or not the BlockPrioritySize
|
||||||
|
// policy setting allots space for high-priority transactions. Transactions
|
||||||
|
// which spend outputs from other transactions in the source pool are added to a
|
||||||
|
// dependency map so they can be added to the priority queue once the
|
||||||
|
// transactions they depend on have been included.
|
||||||
|
//
|
||||||
|
// Once the high-priority area (if configured) has been filled with
|
||||||
|
// transactions, or the priority falls below what is considered high-priority,
|
||||||
|
// the priority queue is updated to prioritize by fees per kilobyte (then
|
||||||
|
// priority).
|
||||||
|
//
|
||||||
|
// When the fees per kilobyte drop below the TxMinFreeFee policy setting, the
|
||||||
|
// transaction will be skipped unless the BlockMinSize policy setting is
|
||||||
|
// nonzero, in which case the block will be filled with the low-fee/free
|
||||||
|
// transactions until the block size reaches that minimum size.
|
||||||
|
//
|
||||||
|
// Any transactions which would cause the block to exceed the BlockMaxMass
|
||||||
|
// policy setting, exceed the maximum allowed signature operations per block, or
|
||||||
|
// otherwise cause the block to be invalid are skipped.
|
||||||
|
//
|
||||||
|
// Given the above, a block generated by this function is of the following form:
|
||||||
|
//
|
||||||
|
// ----------------------------------- -- --
|
||||||
|
// | Coinbase Transaction | | |
|
||||||
|
// |-----------------------------------| | |
|
||||||
|
// | | | | ----- policy.BlockPrioritySize
|
||||||
|
// | High-priority Transactions | | |
|
||||||
|
// | | | |
|
||||||
|
// |-----------------------------------| | --
|
||||||
|
// | | |
|
||||||
|
// | | |
|
||||||
|
// | | |--- policy.BlockMaxMass
|
||||||
|
// | Transactions prioritized by fee | |
|
||||||
|
// | until <= policy.TxMinFreeFee | |
|
||||||
|
// | | |
|
||||||
|
// | | |
|
||||||
|
// | | |
|
||||||
|
// |-----------------------------------| |
|
||||||
|
// | Low-fee/Non high-priority (free) | |
|
||||||
|
// | transactions (while block size | |
|
||||||
|
// | <= policy.BlockMinSize) | |
|
||||||
|
// ----------------------------------- --
|
||||||
|
|
||||||
func (btb *blockTemplateBuilder) GetBlockTemplate(coinbaseData *consensusexternalapi.DomainCoinbaseData) *consensusexternalapi.DomainBlock {
|
func (btb *blockTemplateBuilder) GetBlockTemplate(coinbaseData *consensusexternalapi.DomainCoinbaseData) *consensusexternalapi.DomainBlock {
|
||||||
return nil
|
mempoolTransactions := btb.mempool.Transactions()
|
||||||
|
candidateTxs := make([]*candidateTx, 0, len(mempoolTransactions))
|
||||||
|
for _, tx := range mempoolTransactions {
|
||||||
|
// Calculate the tx value
|
||||||
|
gasLimit := uint64(0)
|
||||||
|
if !subnetworks.IsBuiltInOrNative(tx.SubnetworkID) {
|
||||||
|
panic("We currently don't support non native subnetworks")
|
||||||
|
}
|
||||||
|
candidateTxs = append(candidateTxs, &candidateTx{
|
||||||
|
DomainTransaction: tx,
|
||||||
|
txValue: btb.calcTxValue(tx),
|
||||||
|
gasLimit: gasLimit,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the candidate txs by subnetworkID.
|
||||||
|
sort.Slice(candidateTxs, func(i, j int) bool {
|
||||||
|
return subnetworks.Less(candidateTxs[i].SubnetworkID, candidateTxs[j].SubnetworkID)
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Debugf("Considering %d transactions for inclusion to new block",
|
||||||
|
len(candidateTxs))
|
||||||
|
|
||||||
|
blockTxs := btb.selectTransactions(candidateTxs)
|
||||||
|
blk, err := btb.consensus.BuildBlock(coinbaseData, blockTxs.selectedTxs)
|
||||||
|
|
||||||
|
invalidTxsErr := ruleerrors.ErrInvalidTransactionsInNewBlock{}
|
||||||
|
if errors.As(err, &invalidTxsErr) {
|
||||||
|
log.Criticalf("consensus.BuildBlock returned invalid txs in GetBlockTemplate: %s", err)
|
||||||
|
invalidTxs := make([]*consensusexternalapi.DomainTransaction, 0, len(invalidTxsErr.InvalidTransactions))
|
||||||
|
for _, tx := range invalidTxsErr.InvalidTransactions {
|
||||||
|
invalidTxs = append(invalidTxs, tx.Transaction)
|
||||||
|
}
|
||||||
|
btb.mempool.RemoveTransactions(invalidTxs)
|
||||||
|
// We can call this recursively without worry because this should almost never happen
|
||||||
|
return btb.GetBlockTemplate(coinbaseData)
|
||||||
|
} else if err != nil {
|
||||||
|
log.Errorf("GetBlockTemplate: Failed building block: %s", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Created new block template (%d transactions, %d in fees, %d mass, target difficulty %064x)",
|
||||||
|
len(blk.Transactions), blockTxs.totalFees, blockTxs.totalMass, util.CompactToBig(blk.Header.Bits))
|
||||||
|
|
||||||
|
return blk
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (btb *blockTemplateBuilder) calcTxValue(tx *consensusexternalapi.DomainTransaction) float64 {
|
||||||
|
massLimit := btb.policy.BlockMaxMass
|
||||||
|
|
||||||
|
mass := tx.Mass
|
||||||
|
fee := tx.Fee
|
||||||
|
if subnetworks.IsBuiltInOrNative(tx.SubnetworkID) {
|
||||||
|
return float64(fee) / (float64(mass) / float64(massLimit))
|
||||||
|
}
|
||||||
|
// TODO: Replace with real gas once implemented
|
||||||
|
gasLimit := uint64(math.MaxUint64)
|
||||||
|
return float64(fee) / (float64(mass)/float64(massLimit) + float64(tx.Gas)/float64(gasLimit))
|
||||||
}
|
}
|
||||||
|
11
domain/miningmanager/blocktemplatebuilder/log.go
Normal file
11
domain/miningmanager/blocktemplatebuilder/log.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// Copyright (c) 2016 The btcsuite developers
|
||||||
|
// Use of this source code is governed by an ISC
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package blocktemplatebuilder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/kaspanet/kaspad/infrastructure/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var log, _ = logger.Get(logger.SubsystemTags.MINR)
|
14
domain/miningmanager/blocktemplatebuilder/policy.go
Normal file
14
domain/miningmanager/blocktemplatebuilder/policy.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// Copyright (c) 2014-2016 The btcsuite developers
|
||||||
|
// Use of this source code is governed by an ISC
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package blocktemplatebuilder
|
||||||
|
|
||||||
|
// policy houses the policy (configuration parameters) which is used to control
|
||||||
|
// the generation of block templates. See the documentation for
|
||||||
|
// NewBlockTemplate for more details on each of these parameters are used.
|
||||||
|
type policy struct {
|
||||||
|
// BlockMaxMass is the maximum block mass to be used when generating a
|
||||||
|
// block template.
|
||||||
|
BlockMaxMass uint64
|
||||||
|
}
|
212
domain/miningmanager/blocktemplatebuilder/txselection.go
Normal file
212
domain/miningmanager/blocktemplatebuilder/txselection.go
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
package blocktemplatebuilder
|
||||||
|
|
||||||
|
import (
|
||||||
|
consensusexternalapi "github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
|
||||||
|
"github.com/kaspanet/kaspad/domain/consensus/utils/hashserialization"
|
||||||
|
"github.com/kaspanet/kaspad/domain/consensus/utils/subnetworks"
|
||||||
|
"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 selectedTransactions struct {
|
||||||
|
selectedTxs []*consensusexternalapi.DomainTransaction
|
||||||
|
txMasses []uint64
|
||||||
|
txFees []uint64
|
||||||
|
totalMass uint64
|
||||||
|
totalFees uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectTransactions 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.
|
||||||
|
|
||||||
|
// selectTransactions 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 (btb *blockTemplateBuilder) selectTransactions(candidateTxs []*candidateTx) selectedTransactions {
|
||||||
|
txsForBlockTemplate := selectedTransactions{
|
||||||
|
selectedTxs: make([]*consensusexternalapi.DomainTransaction, 0, len(candidateTxs)),
|
||||||
|
txMasses: make([]uint64, 0, len(candidateTxs)),
|
||||||
|
txFees: make([]uint64, 0, len(candidateTxs)),
|
||||||
|
totalMass: 0,
|
||||||
|
totalFees: 0,
|
||||||
|
}
|
||||||
|
usedCount, usedP := 0, 0.0
|
||||||
|
candidateTxs, totalP := rebalanceCandidates(candidateTxs, true)
|
||||||
|
gasUsageMap := make(map[consensusexternalapi.DomainSubnetworkID]uint64)
|
||||||
|
|
||||||
|
markCandidateTxForDeletion := func(candidateTx *candidateTx) {
|
||||||
|
candidateTx.isMarkedForDeletion = true
|
||||||
|
usedCount++
|
||||||
|
usedP += candidateTx.p
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedTxs := make([]*candidateTx, 0)
|
||||||
|
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 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tx := selectedTx.DomainTransaction
|
||||||
|
|
||||||
|
// Enforce maximum transaction mass per block. Also check
|
||||||
|
// for overflow.
|
||||||
|
if txsForBlockTemplate.totalMass+selectedTx.Mass < txsForBlockTemplate.totalMass ||
|
||||||
|
txsForBlockTemplate.totalMass+selectedTx.Mass > btb.policy.BlockMaxMass {
|
||||||
|
log.Tracef("Tx %s would exceed the max block mass. "+
|
||||||
|
"As such, stopping.", hashserialization.TransactionID(tx))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enforce maximum gas per subnetwork per block. Also check
|
||||||
|
// for overflow.
|
||||||
|
if !subnetworks.IsBuiltInOrNative(tx.SubnetworkID) {
|
||||||
|
subnetworkID := tx.SubnetworkID
|
||||||
|
gasUsage, ok := gasUsageMap[subnetworkID]
|
||||||
|
if !ok {
|
||||||
|
gasUsage = 0
|
||||||
|
}
|
||||||
|
txGas := tx.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.",
|
||||||
|
hashserialization.TransactionID(tx), subnetworkID)
|
||||||
|
for _, candidateTx := range candidateTxs {
|
||||||
|
// candidateTxs are ordered by subnetwork, so we can safely assume
|
||||||
|
// that transactions after subnetworkID will not be relevant.
|
||||||
|
if subnetworks.Less(subnetworkID, candidateTx.SubnetworkID) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if candidateTx.SubnetworkID == 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.
|
||||||
|
selectedTxs = append(selectedTxs, selectedTx)
|
||||||
|
txsForBlockTemplate.totalMass += selectedTx.Mass
|
||||||
|
txsForBlockTemplate.totalFees += selectedTx.Fee
|
||||||
|
|
||||||
|
log.Tracef("Adding tx %s (feePerMegaGram %d)",
|
||||||
|
hashserialization.TransactionID(tx), selectedTx.Fee*1e6/selectedTx.Mass)
|
||||||
|
|
||||||
|
markCandidateTxForDeletion(selectedTx)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(selectedTxs, func(i, j int) bool {
|
||||||
|
return subnetworks.Less(selectedTxs[i].SubnetworkID, selectedTxs[j].SubnetworkID)
|
||||||
|
})
|
||||||
|
for _, selectedTx := range selectedTxs {
|
||||||
|
txsForBlockTemplate.selectedTxs = append(txsForBlockTemplate.selectedTxs, selectedTx.DomainTransaction)
|
||||||
|
txsForBlockTemplate.txMasses = append(txsForBlockTemplate.txMasses, selectedTx.Mass)
|
||||||
|
txsForBlockTemplate.txFees = append(txsForBlockTemplate.txFees, selectedTx.Fee)
|
||||||
|
}
|
||||||
|
return txsForBlockTemplate
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -8,15 +8,15 @@ import (
|
|||||||
|
|
||||||
// Factory instantiates new mining managers
|
// Factory instantiates new mining managers
|
||||||
type Factory interface {
|
type Factory interface {
|
||||||
NewMiningManager(consensus *consensus.Consensus) MiningManager
|
NewMiningManager(consensus consensus.Consensus, blockMaxMass uint64) MiningManager
|
||||||
}
|
}
|
||||||
|
|
||||||
type factory struct{}
|
type factory struct{}
|
||||||
|
|
||||||
// NewMiningManager instantiate a new mining manager
|
// NewMiningManager instantiate a new mining manager
|
||||||
func (f *factory) NewMiningManager(consensus *consensus.Consensus) MiningManager {
|
func (f *factory) NewMiningManager(consensus consensus.Consensus, blockMaxMass uint64) MiningManager {
|
||||||
mempool := mempoolpkg.New(consensus)
|
mempool := mempoolpkg.New(consensus)
|
||||||
blockTemplateBuilder := blocktemplatebuilder.New(consensus, mempool)
|
blockTemplateBuilder := blocktemplatebuilder.New(consensus, mempool, blockMaxMass)
|
||||||
|
|
||||||
return &miningManager{
|
return &miningManager{
|
||||||
mempool: mempool,
|
mempool: mempool,
|
||||||
|
72
domain/miningmanager/mempool/README.md
Normal file
72
domain/miningmanager/mempool/README.md
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
mempool
|
||||||
|
=======
|
||||||
|
|
||||||
|
[](https://choosealicense.com/licenses/isc/)
|
||||||
|
[](http://godoc.org/github.com/kaspanet/kaspad/mempool)
|
||||||
|
|
||||||
|
Package mempool provides a policy-enforced pool of unmined kaspa transactions.
|
||||||
|
|
||||||
|
A key responsbility of the kaspa network is mining user-generated transactions
|
||||||
|
into blocks. In order to facilitate this, the mining process relies on having a
|
||||||
|
readily-available source of transactions to include in a block that is being
|
||||||
|
solved.
|
||||||
|
|
||||||
|
At a high level, this package satisfies that requirement by providing an
|
||||||
|
in-memory pool of fully validated transactions that can also optionally be
|
||||||
|
further filtered based upon a configurable policy.
|
||||||
|
|
||||||
|
One of the policy configuration options controls whether or not "standard"
|
||||||
|
transactions are accepted. In essence, a "standard" transaction is one that
|
||||||
|
satisfies a fairly strict set of requirements that are largely intended to help
|
||||||
|
provide fair use of the system to all users. It is important to note that what
|
||||||
|
is considered a "standard" transaction changes over time. For some insight, at
|
||||||
|
the time of this writing, an example of _some_ of the criteria that are required
|
||||||
|
for a transaction to be considered standard are that it is of the most-recently
|
||||||
|
supported version, finalized, does not exceed a specific size, and only consists
|
||||||
|
of specific script forms.
|
||||||
|
|
||||||
|
Since this package does not deal with other kaspa specifics such as network
|
||||||
|
communication and transaction relay, it returns a list of transactions that were
|
||||||
|
accepted which gives the caller a high level of flexibility in how they want to
|
||||||
|
proceed. Typically, this will involve things such as relaying the transactions
|
||||||
|
to other peers on the network and notifying the mining process that new
|
||||||
|
transactions are available.
|
||||||
|
|
||||||
|
This package has intentionally been designed so it can be used as a standalone
|
||||||
|
package for any projects needing the ability create an in-memory pool of bitcoin
|
||||||
|
transactions that are not only valid by consensus rules, but also adhere to a
|
||||||
|
configurable policy.
|
||||||
|
|
||||||
|
## Feature Overview
|
||||||
|
|
||||||
|
The following is a quick overview of the major features. It is not intended to
|
||||||
|
be an exhaustive list.
|
||||||
|
|
||||||
|
- Maintain a pool of fully validated transactions
|
||||||
|
- Reject non-fully-spent duplicate transactions
|
||||||
|
- Reject coinbase transactions
|
||||||
|
- Reject double spends (both from the DAG and other transactions in pool)
|
||||||
|
- Reject invalid transactions according to the network consensus rules
|
||||||
|
- Full script execution and validation with signature cache support
|
||||||
|
- Individual transaction query support
|
||||||
|
- Orphan transaction support (transactions that spend from unknown outputs)
|
||||||
|
- Configurable limits (see transaction acceptance policy)
|
||||||
|
- Automatic addition of orphan transactions that are no longer orphans as new
|
||||||
|
transactions are added to the pool
|
||||||
|
- Individual orphan transaction query support
|
||||||
|
- Configurable transaction acceptance policy
|
||||||
|
- Option to accept or reject standard transactions
|
||||||
|
- Option to accept or reject transactions based on priority calculations
|
||||||
|
- Rate limiting of low-fee and free transactions
|
||||||
|
- Non-zero fee threshold
|
||||||
|
- Max signature operations per transaction
|
||||||
|
- Max orphan transaction size
|
||||||
|
- Max number of orphan transactions allowed
|
||||||
|
- Additional metadata tracking for each transaction
|
||||||
|
- Timestamp when the transaction was added to the pool
|
||||||
|
- Most recent block height when the transaction was added to the pool
|
||||||
|
- The fee the transaction pays
|
||||||
|
- The starting priority for the transaction
|
||||||
|
- Manual control of transaction removal
|
||||||
|
- Recursive removal of all dependent transactions
|
||||||
|
|
73
domain/miningmanager/mempool/doc.go
Normal file
73
domain/miningmanager/mempool/doc.go
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
Package mempool provides a policy-enforced pool of unmined kaspa transactions.
|
||||||
|
|
||||||
|
A key responsbility of the kaspa network is mining user-generated transactions
|
||||||
|
into blocks. In order to facilitate this, the mining process relies on having a
|
||||||
|
readily-available source of transactions to include in a block that is being
|
||||||
|
solved.
|
||||||
|
|
||||||
|
At a high level, this package satisfies that requirement by providing an
|
||||||
|
in-memory pool of fully validated transactions that can also optionally be
|
||||||
|
further filtered based upon a configurable policy.
|
||||||
|
|
||||||
|
One of the policy configuration options controls whether or not "standard"
|
||||||
|
transactions are accepted. In essence, a "standard" transaction is one that
|
||||||
|
satisfies a fairly strict set of requirements that are largely intended to help
|
||||||
|
provide fair use of the system to all users. It is important to note that what
|
||||||
|
is considered a "standard" transaction changes over time. For some insight, at
|
||||||
|
the time of this writing, an example of SOME of the criteria that are required
|
||||||
|
for a transaction to be considered standard are that it is of the most-recently
|
||||||
|
supported version, finalized, does not exceed a specific size, and only consists
|
||||||
|
of specific script forms.
|
||||||
|
|
||||||
|
Since this package does not deal with other kaspa specifics such as network
|
||||||
|
communication and transaction relay, it returns a list of transactions that were
|
||||||
|
accepted which gives the caller a high level of flexibility in how they want to
|
||||||
|
proceed. Typically, this will involve things such as relaying the transactions
|
||||||
|
to other peers on the network and notifying the mining process that new
|
||||||
|
transactions are available.
|
||||||
|
|
||||||
|
Feature Overview
|
||||||
|
|
||||||
|
The following is a quick overview of the major features. It is not intended to
|
||||||
|
be an exhaustive list.
|
||||||
|
|
||||||
|
- Maintain a pool of fully validated transactions
|
||||||
|
- Reject non-fully-spent duplicate transactions
|
||||||
|
- Reject coinbase transactions
|
||||||
|
- Reject double spends (both from the DAG and other transactions in pool)
|
||||||
|
- Reject invalid transactions according to the network consensus rules
|
||||||
|
- Full script execution and validation with signature cache support
|
||||||
|
- Individual transaction query support
|
||||||
|
- Orphan transaction support (transactions that spend from unknown outputs)
|
||||||
|
- Configurable limits (see transaction acceptance policy)
|
||||||
|
- Automatic addition of orphan transactions that are no longer orphans as new
|
||||||
|
transactions are added to the pool
|
||||||
|
- Individual orphan transaction query support
|
||||||
|
- Configurable transaction acceptance policy
|
||||||
|
- Option to accept or reject standard transactions
|
||||||
|
- Option to accept or reject transactions based on priority calculations
|
||||||
|
- Max signature operations per transaction
|
||||||
|
- Max number of orphan transactions allowed
|
||||||
|
- Additional metadata tracking for each transaction
|
||||||
|
- Timestamp when the transaction was added to the pool
|
||||||
|
- The fee the transaction pays
|
||||||
|
- The starting priority for the transaction
|
||||||
|
- Manual control of transaction removal
|
||||||
|
- Recursive removal of all dependent transactions
|
||||||
|
|
||||||
|
Errors
|
||||||
|
|
||||||
|
Errors returned by this package are either the raw errors provided by underlying
|
||||||
|
calls or of type mempool.RuleError. Since there are two classes of rules
|
||||||
|
(mempool acceptance rules and blockDAG (consensus) acceptance rules), the
|
||||||
|
mempool.RuleError type contains a single Err field which will, in turn, either
|
||||||
|
be a mempool.TxRuleError or a blockdag.RuleError. The first indicates a
|
||||||
|
violation of mempool acceptance rules while the latter indicates a violation of
|
||||||
|
consensus acceptance rules. This allows the caller to easily differentiate
|
||||||
|
between unexpected errors, such as database errors, versus errors due to rule
|
||||||
|
violations through type assertions. In addition, callers can programmatically
|
||||||
|
determine the specific rule violation by type asserting the Err field to one of
|
||||||
|
the aforementioned types and examining their underlying ErrorCode field.
|
||||||
|
*/
|
||||||
|
package mempool
|
150
domain/miningmanager/mempool/error.go
Normal file
150
domain/miningmanager/mempool/error.go
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
// Copyright (c) 2014-2016 The btcsuite developers
|
||||||
|
// Use of this source code is governed by an ISC
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package mempool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/kaspanet/kaspad/domain/blockdag"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RuleError identifies a rule violation. It is used to indicate that
|
||||||
|
// processing of a transaction failed due to one of the many validation
|
||||||
|
// rules. The caller can use type assertions to determine if a failure was
|
||||||
|
// specifically due to a rule violation and use the Err field to access the
|
||||||
|
// underlying error, which will be either a TxRuleError or a
|
||||||
|
// blockdag.RuleError.
|
||||||
|
type RuleError struct {
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error satisfies the error interface and prints human-readable errors.
|
||||||
|
func (e RuleError) Error() string {
|
||||||
|
if e.Err == nil {
|
||||||
|
return "<nil>"
|
||||||
|
}
|
||||||
|
return e.Err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RejectCode represents a numeric value by which a remote peer indicates
|
||||||
|
// why a message was rejected.
|
||||||
|
type RejectCode uint8
|
||||||
|
|
||||||
|
// These constants define the various supported reject codes.
|
||||||
|
const (
|
||||||
|
RejectMalformed RejectCode = 0x01
|
||||||
|
RejectInvalid RejectCode = 0x10
|
||||||
|
RejectObsolete RejectCode = 0x11
|
||||||
|
RejectDuplicate RejectCode = 0x12
|
||||||
|
RejectNotRequested RejectCode = 0x13
|
||||||
|
RejectNonstandard RejectCode = 0x40
|
||||||
|
RejectDust RejectCode = 0x41
|
||||||
|
RejectInsufficientFee RejectCode = 0x42
|
||||||
|
RejectFinality RejectCode = 0x43
|
||||||
|
RejectDifficulty RejectCode = 0x44
|
||||||
|
)
|
||||||
|
|
||||||
|
// Map of reject codes back strings for pretty printing.
|
||||||
|
var rejectCodeStrings = map[RejectCode]string{
|
||||||
|
RejectMalformed: "REJECT_MALFORMED",
|
||||||
|
RejectInvalid: "REJECT_INVALID",
|
||||||
|
RejectObsolete: "REJECT_OBSOLETE",
|
||||||
|
RejectDuplicate: "REJECT_DUPLICATE",
|
||||||
|
RejectNonstandard: "REJECT_NONSTANDARD",
|
||||||
|
RejectDust: "REJECT_DUST",
|
||||||
|
RejectInsufficientFee: "REJECT_INSUFFICIENTFEE",
|
||||||
|
RejectFinality: "REJECT_FINALITY",
|
||||||
|
RejectDifficulty: "REJECT_DIFFICULTY",
|
||||||
|
RejectNotRequested: "REJECT_NOTREQUESTED",
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the RejectCode in human-readable form.
|
||||||
|
func (code RejectCode) String() string {
|
||||||
|
if s, ok := rejectCodeStrings[code]; ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("Unknown RejectCode (%d)", uint8(code))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TxRuleError identifies a rule violation. It is used to indicate that
|
||||||
|
// processing of a transaction failed due to one of the many validation
|
||||||
|
// rules. The caller can use type assertions to determine if a failure was
|
||||||
|
// specifically due to a rule violation and access the ErrorCode field to
|
||||||
|
// ascertain the specific reason for the rule violation.
|
||||||
|
type TxRuleError struct {
|
||||||
|
RejectCode RejectCode // The code to send with reject messages
|
||||||
|
Description string // Human readable description of the issue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error satisfies the error interface and prints human-readable errors.
|
||||||
|
func (e TxRuleError) Error() string {
|
||||||
|
return e.Description
|
||||||
|
}
|
||||||
|
|
||||||
|
// txRuleError creates an underlying TxRuleError with the given a set of
|
||||||
|
// arguments and returns a RuleError that encapsulates it.
|
||||||
|
func txRuleError(c RejectCode, desc string) RuleError {
|
||||||
|
return RuleError{
|
||||||
|
Err: TxRuleError{RejectCode: c, Description: desc},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dagRuleError returns a RuleError that encapsulates the given
|
||||||
|
// blockdag.RuleError.
|
||||||
|
func dagRuleError(dagErr blockdag.RuleError) RuleError {
|
||||||
|
return RuleError{
|
||||||
|
Err: dagErr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractRejectCode attempts to return a relevant reject code for a given error
|
||||||
|
// by examining the error for known types. It will return true if a code
|
||||||
|
// was successfully extracted.
|
||||||
|
func extractRejectCode(err error) (RejectCode, bool) {
|
||||||
|
// Pull the underlying error out of a RuleError.
|
||||||
|
var ruleErr RuleError
|
||||||
|
if ok := errors.As(err, &ruleErr); ok {
|
||||||
|
err = ruleErr.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
var dagRuleErr blockdag.RuleError
|
||||||
|
if errors.As(err, &dagRuleErr) {
|
||||||
|
// Convert the DAG error to a reject code.
|
||||||
|
var code RejectCode
|
||||||
|
switch dagRuleErr.ErrorCode {
|
||||||
|
// Rejected due to duplicate.
|
||||||
|
case blockdag.ErrDuplicateBlock:
|
||||||
|
code = RejectDuplicate
|
||||||
|
|
||||||
|
// Rejected due to obsolete version.
|
||||||
|
case blockdag.ErrBlockVersionTooOld:
|
||||||
|
code = RejectObsolete
|
||||||
|
|
||||||
|
// Rejected due to being earlier than the last finality point.
|
||||||
|
case blockdag.ErrFinalityPointTimeTooOld:
|
||||||
|
code = RejectFinality
|
||||||
|
case blockdag.ErrDifficultyTooLow:
|
||||||
|
code = RejectDifficulty
|
||||||
|
|
||||||
|
// Everything else is due to the block or transaction being invalid.
|
||||||
|
default:
|
||||||
|
code = RejectInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
return code, true
|
||||||
|
}
|
||||||
|
|
||||||
|
var trErr TxRuleError
|
||||||
|
if errors.As(err, &trErr) {
|
||||||
|
return trErr.RejectCode, true
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return RejectInvalid, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return RejectInvalid, false
|
||||||
|
}
|
11
domain/miningmanager/mempool/log.go
Normal file
11
domain/miningmanager/mempool/log.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// Copyright (c) 2013-2016 The btcsuite developers
|
||||||
|
// Use of this source code is governed by an ISC
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package mempool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/kaspanet/kaspad/infrastructure/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var log, _ = logger.Get(logger.SubsystemTags.TXMP)
|
@ -1,36 +1,895 @@
|
|||||||
|
// Copyright (c) 2013-2016 The btcsuite developers
|
||||||
|
// Use of this source code is governed by an ISC
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package mempool
|
package mempool
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"container/list"
|
||||||
|
"fmt"
|
||||||
"github.com/kaspanet/kaspad/domain/consensus"
|
"github.com/kaspanet/kaspad/domain/consensus"
|
||||||
consensusexternalapi "github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
|
consensusexternalapi "github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
|
||||||
|
"github.com/kaspanet/kaspad/domain/consensus/ruleerrors"
|
||||||
|
"github.com/kaspanet/kaspad/domain/consensus/utils/estimatedsize"
|
||||||
|
"github.com/kaspanet/kaspad/domain/consensus/utils/hashserialization"
|
||||||
miningmanagermodel "github.com/kaspanet/kaspad/domain/miningmanager/model"
|
miningmanagermodel "github.com/kaspanet/kaspad/domain/miningmanager/model"
|
||||||
|
"github.com/kaspanet/kaspad/infrastructure/logger"
|
||||||
|
"github.com/kaspanet/kaspad/util"
|
||||||
|
"github.com/kaspanet/kaspad/util/mstime"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// mempool maintains a set of known transactions that
|
const (
|
||||||
// are intended to be mined into new blocks
|
// orphanTTL is the maximum amount of time an orphan is allowed to
|
||||||
type mempool struct {
|
// stay in the orphan pool before it expires and is evicted during the
|
||||||
consensus *consensus.Consensus
|
// next scan.
|
||||||
|
orphanTTL = time.Minute * 15
|
||||||
|
|
||||||
|
// orphanExpireScanInterval is the minimum amount of time in between
|
||||||
|
// scans of the orphan pool to evict expired transactions.
|
||||||
|
orphanExpireScanInterval = time.Minute * 5
|
||||||
|
)
|
||||||
|
|
||||||
|
// policy houses the policy (configuration parameters) which is used to
|
||||||
|
// control the mempool.
|
||||||
|
type policy struct {
|
||||||
|
// MaxTxVersion is the transaction version that the mempool should
|
||||||
|
// accept. All transactions above this version are rejected as
|
||||||
|
// non-standard.
|
||||||
|
MaxTxVersion int32
|
||||||
|
|
||||||
|
// AcceptNonStd defines whether to accept non-standard transactions. If
|
||||||
|
// true, non-standard transactions will be accepted into the mempool.
|
||||||
|
// Otherwise, all non-standard transactions will be rejected.
|
||||||
|
AcceptNonStd bool
|
||||||
|
|
||||||
|
// MaxOrphanTxs is the maximum number of orphan transactions
|
||||||
|
// that can be queued.
|
||||||
|
MaxOrphanTxs int
|
||||||
|
|
||||||
|
// MaxOrphanTxSize is the maximum size allowed for orphan transactions.
|
||||||
|
// This helps prevent memory exhaustion attacks from sending a lot of
|
||||||
|
// of big orphans.
|
||||||
|
MaxOrphanTxSize int
|
||||||
|
|
||||||
|
// MinRelayTxFee defines the minimum transaction fee in KAS/kB to be
|
||||||
|
// considered a non-zero fee.
|
||||||
|
MinRelayTxFee util.Amount
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new mempool
|
// mempool is used as a source of transactions that need to be mined into blocks
|
||||||
func New(consensus *consensus.Consensus) miningmanagermodel.Mempool {
|
// and relayed to other peers. It is safe for concurrent access from multiple
|
||||||
|
// peers.
|
||||||
|
type mempool struct {
|
||||||
|
pool map[consensusexternalapi.DomainTransactionID]*txDescriptor
|
||||||
|
|
||||||
|
chainedTransactions map[consensusexternalapi.DomainTransactionID]*txDescriptor
|
||||||
|
chainedTransactionByPreviousOutpoint map[consensusexternalapi.DomainOutpoint]*txDescriptor
|
||||||
|
|
||||||
|
orphans map[consensusexternalapi.DomainTransactionID]*orphanTx
|
||||||
|
orphansByPrev map[consensusexternalapi.DomainOutpoint]map[consensusexternalapi.DomainTransactionID]*consensusexternalapi.DomainTransaction
|
||||||
|
|
||||||
|
mempoolUTXOSet *mempoolUTXOSet
|
||||||
|
consensus consensus.Consensus
|
||||||
|
|
||||||
|
// nextExpireScan is the time after which the orphan pool will be
|
||||||
|
// scanned in order to evict orphans. This is NOT a hard deadline as
|
||||||
|
// the scan will only run when an orphan is added to the pool as opposed
|
||||||
|
// to on an unconditional timer.
|
||||||
|
nextExpireScan mstime.Time
|
||||||
|
|
||||||
|
mtx sync.RWMutex
|
||||||
|
policy policy
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new memory pool for validating and storing standalone
|
||||||
|
// transactions until they are mined into a block.
|
||||||
|
func New(consensus consensus.Consensus) miningmanagermodel.Mempool {
|
||||||
|
policy := policy{
|
||||||
|
MaxTxVersion: 0,
|
||||||
|
AcceptNonStd: false,
|
||||||
|
MaxOrphanTxs: 5,
|
||||||
|
MaxOrphanTxSize: 100000,
|
||||||
|
MinRelayTxFee: 1000, // 1 sompi per byte
|
||||||
|
}
|
||||||
return &mempool{
|
return &mempool{
|
||||||
consensus: consensus,
|
mtx: sync.RWMutex{},
|
||||||
|
policy: policy,
|
||||||
|
pool: make(map[consensusexternalapi.DomainTransactionID]*txDescriptor),
|
||||||
|
chainedTransactions: make(map[consensusexternalapi.DomainTransactionID]*txDescriptor),
|
||||||
|
chainedTransactionByPreviousOutpoint: make(map[consensusexternalapi.DomainOutpoint]*txDescriptor),
|
||||||
|
orphans: make(map[consensusexternalapi.DomainTransactionID]*orphanTx),
|
||||||
|
orphansByPrev: make(map[consensusexternalapi.DomainOutpoint]map[consensusexternalapi.DomainTransactionID]*consensusexternalapi.DomainTransaction),
|
||||||
|
mempoolUTXOSet: newMempoolUTXOSet(),
|
||||||
|
consensus: consensus,
|
||||||
|
nextExpireScan: mstime.Now().Add(orphanExpireScanInterval),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleNewBlock handles a new block that was just added to the DAG
|
// txDescriptor is a descriptor containing a transaction in the mempool along with
|
||||||
func (mp *mempool) HandleNewBlock(block *consensusexternalapi.DomainBlock) {
|
// additional metadata.
|
||||||
|
type txDescriptor struct {
|
||||||
|
*consensusexternalapi.DomainTransaction
|
||||||
|
|
||||||
|
// depCount is not 0 for a chained transaction. A chained transaction is
|
||||||
|
// one that is accepted to pool, but cannot be mined in next block because it
|
||||||
|
// depends on outputs of accepted, but still not mined transaction
|
||||||
|
depCount int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transactions returns all the transactions in the mempool
|
// orphanTx is normal transaction that references an ancestor transaction
|
||||||
|
// that is not yet available. It also contains additional information related
|
||||||
|
// to it such as an expiration time to help prevent caching the orphan forever.
|
||||||
|
type orphanTx struct {
|
||||||
|
tx *consensusexternalapi.DomainTransaction
|
||||||
|
expiration mstime.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeOrphan removes the passed orphan transaction from the orphan pool and
|
||||||
|
// previous orphan index.
|
||||||
|
//
|
||||||
|
// This function MUST be called with the mempool lock held (for writes).
|
||||||
|
func (mp *mempool) removeOrphan(tx *consensusexternalapi.DomainTransaction, removeRedeemers bool) {
|
||||||
|
// Nothing to do if passed tx is not an orphan.
|
||||||
|
txID := hashserialization.TransactionID(tx)
|
||||||
|
otx, exists := mp.orphans[*txID]
|
||||||
|
if !exists {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the reference from the previous orphan index.
|
||||||
|
for _, txIn := range otx.tx.Inputs {
|
||||||
|
orphans, exists := mp.orphansByPrev[txIn.PreviousOutpoint]
|
||||||
|
if exists {
|
||||||
|
delete(orphans, *txID)
|
||||||
|
|
||||||
|
// Remove the map entry altogether if there are no
|
||||||
|
// longer any orphans which depend on it.
|
||||||
|
if len(orphans) == 0 {
|
||||||
|
delete(mp.orphansByPrev, txIn.PreviousOutpoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any orphans that redeem outputs from this one if requested.
|
||||||
|
if removeRedeemers {
|
||||||
|
prevOut := consensusexternalapi.DomainOutpoint{TransactionID: *txID}
|
||||||
|
for txOutIdx := range tx.Outputs {
|
||||||
|
prevOut.Index = uint32(txOutIdx)
|
||||||
|
for _, orphan := range mp.orphansByPrev[prevOut] {
|
||||||
|
mp.removeOrphan(orphan, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the transaction from the orphan pool.
|
||||||
|
delete(mp.orphans, *txID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// limitNumOrphans limits the number of orphan transactions by evicting a random
|
||||||
|
// orphan if adding a new one would cause it to overflow the max allowed.
|
||||||
|
//
|
||||||
|
// This function MUST be called with the mempool lock held (for writes).
|
||||||
|
func (mp *mempool) limitNumOrphans() error {
|
||||||
|
// Scan through the orphan pool and remove any expired orphans when it's
|
||||||
|
// time. This is done for efficiency so the scan only happens
|
||||||
|
// periodically instead of on every orphan added to the pool.
|
||||||
|
if now := mstime.Now(); now.After(mp.nextExpireScan) {
|
||||||
|
origNumOrphans := len(mp.orphans)
|
||||||
|
for _, otx := range mp.orphans {
|
||||||
|
if now.After(otx.expiration) {
|
||||||
|
// Remove redeemers too because the missing
|
||||||
|
// parents are very unlikely to ever materialize
|
||||||
|
// since the orphan has already been around more
|
||||||
|
// than long enough for them to be delivered.
|
||||||
|
mp.removeOrphan(otx.tx, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set next expiration scan to occur after the scan interval.
|
||||||
|
mp.nextExpireScan = now.Add(orphanExpireScanInterval)
|
||||||
|
|
||||||
|
numOrphans := len(mp.orphans)
|
||||||
|
if numExpired := origNumOrphans - numOrphans; numExpired > 0 {
|
||||||
|
log.Debugf("Expired %d %s (remaining: %d)", numExpired,
|
||||||
|
logger.PickNoun(uint64(numExpired), "orphan", "orphans"),
|
||||||
|
numOrphans)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing to do if adding another orphan will not cause the pool to
|
||||||
|
// exceed the limit.
|
||||||
|
if len(mp.orphans)+1 <= mp.policy.MaxOrphanTxs {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove a random entry from the map. For most compilers, Go's
|
||||||
|
// range statement iterates starting at a random item although
|
||||||
|
// that is not 100% guaranteed by the spec. The iteration order
|
||||||
|
// is not important here because an adversary would have to be
|
||||||
|
// able to pull off preimage attacks on the hashing function in
|
||||||
|
// order to target eviction of specific entries anyways.
|
||||||
|
for _, otx := range mp.orphans {
|
||||||
|
// Don't remove redeemers in the case of a random eviction since
|
||||||
|
// it is quite possible it might be needed again shortly.
|
||||||
|
mp.removeOrphan(otx.tx, false)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// addOrphan adds an orphan transaction to the orphan pool.
|
||||||
|
//
|
||||||
|
// This function MUST be called with the mempool lock held (for writes).
|
||||||
|
func (mp *mempool) addOrphan(tx *consensusexternalapi.DomainTransaction) {
|
||||||
|
// Nothing to do if no orphans are allowed.
|
||||||
|
if mp.policy.MaxOrphanTxs <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit the number orphan transactions to prevent memory exhaustion.
|
||||||
|
// This will periodically remove any expired orphans and evict a random
|
||||||
|
// orphan if space is still needed.
|
||||||
|
mp.limitNumOrphans()
|
||||||
|
txID := hashserialization.TransactionID(tx)
|
||||||
|
mp.orphans[*txID] = &orphanTx{
|
||||||
|
tx: tx,
|
||||||
|
expiration: mstime.Now().Add(orphanTTL),
|
||||||
|
}
|
||||||
|
for _, txIn := range tx.Inputs {
|
||||||
|
if _, exists := mp.orphansByPrev[txIn.PreviousOutpoint]; !exists {
|
||||||
|
mp.orphansByPrev[txIn.PreviousOutpoint] =
|
||||||
|
make(map[consensusexternalapi.DomainTransactionID]*consensusexternalapi.DomainTransaction)
|
||||||
|
}
|
||||||
|
mp.orphansByPrev[txIn.PreviousOutpoint][*txID] = tx
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Stored orphan transaction %s (total: %d)", hashserialization.TransactionID(tx),
|
||||||
|
len(mp.orphans))
|
||||||
|
}
|
||||||
|
|
||||||
|
// maybeAddOrphan potentially adds an orphan to the orphan pool.
|
||||||
|
//
|
||||||
|
// This function MUST be called with the mempool lock held (for writes).
|
||||||
|
func (mp *mempool) maybeAddOrphan(tx *consensusexternalapi.DomainTransaction) error {
|
||||||
|
// Ignore orphan transactions that are too large. This helps avoid
|
||||||
|
// a memory exhaustion attack based on sending a lot of really large
|
||||||
|
// orphans. In the case there is a valid transaction larger than this,
|
||||||
|
// it will ultimtely be rebroadcast after the parent transactions
|
||||||
|
// have been mined or otherwise received.
|
||||||
|
//
|
||||||
|
// Note that the number of orphan transactions in the orphan pool is
|
||||||
|
// also limited, so this equates to a maximum memory used of
|
||||||
|
// mp.policy.MaxOrphanTxSize * mp.policy.MaxOrphanTxs (which is ~5MB
|
||||||
|
// using the default values at the time this comment was written).
|
||||||
|
serializedLen := estimatedsize.TransactionEstimatedSerializedSize(tx)
|
||||||
|
if serializedLen > uint64(mp.policy.MaxOrphanTxSize) {
|
||||||
|
str := fmt.Sprintf("orphan transaction size of %d bytes is "+
|
||||||
|
"larger than max allowed size of %d bytes",
|
||||||
|
serializedLen, mp.policy.MaxOrphanTxSize)
|
||||||
|
return txRuleError(RejectNonstandard, str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the orphan if the none of the above disqualified it.
|
||||||
|
mp.addOrphan(tx)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeOrphanDoubleSpends removes all orphans which spend outputs spent by the
|
||||||
|
// passed transaction from the orphan pool. Removing those orphans then leads
|
||||||
|
// to removing all orphans which rely on them, recursively. This is necessary
|
||||||
|
// when a transaction is added to the main pool because it may spend outputs
|
||||||
|
// that orphans also spend.
|
||||||
|
//
|
||||||
|
// This function MUST be called with the mempool lock held (for writes).
|
||||||
|
func (mp *mempool) removeOrphanDoubleSpends(tx *consensusexternalapi.DomainTransaction) {
|
||||||
|
for _, txIn := range tx.Inputs {
|
||||||
|
for _, orphan := range mp.orphansByPrev[txIn.PreviousOutpoint] {
|
||||||
|
mp.removeOrphan(orphan, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isTransactionInPool returns whether or not the passed transaction already
|
||||||
|
// exists in the main pool.
|
||||||
|
//
|
||||||
|
// This function MUST be called with the mempool lock held (for reads).
|
||||||
|
func (mp *mempool) isTransactionInPool(txID *consensusexternalapi.DomainTransactionID) bool {
|
||||||
|
if _, exists := mp.pool[*txID]; exists {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return mp.isInDependPool(txID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isInDependPool returns whether or not the passed transaction already
|
||||||
|
// exists in the depend pool.
|
||||||
|
//
|
||||||
|
// This function MUST be called with the mempool lock held (for reads).
|
||||||
|
func (mp *mempool) isInDependPool(hash *consensusexternalapi.DomainTransactionID) bool {
|
||||||
|
if _, exists := mp.chainedTransactions[*hash]; exists {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// isOrphanInPool returns whether or not the passed transaction already exists
|
||||||
|
// in the orphan pool.
|
||||||
|
//
|
||||||
|
// This function MUST be called with the mempool lock held (for reads).
|
||||||
|
func (mp *mempool) isOrphanInPool(txID *consensusexternalapi.DomainTransactionID) bool {
|
||||||
|
if _, exists := mp.orphans[*txID]; exists {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// haveTransaction returns whether or not the passed transaction already exists
|
||||||
|
// in the main pool or in the orphan pool.
|
||||||
|
//
|
||||||
|
// This function MUST be called with the mempool lock held (for reads).
|
||||||
|
func (mp *mempool) haveTransaction(txID *consensusexternalapi.DomainTransactionID) bool {
|
||||||
|
return mp.isTransactionInPool(txID) || mp.isOrphanInPool(txID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeBlockTransactionsFromPool removes the transactions that are found in the block
|
||||||
|
// from the mempool, and move their chained mempool transactions (if any) to the main pool.
|
||||||
|
//
|
||||||
|
// This function MUST be called with the mempool lock held (for writes).
|
||||||
|
func (mp *mempool) removeBlockTransactionsFromPool(txs []*consensusexternalapi.DomainTransaction) error {
|
||||||
|
for _, tx := range txs[util.CoinbaseTransactionIndex+1:] {
|
||||||
|
txID := hashserialization.TransactionID(tx)
|
||||||
|
|
||||||
|
if _, exists := mp.fetchTxDesc(txID); !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err := mp.cleanTransactionFromSets(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
mp.updateBlockTransactionChainedTransactions(tx)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeTransactionAndItsChainedTransactions removes a transaction and all of its chained transaction from the mempool.
|
||||||
|
//
|
||||||
|
// This function MUST be called with the mempool lock held (for writes).
|
||||||
|
func (mp *mempool) removeTransactionAndItsChainedTransactions(tx *consensusexternalapi.DomainTransaction) error {
|
||||||
|
txID := hashserialization.TransactionID(tx)
|
||||||
|
// Remove any transactions which rely on this one.
|
||||||
|
for i := uint32(0); i < uint32(len(tx.Outputs)); i++ {
|
||||||
|
prevOut := consensusexternalapi.DomainOutpoint{TransactionID: *txID, Index: i}
|
||||||
|
if txRedeemer, exists := mp.mempoolUTXOSet.poolTransactionBySpendingOutpoint(prevOut); exists {
|
||||||
|
err := mp.removeTransactionAndItsChainedTransactions(txRedeemer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := mp.chainedTransactions[*txID]; exists {
|
||||||
|
mp.removeChainTransaction(tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := mp.cleanTransactionFromSets(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanTransactionFromSets removes the transaction from all mempool related transaction sets.
|
||||||
|
// It assumes that any chained transaction is already cleaned from the mempool.
|
||||||
|
//
|
||||||
|
// This function MUST be called with the mempool lock held (for writes).
|
||||||
|
func (mp *mempool) cleanTransactionFromSets(tx *consensusexternalapi.DomainTransaction) error {
|
||||||
|
err := mp.mempoolUTXOSet.removeTx(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
txID := hashserialization.TransactionID(tx)
|
||||||
|
delete(mp.pool, *txID)
|
||||||
|
delete(mp.chainedTransactions, *txID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateBlockTransactionChainedTransactions processes the dependencies of a
|
||||||
|
// transaction that was included in a block and was just now removed from the mempool.
|
||||||
|
//
|
||||||
|
// This function MUST be called with the mempool lock held (for writes).
|
||||||
|
|
||||||
|
func (mp *mempool) updateBlockTransactionChainedTransactions(tx *consensusexternalapi.DomainTransaction) {
|
||||||
|
prevOut := consensusexternalapi.DomainOutpoint{TransactionID: *hashserialization.TransactionID(tx)}
|
||||||
|
for txOutIdx := range tx.Outputs {
|
||||||
|
// Skip to the next available output if there are none.
|
||||||
|
prevOut.Index = uint32(txOutIdx)
|
||||||
|
txDesc, exists := mp.chainedTransactionByPreviousOutpoint[prevOut]
|
||||||
|
if !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
txDesc.depCount--
|
||||||
|
// If the transaction is not chained anymore, move it into the main pool
|
||||||
|
if txDesc.depCount == 0 {
|
||||||
|
// Transaction may be already removed by recursive calls, if removeRedeemers is true.
|
||||||
|
// So avoid moving it into main pool
|
||||||
|
txDescID := hashserialization.TransactionID(txDesc.DomainTransaction)
|
||||||
|
if _, ok := mp.chainedTransactions[*txDescID]; ok {
|
||||||
|
delete(mp.chainedTransactions, *txDescID)
|
||||||
|
mp.pool[*txDescID] = txDesc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete(mp.chainedTransactionByPreviousOutpoint, prevOut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeChainTransaction removes a chain transaction and all of its relation as a result of double spend.
|
||||||
|
//
|
||||||
|
// This function MUST be called with the mempool lock held (for writes).
|
||||||
|
func (mp *mempool) removeChainTransaction(tx *consensusexternalapi.DomainTransaction) {
|
||||||
|
delete(mp.chainedTransactions, *hashserialization.TransactionID(tx))
|
||||||
|
for _, txIn := range tx.Inputs {
|
||||||
|
delete(mp.chainedTransactionByPreviousOutpoint, txIn.PreviousOutpoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeDoubleSpends removes all transactions which spend outputs spent by the
|
||||||
|
// passed transaction from the memory pool. Removing those transactions then
|
||||||
|
// leads to removing all transactions which rely on them, recursively. This is
|
||||||
|
// necessary when a block is connected to the DAG because the block may
|
||||||
|
// contain transactions which were previously unknown to the memory pool.
|
||||||
|
//
|
||||||
|
// This function MUST be called with the mempool lock held (for writes).
|
||||||
|
func (mp *mempool) removeDoubleSpends(tx *consensusexternalapi.DomainTransaction) error {
|
||||||
|
txID := *hashserialization.TransactionID(tx)
|
||||||
|
for _, txIn := range tx.Inputs {
|
||||||
|
if txRedeemer, ok := mp.mempoolUTXOSet.poolTransactionBySpendingOutpoint(txIn.PreviousOutpoint); ok {
|
||||||
|
if !(*hashserialization.TransactionID(txRedeemer) == txID) {
|
||||||
|
err := mp.removeTransactionAndItsChainedTransactions(txRedeemer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// addTransaction adds the passed transaction to the memory pool. It should
|
||||||
|
// not be called directly as it doesn't perform any validation. This is a
|
||||||
|
// helper for maybeAcceptTransaction.
|
||||||
|
//
|
||||||
|
// This function MUST be called with the mempool lock held (for writes).
|
||||||
|
func (mp *mempool) addTransaction(tx *consensusexternalapi.DomainTransaction, mass uint64, fee uint64, parentsInPool []consensusexternalapi.DomainOutpoint) (*txDescriptor, error) {
|
||||||
|
// Add the transaction to the pool and mark the referenced outpoints
|
||||||
|
// as spent by the pool.
|
||||||
|
txDescriptor := &txDescriptor{
|
||||||
|
DomainTransaction: tx,
|
||||||
|
depCount: len(parentsInPool),
|
||||||
|
}
|
||||||
|
txID := *hashserialization.TransactionID(tx)
|
||||||
|
|
||||||
|
if len(parentsInPool) == 0 {
|
||||||
|
mp.pool[txID] = txDescriptor
|
||||||
|
} else {
|
||||||
|
mp.chainedTransactions[txID] = txDescriptor
|
||||||
|
for _, previousOutpoint := range parentsInPool {
|
||||||
|
mp.chainedTransactionByPreviousOutpoint[previousOutpoint] = txDescriptor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := mp.mempoolUTXOSet.addTx(tx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return txDescriptor, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkPoolDoubleSpend checks whether or not the passed transaction is
|
||||||
|
// attempting to spend coins already spent by other transactions in the pool.
|
||||||
|
// Note it does not check for double spends against transactions already in the
|
||||||
|
// DAG.
|
||||||
|
//
|
||||||
|
// This function MUST be called with the mempool lock held (for reads).
|
||||||
|
func (mp *mempool) checkPoolDoubleSpend(tx *consensusexternalapi.DomainTransaction) error {
|
||||||
|
for _, txIn := range tx.Inputs {
|
||||||
|
if txR, exists := mp.mempoolUTXOSet.poolTransactionBySpendingOutpoint(txIn.PreviousOutpoint); exists {
|
||||||
|
str := fmt.Sprintf("output %s already spent by "+
|
||||||
|
"transaction %s in the memory pool",
|
||||||
|
txIn.PreviousOutpoint, hashserialization.TransactionID(txR))
|
||||||
|
return txRuleError(RejectDuplicate, str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function MUST be called with the mempool lock held (for reads).
|
||||||
|
// This only fetches from the main transaction pool and does not include
|
||||||
|
// orphans.
|
||||||
|
// returns false in the second return parameter if transaction was not found
|
||||||
|
func (mp *mempool) fetchTxDesc(txID *consensusexternalapi.DomainTransactionID) (*txDescriptor, bool) {
|
||||||
|
txDesc, exists := mp.pool[*txID]
|
||||||
|
if !exists {
|
||||||
|
txDesc, exists = mp.chainedTransactions[*txID]
|
||||||
|
}
|
||||||
|
return txDesc, exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// maybeAcceptTransaction is the main workhorse for handling insertion of new
|
||||||
|
// free-standing transactions into a memory pool. It includes functionality
|
||||||
|
// such as rejecting duplicate transactions, ensuring transactions follow all
|
||||||
|
// rules, detecting orphan transactions, and insertion into the memory pool.
|
||||||
|
//
|
||||||
|
// If the transaction is an orphan (missing parent transactions), the
|
||||||
|
// transaction is NOT added to the orphan pool, but each unknown referenced
|
||||||
|
// parent is returned. Use ProcessTransaction instead if new orphans should
|
||||||
|
// be added to the orphan pool.
|
||||||
|
//
|
||||||
|
// This function MUST be called with the mempool lock held (for writes).
|
||||||
|
func (mp *mempool) maybeAcceptTransaction(tx *consensusexternalapi.DomainTransaction, rejectDupOrphans bool) ([]consensusexternalapi.DomainOutpoint, *txDescriptor, error) {
|
||||||
|
txID := hashserialization.TransactionID(tx)
|
||||||
|
|
||||||
|
// Don't accept the transaction if it already exists in the pool. This
|
||||||
|
// applies to orphan transactions as well when the reject duplicate
|
||||||
|
// orphans flag is set. This check is intended to be a quick check to
|
||||||
|
// weed out duplicates.
|
||||||
|
if mp.isTransactionInPool(txID) || (rejectDupOrphans &&
|
||||||
|
mp.isOrphanInPool(txID)) {
|
||||||
|
|
||||||
|
str := fmt.Sprintf("already have transaction %s", txID)
|
||||||
|
return nil, nil, txRuleError(RejectDuplicate, str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't allow non-standard transactions if the network parameters
|
||||||
|
// forbid their acceptance.
|
||||||
|
if !mp.policy.AcceptNonStd {
|
||||||
|
err := checkTransactionStandard(tx, &mp.policy)
|
||||||
|
if err != nil {
|
||||||
|
// Attempt to extract a reject code from the error so
|
||||||
|
// it can be retained. When not possible, fall back to
|
||||||
|
// a non standard error.
|
||||||
|
rejectCode, found := extractRejectCode(err)
|
||||||
|
if !found {
|
||||||
|
rejectCode = RejectNonstandard
|
||||||
|
}
|
||||||
|
str := fmt.Sprintf("transaction %s is not standard: %s",
|
||||||
|
txID, err)
|
||||||
|
return nil, nil, txRuleError(rejectCode, str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The transaction may not use any of the same outputs as other
|
||||||
|
// transactions already in the pool as that would ultimately result in a
|
||||||
|
// double spend. This check is intended to be quick and therefore only
|
||||||
|
// detects double spends within the transaction pool itself. The
|
||||||
|
// transaction could still be double spending coins from the DAG
|
||||||
|
// at this point. There is a more in-depth check that happens later
|
||||||
|
// after fetching the referenced transaction inputs from the DAG
|
||||||
|
// which examines the actual spend data and prevents double spends.
|
||||||
|
err := mp.checkPoolDoubleSpend(tx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't allow the transaction if it exists in the DAG and is
|
||||||
|
// not already fully spent.
|
||||||
|
if mp.mempoolUTXOSet.checkExists(tx) {
|
||||||
|
return nil, nil, txRuleError(RejectDuplicate, "transaction already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction is an orphan if any of the referenced transaction outputs
|
||||||
|
// don't exist or are already spent. Adding orphans to the orphan pool
|
||||||
|
// is not handled by this function, and the caller should use
|
||||||
|
// maybeAddOrphan if this behavior is desired.
|
||||||
|
parentsInPool := mp.mempoolUTXOSet.populateUTXOEntries(tx)
|
||||||
|
|
||||||
|
// This will populate the missing UTXOEntries.
|
||||||
|
err = mp.consensus.ValidateTransactionAndPopulateWithConsensusData(tx)
|
||||||
|
missingOutpoints := ruleerrors.ErrMissingTxOut{}
|
||||||
|
if errors.As(err, &missingOutpoints) {
|
||||||
|
return missingOutpoints.MissingOutpoints, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't allow transactions with non-standard inputs if the network
|
||||||
|
// parameters forbid their acceptance.
|
||||||
|
if !mp.policy.AcceptNonStd {
|
||||||
|
err := checkInputsStandard(tx)
|
||||||
|
if err != nil {
|
||||||
|
// Attempt to extract a reject code from the error so
|
||||||
|
// it can be retained. When not possible, fall back to
|
||||||
|
// a non standard error.
|
||||||
|
rejectCode, found := extractRejectCode(err)
|
||||||
|
if !found {
|
||||||
|
rejectCode = RejectNonstandard
|
||||||
|
}
|
||||||
|
str := fmt.Sprintf("transaction %s has a non-standard "+
|
||||||
|
"input: %s", txID, err)
|
||||||
|
return nil, nil, txRuleError(rejectCode, str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//// NOTE: if you modify this code to accept non-standard transactions,
|
||||||
|
//// you should add code here to check that the transaction does a
|
||||||
|
//// reasonable number of ECDSA signature verifications.
|
||||||
|
//
|
||||||
|
|
||||||
|
// Don't allow transactions with fees too low to get into a mined block.
|
||||||
|
//
|
||||||
|
// Most miners allow a free transaction area in blocks they mine to go
|
||||||
|
// alongside the area used for high-priority transactions as well as
|
||||||
|
// transactions with fees. A transaction size of up to 1000 bytes is
|
||||||
|
// considered safe to go into this section. Further, the minimum fee
|
||||||
|
// calculated below on its own would encourage several small
|
||||||
|
// transactions to avoid fees rather than one single larger transaction
|
||||||
|
// which is more desirable. Therefore, as long as the size of the
|
||||||
|
// transaction does not exceeed 1000 less than the reserved space for
|
||||||
|
// high-priority transactions, don't require a fee for it.
|
||||||
|
serializedSize := int64(estimatedsize.TransactionEstimatedSerializedSize(tx))
|
||||||
|
minFee := uint64(calcMinRequiredTxRelayFee(serializedSize,
|
||||||
|
mp.policy.MinRelayTxFee))
|
||||||
|
if tx.Fee < minFee {
|
||||||
|
str := fmt.Sprintf("transaction %s has %d fees which is under "+
|
||||||
|
"the required amount of %d", txID, tx.Fee,
|
||||||
|
minFee)
|
||||||
|
return nil, nil, txRuleError(RejectInsufficientFee, str)
|
||||||
|
}
|
||||||
|
// Add to transaction pool.
|
||||||
|
txDesc, err := mp.addTransaction(tx, tx.Mass, tx.Fee, parentsInPool)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Accepted transaction %s (pool size: %d)", txID,
|
||||||
|
len(mp.pool))
|
||||||
|
|
||||||
|
return nil, txDesc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// processOrphans determines if there are any orphans which depend on the passed
|
||||||
|
// transaction hash (it is possible that they are no longer orphans) and
|
||||||
|
// potentially accepts them to the memory pool. It repeats the process for the
|
||||||
|
// newly accepted transactions (to detect further orphans which may no longer be
|
||||||
|
// orphans) until there are no more.
|
||||||
|
//
|
||||||
|
// It returns a slice of transactions added to the mempool. A nil slice means
|
||||||
|
// no transactions were moved from the orphan pool to the mempool.
|
||||||
|
//
|
||||||
|
// This function MUST be called with the mempool lock held (for writes).
|
||||||
|
func (mp *mempool) processOrphans(acceptedTx *consensusexternalapi.DomainTransaction) []*txDescriptor {
|
||||||
|
var acceptedTxns []*txDescriptor
|
||||||
|
|
||||||
|
// Start with processing at least the passed transaction.
|
||||||
|
processList := list.New()
|
||||||
|
processList.PushBack(acceptedTx)
|
||||||
|
for processList.Len() > 0 {
|
||||||
|
// Pop the transaction to process from the front of the list.
|
||||||
|
firstElement := processList.Remove(processList.Front())
|
||||||
|
processItem := firstElement.(*consensusexternalapi.DomainTransaction)
|
||||||
|
|
||||||
|
prevOut := consensusexternalapi.DomainOutpoint{TransactionID: *hashserialization.TransactionID(processItem)}
|
||||||
|
for txOutIdx := range processItem.Outputs {
|
||||||
|
// Look up all orphans that redeem the output that is
|
||||||
|
// now available. This will typically only be one, but
|
||||||
|
// it could be multiple if the orphan pool contains
|
||||||
|
// double spends. While it may seem odd that the orphan
|
||||||
|
// pool would allow this since there can only possibly
|
||||||
|
// ultimately be a single redeemer, it's important to
|
||||||
|
// track it this way to prevent malicious actors from
|
||||||
|
// being able to purposely constructing orphans that
|
||||||
|
// would otherwise make outputs unspendable.
|
||||||
|
//
|
||||||
|
// Skip to the next available output if there are none.
|
||||||
|
prevOut.Index = uint32(txOutIdx)
|
||||||
|
orphans, exists := mp.orphansByPrev[prevOut]
|
||||||
|
if !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Potentially accept an orphan into the tx pool.
|
||||||
|
for _, tx := range orphans {
|
||||||
|
missing, txD, err := mp.maybeAcceptTransaction(
|
||||||
|
tx, false)
|
||||||
|
if err != nil {
|
||||||
|
// The orphan is now invalid, so there
|
||||||
|
// is no way any other orphans which
|
||||||
|
// redeem any of its outputs can be
|
||||||
|
// accepted. Remove them.
|
||||||
|
mp.removeOrphan(tx, true)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction is still an orphan. Try the next
|
||||||
|
// orphan which redeems this output.
|
||||||
|
if len(missing) > 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction was accepted into the main pool.
|
||||||
|
//
|
||||||
|
// Add it to the list of accepted transactions
|
||||||
|
// that are no longer orphans, remove it from
|
||||||
|
// the orphan pool, and add it to the list of
|
||||||
|
// transactions to process so any orphans that
|
||||||
|
// depend on it are handled too.
|
||||||
|
acceptedTxns = append(acceptedTxns, txD)
|
||||||
|
mp.removeOrphan(tx, false)
|
||||||
|
processList.PushBack(tx)
|
||||||
|
|
||||||
|
// Only one transaction for this outpoint can be
|
||||||
|
// accepted, so the rest are now double spends
|
||||||
|
// and are removed later.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively remove any orphans that also redeem any outputs redeemed
|
||||||
|
// by the accepted transactions since those are now definitive double
|
||||||
|
// spends.
|
||||||
|
mp.removeOrphanDoubleSpends(acceptedTx)
|
||||||
|
for _, txDescriptor := range acceptedTxns {
|
||||||
|
mp.removeOrphanDoubleSpends(txDescriptor.DomainTransaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
return acceptedTxns
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessTransaction is the main workhorse for handling insertion of new
|
||||||
|
// free-standing transactions into the memory pool. It includes functionality
|
||||||
|
// such as rejecting duplicate transactions, ensuring transactions follow all
|
||||||
|
// rules, orphan transaction handling, and insertion into the memory pool.
|
||||||
|
//
|
||||||
|
// It returns a slice of transactions added to the mempool. When the
|
||||||
|
// error is nil, the list will include the passed transaction itself along
|
||||||
|
// with any additional orphan transaactions that were added as a result of
|
||||||
|
// the passed one being accepted.
|
||||||
|
//
|
||||||
|
// This function is safe for concurrent access.
|
||||||
|
func (mp *mempool) ValidateAndInsertTransaction(tx *consensusexternalapi.DomainTransaction, allowOrphan bool) error {
|
||||||
|
log.Tracef("Processing transaction %s", hashserialization.TransactionID(tx))
|
||||||
|
|
||||||
|
// Protect concurrent access.
|
||||||
|
mp.mtx.Lock()
|
||||||
|
defer mp.mtx.Unlock()
|
||||||
|
|
||||||
|
// Potentially accept the transaction to the memory pool.
|
||||||
|
missingParents, txD, err := mp.maybeAcceptTransaction(tx, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(missingParents) == 0 {
|
||||||
|
// Accept any orphan transactions that depend on this
|
||||||
|
// transaction (they may no longer be orphans if all inputs
|
||||||
|
// are now available) and repeat for those accepted
|
||||||
|
// transactions until there are no more.
|
||||||
|
newTxs := mp.processOrphans(tx)
|
||||||
|
acceptedTxs := make([]*txDescriptor, len(newTxs)+1)
|
||||||
|
|
||||||
|
// Add the parent transaction first so remote nodes
|
||||||
|
// do not add orphans.
|
||||||
|
acceptedTxs[0] = txD
|
||||||
|
copy(acceptedTxs[1:], newTxs)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// The transaction is an orphan (has inputs missing). Reject
|
||||||
|
// it if the flag to allow orphans is not set.
|
||||||
|
if !allowOrphan {
|
||||||
|
// Only use the first missing parent transaction in
|
||||||
|
// the error message.
|
||||||
|
//
|
||||||
|
// NOTE: RejectDuplicate is really not an accurate
|
||||||
|
// reject code here, but it matches the reference
|
||||||
|
// implementation and there isn't a better choice due
|
||||||
|
// to the limited number of reject codes. Missing
|
||||||
|
// inputs is assumed to mean they are already spent
|
||||||
|
// which is not really always the case.
|
||||||
|
str := fmt.Sprintf("orphan transaction %s references "+
|
||||||
|
"outputs of unknown or fully-spent "+
|
||||||
|
"transaction %s", hashserialization.TransactionID(tx), missingParents[0])
|
||||||
|
return txRuleError(RejectDuplicate, str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Potentially add the orphan transaction to the orphan pool.
|
||||||
|
return mp.maybeAddOrphan(tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count returns the number of transactions in the main pool. It does not
|
||||||
|
// include the orphan pool.
|
||||||
|
//
|
||||||
|
// This function is safe for concurrent access.
|
||||||
|
func (mp *mempool) Count() int {
|
||||||
|
mp.mtx.RLock()
|
||||||
|
defer mp.mtx.RUnlock()
|
||||||
|
count := len(mp.pool)
|
||||||
|
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChainedCount returns the number of chained transactions in the mempool. It does not
|
||||||
|
// include the orphan pool.
|
||||||
|
//
|
||||||
|
// This function is safe for concurrent access.
|
||||||
|
func (mp *mempool) ChainedCount() int {
|
||||||
|
mp.mtx.RLock()
|
||||||
|
defer mp.mtx.RUnlock()
|
||||||
|
return len(mp.chainedTransactions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transactions returns a slice of all the transactions in the block
|
||||||
|
// This is safe for concurrent use
|
||||||
func (mp *mempool) Transactions() []*consensusexternalapi.DomainTransaction {
|
func (mp *mempool) Transactions() []*consensusexternalapi.DomainTransaction {
|
||||||
return nil
|
mp.mtx.RLock()
|
||||||
|
defer mp.mtx.RUnlock()
|
||||||
|
descs := make([]*consensusexternalapi.DomainTransaction, len(mp.pool))
|
||||||
|
i := 0
|
||||||
|
for _, desc := range mp.pool {
|
||||||
|
descs[i] = desc.DomainTransaction
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return descs
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateAndInsertTransaction validates the given transaction, and
|
// HandleNewBlockTransactions removes all the transactions in the new block
|
||||||
// adds it to the mempool
|
// from the mempool and the orphan pool, and it also removes
|
||||||
func (mp *mempool) ValidateAndInsertTransaction(transaction *consensusexternalapi.DomainTransaction) error {
|
// from the mempool transactions that double spend a
|
||||||
return nil
|
// transaction that is already in the DAG
|
||||||
|
func (mp *mempool) HandleNewBlockTransactions(txs []*consensusexternalapi.DomainTransaction) {
|
||||||
|
// Protect concurrent access.
|
||||||
|
mp.mtx.Lock()
|
||||||
|
defer mp.mtx.Unlock()
|
||||||
|
|
||||||
|
// Remove all of the transactions (except the coinbase) in the
|
||||||
|
// connected block from the transaction pool. Secondly, remove any
|
||||||
|
// transactions which are now double spends as a result of these
|
||||||
|
// new transactions. Finally, remove any transaction that is
|
||||||
|
// no longer an orphan. Transactions which depend on a confirmed
|
||||||
|
// transaction are NOT removed recursively because they are still
|
||||||
|
// valid.
|
||||||
|
err := mp.removeBlockTransactionsFromPool(txs)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Failed removing txs from pool: '%s'", err)
|
||||||
|
}
|
||||||
|
acceptedTxs := make([]*consensusexternalapi.DomainTransaction, 0)
|
||||||
|
for _, tx := range txs[util.CoinbaseTransactionIndex+1:] {
|
||||||
|
err := mp.removeDoubleSpends(tx)
|
||||||
|
if err != nil {
|
||||||
|
log.Infof("Failed removing tx from mempool: %s, '%s'", hashserialization.TransactionID(tx), err)
|
||||||
|
}
|
||||||
|
mp.removeOrphan(tx, false)
|
||||||
|
acceptedOrphans := mp.processOrphans(tx)
|
||||||
|
for _, acceptedOrphan := range acceptedOrphans {
|
||||||
|
acceptedTxs = append(acceptedTxs, acceptedOrphan.DomainTransaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mp *mempool) RemoveTransactions(txs []*consensusexternalapi.DomainTransaction) {
|
||||||
|
// Protect concurrent access.
|
||||||
|
mp.mtx.Lock()
|
||||||
|
defer mp.mtx.Unlock()
|
||||||
|
|
||||||
|
for _, tx := range txs {
|
||||||
|
err := mp.removeDoubleSpends(tx)
|
||||||
|
if err != nil {
|
||||||
|
log.Infof("Failed removing tx from mempool: %s, '%s'", hashserialization.TransactionID(tx), err)
|
||||||
|
}
|
||||||
|
mp.removeOrphan(tx, true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
102
domain/miningmanager/mempool/mempool_utxoset.go
Normal file
102
domain/miningmanager/mempool/mempool_utxoset.go
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
package mempool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/kaspanet/kaspad/domain/blockdag"
|
||||||
|
consensusexternalapi "github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
|
||||||
|
"github.com/kaspanet/kaspad/domain/consensus/utils/hashserialization"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newMempoolUTXOSet() *mempoolUTXOSet {
|
||||||
|
return &mempoolUTXOSet{
|
||||||
|
transactionByPreviousOutpoint: make(map[consensusexternalapi.DomainOutpoint]*consensusexternalapi.DomainTransaction),
|
||||||
|
poolUnspentOutputs: make(map[consensusexternalapi.DomainOutpoint]*consensusexternalapi.UTXOEntry),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type mempoolUTXOSet struct {
|
||||||
|
transactionByPreviousOutpoint map[consensusexternalapi.DomainOutpoint]*consensusexternalapi.DomainTransaction
|
||||||
|
poolUnspentOutputs map[consensusexternalapi.DomainOutpoint]*consensusexternalapi.UTXOEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate UTXO Entries in the transaction, to allow chained txs.
|
||||||
|
func (mpus *mempoolUTXOSet) populateUTXOEntries(tx *consensusexternalapi.DomainTransaction) (parentsInPool []consensusexternalapi.DomainOutpoint) {
|
||||||
|
for _, txIn := range tx.Inputs {
|
||||||
|
if utxoEntry, exists := mpus.poolUnspentOutputs[txIn.PreviousOutpoint]; exists {
|
||||||
|
txIn.UTXOEntry = utxoEntry
|
||||||
|
parentsInPool = append(parentsInPool, txIn.PreviousOutpoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parentsInPool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mpus *mempoolUTXOSet) checkExists(tx *consensusexternalapi.DomainTransaction) bool {
|
||||||
|
// Check if it was already spent.
|
||||||
|
for _, txIn := range tx.Inputs {
|
||||||
|
if _, exists := mpus.transactionByPreviousOutpoint[txIn.PreviousOutpoint]; exists {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it creates an already existing UTXO
|
||||||
|
outpoint := consensusexternalapi.DomainOutpoint{TransactionID: *hashserialization.TransactionID(tx)}
|
||||||
|
for i := range tx.Outputs {
|
||||||
|
outpoint.Index = uint32(i)
|
||||||
|
if _, exists := mpus.poolUnspentOutputs[outpoint]; exists {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// addTx adds a transaction to the mempool UTXO set. It assumes that it doesn't double spend another transaction
|
||||||
|
// in the mempool, and that its outputs doesn't exist in the mempool UTXO set, and returns error otherwise.
|
||||||
|
func (mpus *mempoolUTXOSet) addTx(tx *consensusexternalapi.DomainTransaction) error {
|
||||||
|
for _, txIn := range tx.Inputs {
|
||||||
|
if existingTx, exists := mpus.transactionByPreviousOutpoint[txIn.PreviousOutpoint]; exists {
|
||||||
|
return errors.Errorf("outpoint %s is already used by %s", txIn.PreviousOutpoint, hashserialization.TransactionID(existingTx))
|
||||||
|
}
|
||||||
|
mpus.transactionByPreviousOutpoint[txIn.PreviousOutpoint] = tx
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, txOut := range tx.Outputs {
|
||||||
|
outpoint := consensusexternalapi.DomainOutpoint{TransactionID: *hashserialization.TransactionID(tx), Index: uint32(i)}
|
||||||
|
if _, exists := mpus.poolUnspentOutputs[outpoint]; exists {
|
||||||
|
return errors.Errorf("outpoint %s already exists", outpoint)
|
||||||
|
}
|
||||||
|
mpus.poolUnspentOutputs[outpoint] = &consensusexternalapi.UTXOEntry{
|
||||||
|
Amount: txOut.Value,
|
||||||
|
ScriptPublicKey: txOut.ScriptPublicKey,
|
||||||
|
BlockBlueScore: blockdag.UnacceptedBlueScore,
|
||||||
|
IsCoinbase: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeTx removes a transaction to the mempool UTXO set.
|
||||||
|
// Note: it doesn't re-add its previous outputs to the mempool UTXO set.
|
||||||
|
func (mpus *mempoolUTXOSet) removeTx(tx *consensusexternalapi.DomainTransaction) error {
|
||||||
|
for _, txIn := range tx.Inputs {
|
||||||
|
if _, exists := mpus.transactionByPreviousOutpoint[txIn.PreviousOutpoint]; !exists {
|
||||||
|
return errors.Errorf("outpoint %s doesn't exist", txIn.PreviousOutpoint)
|
||||||
|
}
|
||||||
|
delete(mpus.transactionByPreviousOutpoint, txIn.PreviousOutpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
outpoint := consensusexternalapi.DomainOutpoint{TransactionID: *hashserialization.TransactionID(tx)}
|
||||||
|
for i := range tx.Outputs {
|
||||||
|
outpoint.Index = uint32(i)
|
||||||
|
if _, exists := mpus.poolUnspentOutputs[outpoint]; !exists {
|
||||||
|
return errors.Errorf("outpoint %s doesn't exist", outpoint)
|
||||||
|
}
|
||||||
|
delete(mpus.poolUnspentOutputs, outpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mpus *mempoolUTXOSet) poolTransactionBySpendingOutpoint(outpoint consensusexternalapi.DomainOutpoint) (*consensusexternalapi.DomainTransaction, bool) {
|
||||||
|
tx, exists := mpus.transactionByPreviousOutpoint[outpoint]
|
||||||
|
return tx, exists
|
||||||
|
}
|
265
domain/miningmanager/mempool/policy.go
Normal file
265
domain/miningmanager/mempool/policy.go
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
// Copyright (c) 2013-2016 The btcsuite developers
|
||||||
|
// Use of this source code is governed by an ISC
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package mempool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
consensusexternalapi "github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
|
||||||
|
"github.com/kaspanet/kaspad/domain/consensus/utils/estimatedsize"
|
||||||
|
"github.com/kaspanet/kaspad/domain/txscript"
|
||||||
|
"github.com/kaspanet/kaspad/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// maxStandardP2SHSigOps is the maximum number of signature operations
|
||||||
|
// that are considered standard in a pay-to-script-hash script.
|
||||||
|
maxStandardP2SHSigOps = 15
|
||||||
|
|
||||||
|
// maxStandardSigScriptSize is the maximum size allowed for a
|
||||||
|
// transaction input signature script to be considered standard. This
|
||||||
|
// value allows for a 15-of-15 CHECKMULTISIG pay-to-script-hash with
|
||||||
|
// compressed keys.
|
||||||
|
//
|
||||||
|
// The form of the overall script is: OP_0 <15 signatures> OP_PUSHDATA2
|
||||||
|
// <2 bytes len> [OP_15 <15 pubkeys> OP_15 OP_CHECKMULTISIG]
|
||||||
|
//
|
||||||
|
// For the p2sh script portion, each of the 15 compressed pubkeys are
|
||||||
|
// 33 bytes (plus one for the OP_DATA_33 opcode), and the thus it totals
|
||||||
|
// to (15*34)+3 = 513 bytes. Next, each of the 15 signatures is a max
|
||||||
|
// of 73 bytes (plus one for the OP_DATA_73 opcode). Also, there is one
|
||||||
|
// extra byte for the initial extra OP_0 push and 3 bytes for the
|
||||||
|
// OP_PUSHDATA2 needed to specify the 513 bytes for the script push.
|
||||||
|
// That brings the total to 1+(15*74)+3+513 = 1627. This value also
|
||||||
|
// adds a few extra bytes to provide a little buffer.
|
||||||
|
// (1 + 15*74 + 3) + (15*34 + 3) + 23 = 1650
|
||||||
|
maxStandardSigScriptSize = 1650
|
||||||
|
|
||||||
|
// MaxStandardTxSize is the maximum size allowed for transactions that
|
||||||
|
// are considered standard and will therefore be relayed and considered
|
||||||
|
// for mining.
|
||||||
|
MaxStandardTxSize = 100000
|
||||||
|
|
||||||
|
// DefaultMinRelayTxFee is the minimum fee in sompi that is required
|
||||||
|
// for a transaction to be treated as free for relay and mining
|
||||||
|
// purposes. It is also used to help determine if a transaction is
|
||||||
|
// considered dust and as a base for calculating minimum required fees
|
||||||
|
// for larger transactions. This value is in sompi/1000 bytes.
|
||||||
|
DefaultMinRelayTxFee = util.Amount(1000)
|
||||||
|
)
|
||||||
|
|
||||||
|
// calcMinRequiredTxRelayFee returns the minimum transaction fee required for a
|
||||||
|
// transaction with the passed serialized size to be accepted into the memory
|
||||||
|
// pool and relayed.
|
||||||
|
func calcMinRequiredTxRelayFee(serializedSize int64, minRelayTxFee util.Amount) int64 {
|
||||||
|
// Calculate the minimum fee for a transaction to be allowed into the
|
||||||
|
// mempool and relayed by scaling the base fee (which is the minimum
|
||||||
|
// free transaction relay fee). minTxRelayFee is in sompi/kB so
|
||||||
|
// multiply by serializedSize (which is in bytes) and divide by 1000 to
|
||||||
|
// get minimum sompis.
|
||||||
|
minFee := (serializedSize * int64(minRelayTxFee)) / 1000
|
||||||
|
|
||||||
|
if minFee == 0 && minRelayTxFee > 0 {
|
||||||
|
minFee = int64(minRelayTxFee)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the minimum fee to the maximum possible value if the calculated
|
||||||
|
// fee is not in the valid range for monetary amounts.
|
||||||
|
if minFee < 0 || minFee > util.MaxSompi {
|
||||||
|
minFee = util.MaxSompi
|
||||||
|
}
|
||||||
|
|
||||||
|
return minFee
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkInputsStandard performs a series of checks on a transaction's inputs
|
||||||
|
// to ensure they are "standard". A standard transaction input within the
|
||||||
|
// context of this function is one whose referenced public key script is of a
|
||||||
|
// standard form and, for pay-to-script-hash, does not have more than
|
||||||
|
// maxStandardP2SHSigOps signature operations.
|
||||||
|
func checkInputsStandard(tx *consensusexternalapi.DomainTransaction) error {
|
||||||
|
// NOTE: The reference implementation also does a coinbase check here,
|
||||||
|
// but coinbases have already been rejected prior to calling this
|
||||||
|
// function so no need to recheck.
|
||||||
|
|
||||||
|
for i, txIn := range tx.Inputs {
|
||||||
|
// It is safe to elide existence and index checks here since
|
||||||
|
// they have already been checked prior to calling this
|
||||||
|
// function.
|
||||||
|
entry := txIn.UTXOEntry
|
||||||
|
originScriptPubKey := entry.ScriptPublicKey
|
||||||
|
switch txscript.GetScriptClass(originScriptPubKey) {
|
||||||
|
case txscript.ScriptHashTy:
|
||||||
|
numSigOps := txscript.GetPreciseSigOpCount(
|
||||||
|
txIn.SignatureScript, originScriptPubKey, true)
|
||||||
|
if numSigOps > maxStandardP2SHSigOps {
|
||||||
|
str := fmt.Sprintf("transaction input #%d has "+
|
||||||
|
"%d signature operations which is more "+
|
||||||
|
"than the allowed max amount of %d",
|
||||||
|
i, numSigOps, maxStandardP2SHSigOps)
|
||||||
|
return txRuleError(RejectNonstandard, str)
|
||||||
|
}
|
||||||
|
|
||||||
|
case txscript.NonStandardTy:
|
||||||
|
str := fmt.Sprintf("transaction input #%d has a "+
|
||||||
|
"non-standard script form", i)
|
||||||
|
return txRuleError(RejectNonstandard, str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isDust returns whether or not the passed transaction output amount is
|
||||||
|
// considered dust or not based on the passed minimum transaction relay fee.
|
||||||
|
// Dust is defined in terms of the minimum transaction relay fee. In
|
||||||
|
// particular, if the cost to the network to spend coins is more than 1/3 of the
|
||||||
|
// minimum transaction relay fee, it is considered dust.
|
||||||
|
func isDust(txOut *consensusexternalapi.DomainTransactionOutput, minRelayTxFee util.Amount) bool {
|
||||||
|
// Unspendable outputs are considered dust.
|
||||||
|
if txscript.IsUnspendable(txOut.ScriptPublicKey) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// The total serialized size consists of the output and the associated
|
||||||
|
// input script to redeem it. Since there is no input script
|
||||||
|
// to redeem it yet, use the minimum size of a typical input script.
|
||||||
|
//
|
||||||
|
// Pay-to-pubkey-hash bytes breakdown:
|
||||||
|
//
|
||||||
|
// Output to hash (34 bytes):
|
||||||
|
// 8 value, 1 script len, 25 script [1 OP_DUP, 1 OP_HASH_160,
|
||||||
|
// 1 OP_DATA_20, 20 hash, 1 OP_EQUALVERIFY, 1 OP_CHECKSIG]
|
||||||
|
//
|
||||||
|
// Input with compressed pubkey (148 bytes):
|
||||||
|
// 36 prev outpoint, 1 script len, 107 script [1 OP_DATA_72, 72 sig,
|
||||||
|
// 1 OP_DATA_33, 33 compressed pubkey], 4 sequence
|
||||||
|
//
|
||||||
|
// Input with uncompressed pubkey (180 bytes):
|
||||||
|
// 36 prev outpoint, 1 script len, 139 script [1 OP_DATA_72, 72 sig,
|
||||||
|
// 1 OP_DATA_65, 65 compressed pubkey], 4 sequence
|
||||||
|
//
|
||||||
|
// Pay-to-pubkey bytes breakdown:
|
||||||
|
//
|
||||||
|
// Output to compressed pubkey (44 bytes):
|
||||||
|
// 8 value, 1 script len, 35 script [1 OP_DATA_33,
|
||||||
|
// 33 compressed pubkey, 1 OP_CHECKSIG]
|
||||||
|
//
|
||||||
|
// Output to uncompressed pubkey (76 bytes):
|
||||||
|
// 8 value, 1 script len, 67 script [1 OP_DATA_65, 65 pubkey,
|
||||||
|
// 1 OP_CHECKSIG]
|
||||||
|
//
|
||||||
|
// Input (114 bytes):
|
||||||
|
// 36 prev outpoint, 1 script len, 73 script [1 OP_DATA_72,
|
||||||
|
// 72 sig], 4 sequence
|
||||||
|
//
|
||||||
|
// Theoretically this could examine the script type of the output script
|
||||||
|
// and use a different size for the typical input script size for
|
||||||
|
// pay-to-pubkey vs pay-to-pubkey-hash inputs per the above breakdowns,
|
||||||
|
// but the only combination which is less than the value chosen is
|
||||||
|
// a pay-to-pubkey script with a compressed pubkey, which is not very
|
||||||
|
// common.
|
||||||
|
//
|
||||||
|
// The most common scripts are pay-to-pubkey-hash, and as per the above
|
||||||
|
// breakdown, the minimum size of a p2pkh input script is 148 bytes. So
|
||||||
|
// that figure is used.
|
||||||
|
totalSize := estimatedsize.TransactionOutputEstimatedSerializedSize(txOut) + 148
|
||||||
|
|
||||||
|
// The output is considered dust if the cost to the network to spend the
|
||||||
|
// coins is more than 1/3 of the minimum free transaction relay fee.
|
||||||
|
// minFreeTxRelayFee is in sompi/KB, so multiply by 1000 to
|
||||||
|
// convert to bytes.
|
||||||
|
//
|
||||||
|
// Using the typical values for a pay-to-pubkey-hash transaction from
|
||||||
|
// the breakdown above and the default minimum free transaction relay
|
||||||
|
// fee of 1000, this equates to values less than 546 sompi being
|
||||||
|
// considered dust.
|
||||||
|
//
|
||||||
|
// The following is equivalent to (value/totalSize) * (1/3) * 1000
|
||||||
|
// without needing to do floating point math.
|
||||||
|
return txOut.Value*1000/(3*totalSize) < uint64(minRelayTxFee)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkTransactionStandard performs a series of checks on a transaction to
|
||||||
|
// ensure it is a "standard" transaction. A standard transaction is one that
|
||||||
|
// conforms to several additional limiting cases over what is considered a
|
||||||
|
// "sane" transaction such as having a version in the supported range, being
|
||||||
|
// finalized, conforming to more stringent size constraints, having scripts
|
||||||
|
// of recognized forms, and not containing "dust" outputs (those that are
|
||||||
|
// so small it costs more to process them than they are worth).
|
||||||
|
func checkTransactionStandard(tx *consensusexternalapi.DomainTransaction, policy *policy) error {
|
||||||
|
|
||||||
|
// The transaction must be a currently supported version.
|
||||||
|
if tx.Version > policy.MaxTxVersion || tx.Version < 1 {
|
||||||
|
str := fmt.Sprintf("transaction version %d is not in the "+
|
||||||
|
"valid range of %d-%d", tx.Version, 1,
|
||||||
|
policy.MaxTxVersion)
|
||||||
|
return txRuleError(RejectNonstandard, str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsFinalizedTransaction is a consensus check, no need to also check it in policy.
|
||||||
|
|
||||||
|
// The transaction must be finalized to be standard and therefore
|
||||||
|
// considered for inclusion in a block.
|
||||||
|
//if !blockdag.IsFinalizedTransaction(tx, blueScore, medianTimePast) {
|
||||||
|
// return txRuleError(RejectNonstandard,
|
||||||
|
// "transaction is not finalized")
|
||||||
|
//}
|
||||||
|
|
||||||
|
// Since extremely large transactions with a lot of inputs can cost
|
||||||
|
// almost as much to process as the sender fees, limit the maximum
|
||||||
|
// size of a transaction. This also helps mitigate CPU exhaustion
|
||||||
|
// attacks.
|
||||||
|
serializedLen := estimatedsize.TransactionEstimatedSerializedSize(tx)
|
||||||
|
if serializedLen > MaxStandardTxSize {
|
||||||
|
str := fmt.Sprintf("transaction size of %d is larger than max "+
|
||||||
|
"allowed size of %d", serializedLen, MaxStandardTxSize)
|
||||||
|
return txRuleError(RejectNonstandard, str)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, txIn := range tx.Inputs {
|
||||||
|
// Each transaction input signature script must not exceed the
|
||||||
|
// maximum size allowed for a standard transaction. See
|
||||||
|
// the comment on maxStandardSigScriptSize for more details.
|
||||||
|
sigScriptLen := len(txIn.SignatureScript)
|
||||||
|
if sigScriptLen > maxStandardSigScriptSize {
|
||||||
|
str := fmt.Sprintf("transaction input %d: signature "+
|
||||||
|
"script size of %d bytes is large than max "+
|
||||||
|
"allowed size of %d bytes", i, sigScriptLen,
|
||||||
|
maxStandardSigScriptSize)
|
||||||
|
return txRuleError(RejectNonstandard, str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each transaction input signature script must only contain
|
||||||
|
// opcodes which push data onto the stack.
|
||||||
|
isPushOnly, err := txscript.IsPushOnlyScript(txIn.SignatureScript)
|
||||||
|
if err != nil {
|
||||||
|
str := fmt.Sprintf("transaction input %d: IsPushOnlyScript: %t. Error %s", i, isPushOnly, err)
|
||||||
|
return txRuleError(RejectNonstandard, str)
|
||||||
|
}
|
||||||
|
if !isPushOnly {
|
||||||
|
str := fmt.Sprintf("transaction input %d: signature "+
|
||||||
|
"script is not push only", i)
|
||||||
|
return txRuleError(RejectNonstandard, str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// None of the output public key scripts can be a non-standard script or
|
||||||
|
// be "dust".
|
||||||
|
for i, txOut := range tx.Outputs {
|
||||||
|
scriptClass := txscript.GetScriptClass(txOut.ScriptPublicKey)
|
||||||
|
if scriptClass == txscript.NonStandardTy {
|
||||||
|
str := fmt.Sprintf("transaction output %d: non-standard script form", i)
|
||||||
|
return txRuleError(RejectNonstandard, str)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isDust(txOut, policy.MinRelayTxFee) {
|
||||||
|
str := fmt.Sprintf("transaction output %d: payment "+
|
||||||
|
"of %d is dust", i, txOut.Value)
|
||||||
|
return txRuleError(RejectDust, str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
331
domain/miningmanager/mempool/policy_test.go
Normal file
331
domain/miningmanager/mempool/policy_test.go
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
// Copyright (c) 2013-2016 The btcsuite developers
|
||||||
|
// Use of this source code is governed by an ISC
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package mempool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"github.com/kaspanet/kaspad/app/appmessage"
|
||||||
|
consensusexternalapi "github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
|
||||||
|
"github.com/kaspanet/kaspad/domain/txscript"
|
||||||
|
"github.com/kaspanet/kaspad/util"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestCalcMinRequiredTxRelayFee tests the calcMinRequiredTxRelayFee API.
|
||||||
|
func TestCalcMinRequiredTxRelayFee(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string // test description.
|
||||||
|
size int64 // Transaction size in bytes.
|
||||||
|
relayFee util.Amount // minimum relay transaction fee.
|
||||||
|
want int64 // Expected fee.
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
// Ensure combination of size and fee that are less than 1000
|
||||||
|
// produce a non-zero fee.
|
||||||
|
"250 bytes with relay fee of 3",
|
||||||
|
250,
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"100 bytes with default minimum relay fee",
|
||||||
|
100,
|
||||||
|
DefaultMinRelayTxFee,
|
||||||
|
100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"max standard tx size with default minimum relay fee",
|
||||||
|
MaxStandardTxSize,
|
||||||
|
DefaultMinRelayTxFee,
|
||||||
|
100000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"max standard tx size with max sompi relay fee",
|
||||||
|
MaxStandardTxSize,
|
||||||
|
util.MaxSompi,
|
||||||
|
util.MaxSompi,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"1500 bytes with 5000 relay fee",
|
||||||
|
1500,
|
||||||
|
5000,
|
||||||
|
7500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"1500 bytes with 3000 relay fee",
|
||||||
|
1500,
|
||||||
|
3000,
|
||||||
|
4500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"782 bytes with 5000 relay fee",
|
||||||
|
782,
|
||||||
|
5000,
|
||||||
|
3910,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"782 bytes with 3000 relay fee",
|
||||||
|
782,
|
||||||
|
3000,
|
||||||
|
2346,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"782 bytes with 2550 relay fee",
|
||||||
|
782,
|
||||||
|
2550,
|
||||||
|
1994,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
got := calcMinRequiredTxRelayFee(test.size, test.relayFee)
|
||||||
|
if got != test.want {
|
||||||
|
t.Errorf("TestCalcMinRequiredTxRelayFee test '%s' "+
|
||||||
|
"failed: got %v want %v", test.name, got,
|
||||||
|
test.want)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDust tests the isDust API.
|
||||||
|
func TestDust(t *testing.T) {
|
||||||
|
ScriptPublicKey := []byte{0x76, 0xa9, 0x21, 0x03, 0x2f, 0x7e, 0x43,
|
||||||
|
0x0a, 0xa4, 0xc9, 0xd1, 0x59, 0x43, 0x7e, 0x84, 0xb9,
|
||||||
|
0x75, 0xdc, 0x76, 0xd9, 0x00, 0x3b, 0xf0, 0x92, 0x2c,
|
||||||
|
0xf3, 0xaa, 0x45, 0x28, 0x46, 0x4b, 0xab, 0x78, 0x0d,
|
||||||
|
0xba, 0x5e, 0x88, 0xac}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string // test description
|
||||||
|
txOut consensusexternalapi.DomainTransactionOutput
|
||||||
|
relayFee util.Amount // minimum relay transaction fee.
|
||||||
|
isDust bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
// Any value is allowed with a zero relay fee.
|
||||||
|
"zero value with zero relay fee",
|
||||||
|
consensusexternalapi.DomainTransactionOutput{Value: 0, ScriptPublicKey: ScriptPublicKey},
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Zero value is dust with any relay fee"
|
||||||
|
"zero value with very small tx fee",
|
||||||
|
consensusexternalapi.DomainTransactionOutput{Value: 0, ScriptPublicKey: ScriptPublicKey},
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"38 byte public key script with value 605",
|
||||||
|
consensusexternalapi.DomainTransactionOutput{Value: 605, ScriptPublicKey: ScriptPublicKey},
|
||||||
|
1000,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"38 byte public key script with value 606",
|
||||||
|
consensusexternalapi.DomainTransactionOutput{Value: 606, ScriptPublicKey: ScriptPublicKey},
|
||||||
|
1000,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Maximum allowed value is never dust.
|
||||||
|
"max sompi amount is never dust",
|
||||||
|
consensusexternalapi.DomainTransactionOutput{Value: util.MaxSompi, ScriptPublicKey: ScriptPublicKey},
|
||||||
|
util.MaxSompi,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Maximum int64 value causes overflow.
|
||||||
|
"maximum int64 value",
|
||||||
|
consensusexternalapi.DomainTransactionOutput{Value: 1<<63 - 1, ScriptPublicKey: ScriptPublicKey},
|
||||||
|
1<<63 - 1,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Unspendable ScriptPublicKey due to an invalid public key
|
||||||
|
// script.
|
||||||
|
"unspendable ScriptPublicKey",
|
||||||
|
consensusexternalapi.DomainTransactionOutput{Value: 5000, ScriptPublicKey: []byte{0x01}},
|
||||||
|
0, // no relay fee
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
res := isDust(&test.txOut, test.relayFee)
|
||||||
|
if res != test.isDust {
|
||||||
|
t.Fatalf("Dust test '%s' failed: want %v got %v",
|
||||||
|
test.name, test.isDust, res)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCheckTransactionStandard tests the checkTransactionStandard API.
|
||||||
|
func TestCheckTransactionStandard(t *testing.T) {
|
||||||
|
// Create some dummy, but otherwise standard, data for transactions.
|
||||||
|
prevOutTxID := &consensusexternalapi.DomainTransactionID{}
|
||||||
|
dummyPrevOut := consensusexternalapi.DomainOutpoint{TransactionID: *prevOutTxID, Index: 1}
|
||||||
|
dummySigScript := bytes.Repeat([]byte{0x00}, 65)
|
||||||
|
dummyTxIn := consensusexternalapi.DomainTransactionInput{
|
||||||
|
PreviousOutpoint: dummyPrevOut,
|
||||||
|
SignatureScript: dummySigScript,
|
||||||
|
Sequence: appmessage.MaxTxInSequenceNum,
|
||||||
|
}
|
||||||
|
addrHash := [20]byte{0x01}
|
||||||
|
addr, err := util.NewAddressPubKeyHash(addrHash[:], util.Bech32PrefixKaspaTest)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewAddressPubKeyHash: unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
dummyScriptPublicKey, err := txscript.PayToAddrScript(addr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("PayToAddrScript: unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
dummyTxOut := consensusexternalapi.DomainTransactionOutput{
|
||||||
|
Value: 100000000, // 1 KAS
|
||||||
|
ScriptPublicKey: dummyScriptPublicKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
tx consensusexternalapi.DomainTransaction
|
||||||
|
height uint64
|
||||||
|
isStandard bool
|
||||||
|
code RejectCode
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Typical pay-to-pubkey-hash transaction",
|
||||||
|
tx: consensusexternalapi.DomainTransaction{Version: 1, Inputs: []*consensusexternalapi.DomainTransactionInput{&dummyTxIn}, Outputs: []*consensusexternalapi.DomainTransactionOutput{&dummyTxOut}},
|
||||||
|
height: 300000,
|
||||||
|
isStandard: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Transaction version too high",
|
||||||
|
tx: consensusexternalapi.DomainTransaction{Version: appmessage.TxVersion + 1, Inputs: []*consensusexternalapi.DomainTransactionInput{&dummyTxIn}, Outputs: []*consensusexternalapi.DomainTransactionOutput{&dummyTxOut}},
|
||||||
|
height: 300000,
|
||||||
|
isStandard: false,
|
||||||
|
code: RejectNonstandard,
|
||||||
|
},
|
||||||
|
// This is commented out, because transaction finaliation is a consensus check, not a policy check.
|
||||||
|
//{
|
||||||
|
// name: "Transaction is not finalized",
|
||||||
|
// tx: consensusexternalapi.DomainTransaction{Version: 1, Inputs: []*consensusexternalapi.DomainTransactionInput{{
|
||||||
|
// PreviousOutpoint: dummyPrevOut,
|
||||||
|
// SignatureScript: dummySigScript,
|
||||||
|
// Sequence: 0,
|
||||||
|
// }}, Outputs: []*consensusexternalapi.DomainTransactionOutput{&dummyTxOut}, LockTime: 300001},
|
||||||
|
// height: 300000,
|
||||||
|
// isStandard: false,
|
||||||
|
// code: RejectNonstandard,
|
||||||
|
//},
|
||||||
|
{
|
||||||
|
name: "Transaction size is too large",
|
||||||
|
tx: consensusexternalapi.DomainTransaction{Version: 1, Inputs: []*consensusexternalapi.DomainTransactionInput{&dummyTxIn}, Outputs: []*consensusexternalapi.DomainTransactionOutput{{
|
||||||
|
Value: 0,
|
||||||
|
ScriptPublicKey: bytes.Repeat([]byte{0x00}, MaxStandardTxSize+1),
|
||||||
|
}}},
|
||||||
|
height: 300000,
|
||||||
|
isStandard: false,
|
||||||
|
code: RejectNonstandard,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Signature script size is too large",
|
||||||
|
tx: consensusexternalapi.DomainTransaction{Version: 1, Inputs: []*consensusexternalapi.DomainTransactionInput{{
|
||||||
|
PreviousOutpoint: dummyPrevOut,
|
||||||
|
SignatureScript: bytes.Repeat([]byte{0x00},
|
||||||
|
maxStandardSigScriptSize+1),
|
||||||
|
Sequence: appmessage.MaxTxInSequenceNum,
|
||||||
|
}}, Outputs: []*consensusexternalapi.DomainTransactionOutput{&dummyTxOut}},
|
||||||
|
height: 300000,
|
||||||
|
isStandard: false,
|
||||||
|
code: RejectNonstandard,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Signature script that does more than push data",
|
||||||
|
tx: consensusexternalapi.DomainTransaction{Version: 1, Inputs: []*consensusexternalapi.DomainTransactionInput{{
|
||||||
|
PreviousOutpoint: dummyPrevOut,
|
||||||
|
SignatureScript: []byte{
|
||||||
|
txscript.OpCheckSigVerify},
|
||||||
|
Sequence: appmessage.MaxTxInSequenceNum,
|
||||||
|
}}, Outputs: []*consensusexternalapi.DomainTransactionOutput{&dummyTxOut}},
|
||||||
|
height: 300000,
|
||||||
|
isStandard: false,
|
||||||
|
code: RejectNonstandard,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid but non standard public key script",
|
||||||
|
tx: consensusexternalapi.DomainTransaction{Version: 1, Inputs: []*consensusexternalapi.DomainTransactionInput{&dummyTxIn}, Outputs: []*consensusexternalapi.DomainTransactionOutput{{
|
||||||
|
Value: 100000000,
|
||||||
|
ScriptPublicKey: []byte{txscript.OpTrue},
|
||||||
|
}}},
|
||||||
|
height: 300000,
|
||||||
|
isStandard: false,
|
||||||
|
code: RejectNonstandard,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Dust output",
|
||||||
|
tx: consensusexternalapi.DomainTransaction{Version: 1, Inputs: []*consensusexternalapi.DomainTransactionInput{&dummyTxIn}, Outputs: []*consensusexternalapi.DomainTransactionOutput{{
|
||||||
|
Value: 0,
|
||||||
|
ScriptPublicKey: dummyScriptPublicKey,
|
||||||
|
}}},
|
||||||
|
height: 300000,
|
||||||
|
isStandard: false,
|
||||||
|
code: RejectDust,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Nulldata transaction",
|
||||||
|
tx: consensusexternalapi.DomainTransaction{Version: 1, Inputs: []*consensusexternalapi.DomainTransactionInput{&dummyTxIn}, Outputs: []*consensusexternalapi.DomainTransactionOutput{{
|
||||||
|
Value: 0,
|
||||||
|
ScriptPublicKey: []byte{txscript.OpReturn},
|
||||||
|
}}},
|
||||||
|
height: 300000,
|
||||||
|
isStandard: false,
|
||||||
|
code: RejectNonstandard,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
// Ensure standardness is as expected.
|
||||||
|
err := checkTransactionStandard(&test.tx, &policy{MinRelayTxFee: DefaultMinRelayTxFee, MaxTxVersion: 1})
|
||||||
|
if err == nil && test.isStandard {
|
||||||
|
// Test passes since function returned standard for a
|
||||||
|
// transaction which is intended to be standard.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err == nil && !test.isStandard {
|
||||||
|
t.Errorf("checkTransactionStandard (%s): standard when "+
|
||||||
|
"it should not be", test.name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err != nil && test.isStandard {
|
||||||
|
t.Errorf("checkTransactionStandard (%s): nonstandard "+
|
||||||
|
"when it should not be: %v", test.name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure error type is a TxRuleError inside of a RuleError.
|
||||||
|
var ruleErr RuleError
|
||||||
|
if !errors.As(err, &ruleErr) {
|
||||||
|
t.Errorf("checkTransactionStandard (%s): unexpected "+
|
||||||
|
"error type - got %T", test.name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
txRuleErr, ok := ruleErr.Err.(TxRuleError)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("checkTransactionStandard (%s): unexpected "+
|
||||||
|
"error type - got %T", test.name, ruleErr.Err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the reject code is the expected one.
|
||||||
|
if txRuleErr.RejectCode != test.code {
|
||||||
|
t.Errorf("checkTransactionStandard (%s): unexpected "+
|
||||||
|
"error code - got %v, want %v", test.name,
|
||||||
|
txRuleErr.RejectCode, test.code)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -9,8 +9,8 @@ import (
|
|||||||
// known transactions that have no yet been added to any block
|
// known transactions that have no yet been added to any block
|
||||||
type MiningManager interface {
|
type MiningManager interface {
|
||||||
GetBlockTemplate(coinbaseData *consensusexternalapi.DomainCoinbaseData) *consensusexternalapi.DomainBlock
|
GetBlockTemplate(coinbaseData *consensusexternalapi.DomainCoinbaseData) *consensusexternalapi.DomainBlock
|
||||||
HandleNewBlock(block *consensusexternalapi.DomainBlock)
|
HandleNewBlockTransactions(txs []*consensusexternalapi.DomainTransaction)
|
||||||
ValidateAndInsertTransaction(transaction *consensusexternalapi.DomainTransaction) error
|
ValidateAndInsertTransaction(transaction *consensusexternalapi.DomainTransaction, allowOrphan bool) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type miningManager struct {
|
type miningManager struct {
|
||||||
@ -23,14 +23,14 @@ func (mm *miningManager) GetBlockTemplate(coinbaseData *consensusexternalapi.Dom
|
|||||||
return mm.blockTemplateBuilder.GetBlockTemplate(coinbaseData)
|
return mm.blockTemplateBuilder.GetBlockTemplate(coinbaseData)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleNewBlock handles a new block that was just added to the DAG
|
// HandleNewBlock handles the transactions for a new block that was just added to the DAG
|
||||||
func (mm *miningManager) HandleNewBlock(block *consensusexternalapi.DomainBlock) {
|
func (mm *miningManager) HandleNewBlockTransactions(txs []*consensusexternalapi.DomainTransaction) {
|
||||||
mm.mempool.HandleNewBlock(block)
|
mm.mempool.HandleNewBlockTransactions(txs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateAndInsertTransaction validates the given transaction, and
|
// ValidateAndInsertTransaction validates the given transaction, and
|
||||||
// adds it to the set of known transactions that have not yet been
|
// adds it to the set of known transactions that have not yet been
|
||||||
// added to any block
|
// added to any block
|
||||||
func (mm *miningManager) ValidateAndInsertTransaction(transaction *consensusexternalapi.DomainTransaction) error {
|
func (mm *miningManager) ValidateAndInsertTransaction(transaction *consensusexternalapi.DomainTransaction, allowOrphan bool) error {
|
||||||
return mm.mempool.ValidateAndInsertTransaction(transaction)
|
return mm.mempool.ValidateAndInsertTransaction(transaction, allowOrphan)
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,8 @@ import (
|
|||||||
// Mempool maintains a set of known transactions that
|
// Mempool maintains a set of known transactions that
|
||||||
// are intended to be mined into new blocks
|
// are intended to be mined into new blocks
|
||||||
type Mempool interface {
|
type Mempool interface {
|
||||||
HandleNewBlock(block *consensusexternalapi.DomainBlock)
|
HandleNewBlockTransactions(txs []*consensusexternalapi.DomainTransaction)
|
||||||
Transactions() []*consensusexternalapi.DomainTransaction
|
Transactions() []*consensusexternalapi.DomainTransaction
|
||||||
ValidateAndInsertTransaction(transaction *consensusexternalapi.DomainTransaction) error
|
ValidateAndInsertTransaction(transaction *consensusexternalapi.DomainTransaction, allowOrphan bool) error
|
||||||
|
RemoveTransactions(txs []*consensusexternalapi.DomainTransaction)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user