[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:
Elichai Turkel 2020-11-01 18:27:49 +02:00 committed by GitHub
parent c59adaa4db
commit 87ad9dfc59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 2277 additions and 32 deletions

View File

@ -22,7 +22,7 @@ func TransactionEstimatedSerializedSize(tx *externalapi.DomainTransaction) uint6
size += 8 // number of outputs (uint64)
for _, output := range tx.Outputs {
size += transactionOutputEstimatedSerializedSize(output)
size += TransactionOutputEstimatedSerializedSize(output)
}
size += 8 // lock time (uint64)
@ -54,7 +54,8 @@ func outpointEstimatedSerializedSize() uint64 {
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 += 8 // value (uint64)
size += 8 // length of script public key (uint64)

View File

@ -3,24 +3,167 @@ package blocktemplatebuilder
import (
"github.com/kaspanet/kaspad/domain/consensus"
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"
"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
type blockTemplateBuilder struct {
consensus *consensus.Consensus
consensus consensus.Consensus
mempool miningmanagerapi.Mempool
policy policy
}
// 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{
consensus: consensus,
mempool: mempool,
policy: policy{BlockMaxMass: blockMaxMass},
}
}
// 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 {
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))
}

View 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)

View 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
}

View 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
}
}

View File

@ -8,15 +8,15 @@ import (
// Factory instantiates new mining managers
type Factory interface {
NewMiningManager(consensus *consensus.Consensus) MiningManager
NewMiningManager(consensus consensus.Consensus, blockMaxMass uint64) MiningManager
}
type factory struct{}
// 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)
blockTemplateBuilder := blocktemplatebuilder.New(consensus, mempool)
blockTemplateBuilder := blocktemplatebuilder.New(consensus, mempool, blockMaxMass)
return &miningManager{
mempool: mempool,

View File

@ -0,0 +1,72 @@
mempool
=======
[![ISC License](http://img.shields.io/badge/license-ISC-blue.svg)](https://choosealicense.com/licenses/isc/)
[![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg)](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

View 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

View 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
}

View 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)

View File

@ -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
import (
"container/list"
"fmt"
"github.com/kaspanet/kaspad/domain/consensus"
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"
"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
// are intended to be mined into new blocks
type mempool struct {
consensus *consensus.Consensus
const (
// orphanTTL is the maximum amount of time an orphan is allowed to
// stay in the orphan pool before it expires and is evicted during the
// 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
func New(consensus *consensus.Consensus) miningmanagermodel.Mempool {
// mempool is used as a source of transactions that need to be mined into blocks
// 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{
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
func (mp *mempool) HandleNewBlock(block *consensusexternalapi.DomainBlock) {
// txDescriptor is a descriptor containing a transaction in the mempool along with
// 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 {
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
// adds it to the mempool
func (mp *mempool) ValidateAndInsertTransaction(transaction *consensusexternalapi.DomainTransaction) error {
return nil
// HandleNewBlockTransactions removes all the transactions in the new block
// from the mempool and the orphan pool, and it also removes
// from the mempool transactions that double spend a
// 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)
}
}

View 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
}

View 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
}

View 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
}
}
}

View File

@ -9,8 +9,8 @@ import (
// known transactions that have no yet been added to any block
type MiningManager interface {
GetBlockTemplate(coinbaseData *consensusexternalapi.DomainCoinbaseData) *consensusexternalapi.DomainBlock
HandleNewBlock(block *consensusexternalapi.DomainBlock)
ValidateAndInsertTransaction(transaction *consensusexternalapi.DomainTransaction) error
HandleNewBlockTransactions(txs []*consensusexternalapi.DomainTransaction)
ValidateAndInsertTransaction(transaction *consensusexternalapi.DomainTransaction, allowOrphan bool) error
}
type miningManager struct {
@ -23,14 +23,14 @@ func (mm *miningManager) GetBlockTemplate(coinbaseData *consensusexternalapi.Dom
return mm.blockTemplateBuilder.GetBlockTemplate(coinbaseData)
}
// HandleNewBlock handles a new block that was just added to the DAG
func (mm *miningManager) HandleNewBlock(block *consensusexternalapi.DomainBlock) {
mm.mempool.HandleNewBlock(block)
// HandleNewBlock handles the transactions for a new block that was just added to the DAG
func (mm *miningManager) HandleNewBlockTransactions(txs []*consensusexternalapi.DomainTransaction) {
mm.mempool.HandleNewBlockTransactions(txs)
}
// ValidateAndInsertTransaction validates the given transaction, and
// adds it to the set of known transactions that have not yet been
// added to any block
func (mm *miningManager) ValidateAndInsertTransaction(transaction *consensusexternalapi.DomainTransaction) error {
return mm.mempool.ValidateAndInsertTransaction(transaction)
func (mm *miningManager) ValidateAndInsertTransaction(transaction *consensusexternalapi.DomainTransaction, allowOrphan bool) error {
return mm.mempool.ValidateAndInsertTransaction(transaction, allowOrphan)
}

View File

@ -7,7 +7,8 @@ import (
// Mempool maintains a set of known transactions that
// are intended to be mined into new blocks
type Mempool interface {
HandleNewBlock(block *consensusexternalapi.DomainBlock)
HandleNewBlockTransactions(txs []*consensusexternalapi.DomainTransaction)
Transactions() []*consensusexternalapi.DomainTransaction
ValidateAndInsertTransaction(transaction *consensusexternalapi.DomainTransaction) error
ValidateAndInsertTransaction(transaction *consensusexternalapi.DomainTransaction, allowOrphan bool) error
RemoveTransactions(txs []*consensusexternalapi.DomainTransaction)
}