From 70edb35d0d5b4c79e72c19ab543d16b561c4c180 Mon Sep 17 00:00:00 2001 From: Ryan Staudt Date: Tue, 3 Aug 2021 06:19:54 -0500 Subject: [PATCH] multi: Check tx inputs auto revocations flag. This adds an isAutoRevocationsEnabled boolean to checkTicketRedeemerCommitments and updates all callers accordingly. This is necessary because additional validation rules will be added in subsequent commits based on whether the automatic ticket revocations agenda is active. Note that this is intentionally not using blockchain.AgendaFlags, and is instead adding an additional boolean parameter, since we should be able to retroactively update the transaction checks to be based on just the transaction version if the agenda passes and there are no legacy violations (i.e., version 2 revocation transactions prior to the agenda passing), in which case this boolean parameter could be removed. --- blockchain/validate.go | 38 ++++++++++++++++++++------ internal/mempool/mempool.go | 3 +- internal/mining/mining.go | 16 +++++++++-- internal/mining/mining_harness_test.go | 20 +++++++++++--- server.go | 8 ++++-- 5 files changed, 66 insertions(+), 19 deletions(-) diff --git a/blockchain/validate.go b/blockchain/validate.go index c2955ab8..ea83763b 100644 --- a/blockchain/validate.go +++ b/blockchain/validate.go @@ -2245,7 +2245,10 @@ func checkTicketSubmissionInput(ticketUtxo *UtxoEntry) error { // // NOTE: This is only intended to be a helper to refactor out common code from // checkVoteInputs and checkRevocationInputs. -func checkTicketRedeemerCommitments(ticketHash *chainhash.Hash, ticketOuts []*stake.MinimalOutput, msgTx *wire.MsgTx, isVote bool, voteSubsidy int64, isTreasuryEnabled bool) error { +func checkTicketRedeemerCommitments(ticketHash *chainhash.Hash, + ticketOuts []*stake.MinimalOutput, msgTx *wire.MsgTx, isVote bool, + voteSubsidy int64, isTreasuryEnabled, isAutoRevocationsEnabled bool) error { + // Make an initial pass over the ticket commitments to calculate the overall // contribution sum. This is necessary because the output amounts are // required to be scaled to maintain the same proportions as the original @@ -2414,7 +2417,10 @@ func checkTicketRedeemerCommitments(ticketHash *chainhash.Hash, ticketOuts []*st // // NOTE: The caller MUST have already determined that the provided transaction // is a vote. -func checkVoteInputs(subsidyCache *standalone.SubsidyCache, tx *dcrutil.Tx, txHeight int64, view *UtxoViewpoint, params *chaincfg.Params, isTreasuryEnabled bool) error { +func checkVoteInputs(subsidyCache *standalone.SubsidyCache, tx *dcrutil.Tx, + txHeight int64, view *UtxoViewpoint, params *chaincfg.Params, + isTreasuryEnabled, isAutoRevocationsEnabled bool) error { + ticketMaturity := int64(params.TicketMaturity) voteHash := tx.Hash() msgTx := tx.MsgTx() @@ -2523,7 +2529,7 @@ func checkVoteInputs(subsidyCache *standalone.SubsidyCache, tx *dcrutil.Tx, txHe // Ensure the outputs adhere to the ticket commitments. return checkTicketRedeemerCommitments(ticketHash, ticketOuts, msgTx, - true, voteSubsidy, isTreasuryEnabled) + true, voteSubsidy, isTreasuryEnabled, isAutoRevocationsEnabled) } // checkRevocationInputs performs a series of checks on the inputs to a @@ -2533,7 +2539,10 @@ func checkVoteInputs(subsidyCache *standalone.SubsidyCache, tx *dcrutil.Tx, txHe // // NOTE: The caller MUST have already determined that the provided transaction // is a revocation. -func checkRevocationInputs(tx *dcrutil.Tx, txHeight int64, view *UtxoViewpoint, params *chaincfg.Params, isTreasuryEnabled bool) error { +func checkRevocationInputs(tx *dcrutil.Tx, txHeight int64, view *UtxoViewpoint, + params *chaincfg.Params, isTreasuryEnabled, + isAutoRevocationsEnabled bool) error { + ticketMaturity := int64(params.TicketMaturity) revokeHash := tx.Hash() msgTx := tx.MsgTx() @@ -2615,7 +2624,7 @@ func checkRevocationInputs(tx *dcrutil.Tx, txHeight int64, view *UtxoViewpoint, // Ensure the outputs adhere to the ticket commitments. Zero is passed for // the vote subsidy since revocations do not produce any subsidy. return checkTicketRedeemerCommitments(ticketHash, ticketOuts, msgTx, - false, 0, isTreasuryEnabled) + false, 0, isTreasuryEnabled, isAutoRevocationsEnabled) } // CheckTransactionInputs performs a series of checks on the inputs to a @@ -2630,7 +2639,11 @@ func checkRevocationInputs(tx *dcrutil.Tx, txHeight int64, view *UtxoViewpoint, // // NOTE: The transaction MUST have already been sanity checked with the // CheckTransactionSanity function prior to calling this function. -func CheckTransactionInputs(subsidyCache *standalone.SubsidyCache, tx *dcrutil.Tx, txHeight int64, view *UtxoViewpoint, checkFraudProof bool, chainParams *chaincfg.Params, isTreasuryEnabled bool) (int64, error) { +func CheckTransactionInputs(subsidyCache *standalone.SubsidyCache, + tx *dcrutil.Tx, txHeight int64, view *UtxoViewpoint, checkFraudProof bool, + chainParams *chaincfg.Params, isTreasuryEnabled, + isAutoRevocationsEnabled bool) (int64, error) { + // Coinbase transactions have no inputs. msgTx := tx.MsgTx() if standalone.IsCoinBaseTx(msgTx, isTreasuryEnabled) { @@ -2666,7 +2679,7 @@ func CheckTransactionInputs(subsidyCache *standalone.SubsidyCache, tx *dcrutil.T isVote := stake.IsSSGen(msgTx, isTreasuryEnabled) if isVote { err := checkVoteInputs(subsidyCache, tx, txHeight, view, - chainParams, isTreasuryEnabled) + chainParams, isTreasuryEnabled, isAutoRevocationsEnabled) if err != nil { return 0, err } @@ -2681,7 +2694,7 @@ func CheckTransactionInputs(subsidyCache *standalone.SubsidyCache, tx *dcrutil.T isRevocation := stake.IsSSRtx(msgTx) if isRevocation { err := checkRevocationInputs(tx, txHeight, view, chainParams, - isTreasuryEnabled) + isTreasuryEnabled, isAutoRevocationsEnabled) if err != nil { return 0, err } @@ -3257,6 +3270,13 @@ func (b *BlockChain) checkTransactionsAndConnect(inputFees dcrutil.Amount, node return err } + // Determine if the automatic ticket revocations agenda is active as of the + // block being checked. + isAutoRevocationsEnabled, err := b.isAutoRevocationsAgendaActive(node.parent) + if err != nil { + return err + } + // Perform several checks on the inputs for each transaction. Also // accumulate the total fees. This could technically be combined with // the loop above instead of running another loop over the @@ -3280,7 +3300,7 @@ func (b *BlockChain) checkTransactionsAndConnect(inputFees dcrutil.Amount, node // spent, so be aware of this. txFee, err := CheckTransactionInputs(b.subsidyCache, tx, node.height, view, true, /* check fraud proofs */ - b.chainParams, isTreasuryEnabled) + b.chainParams, isTreasuryEnabled, isAutoRevocationsEnabled) if err != nil { log.Tracef("CheckTransactionInputs failed; error "+ "returned: %v", err) diff --git a/internal/mempool/mempool.go b/internal/mempool/mempool.go index 7eadfcf0..9ac5b08d 100644 --- a/internal/mempool/mempool.go +++ b/internal/mempool/mempool.go @@ -1244,6 +1244,7 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit, // Determine active agendas based on flags. isTreasuryEnabled := checkTxFlags.IsTreasuryEnabled() + isAutoRevocationsEnabled := checkTxFlags.IsAutoRevocationsEnabled() // A standalone transaction must not be a coinbase transaction. if standalone.IsCoinBaseTx(msgTx, isTreasuryEnabled) { @@ -1490,7 +1491,7 @@ func (mp *TxPool) maybeAcceptTransaction(tx *dcrutil.Tx, isNew, rateLimit, // filled in by the miner. txFee, err := blockchain.CheckTransactionInputs(mp.cfg.SubsidyCache, tx, nextBlockHeight, utxoView, false, mp.cfg.ChainParams, - isTreasuryEnabled) + isTreasuryEnabled, isAutoRevocationsEnabled) if err != nil { var cerr blockchain.RuleError if errors.As(err, &cerr) { diff --git a/internal/mining/mining.go b/internal/mining/mining.go index e2415ca3..8ace38ac 100644 --- a/internal/mining/mining.go +++ b/internal/mining/mining.go @@ -104,7 +104,9 @@ type Config struct { // CheckTransactionInputs defines the function to use to perform a series of // checks on the inputs to a transaction to ensure they are valid. - CheckTransactionInputs func(tx *dcrutil.Tx, txHeight int64, view *blockchain.UtxoViewpoint, checkFraudProof bool, isTreasuryEnabled bool) (int64, error) + CheckTransactionInputs func(tx *dcrutil.Tx, txHeight int64, + view *blockchain.UtxoViewpoint, checkFraudProof, isTreasuryEnabled, + isAutoRevocationsEnabled bool) (int64, error) // CheckTSpendHasVotes defines the function to use to check whether the given // tspend has enough votes to be included in a block AFTER the specified block. @@ -163,6 +165,11 @@ type Config struct { // treasury agenda is active or not for the block AFTER the given block. IsTreasuryAgendaActive func(prevHash *chainhash.Hash) (bool, error) + // IsAutoRevocationsAgendaActive defines the function to use to determine if + // the automatic ticket revocations agenda is active or not for the block + // AFTER the given block. + IsAutoRevocationsAgendaActive func(prevHash *chainhash.Hash) (bool, error) + // MaxTreasuryExpenditure defines the function to use to get the maximum amount // of funds that can be spent from the treasury by a set of TSpends for a block // that extends the given block hash. The function should return 0 if it is @@ -1062,6 +1069,11 @@ func (g *BlkTmplGenerator) NewBlockTemplate(payToAddress stdaddr.Address) (*Bloc return nil, err } + isAutoRevocationsEnabled, err := g.cfg.IsAutoRevocationsAgendaActive(&prevHash) + if err != nil { + return nil, err + } + var ( isTVI bool maxTreasurySpend int64 @@ -1551,7 +1563,7 @@ nextPriorityQueueItem: // The fraud proof is not checked because it will be filled in // by the miner. _, err = g.cfg.CheckTransactionInputs(bundledTx.Tx, nextBlockHeight, - blockUtxos, false, isTreasuryEnabled) + blockUtxos, false, isTreasuryEnabled, isAutoRevocationsEnabled) if err != nil { log.Tracef("Skipping tx %s due to error in "+ "CheckTransactionInputs: %v", bundledTx.Tx.Hash(), err) diff --git a/internal/mining/mining_harness_test.go b/internal/mining/mining_harness_test.go index 9f5dc837..808a23ad 100644 --- a/internal/mining/mining_harness_test.go +++ b/internal/mining/mining_harness_test.go @@ -69,6 +69,8 @@ type fakeChain struct { isHeaderCommitmentsAgendaActiveErr error isTreasuryAgendaActive bool isTreasuryAgendaActiveErr error + isAutoRevocationsAgendaActive bool + isAutoRevocationsAgendaActiveErr error maxTreasuryExpenditure int64 maxTreasuryExpenditureErr error parentUtxos *blockchain.UtxoViewpoint @@ -194,6 +196,13 @@ func (c *fakeChain) IsTreasuryAgendaActive(prevHash *chainhash.Hash) (bool, erro return c.isTreasuryAgendaActive, c.isTreasuryAgendaActiveErr } +// IsAutoRevocationsAgendaActive returns a mocked bool representing whether the +// automatic ticket revocations agenda is active or not for the block AFTER the +// given block. +func (c *fakeChain) IsAutoRevocationsAgendaActive(prevHash *chainhash.Hash) (bool, error) { + return c.isAutoRevocationsAgendaActive, c.isAutoRevocationsAgendaActiveErr +} + // MaxTreasuryExpenditure returns a mocked maximum amount of funds that can be // spent from the treasury by a set of TSpends for a block that extends the // given block hash. @@ -635,6 +644,7 @@ func (p *fakeTxSource) maybeAcceptTransaction(tx *dcrutil.Tx, isNew bool) ([]*ch height := p.chain.BestSnapshot().Height nextHeight := height + 1 isTreasuryEnabled := p.chain.isTreasuryAgendaActive + isAutoRevocationsEnabled := p.chain.isAutoRevocationsAgendaActive // 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 @@ -698,7 +708,7 @@ func (p *fakeTxSource) maybeAcceptTransaction(tx *dcrutil.Tx, isNew bool) ([]*ch } txFee, err := blockchain.CheckTransactionInputs(p.subsidyCache, tx, nextHeight, - utxoView, false, p.chainParams, isTreasuryEnabled) + utxoView, false, p.chainParams, isTreasuryEnabled, isAutoRevocationsEnabled) if err != nil { return nil, err } @@ -1388,11 +1398,12 @@ func newMiningHarness(chainParams *chaincfg.Params) (*miningHarness, []spendable CheckConnectBlockTemplate: chain.CheckConnectBlockTemplate, CheckTicketExhaustion: chain.CheckTicketExhaustion, CheckTransactionInputs: func(tx *dcrutil.Tx, txHeight int64, - view *blockchain.UtxoViewpoint, checkFraudProof bool, - isTreasuryEnabled bool) (int64, error) { + view *blockchain.UtxoViewpoint, checkFraudProof, isTreasuryEnabled, + isAutoRevocationsEnabled bool) (int64, error) { return blockchain.CheckTransactionInputs(subsidyCache, tx, txHeight, - view, checkFraudProof, chainParams, isTreasuryEnabled) + view, checkFraudProof, chainParams, isTreasuryEnabled, + isAutoRevocationsEnabled) }, CheckTSpendHasVotes: chain.CheckTSpendHasVotes, CountSigOps: blockchain.CountSigOps, @@ -1403,6 +1414,7 @@ func newMiningHarness(chainParams *chaincfg.Params) (*miningHarness, []spendable IsFinalizedTransaction: blockchain.IsFinalizedTransaction, IsHeaderCommitmentsAgendaActive: chain.IsHeaderCommitmentsAgendaActive, IsTreasuryAgendaActive: chain.IsTreasuryAgendaActive, + IsAutoRevocationsAgendaActive: chain.IsAutoRevocationsAgendaActive, MaxTreasuryExpenditure: chain.MaxTreasuryExpenditure, NewUtxoViewpoint: chain.NewUtxoViewpoint, TipGeneration: chain.TipGeneration, diff --git a/server.go b/server.go index 82625548..fe56a7fc 100644 --- a/server.go +++ b/server.go @@ -3560,11 +3560,12 @@ func newServer(ctx context.Context, listenAddrs []string, db database.DB, CheckConnectBlockTemplate: s.chain.CheckConnectBlockTemplate, CheckTicketExhaustion: s.chain.CheckTicketExhaustion, CheckTransactionInputs: func(tx *dcrutil.Tx, txHeight int64, - view *blockchain.UtxoViewpoint, checkFraudProof bool, - isTreasuryEnabled bool) (int64, error) { + view *blockchain.UtxoViewpoint, checkFraudProof, isTreasuryEnabled, + isAutoRevocationsEnabled bool) (int64, error) { return blockchain.CheckTransactionInputs(s.subsidyCache, tx, txHeight, - view, checkFraudProof, s.chainParams, isTreasuryEnabled) + view, checkFraudProof, s.chainParams, isTreasuryEnabled, + isAutoRevocationsEnabled) }, CheckTSpendHasVotes: s.chain.CheckTSpendHasVotes, CountSigOps: blockchain.CountSigOps, @@ -3575,6 +3576,7 @@ func newServer(ctx context.Context, listenAddrs []string, db database.DB, IsFinalizedTransaction: blockchain.IsFinalizedTransaction, IsHeaderCommitmentsAgendaActive: s.chain.IsHeaderCommitmentsAgendaActive, IsTreasuryAgendaActive: s.chain.IsTreasuryAgendaActive, + IsAutoRevocationsAgendaActive: s.chain.IsAutoRevocationsAgendaActive, MaxTreasuryExpenditure: s.chain.MaxTreasuryExpenditure, NewUtxoViewpoint: func() *blockchain.UtxoViewpoint { return blockchain.NewUtxoViewpoint(utxoCache)