diff --git a/src/bitcoin-tx.cpp b/src/bitcoin-tx.cpp index 710d0082f..4fcc13e41 100644 --- a/src/bitcoin-tx.cpp +++ b/src/bitcoin-tx.cpp @@ -164,6 +164,15 @@ static void MutateTxVersion(CMutableTransaction& tx, const string& cmdVal) tx.nVersion = (int) newVersion; } +static void MutateTxExpiry(CMutableTransaction& tx, const string& cmdVal) +{ + int64_t newExpiry = atoi64(cmdVal); + if (newExpiry >= TX_EXPIRY_HEIGHT_THRESHOLD) { + throw runtime_error("Invalid TX expiry requested"); + } + tx.nExpiryHeight = (int) newExpiry; +} + static void MutateTxLocktime(CMutableTransaction& tx, const string& cmdVal) { int64_t newLocktime = atoi64(cmdVal); @@ -503,6 +512,8 @@ static void MutateTx(CMutableTransaction& tx, const string& command, MutateTxVersion(tx, commandVal); else if (command == "locktime") MutateTxLocktime(tx, commandVal); + else if (command == "expiry") + MutateTxExpiry(tx, commandVal); else if (command == "delin") MutateTxDelInput(tx, commandVal); diff --git a/src/consensus/consensus.h b/src/consensus/consensus.h index 43fd1e828..8650c453a 100644 --- a/src/consensus/consensus.h +++ b/src/consensus/consensus.h @@ -22,6 +22,8 @@ static const unsigned int MAX_BLOCK_SIGOPS = 20000; static const unsigned int MAX_TX_SIZE = 100000; /** Coinbase transaction outputs can only be spent after this number of new blocks (network rule) */ static const int COINBASE_MATURITY = 100; +/** The minimum value which is invalid for expiry height, used by CTransaction and CMutableTransaction */ +static constexpr uint32_t TX_EXPIRY_HEIGHT_THRESHOLD = 500000000; /** Flags for LockTime() */ enum { diff --git a/src/main.cpp b/src/main.cpp index 9e41d9c4e..161c3933c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -73,6 +73,8 @@ size_t nCoinCacheUsage = 5000 * 300; uint64_t nPruneTarget = 0; bool fAlerts = DEFAULT_ALERTS; +unsigned int expiryDelta = DEFAULT_TX_EXPIRY_DELTA; + /** Fees smaller than this (in satoshi) are considered zero fee (for relaying and mining) */ CFeeRate minRelayTxFee = CFeeRate(DEFAULT_MIN_RELAY_TX_FEE); @@ -718,6 +720,14 @@ bool IsFinalTx(const CTransaction &tx, int nBlockHeight, int64_t nBlockTime) return true; } +bool IsExpiredTx(const CTransaction &tx, int nBlockHeight) +{ + if (tx.nExpiryHeight == 0 || tx.IsCoinBase()) { + return false; + } + return static_cast(nBlockHeight) > tx.nExpiryHeight; +} + bool CheckFinalTx(const CTransaction &tx, int flags) { AssertLockHeld(cs_main); @@ -884,6 +894,11 @@ bool ContextualCheckTransaction(const CTransaction& tx, CValidationState &state, return state.DoS(dosLevel, error("ContextualCheckTransaction: overwinter is active"), REJECT_INVALID, "tx-overwinter-active"); } + + // Check that all transactions are unexpired + if (IsExpiredTx(tx, nHeight)) { + return state.DoS(dosLevel, error("ContextualCheckTransaction(): transaction is expired"), REJECT_INVALID, "tx-overwinter-expired"); + } } if (!(tx.IsCoinBase() || tx.vjoinsplit.empty())) { @@ -2659,6 +2674,10 @@ bool static ConnectTip(CValidationState &state, CBlockIndex *pindexNew, CBlock * // Remove conflicting transactions from the mempool. list txConflicted; mempool.removeForBlock(pblock->vtx, pindexNew->nHeight, txConflicted, !IsInitialBlockDownload()); + + // Remove transactions that expire at new block height from mempool + mempool.removeExpired(pindexNew->nHeight); + // Update chainActive & related variables. UpdateTip(pindexNew); // Tell wallet about transactions that went from mempool diff --git a/src/main.h b/src/main.h index 2cae24b22..2a3b4175a 100644 --- a/src/main.h +++ b/src/main.h @@ -68,6 +68,8 @@ static const unsigned int MAX_STANDARD_TX_SIGOPS = MAX_BLOCK_SIGOPS/5; static const unsigned int DEFAULT_MIN_RELAY_TX_FEE = 100; /** Default for -maxorphantx, maximum number of orphan transactions kept in memory */ static const unsigned int DEFAULT_MAX_ORPHAN_TRANSACTIONS = 100; +/** Default for -txexpirydelta, in number of blocks */ +static const unsigned int DEFAULT_TX_EXPIRY_DELTA = 20; /** The maximum size of a blk?????.dat file (since 0.8) */ static const unsigned int MAX_BLOCKFILE_SIZE = 0x8000000; // 128 MiB /** The pre-allocation chunk size for blk?????.dat files (since 0.8) */ @@ -110,6 +112,7 @@ struct BlockHasher size_t operator()(const uint256& hash) const { return hash.GetCheapHash(); } }; +extern unsigned int expiryDelta; extern CScript COINBASE_FLAGS; extern CCriticalSection cs_main; extern CTxMemPool mempool; @@ -371,6 +374,12 @@ bool CheckTxInputs(const CTransaction& tx, CValidationState& state, const CCoins */ bool IsFinalTx(const CTransaction &tx, int nBlockHeight, int64_t nBlockTime); +/** + * Check if transaction is expired and can be included in a block with the + * specified height. Consensus critical. + */ +bool IsExpiredTx(const CTransaction &tx, int nBlockHeight); + /** * Check if transaction will be final in the next block to be created. * diff --git a/src/miner.cpp b/src/miner.cpp index d87205c64..ccbee75ab 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -166,7 +166,7 @@ CBlockTemplate* CreateNewBlock(const CScript& scriptPubKeyIn) ? nMedianTimePast : pblock->GetBlockTime(); - if (tx.IsCoinBase() || !IsFinalTx(tx, nHeight, nLockTimeCutoff)) + if (tx.IsCoinBase() || !IsFinalTx(tx, nHeight, nLockTimeCutoff) || IsExpiredTx(tx, nHeight)) continue; COrphan* porphan = NULL; @@ -345,6 +345,8 @@ CBlockTemplate* CreateNewBlock(const CScript& scriptPubKeyIn) txNew.vout.resize(1); txNew.vout[0].scriptPubKey = scriptPubKeyIn; txNew.vout[0].nValue = GetBlockSubsidy(nHeight, chainparams.GetConsensus()); + // Set to 0 so expiry height does not apply to coinbase txs + txNew.nExpiryHeight = 0; if ((nHeight > 0) && (nHeight <= chainparams.GetConsensus().GetLastFoundersRewardBlockHeight())) { // Founders reward is 20% of the block subsidy diff --git a/src/primitives/transaction.h b/src/primitives/transaction.h index 654a68b8a..bcd7eaa8a 100644 --- a/src/primitives/transaction.h +++ b/src/primitives/transaction.h @@ -304,9 +304,6 @@ public: std::string ToString() const; }; -// The maximum value which is valid for expiry height, used by CTransaction and CMutableTransaction -static constexpr uint32_t TX_EXPIRY_HEIGHT_THRESHOLD = 500000000; - // Overwinter version group id static constexpr uint32_t OVERWINTER_VERSION_GROUP_ID = 0x03C48270; static_assert(OVERWINTER_VERSION_GROUP_ID != 0, "version group id must be non-zero as specified in ZIP 202"); diff --git a/src/rpcrawtransaction.cpp b/src/rpcrawtransaction.cpp index 2f89529de..f28066300 100644 --- a/src/rpcrawtransaction.cpp +++ b/src/rpcrawtransaction.cpp @@ -198,6 +198,7 @@ UniValue getrawtransaction(const UniValue& params, bool fHelp) " \"txid\" : \"id\", (string) The transaction id (same as provided)\n" " \"version\" : n, (numeric) The version\n" " \"locktime\" : ttt, (numeric) The lock time\n" + " \"expiryheight\" : ttt, (numeric, optional) The block height after which the transaction expires\n" " \"vin\" : [ (array of json objects)\n" " {\n" " \"txid\": \"id\", (string) The transaction id\n" @@ -443,8 +444,16 @@ UniValue createrawtransaction(const UniValue& params, bool fHelp) UniValue inputs = params[0].get_array(); UniValue sendTo = params[1].get_obj(); + int nextBlockHeight = chainActive.Height() + 1; CMutableTransaction rawTx = CreateNewContextualCMutableTransaction( - Params().GetConsensus(), chainActive.Height() + 1); + Params().GetConsensus(), nextBlockHeight); + + if (NetworkUpgradeActive(nextBlockHeight, Params().GetConsensus(), Consensus::UPGRADE_OVERWINTER)) { + rawTx.nExpiryHeight = nextBlockHeight + expiryDelta; + if (rawTx.nExpiryHeight >= TX_EXPIRY_HEIGHT_THRESHOLD){ + throw JSONRPCError(RPC_INVALID_PARAMETER, "nExpiryHeight must be less than TX_EXPIRY_HEIGHT_THRESHOLD."); + } + } for (size_t idx = 0; idx < inputs.size(); idx++) { const UniValue& input = inputs[idx]; @@ -497,7 +506,7 @@ UniValue decoderawtransaction(const UniValue& params, bool fHelp) "\nResult:\n" "{\n" " \"txid\" : \"id\", (string) The transaction id\n" - " \"overwintered\" : bool (boolean) The Overwintered flag\n" + " \"overwintered\" : bool (boolean) The Overwintered flag\n" " \"version\" : n, (numeric) The version\n" " \"versiongroupid\": \"hex\" (string, optional) The version group id (Overwintered txs)\n" " \"locktime\" : ttt, (numeric) The lock time\n" diff --git a/src/txmempool.cpp b/src/txmempool.cpp index 6b3c16f2a..acb671ec2 100644 --- a/src/txmempool.cpp +++ b/src/txmempool.cpp @@ -256,6 +256,24 @@ void CTxMemPool::removeConflicts(const CTransaction &tx, std::list } } +void CTxMemPool::removeExpired(unsigned int nBlockHeight) +{ + // Remove expired txs from the mempool + LOCK(cs); + list transactionsToRemove; + for (indexed_transaction_set::const_iterator it = mapTx.begin(); it != mapTx.end(); it++) + { + const CTransaction& tx = it->GetTx(); + if (IsExpiredTx(tx, nBlockHeight)) { + transactionsToRemove.push_back(tx); + } + } + for (const CTransaction& tx : transactionsToRemove) { + list removed; + remove(tx, removed, true); + } +} + /** * Called when a block is connected. Removes from mempool and updates the miner fee estimator. */ diff --git a/src/txmempool.h b/src/txmempool.h index 2cb2c8f05..6b6434f1e 100644 --- a/src/txmempool.h +++ b/src/txmempool.h @@ -168,6 +168,7 @@ public: void removeWithAnchor(const uint256 &invalidRoot); void removeForReorg(const CCoinsViewCache *pcoins, unsigned int nMemPoolHeight, int flags); void removeConflicts(const CTransaction &tx, std::list& removed); + void removeExpired(unsigned int nBlockHeight); void removeForBlock(const std::vector& vtx, unsigned int nBlockHeight, std::list& conflicts, bool fCurrentEstimate = true); void removeWithoutBranchId(uint32_t nMemPoolBranchId); diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index d3da306ed..9050c4b1b 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -86,6 +86,7 @@ void WalletTxToJSON(const CWalletTx& wtx, UniValue& entry) entry.push_back(Pair("blockhash", wtx.hashBlock.GetHex())); entry.push_back(Pair("blockindex", wtx.nIndex)); entry.push_back(Pair("blocktime", mapBlockIndex[wtx.hashBlock]->GetBlockTime())); + entry.push_back(Pair("expiryheight", (int64_t)wtx.nExpiryHeight)); } uint256 hash = wtx.GetHash(); entry.push_back(Pair("txid", hash.GetHex())); @@ -3534,11 +3535,15 @@ UniValue z_sendmany(const UniValue& params, bool fHelp) UniValue contextInfo = o; // Contextual transaction we will build on - CMutableTransaction contextualTx = CreateNewContextualCMutableTransaction(Params().GetConsensus(), chainActive.Height() + 1); + int nextBlockHeight = chainActive.Height() + 1; + CMutableTransaction contextualTx = CreateNewContextualCMutableTransaction(Params().GetConsensus(), nextBlockHeight); bool isShielded = !fromTaddr || zaddrRecipients.size() > 0; if (contextualTx.nVersion == 1 && isShielded) { contextualTx.nVersion = 2; // Tx format should support vjoinsplits } + if (NetworkUpgradeActive(nextBlockHeight, Params().GetConsensus(), Consensus::UPGRADE_OVERWINTER)) { + contextualTx.nExpiryHeight = nextBlockHeight + expiryDelta; + } // Create operation and add to global queue std::shared_ptr q = getAsyncRPCQueue(); @@ -3725,12 +3730,15 @@ UniValue z_shieldcoinbase(const UniValue& params, bool fHelp) contextInfo.push_back(Pair("fee", ValueFromAmount(nFee))); // Contextual transaction we will build on + int nextBlockHeight = chainActive.Height() + 1; CMutableTransaction contextualTx = CreateNewContextualCMutableTransaction( - Params().GetConsensus(), - chainActive.Height() + 1); + Params().GetConsensus(), nextBlockHeight); if (contextualTx.nVersion == 1) { contextualTx.nVersion = 2; // Tx format should support vjoinsplits } + if (NetworkUpgradeActive(nextBlockHeight, Params().GetConsensus(), Consensus::UPGRADE_OVERWINTER)) { + contextualTx.nExpiryHeight = nextBlockHeight + expiryDelta; + } // Create operation and add to global queue std::shared_ptr q = getAsyncRPCQueue(); diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index ce86cbad0..253eb6169 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -10,6 +10,7 @@ #include "coincontrol.h" #include "consensus/upgrades.h" #include "consensus/validation.h" +#include "consensus/consensus.h" #include "init.h" #include "main.h" #include "net.h" @@ -2523,6 +2524,7 @@ bool CWallet::FundTransaction(CMutableTransaction& tx, CAmount &nFeeRet, int& nC CReserveKey reservekey(this); CWalletTx wtx; + if (!CreateTransaction(vecSend, wtx, reservekey, nFeeRet, nChangePosRet, strFailReason, &coinControl, false)) return false; @@ -2573,9 +2575,20 @@ bool CWallet::CreateTransaction(const vector& vecSend, CWalletTx& wt wtxNew.fTimeReceivedIsTxTime = true; wtxNew.BindWallet(this); + int nextBlockHeight = chainActive.Height() + 1; CMutableTransaction txNew = CreateNewContextualCMutableTransaction( - Params().GetConsensus(), chainActive.Height() + 1); + Params().GetConsensus(), nextBlockHeight); + // Activates after Overwinter network upgrade + // Set nExpiryHeight to expiryDelta (default 20) blocks past current block height + if (NetworkUpgradeActive(nextBlockHeight, Params().GetConsensus(), Consensus::UPGRADE_OVERWINTER)) { + if (nextBlockHeight + expiryDelta >= TX_EXPIRY_HEIGHT_THRESHOLD){ + strFailReason = _("nExpiryHeight must be less than TX_EXPIRY_HEIGHT_THRESHOLD."); + } else { + txNew.nExpiryHeight = nextBlockHeight + expiryDelta; + } + } + // Discourage fee sniping. // // However because of a off-by-one-error in previous versions we need to