mempool: Explicitly reject standalone treasurybase.

This updates the mempool to explicitly reject standalone treasurybases
and adds a test to ensure proper functionality.

Note that treasurybases are already rejected because IsCoinBaseTx
currently detects a treasurybase as a coinbase as well, but it is safer
to be explicit in case the coinbase detection function is updated in the
future to exclude treasurybases.

Also since treasurybases are rejected early, remove the code that deals
with special casing them later given there is no way the transaction is
one.
This commit is contained in:
Dave Collins 2022-06-22 21:47:36 -05:00
parent db05fd9ceb
commit dc0351646f
No known key found for this signature in database
GPG Key ID: B8904D9D9C93D1F2
4 changed files with 103 additions and 23 deletions

View File

@ -38,6 +38,10 @@ const (
// ErrCoinbase indicates a transaction is a standalone coinbase transaction.
ErrCoinbase = ErrorKind("ErrCoinbase")
// ErrTreasurybase indicates a transaction is a standalone treasurybase
// transaction.
ErrTreasurybase = ErrorKind("ErrTreasurybase")
// ErrExpired indicates a transaction will be expired as of the next block.
ErrExpired = ErrorKind("ErrExpired")

View File

@ -22,6 +22,7 @@ func TestErrorKindStringer(t *testing.T) {
{ErrAlreadyVoted, "ErrorAlreadyVoted"},
{ErrDuplicate, "ErrDuplicate"},
{ErrCoinbase, "ErrCoinbase"},
{ErrTreasurybase, "ErrTreasurybase"},
{ErrExpired, "ErrExpired"},
{ErrNonStandard, "ErrNonStandard"},
{ErrDustOutput, "ErrDustOutput"},

View File

@ -1247,6 +1247,24 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit,
isAutoRevocationsEnabled := checkTxFlags.IsAutoRevocationsEnabled()
isSubsidyEnabled := checkTxFlags.IsSubsidySplitEnabled()
// Determine the type of transaction (regular or stake) and be sure to set
// the transaction tree correctly as it's possible a user submitted it to
// the network with TxTreeUnknown.
txType := stake.DetermineTxType(msgTx)
tree := wire.TxTreeRegular
if txType != stake.TxTypeRegular {
tree = wire.TxTreeStake
}
tx.SetTree(tree)
// A standalone transaction must not be a treasurybase transaction.
isTreasurybase := isTreasuryEnabled && txType == stake.TxTypeTreasuryBase
if isTreasurybase {
str := fmt.Sprintf("transaction %v is an individual treasurybase",
txHash)
return nil, txRuleError(ErrTreasurybase, str)
}
// A standalone transaction must not be a coinbase transaction.
if standalone.IsCoinBaseTx(msgTx, isTreasuryEnabled) {
str := fmt.Sprintf("transaction %v is an individual coinbase",
@ -1267,29 +1285,14 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit,
return nil, txRuleError(ErrExpired, str)
}
// Determine what type of transaction we're dealing with (regular or stake).
// Then, be sure to set the tx tree correctly as it's possible a user submitted
// it to the network with TxTreeUnknown.
txType := stake.DetermineTxType(msgTx)
tree := wire.TxTreeRegular
if txType != stake.TxTypeRegular {
tree = wire.TxTreeStake
}
tx.SetTree(tree)
// Reject votes and treasury spends before stake validation height.
isVote := txType == stake.TxTypeSSGen
var isTreasuryBase, isTSpend bool
if isTreasuryEnabled {
isTSpend = txType == stake.TxTypeTSpend
isTreasuryBase = txType == stake.TxTypeTreasuryBase
}
// Reject votes before stake validation height.
isTSpend := isTreasuryEnabled && txType == stake.TxTypeTSpend
stakeValidationHeight := mp.cfg.ChainParams.StakeValidationHeight
if (isVote || isTSpend) && nextBlockHeight < stakeValidationHeight {
strType := "votes"
if isTSpend {
strType = "tspends"
strType = "treasury spends"
}
str := fmt.Sprintf("%s are not valid until block height %d (next "+
"block height %d)", strType, stakeValidationHeight, nextBlockHeight)
@ -1428,12 +1431,12 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit,
utxoView.RemoveEntry(outpoint)
}
// Transaction is an orphan if any of the referenced transaction outputs don't
// exist or are already spent.
// Transaction is an orphan if any of the referenced transaction outputs
// don't exist or are already spent.
var missingParents []*chainhash.Hash
var updateFraudProof bool
for i, txIn := range msgTx.TxIn {
if (i == 0 && (isVote || isTreasuryBase)) || isTSpend {
if (i == 0 && isVote) || isTSpend {
continue
}
@ -1489,8 +1492,8 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit,
msgTx = tx.MsgTx()
for i, txIn := range msgTx.TxIn {
// Skip stakebase inputs, treasury base inputs, and tspends.
if (i == 0 && (isVote || isTreasuryBase)) || isTSpend {
// Skip stakebase inputs and treasury spends.
if (i == 0 && isVote) || isTSpend {
continue
}

View File

@ -2714,6 +2714,78 @@ func TestHandlesTAdds(t *testing.T) {
acceptTAdd(tadd)
}
// TestRejectTreasurybases ensures that treasurybases are rejected from the pool
// as expected.
func TestRejectTreasurybases(t *testing.T) {
t.Parallel()
harness, _, err := newPoolHarness(chaincfg.MainNetParams())
if err != nil {
t.Fatalf("unable to create test pool: %v", err)
}
tc := &testContext{t, harness}
subsidyCache := harness.txPool.cfg.SubsidyCache
// Setup the harness for the test and activate the treasury agenda.
harness.SetTreasuryAgendaActive(true)
// createTreasurybase returns a valid treasurybase transaction for the given
// block height.
createTreasurybase := func(blockHeight int64) (*dcrutil.Tx, error) {
// Create provably pruneable script for the output that encodes the
// block height used to ensure a unique overall transaction hash. This
// is necessary because neither the input nor the output that adds to
// the treasury account balance are unique for a treasurybase.
extraNonce := uint64(0)
opRetData := make([]byte, 12)
binary.LittleEndian.PutUint32(opRetData[0:4], uint32(blockHeight))
binary.LittleEndian.PutUint64(opRetData[4:12], extraNonce)
opReturnTreasury, err := stdscript.ProvablyPruneableScriptV0(opRetData)
if err != nil {
return nil, err
}
// Calculate subsidy for the block height.
trsySubsidy := subsidyCache.CalcTreasurySubsidy(blockHeight, 5, true)
tx := wire.NewMsgTx()
tx.Version = wire.TxVersionTreasury
tx.AddTxIn(&wire.TxIn{
// Treasurybase transactions have no inputs, so previous outpoint is
// zero hash and max index.
PreviousOutPoint: *wire.NewOutPoint(&chainhash.Hash{},
wire.MaxPrevOutIndex, wire.TxTreeRegular),
Sequence: wire.MaxTxInSequenceNum,
ValueIn: trsySubsidy,
BlockHeight: wire.NullBlockHeight,
BlockIndex: wire.NullBlockIndex,
SignatureScript: nil, // Must be nil by consensus.
})
tx.AddTxOut(newTxOut(trsySubsidy, 0, []byte{txscript.OP_TADD}))
tx.AddTxOut(newTxOut(0, 0, opReturnTreasury))
return dcrutil.NewTx(tx), nil
}
// Set the block height to something after the first few blocks and create
// a valid treasurybase for it.
blockHeight := int64(10)
harness.chain.SetHeight(blockHeight)
trsybase, err := createTreasurybase(blockHeight)
if err != nil {
t.Fatal(err)
}
// Attempt to add the treasurybase and ensure it is rejected. Also, ensure
// it is not in the orphan pool, not in the transaction pool, and not
// reported as available.
_, err = harness.txPool.ProcessTransaction(trsybase, false, false, true, 0)
if !errors.Is(err, ErrTreasurybase) {
t.Fatal("Process Transaction: did not get expected ErrTreasurybase")
}
testPoolMembership(tc, trsybase, false, false)
}
// TestStagedTransactionHeight verifies that the height of a transaction
// that moves from the stage pool into the main pool is set to the height it
// was initially added to the mempool, rather than the height it was unstaged.