diff --git a/domain/consensus/utils/estimatedsize/transaction_estimated_size.go b/domain/consensus/utils/estimatedsize/transaction_estimated_size.go index 5977db976..8ebf8b695 100644 --- a/domain/consensus/utils/estimatedsize/transaction_estimated_size.go +++ b/domain/consensus/utils/estimatedsize/transaction_estimated_size.go @@ -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) diff --git a/domain/miningmanager/blocktemplatebuilder/blocktemplatebuilder.go b/domain/miningmanager/blocktemplatebuilder/blocktemplatebuilder.go index d10b1ddfd..be0f1eecb 100644 --- a/domain/miningmanager/blocktemplatebuilder/blocktemplatebuilder.go +++ b/domain/miningmanager/blocktemplatebuilder/blocktemplatebuilder.go @@ -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)) } diff --git a/domain/miningmanager/blocktemplatebuilder/log.go b/domain/miningmanager/blocktemplatebuilder/log.go new file mode 100644 index 000000000..425d2192d --- /dev/null +++ b/domain/miningmanager/blocktemplatebuilder/log.go @@ -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) diff --git a/domain/miningmanager/blocktemplatebuilder/policy.go b/domain/miningmanager/blocktemplatebuilder/policy.go new file mode 100644 index 000000000..58980f8ed --- /dev/null +++ b/domain/miningmanager/blocktemplatebuilder/policy.go @@ -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 +} diff --git a/domain/miningmanager/blocktemplatebuilder/txselection.go b/domain/miningmanager/blocktemplatebuilder/txselection.go new file mode 100644 index 000000000..3b3851deb --- /dev/null +++ b/domain/miningmanager/blocktemplatebuilder/txselection.go @@ -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 + } +} diff --git a/domain/miningmanager/factory.go b/domain/miningmanager/factory.go index 261f0f0c4..4590e46cf 100644 --- a/domain/miningmanager/factory.go +++ b/domain/miningmanager/factory.go @@ -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, diff --git a/domain/miningmanager/mempool/README.md b/domain/miningmanager/mempool/README.md new file mode 100644 index 000000000..aed25cbd5 --- /dev/null +++ b/domain/miningmanager/mempool/README.md @@ -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 + diff --git a/domain/miningmanager/mempool/doc.go b/domain/miningmanager/mempool/doc.go new file mode 100644 index 000000000..c0dbd5d2d --- /dev/null +++ b/domain/miningmanager/mempool/doc.go @@ -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 diff --git a/domain/miningmanager/mempool/error.go b/domain/miningmanager/mempool/error.go new file mode 100644 index 000000000..2a09d4641 --- /dev/null +++ b/domain/miningmanager/mempool/error.go @@ -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 "" + } + 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 +} diff --git a/domain/miningmanager/mempool/log.go b/domain/miningmanager/mempool/log.go new file mode 100644 index 000000000..ce5fd7467 --- /dev/null +++ b/domain/miningmanager/mempool/log.go @@ -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) diff --git a/domain/miningmanager/mempool/mempool.go b/domain/miningmanager/mempool/mempool.go index 93bc4038d..829357595 100644 --- a/domain/miningmanager/mempool/mempool.go +++ b/domain/miningmanager/mempool/mempool.go @@ -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) + } } diff --git a/domain/miningmanager/mempool/mempool_utxoset.go b/domain/miningmanager/mempool/mempool_utxoset.go new file mode 100644 index 000000000..29b5f1e06 --- /dev/null +++ b/domain/miningmanager/mempool/mempool_utxoset.go @@ -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 +} diff --git a/domain/miningmanager/mempool/policy.go b/domain/miningmanager/mempool/policy.go new file mode 100644 index 000000000..f2793b6df --- /dev/null +++ b/domain/miningmanager/mempool/policy.go @@ -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 +} diff --git a/domain/miningmanager/mempool/policy_test.go b/domain/miningmanager/mempool/policy_test.go new file mode 100644 index 000000000..eae4ff37f --- /dev/null +++ b/domain/miningmanager/mempool/policy_test.go @@ -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 + } + } +} diff --git a/domain/miningmanager/miningmanager.go b/domain/miningmanager/miningmanager.go index bdc3368b8..f615a55a0 100644 --- a/domain/miningmanager/miningmanager.go +++ b/domain/miningmanager/miningmanager.go @@ -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) } diff --git a/domain/miningmanager/model/interface_mempool.go b/domain/miningmanager/model/interface_mempool.go index 74cdff2e9..9293ec742 100644 --- a/domain/miningmanager/model/interface_mempool.go +++ b/domain/miningmanager/model/interface_mempool.go @@ -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) }