From 62062ade0ff628823b012e7d8d07ac022cdbf104 Mon Sep 17 00:00:00 2001 From: Peter Bushnell Date: Tue, 14 Jun 2022 13:10:48 +0100 Subject: [PATCH] TX version and expiration --- src/consensus/tx_check.cpp | 31 ++++-- src/consensus/tx_check.h | 13 ++- src/init.cpp | 4 + src/masternodes/masternodes.cpp | 10 ++ src/masternodes/masternodes.h | 6 ++ src/masternodes/mn_checks.cpp | 30 ++++-- src/masternodes/mn_checks.h | 27 +++-- src/masternodes/mn_rpc.cpp | 44 ++++++++- src/masternodes/mn_rpc.h | 1 + src/masternodes/rpc_customtx.cpp | 5 +- src/masternodes/rpc_poolpair.cpp | 4 + src/primitives/transaction.h | 16 +++ src/rpc/client.cpp | 1 + src/txmempool.cpp | 22 ++++- src/txmempool.h | 8 +- src/validation.cpp | 24 +++-- test/functional/feature_reject_customtxs.py | 47 ++++++++- .../feature_tx_versioning_and_expiration.py | 99 +++++++++++++++++++ test/functional/test_runner.py | 1 + 19 files changed, 354 insertions(+), 39 deletions(-) create mode 100755 test/functional/feature_tx_versioning_and_expiration.py diff --git a/src/consensus/tx_check.cpp b/src/consensus/tx_check.cpp index 39716eb3a42..74a6fb37b1f 100644 --- a/src/consensus/tx_check.cpp +++ b/src/consensus/tx_check.cpp @@ -4,6 +4,7 @@ #include +#include #include #include @@ -68,7 +69,8 @@ bool CheckTransaction(const CTransaction& tx, CValidationState &state, bool fChe bool ParseScriptByMarker(CScript const & script, const std::vector & marker, std::vector & metadata, - bool& hasAdditionalOpcodes) + uint8_t& hasAdditionalOpcodes, + CExpirationAndVersion* customTxParams) { opcodetype opcode; auto pc = script.begin(); @@ -83,8 +85,20 @@ bool ParseScriptByMarker(CScript const & script, } // Check that no more opcodes are found in the script - if (script.GetOp(pc, opcode)) { - hasAdditionalOpcodes = true; + std::vector expirationAndVersion; + if (script.GetOp(pc, opcode, expirationAndVersion)) { + hasAdditionalOpcodes |= HasForks::FortCanning; + if (expirationAndVersion.size() == sizeof(uint32_t) + sizeof(uint8_t)) { + if (customTxParams) { + VectorReader stream(SER_DISK, CLIENT_VERSION, expirationAndVersion, 0); + stream >> *customTxParams; + } + } else { + hasAdditionalOpcodes |= HasForks::GreatWorld; + } + if (pc != script.end()) { + hasAdditionalOpcodes |= HasForks::GreatWorld; + } } metadata.erase(metadata.begin(), metadata.begin() + marker.size()); @@ -96,7 +110,7 @@ bool IsAnchorRewardTx(CTransaction const & tx, std::vector & meta if (!tx.IsCoinBase() || tx.vout.size() != 2 || tx.vout[0].nValue != 0) { return false; } - bool hasAdditionalOpcodes{false}; + uint8_t hasAdditionalOpcodes{HasForks::None}; const auto result = ParseScriptByMarker(tx.vout[0].scriptPubKey, DfAnchorFinalizeTxMarker, metadata, hasAdditionalOpcodes); if (fortCanning && hasAdditionalOpcodes) { return false; @@ -104,14 +118,15 @@ bool IsAnchorRewardTx(CTransaction const & tx, std::vector & meta return result; } -bool IsAnchorRewardTxPlus(CTransaction const & tx, std::vector & metadata, bool fortCanning) +bool IsAnchorRewardTxPlus(CTransaction const & tx, std::vector & metadata, uint8_t hasForks) { if (!tx.IsCoinBase() || tx.vout.size() != 2 || tx.vout[0].nValue != 0) { return false; } - bool hasAdditionalOpcodes{false}; + uint8_t hasAdditionalOpcodes{HasForks::None}; const auto result = ParseScriptByMarker(tx.vout[0].scriptPubKey, DfAnchorFinalizeTxMarkerPlus, metadata, hasAdditionalOpcodes); - if (fortCanning && hasAdditionalOpcodes) { + if ((hasForks & HasForks::FortCanning && !(hasForks & HasForks::GreatWorld) && hasAdditionalOpcodes & HasForks::FortCanning) || + (hasForks & HasForks::GreatWorld && hasAdditionalOpcodes & HasForks::GreatWorld)) { return false; } return result; @@ -125,7 +140,7 @@ bool IsTokenSplitTx(CTransaction const & tx, std::vector & metada if (!tx.IsCoinBase() || tx.vout.size() != 1 || tx.vout[0].nValue != 0) { return false; } - bool hasAdditionalOpcodes{false}; + uint8_t hasAdditionalOpcodes{HasForks::None}; const auto result = ParseScriptByMarker(tx.vout[0].scriptPubKey, DfTokenSplitMarker, metadata, hasAdditionalOpcodes); if (hasAdditionalOpcodes) { return false; diff --git a/src/consensus/tx_check.h b/src/consensus/tx_check.h index eac08d11edc..ef8fc97d2d1 100644 --- a/src/consensus/tx_check.h +++ b/src/consensus/tx_check.h @@ -12,6 +12,7 @@ * belongs in tx_verify.h/cpp instead. */ +#include #include extern const std::vector DfTxMarker; @@ -19,18 +20,26 @@ extern const std::vector DfAnchorFinalizeTxMarker; extern const std::vector DfAnchorFinalizeTxMarkerPlus; extern const std::vector DfTokenSplitMarker; +struct CExpirationAndVersion; class CScript; class CTransaction; class CValidationState; +enum HasForks : uint8_t { + None = 0, + FortCanning = 1 << 0, + GreatWorld = 1 << 1, +}; + bool CheckTransaction(const CTransaction& tx, CValidationState& state, bool fCheckDuplicateInputs=true); bool ParseScriptByMarker(CScript const & script, const std::vector & marker, std::vector & metadata, - bool& hasAdditionalOpcodes); + uint8_t& hasAdditionalOpcodes, + CExpirationAndVersion* customTxParams = nullptr); bool IsAnchorRewardTx(CTransaction const & tx, std::vector & metadata, bool fortCanning = false); -bool IsAnchorRewardTxPlus(CTransaction const & tx, std::vector & metadata, bool fortCanning = false); +bool IsAnchorRewardTxPlus(CTransaction const & tx, std::vector & metadata, uint8_t hasForks = HasForks::None); bool IsTokenSplitTx(CTransaction const & tx, std::vector & metadata, bool fortCanningCrunch = true); #endif // DEFI_CONSENSUS_TX_CHECK_H diff --git a/src/init.cpp b/src/init.cpp index 055dcf320f4..539e939e01a 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -400,6 +400,7 @@ void SetupServerArgs() gArgs.AddArg("-dbbatchsize", strprintf("Maximum database write batch size in bytes (default: %u)", nDefaultDbBatchSize), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::OPTIONS); gArgs.AddArg("-dbcache=", strprintf("Maximum database cache size MiB (%d to %d, default: %d). In addition, unused mempool memory is shared for this cache (see -maxmempool).", nMinDbCache, nMaxDbCache, nDefaultDbCache), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); gArgs.AddArg("-debuglogfile=", strprintf("Specify location of debug log file. Relative paths will be prefixed by a net-specific datadir location. (-nodebuglogfile to disable; default: %s)", DEFAULT_DEBUGLOGFILE), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); + gArgs.AddArg("-customtxexpiration=", strprintf("Number of blocks ahead of tip locally created transaction will expire (default: %u)", DEFAULT_CUSTOM_TX_EXPIRATION), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); gArgs.AddArg("-feefilter", strprintf("Tell other nodes to filter invs to us by our mempool min fee (default: %u)", DEFAULT_FEEFILTER), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::OPTIONS); gArgs.AddArg("-includeconf=", "Specify additional configuration file, relative to the -datadir path (only useable from configuration file, not command line)", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); gArgs.AddArg("-loadblock=", "Imports blocks from external blk000??.dat file on startup", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); @@ -1687,6 +1688,9 @@ bool AppInitMain(InitInterfaces& interfaces) // Ensure we are on latest DB version pcustomcsview->SetDbVersion(CCustomCSView::DbVersion); + // Set custom Tx expiration + pcustomcsview->SetGlobalCustomTxExpiration(gArgs.GetArg("-customtxexpiration", DEFAULT_CUSTOM_TX_EXPIRATION)); + // make account history db paccountHistoryDB.reset(); if (gArgs.GetBoolArg("-acindex", DEFAULT_ACINDEX)) { diff --git a/src/masternodes/masternodes.cpp b/src/masternodes/masternodes.cpp index 6f2d3f9bfb7..fc6f0f2e2ac 100644 --- a/src/masternodes/masternodes.cpp +++ b/src/masternodes/masternodes.cpp @@ -727,6 +727,16 @@ std::vector CAnchorConfirmsView::GetAnchorConfirmData() /* * CCustomCSView */ +void CCustomCSView::SetGlobalCustomTxExpiration(const uint32_t height) +{ + globalCustomTxExpiration = height; +} + +uint32_t CCustomCSView::GetGlobalCustomTxExpiration() const +{ + return globalCustomTxExpiration; +} + int CCustomCSView::GetDbVersion() const { int version; diff --git a/src/masternodes/masternodes.h b/src/masternodes/masternodes.h index 81bace5c3a3..00306f665fc 100644 --- a/src/masternodes/masternodes.h +++ b/src/masternodes/masternodes.h @@ -41,6 +41,7 @@ CAmount GetTokenCreationFee(int height); CAmount GetMnCollateralAmount(int height); constexpr uint8_t SUBNODE_COUNT{4}; +constexpr uint32_t DEFAULT_CUSTOM_TX_EXPIRATION{120}; class CMasternode { @@ -386,6 +387,8 @@ class CCustomCSView Res PopulateLoansData(CCollateralLoans& result, CVaultId const& vaultId, uint32_t height, int64_t blockTime, bool useNextPrice, bool requireLivePrice); Res PopulateCollateralData(CCollateralLoans& result, CVaultId const& vaultId, CBalances const& collaterals, uint32_t height, int64_t blockTime, bool useNextPrice, bool requireLivePrice); + uint32_t globalCustomTxExpiration{DEFAULT_CUSTOM_TX_EXPIRATION}; + public: // Increase version when underlaying tables are changed static constexpr const int DbVersion = 1; @@ -440,6 +443,9 @@ class CCustomCSView int GetDbVersion() const; + void SetGlobalCustomTxExpiration(const uint32_t height); + uint32_t GetGlobalCustomTxExpiration() const; + uint256 MerkleRoot(); // we construct it as it diff --git a/src/masternodes/mn_checks.cpp b/src/masternodes/mn_checks.cpp index 8891a427f61..d51f0eaa62e 100644 --- a/src/masternodes/mn_checks.cpp +++ b/src/masternodes/mn_checks.cpp @@ -124,7 +124,7 @@ static ResVal MintedTokens(CTransaction const & tx, uint32_t mintingO return {balances, Res::Ok()}; } -CCustomTxMessage customTypeToMessage(CustomTxType txType) { +CCustomTxMessage customTypeToMessage(CustomTxType txType, uint8_t version) { switch (txType) { case CustomTxType::CreateMasternode: return CCreateMasterNodeMessage{}; @@ -3807,7 +3807,8 @@ Res RevertCustomTx(CCustomCSView& mnview, const CCoinsViewCache& coins, const CT } auto res = Res::Ok(); std::vector metadata; - auto txType = GuessCustomTxType(tx, metadata); + CExpirationAndVersion customTxParams; + auto txType = GuessCustomTxType(tx, metadata, false, 0, &customTxParams); switch(txType) { case CustomTxType::CreateMasternode: @@ -3820,7 +3821,7 @@ Res RevertCustomTx(CCustomCSView& mnview, const CCoinsViewCache& coins, const CT default: break; } - auto txMessage = customTypeToMessage(txType); + auto txMessage = customTypeToMessage(txType, customTxParams.version); CAccountsHistoryEraser view(mnview, height, txn, erasers); if ((res = CustomMetadataParse(height, consensus, metadata, txMessage))) { res = CustomTxRevert(view, coins, tx, height, consensus, txMessage); @@ -3890,7 +3891,7 @@ void PopulateVaultHistoryData(CHistoryWriters* writers, CAccountsHistoryWriter& } } -Res ApplyCustomTx(CCustomCSView& mnview, const CCoinsViewCache& coins, const CTransaction& tx, const Consensus::Params& consensus, uint32_t height, uint64_t time, uint32_t txn, CHistoryWriters* writers) { +Res ApplyCustomTx(CCustomCSView& mnview, const CCoinsViewCache& coins, const CTransaction& tx, const Consensus::Params& consensus, uint32_t height, uint64_t time, uint32_t* customTxExpiration, uint32_t txn, CHistoryWriters* writers) { auto res = Res::Ok(); if (tx.IsCoinBase() && height > 0) { // genesis contains custom coinbase txs return res; @@ -3898,7 +3899,8 @@ Res ApplyCustomTx(CCustomCSView& mnview, const CCoinsViewCache& coins, const CTr std::vector metadata; const auto metadataValidation = height >= static_cast(consensus.FortCanningHeight); - auto txType = GuessCustomTxType(tx, metadata, metadataValidation); + CExpirationAndVersion customTxParams; + auto txType = GuessCustomTxType(tx, metadata, metadataValidation, height, &customTxParams); if (txType == CustomTxType::None) { return res; } @@ -3906,7 +3908,23 @@ Res ApplyCustomTx(CCustomCSView& mnview, const CCoinsViewCache& coins, const CTr if (metadataValidation && txType == CustomTxType::Reject) { return Res::ErrCode(CustomTxErrCodes::Fatal, "Invalid custom transaction"); } - auto txMessage = customTypeToMessage(txType); + + if (height >= static_cast(consensus.GreatWorldHeight)) { + if (customTxParams.expiration == 0) { + return Res::ErrCode(CustomTxErrCodes::Fatal, "Invalid transaction expiration set"); + } + if (customTxParams.version > static_cast(MetadataVersion::Two)) { + return Res::ErrCode(CustomTxErrCodes::Fatal, "Invalid transaction version set"); + } + if (height > customTxParams.expiration) { + return Res::ErrCode(CustomTxErrCodes::Fatal, "Transaction has expired"); + } + if (customTxExpiration) { + *customTxExpiration = customTxParams.expiration; + } + } + + auto txMessage = customTypeToMessage(txType, customTxParams.version); CAccountsHistoryWriter view(mnview, height, txn, tx.GetHash(), uint8_t(txType), writers); if ((res = CustomMetadataParse(height, consensus, metadata, txMessage))) { if (pvaultHistoryDB && writers) { diff --git a/src/masternodes/mn_checks.h b/src/masternodes/mn_checks.h index b484fb19a0f..603073c2ad3 100644 --- a/src/masternodes/mn_checks.h +++ b/src/masternodes/mn_checks.h @@ -105,6 +105,12 @@ enum class CustomTxType : uint8_t FutureSwapRefund = 'w', }; +enum class MetadataVersion : uint8_t { + None = 0, + One = 1, + Two = 2, +}; + inline CustomTxType CustomTxCodeToType(uint8_t ch) { auto type = static_cast(ch); switch(type) { @@ -381,11 +387,11 @@ using CCustomTxMessage = std::variant< CAuctionBidMessage >; -CCustomTxMessage customTypeToMessage(CustomTxType txType); +CCustomTxMessage customTypeToMessage(CustomTxType txType, uint8_t version); bool IsMempooledCustomTxCreate(const CTxMemPool& pool, const uint256& txid); Res RpcInfo(const CTransaction& tx, uint32_t height, CustomTxType& type, UniValue& results); Res CustomMetadataParse(uint32_t height, const Consensus::Params& consensus, const std::vector& metadata, CCustomTxMessage& txMessage); -Res ApplyCustomTx(CCustomCSView& mnview, const CCoinsViewCache& coins, const CTransaction& tx, const Consensus::Params& consensus, uint32_t height, uint64_t time = 0, uint32_t txn = 0, CHistoryWriters* writers = nullptr); +Res ApplyCustomTx(CCustomCSView& mnview, const CCoinsViewCache& coins, const CTransaction& tx, const Consensus::Params& consensus, uint32_t height, uint64_t time = 0, uint32_t* customTxExpiration = nullptr, uint32_t txn = 0, CHistoryWriters* writers = nullptr); Res RevertCustomTx(CCustomCSView& mnview, const CCoinsViewCache& coins, const CTransaction& tx, const Consensus::Params& consensus, uint32_t height, uint32_t txn, CHistoryErasers& erasers); Res CustomTxVisit(CCustomCSView& mnview, const CCoinsViewCache& coins, const CTransaction& tx, uint32_t height, const Consensus::Params& consensus, const CCustomTxMessage& txMessage, uint64_t time, uint32_t txn = 0); ResVal ApplyAnchorRewardTx(CCustomCSView& mnview, const CTransaction& tx, int height, const uint256& prevStakeModifier, const std::vector& metadata, const Consensus::Params& consensusParams); @@ -410,7 +416,8 @@ inline bool OraclePriceFeed(CCustomCSView& view, const CTokenCurrencyPair& price /* * Checks if given tx is probably one of 'CustomTx', returns tx type and serialized metadata in 'data' */ -inline CustomTxType GuessCustomTxType(CTransaction const & tx, std::vector & metadata, bool metadataValidation = false){ +inline CustomTxType GuessCustomTxType(CTransaction const & tx, std::vector & metadata, bool metadataValidation = false, + uint32_t height = 0, CExpirationAndVersion* customTxParams = nullptr){ if (tx.vout.empty()) { return CustomTxType::None; } @@ -419,21 +426,25 @@ inline CustomTxType GuessCustomTxType(CTransaction const & tx, std::vector dummydata; - bool dummyOpcodes{false}; + uint8_t dummyOpcodes{HasForks::None}; if (ParseScriptByMarker(tx.vout[i].scriptPubKey, DfTxMarker, dummydata, dummyOpcodes)) { return CustomTxType::Reject; } } } - bool hasAdditionalOpcodes{false}; - if (!ParseScriptByMarker(tx.vout[0].scriptPubKey, DfTxMarker, metadata, hasAdditionalOpcodes)) { + uint8_t hasAdditionalOpcodes{HasForks::None}; + if (!ParseScriptByMarker(tx.vout[0].scriptPubKey, DfTxMarker, metadata, hasAdditionalOpcodes, customTxParams)) { return CustomTxType::None; } // If metadata contains additional opcodes mark as Reject. - if (metadataValidation && hasAdditionalOpcodes) { - return CustomTxType::Reject; + if (metadataValidation) { + if (height < static_cast(Params().GetConsensus().GreatWorldHeight) && hasAdditionalOpcodes & HasForks::FortCanning) { + return CustomTxType::Reject; + } else if (height >= static_cast(Params().GetConsensus().GreatWorldHeight) && hasAdditionalOpcodes & HasForks::GreatWorld) { + return CustomTxType::Reject; + } } auto txType = CustomTxCodeToType(metadata[0]); diff --git a/src/masternodes/mn_rpc.cpp b/src/masternodes/mn_rpc.cpp index 521f0c3677e..b10db8d3117 100644 --- a/src/masternodes/mn_rpc.cpp +++ b/src/masternodes/mn_rpc.cpp @@ -425,8 +425,9 @@ std::vector GetAuthInputsSmart(CWalletCoinsUnlocker& pwallet, int32_t txV void execTestTx(const CTransaction& tx, uint32_t height, CTransactionRef optAuthTx) { std::vector metadata; - auto txType = GuessCustomTxType(tx, metadata); - auto txMessage = customTypeToMessage(txType); + CExpirationAndVersion customTxParams; + auto txType = GuessCustomTxType(tx, metadata, false, 0, &customTxParams); + auto txMessage = customTypeToMessage(txType, customTxParams.version); auto res = CustomMetadataParse(height, Params().GetConsensus(), metadata, txMessage); if (res) { LOCK(cs_main); @@ -947,6 +948,44 @@ static UniValue clearmempool(const JSONRPCRequest& request) return removed; } +UniValue setcustomtxexpiration(const JSONRPCRequest& request) { + RPCHelpMan{"setcustomtxexpiration", + "\nSet the expiration in blocks of locally created transactions. This expiration is to be\n" + "added to the current block height at the point of transaction creation. Once the chain reaches the\n" + "combined height if the transaction has not been added to a block it will be removed from the mempool\n" + "and can no longer be added to a block\n", + { + {"blockCount", RPCArg::Type::NUM, RPCArg::Optional::NO, ""} + }, + RPCResults{}, + RPCExamples{ + HelpExampleCli("setcustomtxexpiration", "10") + + HelpExampleRpc("setcustomtxexpiration", "10") + }, + }.Check(request); + + RPCTypeCheck(request.params, {UniValue::VNUM}, false); + + LOCK(cs_main); + + pcustomcsview->SetGlobalCustomTxExpiration(request.params[0].get_int()); + + return {}; +} + +void AddVersionAndExpiration(CScript& metaData, const uint32_t height, const MetadataVersion version) +{ + if (height < static_cast(Params().GetConsensus().GreatWorldHeight)) { + return; + } + + CExpirationAndVersion customTxParams{height + pcustomcsview->GetGlobalCustomTxExpiration(), static_cast(version)}; + + CDataStream stream(SER_NETWORK, PROTOCOL_VERSION); + stream << customTxParams; + + metaData << ToByteVector(stream); +} static const CRPCCommand commands[] = { @@ -959,6 +998,7 @@ static const CRPCCommand commands[] = {"blockchain", "isappliedcustomtx", &isappliedcustomtx, {"txid", "blockHeight"}}, {"blockchain", "listsmartcontracts", &listsmartcontracts, {}}, {"blockchain", "clearmempool", &clearmempool, {} }, + {"blockchain", "setcustomtxexpiration", &setcustomtxexpiration, {"blockHeight"}}, }; void RegisterMNBlockchainRPCCommands(CRPCTable& tableRPC) { diff --git a/src/masternodes/mn_rpc.h b/src/masternodes/mn_rpc.h index 7a2cb3f8fa1..3c765de3f49 100644 --- a/src/masternodes/mn_rpc.h +++ b/src/masternodes/mn_rpc.h @@ -63,5 +63,6 @@ void execTestTx(const CTransaction& tx, uint32_t height, CTransactionRef optAuth CScript CreateScriptForHTLC(const JSONRPCRequest& request, uint32_t &blocks, std::vector& image); CPubKey PublickeyFromString(const std::string &pubkey); std::optional GetFuturesBlock(); +void AddVersionAndExpiration(CScript& metadata, const uint32_t height, const MetadataVersion version = MetadataVersion::One); #endif // DEFI_MASTERNODES_MN_RPC_H diff --git a/src/masternodes/rpc_customtx.cpp b/src/masternodes/rpc_customtx.cpp index 25563724f3d..3f4b5901760 100644 --- a/src/masternodes/rpc_customtx.cpp +++ b/src/masternodes/rpc_customtx.cpp @@ -486,11 +486,12 @@ class CCustomTxRpcVisitor Res RpcInfo(const CTransaction& tx, uint32_t height, CustomTxType& txType, UniValue& results) { std::vector metadata; - txType = GuessCustomTxType(tx, metadata); + CExpirationAndVersion customTxParams; + txType = GuessCustomTxType(tx, metadata, false, 0, &customTxParams); if (txType == CustomTxType::None) { return Res::Ok(); } - auto txMessage = customTypeToMessage(txType); + auto txMessage = customTypeToMessage(txType, customTxParams.version); auto res = CustomMetadataParse(height, Params().GetConsensus(), metadata, txMessage); if (res) { CCustomCSView mnview(*pcustomcsview); diff --git a/src/masternodes/rpc_poolpair.cpp b/src/masternodes/rpc_poolpair.cpp index b43ade3da5a..a3bd541e517 100644 --- a/src/masternodes/rpc_poolpair.cpp +++ b/src/masternodes/rpc_poolpair.cpp @@ -347,6 +347,7 @@ UniValue addpoolliquidity(const JSONRPCRequest& request) { << msg; CScript scriptMeta; scriptMeta << OP_RETURN << ToByteVector(markedMetadata); + AddVersionAndExpiration(scriptMeta, chainHeight(*pwallet->chain().lock())); int targetHeight = chainHeight(*pwallet->chain().lock()) + 1; @@ -436,6 +437,7 @@ UniValue removepoolliquidity(const JSONRPCRequest& request) { << msg; CScript scriptMeta; scriptMeta << OP_RETURN << ToByteVector(markedMetadata); + AddVersionAndExpiration(scriptMeta, chainHeight(*pwallet->chain().lock())); int targetHeight = chainHeight(*pwallet->chain().lock()) + 1; @@ -826,6 +828,7 @@ UniValue poolswap(const JSONRPCRequest& request) { CScript scriptMeta; scriptMeta << OP_RETURN << ToByteVector(metadata); + AddVersionAndExpiration(scriptMeta, chainHeight(*pwallet->chain().lock())); const auto txVersion = GetTransactionVersion(targetHeight); CMutableTransaction rawTx(txVersion); @@ -956,6 +959,7 @@ UniValue compositeswap(const JSONRPCRequest& request) { CScript scriptMeta; scriptMeta << OP_RETURN << ToByteVector(metadata); + AddVersionAndExpiration(scriptMeta, chainHeight(*pwallet->chain().lock())); const auto txVersion = GetTransactionVersion(targetHeight); CMutableTransaction rawTx(txVersion); diff --git a/src/primitives/transaction.h b/src/primitives/transaction.h index d4a12b875a1..7aa5c649dd1 100644 --- a/src/primitives/transaction.h +++ b/src/primitives/transaction.h @@ -479,6 +479,22 @@ inline void SerializeTransaction(const TxType& tx, Stream& s) { s << tx.nLockTime; } +/* + * DeFiChain custom transaction expiration and version added after metadata. + */ +struct CExpirationAndVersion { + uint32_t expiration{std::numeric_limits::max()}; + uint8_t version{0}; + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action) + { + READWRITE(expiration); + READWRITE(version); + } +}; typedef std::shared_ptr CTransactionRef; static inline CTransactionRef MakeTransactionRef() { return std::make_shared(); } diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 907f3c2b998..f1d639dcfe4 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -314,6 +314,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "setgovheight", 2, "inputs" }, { "isappliedcustomtx", 1, "blockHeight" }, + { "setcustomtxexpiration", 0, "blockHeight" }, { "sendtokenstoaddress", 0, "from" }, { "sendtokenstoaddress", 1, "to" }, { "getanchorteams", 0, "blockHeight" }, diff --git a/src/txmempool.cpp b/src/txmempool.cpp index f1714a42669..ec37356e446 100644 --- a/src/txmempool.cpp +++ b/src/txmempool.cpp @@ -21,9 +21,10 @@ CTxMemPoolEntry::CTxMemPoolEntry(const CTransactionRef& _tx, const CAmount& _nFee, int64_t _nTime, unsigned int _entryHeight, - bool _spendsCoinbase, int64_t _sigOpsCost, LockPoints lp) + bool _spendsCoinbase, int64_t _sigOpsCost, + LockPoints lp, uint32_t customTxExpiration) : tx(_tx), nFee(_nFee), nTxWeight(GetTransactionWeight(*tx)), nUsageSize(RecursiveDynamicUsage(tx)), nTime(_nTime), entryHeight(_entryHeight), - spendsCoinbase(_spendsCoinbase), sigOpCost(_sigOpsCost), lockPoints(lp) + spendsCoinbase(_spendsCoinbase), sigOpCost(_sigOpsCost), lockPoints(lp), customTxExpiration(customTxExpiration) { nCountWithDescendants = 1; nSizeWithDescendants = GetTxSize(); @@ -970,6 +971,23 @@ int CTxMemPool::Expire(int64_t time) { return stage.size(); } +uint32_t CTxMemPool::ExpireByHeight(const uint32_t blockHeight) { + AssertLockHeld(cs); + indexed_transaction_set::index::type::iterator it = mapTx.get().begin(); + setEntries toremove; + for (; it != mapTx.get().end(); ++it) { + if (blockHeight >= it->GetCustomTxExpiration()) { + toremove.insert(mapTx.project<0>(it)); + } + } + setEntries stage; + for (txiter removeit : toremove) { + CalculateDescendants(removeit, stage); + } + RemoveStaged(stage, false, MemPoolRemovalReason::EXPIRY); + return stage.size(); +} + void CTxMemPool::addUnchecked(const CTxMemPoolEntry &entry, bool validFeeEstimate) { setEntries setAncestors; diff --git a/src/txmempool.h b/src/txmempool.h index 53d988fd83c..b29e7095819 100644 --- a/src/txmempool.h +++ b/src/txmempool.h @@ -79,6 +79,7 @@ class CTxMemPoolEntry const int64_t sigOpCost; //!< Total sigop cost int64_t feeDelta; //!< Used for determining the priority of the transaction for mining in a block LockPoints lockPoints; //!< Track the height and time at which tx was final + uint32_t customTxExpiration; //!< Block height at which transaction will expire // Information about descendants of this transaction that are in the // mempool; if we remove this transaction we must remove all of these @@ -97,7 +98,8 @@ class CTxMemPoolEntry CTxMemPoolEntry(const CTransactionRef& _tx, const CAmount& _nFee, int64_t _nTime, unsigned int _entryHeight, bool spendsCoinbase, - int64_t nSigOpsCost, LockPoints lp); + int64_t nSigOpsCost, LockPoints lp, + uint32_t customTxExpiration = std::numeric_limits::max()); const CTransaction& GetTx() const { return *this->tx; } CTransactionRef GetSharedTx() const { return this->tx; } @@ -131,6 +133,7 @@ class CTxMemPoolEntry uint64_t GetSizeWithAncestors() const { return nSizeWithAncestors; } CAmount GetModFeesWithAncestors() const { return nModFeesWithAncestors; } int64_t GetSigOpCostWithAncestors() const { return nSigOpCostWithAncestors; } + uint32_t GetCustomTxExpiration() const { return customTxExpiration; } mutable size_t vTxHashesIdx; //!< Index in mempool's vTxHashes }; @@ -666,6 +669,9 @@ class CTxMemPool /** Expire all transaction (and their dependencies) in the mempool older than time. Return the number of removed transactions. */ int Expire(int64_t time) EXCLUSIVE_LOCKS_REQUIRED(cs); + /** Expire all custom transaction (and their dependencies) in the mempool by their expiration height. Return the number of removed transactions. */ + uint32_t ExpireByHeight(const uint32_t blockHeight); + /** * Calculate the ancestor and descendant count for the given transaction. * The counts include the transaction itself. diff --git a/src/validation.cpp b/src/validation.cpp index cc7c089ff54..3a40a042914 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -644,7 +644,8 @@ static bool AcceptToMemoryPoolWorker(const CChainParams& chainparams, CTxMemPool return state.Invalid(ValidationInvalidReason::TX_MEMPOOL_POLICY, false, REJECT_INVALID, "bad-txns-inputs-below-tx-fee"); } - auto res = ApplyCustomTx(mnview, view, tx, chainparams.GetConsensus(), height, nAcceptTime); + uint32_t customTxExpiration{std::numeric_limits::max()}; + auto res = ApplyCustomTx(mnview, view, tx, chainparams.GetConsensus(), height, nAcceptTime, &customTxExpiration); if (!res.ok || (res.code & CustomTxErrCodes::Fatal)) { return state.Invalid(ValidationInvalidReason::TX_MEMPOOL_POLICY, false, REJECT_INVALID, res.msg); } @@ -686,7 +687,7 @@ static bool AcceptToMemoryPoolWorker(const CChainParams& chainparams, CTxMemPool } CTxMemPoolEntry entry(ptx, nFees, nAcceptTime, ::ChainActive().Height(), - fSpendsCoinbase, nSigOpsCost, lp); + fSpendsCoinbase, nSigOpsCost, lp, customTxExpiration); unsigned int nSize = entry.GetTxSize(); if (nSigOpsCost > MAX_STANDARD_TX_SIGOPS_COST) @@ -1655,7 +1656,7 @@ int ApplyTxInUndo(Coin&& undo, CCoinsViewCache& view, const COutPoint& out) } static bool GetCreationTransactions(const CBlock& block, const uint32_t id, const int32_t multiplier, uint256& tokenCreationTx, std::vector& poolCreationTx) { - bool opcodes{false}; + uint8_t opcodes{HasForks::None}; std::vector metadata; uint32_t type; uint32_t metaId; @@ -2532,7 +2533,7 @@ bool CChainState::ConnectBlock(const CBlock& block, CValidationState& state, CBl // init view|db with genesis here for (size_t i = 0; i < block.vtx.size(); ++i) { CHistoryWriters writers{paccountHistoryDB.get(), nullptr, nullptr}; - const auto res = ApplyCustomTx(mnview, view, *block.vtx[i], chainparams.GetConsensus(), pindex->nHeight, pindex->GetBlockTime(), i, &writers); + const auto res = ApplyCustomTx(mnview, view, *block.vtx[i], chainparams.GetConsensus(), pindex->nHeight, pindex->GetBlockTime(), nullptr, i, &writers); if (!res.ok) { return error("%s: Genesis block ApplyCustomTx failed. TX: %s Error: %s", __func__, block.vtx[i]->GetHash().ToString(), res.msg); @@ -2800,7 +2801,7 @@ bool CChainState::ConnectBlock(const CBlock& block, CValidationState& state, CBl } CHistoryWriters writers{paccountHistoryDB.get(), pburnHistoryDB.get(), pvaultHistoryDB.get()}; - const auto res = ApplyCustomTx(accountsView, view, tx, chainparams.GetConsensus(), pindex->nHeight, pindex->GetBlockTime(), i, &writers); + const auto res = ApplyCustomTx(accountsView, view, tx, chainparams.GetConsensus(), pindex->nHeight, pindex->GetBlockTime(), nullptr, i, &writers); if (!res.ok && (res.code & CustomTxErrCodes::Fatal)) { if (pindex->nHeight >= chainparams.GetConsensus().EunosHeight) { return state.Invalid(ValidationInvalidReason::CONSENSUS, @@ -3080,6 +3081,14 @@ bool CChainState::ConnectBlock(const CBlock& block, CValidationState& state, CBl } } + if (pindex->nHeight >= chainparams.GetConsensus().GreatWorldHeight) { + // Remove any TXs from mempool that are now expired + const auto removed = mempool.ExpireByHeight(pindex->nHeight); + if (removed) { + LogPrintf("%s: %d transaction expired at height %d\n", __func__, removed, pindex->nHeight); + } + } + if (isSplitsBlock) { LogPrintf("Token split block validation time: %.2fms\n", MILLI * (GetTimeMicros() - nTime1)); } @@ -5850,10 +5859,13 @@ bool CheckBlock(const CBlock& block, CValidationState& state, const Consensus::P // skip this validation if it is Genesis (due to mn creation txs) if (block.GetHash() != consensusParams.hashGenesisBlock) { TBytes dummy; + uint8_t hashForks{HasForks::None}; + hashForks |= height >= consensusParams.FortCanningHeight ? HasForks::FortCanning : HasForks::None; + hashForks |= height >= consensusParams.GreatWorldHeight ? HasForks::GreatWorld : HasForks::None; for (unsigned int i = 1; i < block.vtx.size(); i++) { if (block.vtx[i]->IsCoinBase() && !IsAnchorRewardTx(*block.vtx[i], dummy, height >= consensusParams.FortCanningHeight) && - !IsAnchorRewardTxPlus(*block.vtx[i], dummy, height >= consensusParams.FortCanningHeight) && + !IsAnchorRewardTxPlus(*block.vtx[i], dummy, hashForks) && !IsTokenSplitTx(*block.vtx[i], dummy, height >= consensusParams.FortCanningCrunchHeight)) return state.Invalid(ValidationInvalidReason::CONSENSUS, false, REJECT_INVALID, "bad-cb-multiple", "more than one coinbase"); } diff --git a/test/functional/feature_reject_customtxs.py b/test/functional/feature_reject_customtxs.py index e11272d74c6..2e865f82486 100755 --- a/test/functional/feature_reject_customtxs.py +++ b/test/functional/feature_reject_customtxs.py @@ -8,13 +8,13 @@ from test_framework.test_framework import DefiTestFramework from test_framework.authproxy import JSONRPCException -from test_framework.util import assert_equal +from test_framework.util import assert_equal, assert_raises_rpc_error class RejectCustomTx(DefiTestFramework): def set_test_params(self): self.num_nodes = 1 self.setup_clean_chain = True - self.extra_args = [['-txnotokens=0', '-amkheight=1', '-bayfrontheight=1', '-bayfrontgardensheight=1', '-dakotaheight=1', '-fortcanningheight=120']] + self.extra_args = [['-txnotokens=0', '-amkheight=1', '-bayfrontheight=1', '-bayfrontgardensheight=1', '-dakotaheight=1', '-fortcanningheight=120', '-greatworldheight=140', '-customtxexpiration=6']] def run_test(self): self.nodes[0].generate(101) @@ -114,5 +114,48 @@ def run_test(self): errorString = e.error['message'] assert("Invalid custom transaction" in errorString) + # Set up for GW TX expiration tests + address = self.nodes[0].getnewaddress("", "legacy") + + # Create token + self.nodes[0].createtoken({ + "symbol": "LTC", + "name": "Litecoin", + "isDAT": True, + "collateralAddress": address + }) + self.nodes[0].generate(1) + + # Create pool + self.nodes[0].createpoolpair({ + "tokenA": 'LTC', + "tokenB": 'DFI', + "commission": 0.01, + "status": True, + "ownerAddress": address + }, []) + self.nodes[0].generate(1) + + # Fund address with DFI and LTC + self.nodes[0].minttokens(["0.1@LTC"]) + self.nodes[0].sendtoaddress(address, 0.1) + self.nodes[0].utxostoaccount({address: "10@DFI"}) + self.nodes[0].generate(1) + + # Move to GreatWorld height + self.nodes[0].generate(140 - self.nodes[0].getblockcount()) + + # Create transaction with new expiration and version fields + tx = self.nodes[0].addpoolliquidity({address: ["0.1@LTC", "10@DFI"]}, address) + rawtx = self.nodes[0].getrawtransaction(tx) + self.nodes[0].clearmempool() + + # Append extra data and test failure + rawtx = rawtx.replace('ffffffff0200000000000000005c', 'ffffffff0200000000000000005d') + expiration = self.nodes[0].getblockcount() + 6 + rawtx = rawtx.replace('05' + hex(expiration)[2:] + '00000001', '05' + hex(expiration)[2:] + '0000000100') + signed_rawtx = self.nodes[0].signrawtransactionwithwallet(rawtx) + assert_raises_rpc_error(-26, "Invalid custom transaction", self.nodes[0].sendrawtransaction, signed_rawtx['hex']) + if __name__ == '__main__': RejectCustomTx().main() diff --git a/test/functional/feature_tx_versioning_and_expiration.py b/test/functional/feature_tx_versioning_and_expiration.py new file mode 100755 index 00000000000..8e66a00ee1f --- /dev/null +++ b/test/functional/feature_tx_versioning_and_expiration.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +# Copyright (c) 2014-2019 The Bitcoin Core developers +# Copyright (c) DeFi Blockchain Developers +# Distributed under the MIT software license, see the accompanying +# file LICENSE or http://www.opensource.org/licenses/mit-license.php. +"""Test Tx version and expiration.""" + +from test_framework.test_framework import DefiTestFramework + +from test_framework.util import assert_equal, assert_raises_rpc_error + +class TxVersionAndExpirationTest (DefiTestFramework): + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + self.extra_args = [['-txnotokens=0', '-amkheight=1', '-bayfrontheight=1', '-eunosheight=1', '-txindex=1', '-fortcanningheight=101', '-greatworldheight=101']] + + def run_test(self): + self.nodes[0].generate(101) + + # Create token and pool address + address = self.nodes[0].get_genesis_keys().ownerAuthAddress + + # Create token + self.nodes[0].createtoken({ + "symbol": "LTC", + "name": "Litecoin", + "isDAT": True, + "collateralAddress": address + }) + self.nodes[0].generate(1) + + # Create pool + self.nodes[0].createpoolpair({ + "tokenA": 'LTC', + "tokenB": 'DFI', + "commission": 0.01, + "status": True, + "ownerAddress": address + }, []) + self.nodes[0].generate(1) + + # Fund address with DFI and LTC + self.nodes[0].minttokens(["0.1@LTC"]) + self.nodes[0].utxostoaccount({address: "10@DFI"}) + self.nodes[0].generate(1) + + # Create transaction to use in testing + tx = self.nodes[0].addpoolliquidity({address: ["0.1@LTC", "10@DFI"]}, address) + rawtx = self.nodes[0].getrawtransaction(tx) + self.nodes[0].clearmempool() + + # Test invalid version + print(rawtx) + invalid_version = rawtx.replace('05e000000001', '05e0000000ff') + signed_rawtx = self.nodes[0].signrawtransactionwithwallet(invalid_version) + assert_raises_rpc_error(-26, "Invalid transaction version set", self.nodes[0].sendrawtransaction, signed_rawtx['hex']) + + # Test invalid expiration + invalid_expiration = rawtx.replace('05e000000001', '050000000001') + signed_rawtx = self.nodes[0].signrawtransactionwithwallet(invalid_expiration) + assert_raises_rpc_error(-26, "Invalid transaction expiration set", self.nodes[0].sendrawtransaction, signed_rawtx['hex']) + + # Check block acceptance just below expiration height + self.nodes[0].generate(119) + self.nodes[0].sendrawtransaction(rawtx) + self.nodes[0].clearmempool() + + # Test mempool rejection at expiration height + self.nodes[0].generate(1) + assert_raises_rpc_error(-26, "Transaction has expired", self.nodes[0].sendrawtransaction, rawtx) + + # Test expiration value set by startup flag + self.stop_node(0) + self.start_node(0, ['-txnotokens=0', '-amkheight=1', '-bayfrontheight=1', '-eunosheight=1', '-txindex=1', '-fortcanningheight=101', '-greatworldheight=101', '-customtxexpiration=1']) + + # Create expiration TX + tx = self.nodes[0].addpoolliquidity({address: ["0.1@LTC", "10@DFI"]}, address) + rawtx = self.nodes[0].getrawtransaction(tx, 1) + self.nodes[0].clearmempool() + + # Check expiration is now 1 block + expiration = self.nodes[0].getblockcount() + 1 + assert_equal(rawtx['vout'][0]['scriptPubKey']['hex'][172:], '05' + hex(expiration)[2:] + '00000001') + self.nodes[0].clearmempool() + + # Create expiration TX + self.nodes[0].setcustomtxexpiration(10) + tx = self.nodes[0].addpoolliquidity({address: ["0.1@LTC", "10@DFI"]}, address) + rawtx = self.nodes[0].getrawtransaction(tx, 1) + self.nodes[0].clearmempool() + + # Check expiration is now 10 blocks + expiration = self.nodes[0].getblockcount() + 10 + assert_equal(rawtx['vout'][0]['scriptPubKey']['hex'][172:], '05' + hex(expiration)[2:] + '00000001') + self.nodes[0].clearmempool() + +if __name__ == '__main__': + TxVersionAndExpirationTest().main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 419d01bd7d2..da3c3d9a170 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -289,6 +289,7 @@ 'feature_checkpoint.py', 'rpc_getmininginfo.py', 'feature_burn_address.py', + 'feature_tx_versioning_and_expiration.py', 'feature_eunos_balances.py', 'feature_sendutxosfrom.py', 'feature_update_mn.py',