From 4d9318928913b09d8c02c01c28adb7a505e7dfd9 Mon Sep 17 00:00:00 2001 From: Peter John Bushnell Date: Thu, 15 Jul 2021 14:54:22 +0100 Subject: [PATCH] V1.8.0 (#530) * v1.8.0 * Remove FortCanning from Eunos regtest setting * Take anchor block deeper to avoid "Anchor too new" error (#531) * Add tests for epic/* * Switch fork to EunosPaya (#584) * Switch canning to paya 2 (#585) * Switch fork to EunosPaya * Remove left over canning changes * Onchain timelock (#562) * Prevent resign during PRE_ENABLED state (#518) * Prevent resign during PRE_ENABLED and staking during PRE_RESIGNED. * Change FortCanning to EunosPaya * Reduce future block time to 30 seconds (#586) * Reduce future time to 30 seconds * Ignore 30s time rule on regtest * Check for and reject entries with duplicates (#592) * Check for and reject duplicates * Allow duplicates as long as overall sigs meet quorum * Add tests. Update confirms to store unique on signer ID. * Restore previous behaviour as collection ignores duplicate CKeyIDs * Hold confirms on BTC TX height, hash and signed CKeyID (#598) * Add hard coded seed for devnet (#601) --- configure.ac | 4 +- src/chain.h | 1 + src/chainparams.cpp | 13 +- src/chainparamsseeds.h | 4 + src/consensus/params.h | 1 + src/masternodes/anchors.cpp | 30 ++- src/masternodes/anchors.h | 25 ++- src/masternodes/icxorder.cpp | 37 +++- src/masternodes/icxorder.h | 10 +- src/masternodes/masternodes.cpp | 57 ++++- src/masternodes/masternodes.h | 7 +- src/masternodes/mn_checks.cpp | 164 ++++++++++++--- src/masternodes/mn_checks.h | 6 + src/masternodes/rpc_icxorderbook.cpp | 53 +++-- src/masternodes/rpc_masternodes.cpp | 44 +++- src/miner.cpp | 23 +- src/miner.h | 2 +- src/pos.cpp | 6 +- src/pos_kernel.cpp | 30 +-- src/pos_kernel.h | 5 +- src/rpc/mining.cpp | 12 +- src/test/anchor_tests.cpp | 72 ++++++- src/test/blockencodings_tests.cpp | 2 +- src/test/mn_blocktime_tests.cpp | 2 +- src/test/pos_tests.cpp | 4 +- src/validation.cpp | 19 +- src/version.h | 2 +- test/functional/feature_burn_address.py | 2 +- test/functional/feature_icx_orderbook.py | 199 +++++++++++++++--- .../feature_icx_orderbook_errors.py | 64 +++--- test/functional/feature_longterm_lockin.py | 166 +++++++++++++++ test/functional/rpc_mn_basic.py | 11 + test/functional/test_runner.py | 1 + 33 files changed, 875 insertions(+), 203 deletions(-) create mode 100644 test/functional/feature_longterm_lockin.py diff --git a/configure.ac b/configure.ac index f782868a2d0..5197db61e37 100644 --- a/configure.ac +++ b/configure.ac @@ -1,8 +1,8 @@ dnl require autoconf 2.60 (AS_ECHO/AS_ECHO_N) AC_PREREQ([2.60]) define(_CLIENT_VERSION_MAJOR, 1) -define(_CLIENT_VERSION_MINOR, 7) -define(_CLIENT_VERSION_REVISION, 11) +define(_CLIENT_VERSION_MINOR, 8) +define(_CLIENT_VERSION_REVISION, 0) define(_CLIENT_VERSION_BUILD, 0) define(_CLIENT_VERSION_RC, 0) define(_CLIENT_VERSION_IS_RELEASE, true) diff --git a/src/chain.h b/src/chain.h index 61159c6ee1c..4db8da92e6d 100644 --- a/src/chain.h +++ b/src/chain.h @@ -23,6 +23,7 @@ static constexpr int64_t MAX_FUTURE_BLOCK_TIME = 2 * 60 * 60; static constexpr int64_t MAX_FUTURE_BLOCK_TIME_DAKOTACRESCENT = 5 * 60; +static constexpr int64_t MAX_FUTURE_BLOCK_TIME_EUNOSPAYA = 30; /** * Timestamp window used as a grace period by code that compares external diff --git a/src/chainparams.cpp b/src/chainparams.cpp index 32f3fe828ec..02284fa38e3 100644 --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -127,6 +127,7 @@ class CMainParams : public CChainParams { consensus.EunosHeight = 894000; // 3rd June 2021 consensus.EunosSimsHeight = consensus.EunosHeight; consensus.EunosKampungHeight = 895743; + consensus.EunosPayaHeight = std::numeric_limits::max(); consensus.pos.diffLimit = uint256S("00000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); // consensus.pos.nTargetTimespan = 14 * 24 * 60 * 60; // two weeks @@ -345,6 +346,7 @@ class CTestNetParams : public CChainParams { consensus.EunosHeight = 354950; consensus.EunosSimsHeight = consensus.EunosHeight; consensus.EunosKampungHeight = consensus.EunosHeight; + consensus.EunosPayaHeight = 463300; consensus.pos.diffLimit = uint256S("00000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); // consensus.pos.nTargetTimespan = 14 * 24 * 60 * 60; // two weeks @@ -522,9 +524,10 @@ class CDevNetParams : public CChainParams { consensus.ClarkeQuayHeight = 0; consensus.DakotaHeight = 10; consensus.DakotaCrescentHeight = 10; - consensus.EunosHeight = 125; - consensus.EunosSimsHeight = 125; - consensus.EunosKampungHeight = 125; + consensus.EunosHeight = 150; + consensus.EunosSimsHeight = consensus.EunosHeight; + consensus.EunosKampungHeight = consensus.EunosHeight; + consensus.EunosPayaHeight = 300; consensus.pos.diffLimit = uint256S("00000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); consensus.pos.nTargetTimespan = 5 * 60; // 5 min == 10 blocks @@ -647,7 +650,7 @@ class CDevNetParams : public CChainParams { vSeeds.clear(); // nodes with support for servicebits filtering should be at the top // vSeeds.emplace_back("testnet-seed.defichain.io"); -// vFixedSeeds = std::vector(pnSeed6_test, pnSeed6_test + ARRAYLEN(pnSeed6_test)); + vFixedSeeds = std::vector(pnSeed6_devnet, pnSeed6_devnet + ARRAYLEN(pnSeed6_devnet)); fDefaultConsistencyChecks = false; fRequireStandard = false; @@ -697,6 +700,7 @@ class CRegTestParams : public CChainParams { consensus.EunosHeight = 10000000; consensus.EunosSimsHeight = 10000000; consensus.EunosKampungHeight = 10000000; + consensus.EunosPayaHeight = 10000000; consensus.pos.diffLimit = uint256S("00000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); consensus.pos.nTargetTimespan = 14 * 24 * 60 * 60; // two weeks @@ -965,6 +969,7 @@ void CRegTestParams::UpdateActivationParametersFromArgs(const ArgsManager& args) consensus.EunosHeight = static_cast(height); consensus.EunosSimsHeight = static_cast(height); consensus.EunosKampungHeight = static_cast(height); + consensus.EunosPayaHeight = static_cast(height); } if (!args.IsArgSet("-vbparams")) return; diff --git a/src/chainparamsseeds.h b/src/chainparamsseeds.h index bc20e7633fc..6392ca63345 100644 --- a/src/chainparamsseeds.h +++ b/src/chainparamsseeds.h @@ -27,4 +27,8 @@ static SeedSpec6 pnSeed6_test[] = { {{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x2d,0x38,0x48,0xc9}, 18555}, {{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xac,0x69,0x31,0x21}, 18555} }; + +static SeedSpec6 pnSeed6_devnet[] = { + {{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0x12,0xc5,0xa1,0x0e}, 20555} +}; #endif // DEFI_CHAINPARAMSSEEDS_H diff --git a/src/consensus/params.h b/src/consensus/params.h index 4774020735e..ed7f6ef4744 100644 --- a/src/consensus/params.h +++ b/src/consensus/params.h @@ -89,6 +89,7 @@ struct Params { int EunosHeight; int EunosSimsHeight; int EunosKampungHeight; + int EunosPayaHeight; /** Foundation share after AMK, normalized to COIN = 100% */ CAmount foundationShareDFIP1; /** Trackable burn address */ diff --git a/src/masternodes/anchors.cpp b/src/masternodes/anchors.cpp index 1943a5bbf83..151671a1173 100644 --- a/src/masternodes/anchors.cpp +++ b/src/masternodes/anchors.cpp @@ -29,17 +29,6 @@ static const char DB_ANCHORS = 'A'; static const char DB_PENDING = 'p'; static const char DB_BITCOININDEX = 'Z'; // Bitcoin height to blockhash table -template -bool CheckSigs(uint256 const & sigHash, TContainer const & sigs, std::set const & keys) -{ - for (auto const & sig : sigs) { - CPubKey pubkey; - if (!pubkey.RecoverCompact(sigHash, sig) || keys.find(pubkey.GetID()) == keys.end()) - return false; - } - return true; -} - uint256 CAnchorData::GetSignHash() const { CDataStream ss{SER_GETHASH, PROTOCOL_VERSION}; @@ -96,14 +85,20 @@ CAnchor CAnchor::Create(const std::vector & auths, CTxDestin return {}; } -bool CAnchor::CheckAuthSigs(CTeam const & team) const +bool CAnchor::CheckAuthSigs(CTeam const & team, const uint32_t height) const { // Sigs must meet quorum size. - if (sigs.size() < GetMinAnchorQuorum(team)) { + auto quorum = GetMinAnchorQuorum(team); + if (sigs.size() < quorum) { return error("%s: Anchor auth team quorum not met. Min quorum: %d sigs size %d", __func__, GetMinAnchorQuorum(team), sigs.size()); } - return CheckSigs(GetSignHash(), sigs, team); + auto uniqueKeys = CheckSigs(GetSignHash(), sigs, team); + if (height >= Params().GetConsensus().EunosPayaHeight && uniqueKeys < quorum) { + return error("%s: Anchor auth team unique key quorum not met. Min quorum: %d keys size %d", __func__, GetMinAnchorQuorum(team), uniqueKeys); + } + + return uniqueKeys; } const CAnchorAuthIndex::Auth * CAnchorAuthIndex::GetAuth(uint256 const & msgHash) const @@ -550,8 +545,7 @@ void CAnchorIndex::CheckPendingAnchors() } // Validate the anchor sigs - CPubKey pubKey; - if (!rec.anchor.CheckAuthSigs(*anchorTeam)) { + if (!rec.anchor.CheckAuthSigs(*anchorTeam, anchorCreationHeight)) { LogPrint(BCLog::ANCHORING, "Signature validation fails. Deleting anchor txHash %s\n", rec.txHash.ToString()); deletePending.insert(rec.txHash); continue; @@ -949,7 +943,7 @@ bool CAnchorFinalizationMessage::CheckConfirmSigs() return CheckSigs(GetSignHash(), sigs, currentTeam); } -bool CAnchorFinalizationMessagePlus::CheckConfirmSigs(const uint32_t height) +size_t CAnchorFinalizationMessagePlus::CheckConfirmSigs(const uint32_t height) { auto team = pcustomcsview->GetConfirmTeam(height); if (!team) { @@ -1078,7 +1072,7 @@ std::vector CAnchorAwaitingConfirms::GetQuorumFor(const C for (auto it = list.begin(); it != list.end(); /* w/o advance! */) { // get next group of confirms KList::iterator it0, it1; - std::tie(it0,it1) = list.equal_range(std::make_tuple(it->btcTxHeight, it->GetSignHash())); + std::tie(it0,it1) = list.equal_range(std::make_tuple(it->btcTxHeight, it->btcTxHash)); if (std::distance(it0,it1) >= quorum) { result.clear(); for (; result.size() < quorum && it0 != it1; ++it0) { diff --git a/src/masternodes/anchors.h b/src/masternodes/anchors.h index dfa3bd68064..70cb8d9471d 100644 --- a/src/masternodes/anchors.h +++ b/src/masternodes/anchors.h @@ -120,7 +120,7 @@ class CAnchor : public CAnchorData {} static CAnchor Create(std::vector const & auths, CTxDestination const & rewardDest); - bool CheckAuthSigs(CTeam const & team) const; + bool CheckAuthSigs(CTeam const & team, const uint32_t height) const; ADD_SERIALIZE_METHODS; @@ -433,7 +433,7 @@ struct CAnchorFinalizationMessagePlus : public CAnchorConfirmDataPlus , sigs() {} - bool CheckConfirmSigs(const uint32_t height); + size_t CheckConfirmSigs(const uint32_t height); ADD_SERIALIZE_METHODS; @@ -462,12 +462,13 @@ class CAnchorAwaitingConfirms ordered_non_unique< tag, member >, - // index for quorum selection (miner affected) + // index for quorum selection (miner affected) Protected against double signing. // just to remember that there may be confirms with equal btcTxHeight, but with different teams! - ordered_non_unique< + ordered_unique< tag, composite_key, - const_mem_fun + member, + const_mem_fun > >, // restriction index that helps detect doublesigning @@ -495,6 +496,20 @@ class CAnchorAwaitingConfirms void ForEachConfirm(std::function callback) const; }; +template +size_t CheckSigs(uint256 const & sigHash, TContainer const & sigs, std::set const & keys) +{ + std::set uniqueKeys; + for (auto const & sig : sigs) { + CPubKey pubkey; + if (!pubkey.RecoverCompact(sigHash, sig) || keys.find(pubkey.GetID()) == keys.end()) + return false; + + uniqueKeys.insert(pubkey); + } + return uniqueKeys.size(); +} + /// dummy, unknown consensus rules yet. may be additional params needed (smth like 'height') /// even may be not here, but in CCustomCSView uint32_t GetMinAnchorQuorum(CAnchorData::CTeam const & team); diff --git a/src/masternodes/icxorder.cpp b/src/masternodes/icxorder.cpp index 2bd014ffe88..4453e2d8a68 100644 --- a/src/masternodes/icxorder.cpp +++ b/src/masternodes/icxorder.cpp @@ -38,7 +38,7 @@ const uint8_t CICXOrder::STATUS_EXPIRED = 3; const std::string CICXOrder::CHAIN_BTC = "BTC"; const std::string CICXOrder::TOKEN_BTC = "BTC"; -const uint32_t CICXMakeOffer::DEFAULT_EXPIRY = 10; +const uint32_t CICXMakeOffer::DEFAULT_EXPIRY = 20; const uint32_t CICXMakeOffer::MAKER_DEPOSIT_REFUND_TIMEOUT = 100; const uint8_t CICXMakeOffer::STATUS_OPEN = 0; const uint8_t CICXMakeOffer::STATUS_CLOSED = 1; @@ -47,6 +47,8 @@ const CAmount CICXMakeOffer::DEFAULT_TAKER_FEE_PER_BTC = AmountFromValue(0.003); const uint32_t CICXSubmitDFCHTLC::MINIMUM_TIMEOUT = 500; const uint32_t CICXSubmitDFCHTLC::MINIMUM_2ND_TIMEOUT = 250; +const uint32_t CICXSubmitDFCHTLC::EUNOSPAYA_MINIMUM_TIMEOUT = 1440; +const uint32_t CICXSubmitDFCHTLC::EUNOSPAYA_MINIMUM_2ND_TIMEOUT = 480; const uint8_t CICXSubmitDFCHTLC::STATUS_OPEN = 0; const uint8_t CICXSubmitDFCHTLC::STATUS_CLAIMED = 1; const uint8_t CICXSubmitDFCHTLC::STATUS_REFUNDED = 2; @@ -54,8 +56,11 @@ const uint8_t CICXSubmitDFCHTLC::STATUS_EXPIRED = 3; const uint32_t CICXSubmitEXTHTLC::MINIMUM_TIMEOUT = 30; const uint32_t CICXSubmitEXTHTLC::MINIMUM_2ND_TIMEOUT = 15; +const uint32_t CICXSubmitEXTHTLC::EUNOSPAYA_MINIMUM_TIMEOUT = 72; +const uint32_t CICXSubmitEXTHTLC::EUNOSPAYA_MINIMUM_2ND_TIMEOUT = 24; // constant for calculating BTC block period in DFI block period per hour (BTC estimated to 6 blocks/h, DFI to 96 blocks/h) const uint32_t CICXSubmitEXTHTLC::BTC_BLOCKS_IN_DFI_BLOCKS = 16; +const uint32_t CICXSubmitEXTHTLC::EUNOSPAYA_BTC_BLOCKS_IN_DFI_BLOCKS = 20; const uint8_t CICXSubmitEXTHTLC::STATUS_OPEN = 0; const uint8_t CICXSubmitEXTHTLC::STATUS_CLOSED = 1; const uint8_t CICXSubmitEXTHTLC::STATUS_EXPIRED = 3; @@ -295,6 +300,21 @@ std::unique_ptr CICXOrderView::HasICXSubmi return {}; } +bool CICXOrderView::ExistedICXSubmitDFCHTLC(uint256 const & offertxid, bool isPreEunosPaya) +{ + bool result = false; + + if (HasICXSubmitDFCHTLCOpen(offertxid)) + result = true; + if (isPreEunosPaya) + return (result); + auto it = LowerBound(TxidPairKey{offertxid, {}}); + if (it.Valid() && it.Key().first == offertxid) + result = true; + + return (result); +} + std::unique_ptr CICXOrderView::GetICXSubmitEXTHTLCByCreationTx(const uint256 & txid) const { auto submitexthtlc = ReadBy(txid); @@ -358,6 +378,21 @@ std::unique_ptr CICXOrderView::HasICXSubmi return {}; } +bool CICXOrderView::ExistedICXSubmitEXTHTLC(uint256 const & offertxid, bool isPreEunosPaya) +{ + bool result = false; + + if (HasICXSubmitEXTHTLCOpen(offertxid)) + result = true; + if (isPreEunosPaya) + return (result); + auto it = LowerBound(TxidPairKey{offertxid, {}}); + if (it.Valid() && it.Key().first == offertxid) + result = true; + + return (result); +} + std::unique_ptr CICXOrderView::GetICXClaimDFCHTLCByCreationTx(uint256 const & txid) const { auto claimdfchtlc = ReadBy(txid); diff --git a/src/masternodes/icxorder.h b/src/masternodes/icxorder.h index 18f46b6c233..29e841dd792 100644 --- a/src/masternodes/icxorder.h +++ b/src/masternodes/icxorder.h @@ -142,7 +142,9 @@ class CICXSubmitDFCHTLC { public: static const uint32_t MINIMUM_TIMEOUT; // minimum period in blocks after htlc automatically timeouts and funds are returned to owner when it is first htlc + static const uint32_t EUNOSPAYA_MINIMUM_TIMEOUT; static const uint32_t MINIMUM_2ND_TIMEOUT; // minimum period in blocks after htlc automatically timeouts and funds are returned to owner when it is second htlc + static const uint32_t EUNOSPAYA_MINIMUM_2ND_TIMEOUT; static const uint8_t STATUS_OPEN; static const uint8_t STATUS_CLAIMED; static const uint8_t STATUS_REFUNDED; @@ -152,7 +154,7 @@ class CICXSubmitDFCHTLC uint256 offerTx; // txid for which offer is this HTLC CAmount amount = 0; // amount that is put in HTLC uint256 hash; // hash for the hash lock part - uint32_t timeout = MINIMUM_TIMEOUT; // timeout (absolute in blocks) for timelock part + uint32_t timeout = 0; // timeout (absolute in blocks) for timelock part ADD_SERIALIZE_METHODS; @@ -195,8 +197,11 @@ class CICXSubmitEXTHTLC { public: static const uint32_t MINIMUM_TIMEOUT; // default period in blocks after htlc timeouts when it is first htlc + static const uint32_t EUNOSPAYA_MINIMUM_TIMEOUT; static const uint32_t MINIMUM_2ND_TIMEOUT; // default period in blocks after htlc timeouts when it is second htlc + static const uint32_t EUNOSPAYA_MINIMUM_2ND_TIMEOUT; static const uint32_t BTC_BLOCKS_IN_DFI_BLOCKS; // number of BTC blocks in DFI blocks period + static const uint32_t EUNOSPAYA_BTC_BLOCKS_IN_DFI_BLOCKS; // number of BTC blocks in DFI blocks period static const uint8_t STATUS_OPEN; static const uint8_t STATUS_CLOSED; static const uint8_t STATUS_EXPIRED; @@ -413,6 +418,8 @@ class CICXOrderView : public virtual CStorageView { void ForEachICXSubmitDFCHTLCClose(std::function callback, uint256 const & offertxid = uint256()); void ForEachICXSubmitDFCHTLCExpire(std::function callback, uint32_t const & height = 0); std::unique_ptr HasICXSubmitDFCHTLCOpen(uint256 const & offertxid); + bool ExistedICXSubmitDFCHTLC(uint256 const & offertxid, bool isPreEunosPaya); + //SubmitEXTHTLC std::unique_ptr GetICXSubmitEXTHTLCByCreationTx(uint256 const & txid) const; @@ -422,6 +429,7 @@ class CICXOrderView : public virtual CStorageView { void ForEachICXSubmitEXTHTLCClose(std::function callback, uint256 const & offertxid = uint256()); void ForEachICXSubmitEXTHTLCExpire(std::function callback, uint32_t const & height = 0); std::unique_ptr HasICXSubmitEXTHTLCOpen(uint256 const & offertxid); + bool ExistedICXSubmitEXTHTLC(uint256 const & offertxid, bool isPreEunosPaya); //ClaimDFCHTLC std::unique_ptr GetICXClaimDFCHTLCByCreationTx(uint256 const & txid) const; diff --git a/src/masternodes/masternodes.cpp b/src/masternodes/masternodes.cpp index 4b91534d1ad..b549b1c7ee3 100644 --- a/src/masternodes/masternodes.cpp +++ b/src/masternodes/masternodes.cpp @@ -21,11 +21,12 @@ #include /// @attention make sure that it does not overlap with those in tokens.cpp !!! -// Prefixes for the 'custom chainstate database' (customsc/) +// Prefixes for the 'custom chainstate database' (enhancedcs/) const unsigned char DB_MASTERNODES = 'M'; // main masternodes table const unsigned char DB_MN_OPERATORS = 'o'; // masternodes' operators index const unsigned char DB_MN_OWNERS = 'w'; // masternodes' owners index const unsigned char DB_MN_STAKER = 'X'; // masternodes' last staked block time +const unsigned char DB_MN_TIMELOCK = 'K'; const unsigned char DB_MN_HEIGHT = 'H'; // single record with last processed chain height const unsigned char DB_MN_VERSION = 'D'; const unsigned char DB_MN_ANCHOR_REWARD = 'r'; @@ -39,6 +40,7 @@ const unsigned char CMasternodesView::ID ::prefix = DB_MASTERNODES; const unsigned char CMasternodesView::Operator::prefix = DB_MN_OPERATORS; const unsigned char CMasternodesView::Owner ::prefix = DB_MN_OWNERS; const unsigned char CMasternodesView::Staker ::prefix = DB_MN_STAKER; +const unsigned char CMasternodesView::Timelock::prefix = DB_MN_TIMELOCK; const unsigned char CAnchorRewardsView::BtcTx ::prefix = DB_MN_ANCHOR_REWARD; const unsigned char CAnchorConfirmsView::BtcTx::prefix = DB_MN_ANCHOR_CONFIRM; const unsigned char CTeamView::AuthTeam ::prefix = DB_MN_AUTH_TEAM; @@ -135,6 +137,9 @@ bool CMasternode::IsActive() const bool CMasternode::IsActive(int height) const { State state = GetState(height); + if (height >= Params().GetConsensus().EunosPayaHeight) { + return state == ENABLED; + } return state == ENABLED || state == PRE_RESIGNED; } @@ -275,7 +280,7 @@ boost::optional > CMasternodesView::AmIOwner() const return {}; } -Res CMasternodesView::CreateMasternode(const uint256 & nodeId, const CMasternode & node) +Res CMasternodesView::CreateMasternode(const uint256 & nodeId, const CMasternode & node, uint16_t timelock) { // Check auth addresses and that there in no MN with such owner or operator if ((node.operatorType != 1 && node.operatorType != 4) || (node.ownerType != 1 && node.ownerType != 4) || @@ -291,6 +296,10 @@ Res CMasternodesView::CreateMasternode(const uint256 & nodeId, const CMasternode WriteBy(node.ownerAuthAddress, nodeId); WriteBy(node.operatorAuthAddress, nodeId); + if (timelock > 0) { + WriteBy(nodeId, timelock); + } + return Res::Ok(); } @@ -302,10 +311,19 @@ Res CMasternodesView::ResignMasternode(const uint256 & nodeId, const uint256 & t return Res::Err("node %s does not exists", nodeId.ToString()); } auto state = node->GetState(height); - if ((state != CMasternode::PRE_ENABLED && state != CMasternode::ENABLED) /*|| IsAnchorInvolved(nodeId, height)*/) { // if already spoiled by resign or ban, or need for anchor + if (height >= Params().GetConsensus().EunosPayaHeight) { + if (state != CMasternode::ENABLED) { + return Res::Err("node %s state is not 'ENABLED'", nodeId.ToString()); + } + } else if ((state != CMasternode::PRE_ENABLED && state != CMasternode::ENABLED)) { return Res::Err("node %s state is not 'PRE_ENABLED' or 'ENABLED'", nodeId.ToString()); } + const auto timelock = GetTimelock(nodeId, *node, height); + if (timelock) { + return Res::Err("Trying to resign masternode before timelock expiration."); + } + node->resignTx = txid; node->resignHeight = height; WriteBy(nodeId, *node); @@ -381,6 +399,39 @@ Res CMasternodesView::UnResignMasternode(const uint256 & nodeId, const uint256 & return Res::Err("No such masternode %s, resignTx: %s", nodeId.GetHex(), resignTx.GetHex()); } +uint16_t CMasternodesView::GetTimelock(const uint256& nodeId, const CMasternode& node, const uint64_t height) const +{ + auto timelock = ReadBy(nodeId); + if (timelock) { + LOCK(cs_main); + // Get last height + auto lastHeight = height - 1; + + // Cannot expire below block count required to calculate average time + if (lastHeight < Params().GetConsensus().mn.newResignDelay) { + return *timelock; + } + + // Get timelock expiration time. Timelock set in weeks, convert to seconds. + const auto timelockExpire = ::ChainActive()[node.creationHeight]->nTime + (*timelock * 7 * 24 * 60 * 60); + + // Get average time of the last two times the activation delay worth of blocks + uint64_t totalTime{0}; + for (; lastHeight + Params().GetConsensus().mn.newResignDelay >= height; --lastHeight) { + totalTime += ::ChainActive()[lastHeight]->nTime; + } + const uint32_t averageTime = totalTime / Params().GetConsensus().mn.newResignDelay; + + // Below expiration return timelock + if (averageTime < timelockExpire) { + return *timelock; + } else { // Expired. Return null. + return 0; + } + } + return 0; +} + /* * CLastHeightView */ diff --git a/src/masternodes/masternodes.h b/src/masternodes/masternodes.h index ab3bb20eb64..c9c6fd32f88 100644 --- a/src/masternodes/masternodes.h +++ b/src/masternodes/masternodes.h @@ -150,7 +150,7 @@ class CMasternodesView : public virtual CStorageView // Multiple operator support std::set> GetOperatorsMulti() const; - Res CreateMasternode(uint256 const & nodeId, CMasternode const & node); + Res CreateMasternode(uint256 const & nodeId, CMasternode const & node, uint16_t timelock); Res ResignMasternode(uint256 const & nodeId, uint256 const & txid, int height); Res UnCreateMasternode(uint256 const & nodeId); Res UnResignMasternode(uint256 const & nodeId, uint256 const & resignTx); @@ -161,6 +161,8 @@ class CMasternodesView : public virtual CStorageView void ForEachMinterNode(std::function)> callback, MNBlockTimeKey const & start = {}); + uint16_t GetTimelock(const uint256& nodeId, const CMasternode& node, const uint64_t height) const; + // tags struct ID { static const unsigned char prefix; }; struct Operator { static const unsigned char prefix; }; @@ -168,6 +170,9 @@ class CMasternodesView : public virtual CStorageView // For storing last staked block time struct Staker { static const unsigned char prefix; }; + + // Store long term time lock + struct Timelock { static const unsigned char prefix; }; }; class CLastHeightView : public virtual CStorageView diff --git a/src/masternodes/mn_checks.cpp b/src/masternodes/mn_checks.cpp index 9deb32b7858..891213c09af 100644 --- a/src/masternodes/mn_checks.cpp +++ b/src/masternodes/mn_checks.cpp @@ -172,6 +172,13 @@ class CCustomMetadataParseVisitor : public boost::static_visitor return Res::Ok(); } + Res isPostEunosPayaFork() const { + if(static_cast(height) < consensus.EunosPayaHeight) { + return Res::Err("called before EunosPaya height"); + } + return Res::Ok(); + } + template Res serialize(T& obj) const { CDataStream ss(metadata, SER_NETWORK, PROTOCOL_VERSION); @@ -448,9 +455,12 @@ class CCustomTxVisitor : public boost::static_visitor } Res CheckICXTx() const { - if (tx.vout.size() != 2) { + if (static_cast(height) < consensus.EunosPayaHeight && tx.vout.size() != 2) { return Res::Err("malformed tx vouts ((wrong number of vouts)"); } + if (static_cast(height) >= consensus.EunosPayaHeight && tx.vout[0].nValue != 0) { + return Res::Err("malformed tx vouts, first vout must be OP_RETURN vout with value 0"); + } return Res::Ok(); } @@ -677,6 +687,10 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor return Res::Err("masternode creation needs owner auth"); } + if (height < static_cast(Params().GetConsensus().EunosPayaHeight) && obj.timelock != 0) { + return Res::Err("collateral timelock cannot be set below EunosPaya"); + } + CMasternode node; CTxDestination dest; if (ExtractDestination(tx.vout[1].scriptPubKey, dest)) { @@ -691,7 +705,7 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor node.creationHeight = height; node.operatorType = obj.operatorType; node.operatorAuthAddress = obj.operatorAuthAddress; - res = mnview.CreateMasternode(tx.GetHash(), node); + res = mnview.CreateMasternode(tx.GetHash(), node, obj.timelock); // Build coinage from the point of masternode creation if (res && height >= static_cast(Params().GetConsensus().DakotaCrescentHeight)) { mnview.SetMasternodeLastBlockTime(node.operatorAuthAddress, static_cast(height), time); @@ -1254,20 +1268,38 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor if (!mnview.HasICXMakeOfferOpen(offer->orderTx, submitdfchtlc.offerTx)) return Res::Err("offerTx (%s) has expired", submitdfchtlc.offerTx.GetHex()); - if (submitdfchtlc.timeout < CICXSubmitDFCHTLC::MINIMUM_TIMEOUT) - return Res::Err("timeout must be greater than %d", CICXSubmitDFCHTLC::MINIMUM_TIMEOUT - 1); + uint32_t timeout; + if (static_cast(height) < consensus.EunosPayaHeight) + timeout = CICXSubmitDFCHTLC::MINIMUM_TIMEOUT; + else + timeout = CICXSubmitDFCHTLC::EUNOSPAYA_MINIMUM_TIMEOUT; + + if (submitdfchtlc.timeout < timeout) + return Res::Err("timeout must be greater than %d", timeout - 1); srcAddr = CScript(order->creationTx.begin(), order->creationTx.end()); + CScript offerTxidAddr(offer->creationTx.begin(), offer->creationTx.end()); + CAmount calcAmount(static_cast((arith_uint256(submitdfchtlc.amount) * arith_uint256(order->orderPrice) / arith_uint256(COIN)).GetLow64())); if (calcAmount > offer->amount) return Res::Err("amount must be lower or equal the offer one"); - CScript offerTxidAddr(offer->creationTx.begin(), offer->creationTx.end()); - - //calculating adjusted takerFee - CAmount BTCAmount(static_cast((arith_uint256(submitdfchtlc.amount) * arith_uint256(order->orderPrice) / arith_uint256(COIN)).GetLow64())); - auto takerFee = CalculateTakerFee(BTCAmount); + CAmount takerFee = offer->takerFee; + //EunosPaya: calculating adjusted takerFee only if amount in htlc different than in offer + if (static_cast(height) >= consensus.EunosPayaHeight) + { + if (calcAmount < offer->amount) + { + CAmount BTCAmount(static_cast((arith_uint256(submitdfchtlc.amount) * arith_uint256(order->orderPrice) / arith_uint256(COIN)).GetLow64())); + takerFee = static_cast((arith_uint256(BTCAmount) * arith_uint256(offer->takerFee) / arith_uint256(offer->amount)).GetLow64()); + } + } + else + { + CAmount BTCAmount(static_cast((arith_uint256(submitdfchtlc.amount) * arith_uint256(order->orderPrice) / arith_uint256(COIN)).GetLow64())); + takerFee = CalculateTakerFee(BTCAmount); + } // refund the rest of locked takerFee if there is difference if (offer->takerFee - takerFee) { @@ -1312,10 +1344,22 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor return Res::Err("Invalid hash, dfc htlc hash is different than extarnal htlc hash - %s != %s", submitdfchtlc.hash.GetHex(),exthtlc->hash.GetHex()); - if (submitdfchtlc.timeout < CICXSubmitDFCHTLC::MINIMUM_2ND_TIMEOUT) - return Res::Err("timeout must be greater than %d", CICXSubmitDFCHTLC::MINIMUM_2ND_TIMEOUT - 1); + uint32_t timeout, btcBlocksInDfi; + if (static_cast(height) < consensus.EunosPayaHeight) + { + timeout = CICXSubmitDFCHTLC::MINIMUM_2ND_TIMEOUT; + btcBlocksInDfi = CICXSubmitEXTHTLC::BTC_BLOCKS_IN_DFI_BLOCKS; + } + else + { + timeout = CICXSubmitDFCHTLC::EUNOSPAYA_MINIMUM_2ND_TIMEOUT; + btcBlocksInDfi = CICXSubmitEXTHTLC::BTC_BLOCKS_IN_DFI_BLOCKS; + } + + if (submitdfchtlc.timeout < timeout) + return Res::Err("timeout must be greater than %d", timeout - 1); - if (submitdfchtlc.timeout >= (exthtlc->creationHeight + (exthtlc->timeout * CICXSubmitEXTHTLC::BTC_BLOCKS_IN_DFI_BLOCKS)) - height) + if (submitdfchtlc.timeout >= (exthtlc->creationHeight + (exthtlc->timeout * btcBlocksInDfi)) - height) return Res::Err("timeout must be less than expiration period of 1st htlc in DFI blocks"); } @@ -1362,15 +1406,27 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor CAmount calcAmount(static_cast((arith_uint256(dfchtlc->amount) * arith_uint256(order->orderPrice) / arith_uint256(COIN)).GetLow64())); if (submitexthtlc.amount != calcAmount) - return Res::Err("amount %d must be equal to calculated dfchtlc amount %d", submitexthtlc.amount, calcAmount); + return Res::Err("amount must be equal to calculated dfchtlc amount"); if (submitexthtlc.hash != dfchtlc->hash) return Res::Err("Invalid hash, external htlc hash is different than dfc htlc hash"); - if (submitexthtlc.timeout < CICXSubmitEXTHTLC::MINIMUM_2ND_TIMEOUT) - return Res::Err("timeout must be greater than %d", CICXSubmitEXTHTLC::MINIMUM_2ND_TIMEOUT - 1); + uint32_t timeout, btcBlocksInDfi; + if (static_cast(height) < consensus.EunosPayaHeight) + { + timeout = CICXSubmitEXTHTLC::MINIMUM_2ND_TIMEOUT; + btcBlocksInDfi = CICXSubmitEXTHTLC::BTC_BLOCKS_IN_DFI_BLOCKS; + } + else + { + timeout = CICXSubmitEXTHTLC::EUNOSPAYA_MINIMUM_2ND_TIMEOUT; + btcBlocksInDfi = CICXSubmitEXTHTLC::EUNOSPAYA_BTC_BLOCKS_IN_DFI_BLOCKS; + } + + if (submitexthtlc.timeout < timeout) + return Res::Err("timeout must be greater than %d", timeout - 1); - if (submitexthtlc.timeout * CICXSubmitEXTHTLC::BTC_BLOCKS_IN_DFI_BLOCKS >= (dfchtlc->creationHeight + dfchtlc->timeout) - height) + if (submitexthtlc.timeout * btcBlocksInDfi >= (dfchtlc->creationHeight + dfchtlc->timeout) - height) return Res::Err("timeout must be less than expiration period of 1st htlc in DFC blocks"); } else if (order->orderType == CICXOrder::TYPE_EXTERNAL) { @@ -1380,17 +1436,35 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor if (!mnview.HasICXMakeOfferOpen(offer->orderTx, submitexthtlc.offerTx)) return Res::Err("offerTx (%s) has expired", submitexthtlc.offerTx.GetHex()); - if (submitexthtlc.timeout < CICXSubmitEXTHTLC::MINIMUM_TIMEOUT) - return Res::Err("timeout must be greater than %d", CICXSubmitEXTHTLC::MINIMUM_TIMEOUT - 1); + uint32_t timeout; + if (static_cast(height) < consensus.EunosPayaHeight) + timeout = CICXSubmitEXTHTLC::MINIMUM_TIMEOUT; + else + timeout = CICXSubmitEXTHTLC::EUNOSPAYA_MINIMUM_TIMEOUT; + + if (submitexthtlc.timeout < timeout) + return Res::Err("timeout must be greater than %d", timeout - 1); + + CScript offerTxidAddr(offer->creationTx.begin(), offer->creationTx.end()); CAmount calcAmount(static_cast((arith_uint256(submitexthtlc.amount) * arith_uint256(order->orderPrice) / arith_uint256(COIN)).GetLow64())); if (calcAmount > offer->amount) return Res::Err("amount must be lower or equal the offer one"); - CScript offerTxidAddr(offer->creationTx.begin(), offer->creationTx.end()); - - //calculating adjusted takerFee - auto takerFee = CalculateTakerFee(submitexthtlc.amount); + CAmount takerFee = offer->takerFee; + //EunosPaya: calculating adjusted takerFee only if amount in htlc different than in offer + if (static_cast(height) >= consensus.EunosPayaHeight) + { + if (calcAmount < offer->amount) + { + CAmount BTCAmount(static_cast((arith_uint256(offer->amount) * arith_uint256(COIN) / arith_uint256(order->orderPrice)).GetLow64())); + takerFee = static_cast((arith_uint256(submitexthtlc.amount) * arith_uint256(offer->takerFee) / arith_uint256(BTCAmount)).GetLow64()); + } + } + else + { + takerFee = CalculateTakerFee(submitexthtlc.amount); + } // refund the rest of locked takerFee if there is difference if (offer->takerFee - takerFee) { @@ -1454,7 +1528,7 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor return Res::Err("order with creation tx %s does not exists!", offer->orderTx.GetHex()); auto exthtlc = mnview.HasICXSubmitEXTHTLCOpen(dfchtlc->offerTx); - if (!exthtlc) + if (static_cast(height) < consensus.EunosPayaHeight && !exthtlc) return Res::Err("cannot claim, external htlc for this offer does not exists or expired!"); // claim DFC HTLC to receiveAddress @@ -1513,7 +1587,15 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor if (!res) return res; - return mnview.ICXCloseEXTHTLC(*exthtlc, CICXSubmitEXTHTLC::STATUS_CLOSED); + if (static_cast(height) >= consensus.EunosPayaHeight) + { + if (exthtlc) + return mnview.ICXCloseEXTHTLC(*exthtlc, CICXSubmitEXTHTLC::STATUS_CLOSED); + else + return (Res::Ok()); + } + else + return mnview.ICXCloseEXTHTLC(*exthtlc, CICXSubmitEXTHTLC::STATUS_CLOSED); } Res operator()(const CICXCloseOrderMessage& obj) const { @@ -1589,7 +1671,10 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor offer->closeTx = closeoffer.creationTx; offer->closeHeight = closeoffer.creationHeight; - if (order->orderType == CICXOrder::TYPE_INTERNAL && !mnview.HasICXSubmitDFCHTLCOpen(offer->creationTx)) { + bool isPreEunosPaya = static_cast(height) < consensus.EunosPayaHeight; + + if (order->orderType == CICXOrder::TYPE_INTERNAL && !mnview.ExistedICXSubmitDFCHTLC(offer->creationTx, isPreEunosPaya)) + { // subtract takerFee from txidAddr and return to owner CScript txidAddr(offer->creationTx.begin(), offer->creationTx.end()); CalculateOwnerRewards(offer->ownerAddress); @@ -1601,10 +1686,13 @@ class CCustomTxApplyVisitor : public CCustomTxVisitor // subtract the balance from txidAddr and return to owner CScript txidAddr(offer->creationTx.begin(), offer->creationTx.end()); CalculateOwnerRewards(offer->ownerAddress); - res = ICXTransfer(order->idToken, offer->amount, txidAddr, offer->ownerAddress); - if (!res) - return res; - if (!mnview.HasICXSubmitEXTHTLCOpen(offer->creationTx)) + if (isPreEunosPaya) + { + res = ICXTransfer(order->idToken, offer->amount, txidAddr, offer->ownerAddress); + if (!res) + return res; + } + if (!mnview.ExistedICXSubmitEXTHTLC(offer->creationTx, isPreEunosPaya)) { res = ICXTransfer(DCT_ID{0}, offer->takerFee, txidAddr, offer->ownerAddress); if (!res) @@ -1932,18 +2020,26 @@ ResVal ApplyAnchorRewardTxPlus(CCustomCSView & mnview, CTransaction con } // Miner used confirm team at chain height when creating this TX, this is height - 1. - if (!finMsg.CheckConfirmSigs(height - 1)) { + int anchorHeight = height - 1; + auto uniqueKeys = finMsg.CheckConfirmSigs(anchorHeight); + if (!uniqueKeys) { return Res::ErrDbg("bad-ar-sigs", "anchor signatures are incorrect"); } - auto team = mnview.GetConfirmTeam(height - 1); + auto team = mnview.GetConfirmTeam(anchorHeight); if (!team) { - return Res::ErrDbg("bad-ar-team", "could not get confirm team for height: %d", height - 1); + return Res::ErrDbg("bad-ar-team", "could not get confirm team for height: %d", anchorHeight); } - if (finMsg.sigs.size() < GetMinAnchorQuorum(*team)) { + auto quorum = GetMinAnchorQuorum(*team); + if (finMsg.sigs.size() < quorum) { return Res::ErrDbg("bad-ar-sigs-quorum", "anchor sigs (%d) < min quorum (%) ", - finMsg.sigs.size(), GetMinAnchorQuorum(*team)); + finMsg.sigs.size(), quorum); + } + + if (anchorHeight >= Params().GetConsensus().EunosPayaHeight && uniqueKeys < quorum) { + return Res::ErrDbg("bad-ar-sigs-quorum", "anchor unique keys (%d) < min quorum (%) ", + uniqueKeys, quorum); } // Make sure anchor block height and hash exist in chain. diff --git a/src/masternodes/mn_checks.h b/src/masternodes/mn_checks.h index 05951a73b6b..f21bd4beedb 100644 --- a/src/masternodes/mn_checks.h +++ b/src/masternodes/mn_checks.h @@ -156,12 +156,18 @@ struct CICXCloseOfferMessage; struct CCreateMasterNodeMessage { char operatorType; CKeyID operatorAuthAddress; + uint16_t timelock{0}; ADD_SERIALIZE_METHODS; template inline void SerializationOp(Stream& s, Operation ser_action) { READWRITE(operatorType); READWRITE(operatorAuthAddress); + + // Only available after EunosPaya + if (!s.eof()) { + READWRITE(timelock); + } } }; diff --git a/src/masternodes/rpc_icxorderbook.cpp b/src/masternodes/rpc_icxorderbook.cpp index 6ac8cca2b29..0f0ea896ab3 100644 --- a/src/masternodes/rpc_icxorderbook.cpp +++ b/src/masternodes/rpc_icxorderbook.cpp @@ -576,6 +576,9 @@ UniValue icxsubmitdfchtlc(const JSONRPCRequest& request) { CScript authScript; { LOCK(cs_main); + + targetHeight = ::ChainActive().Height() + 1; + auto offer = pcustomcsview->GetICXMakeOfferByCreationTx(submitdfchtlc.offerTx); if (!offer) throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("offerTx (%s) does not exist",submitdfchtlc.offerTx.GetHex())); @@ -587,18 +590,22 @@ UniValue icxsubmitdfchtlc(const JSONRPCRequest& request) { if (order->orderType == CICXOrder::TYPE_INTERNAL) { authScript = order->ownerAddress; + + if (!submitdfchtlc.timeout) + submitdfchtlc.timeout = (targetHeight < Params().GetConsensus().EunosPayaHeight) ? CICXSubmitDFCHTLC::MINIMUM_TIMEOUT : CICXSubmitDFCHTLC::EUNOSPAYA_MINIMUM_TIMEOUT; } else if (order->orderType == CICXOrder::TYPE_EXTERNAL) { authScript = offer->ownerAddress; + if (!submitdfchtlc.timeout) + submitdfchtlc.timeout = (targetHeight < Params().GetConsensus().EunosPayaHeight) ? CICXSubmitDFCHTLC::MINIMUM_2ND_TIMEOUT : CICXSubmitDFCHTLC::EUNOSPAYA_MINIMUM_2ND_TIMEOUT; + CTokenAmount balance = pcustomcsview->GetBalance(offer->ownerAddress,order->idToken); if (balance.nValue < offer->amount) throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Not enough balance for Token %s on address %s!", pcustomcsview->GetToken(order->idToken)->CreateSymbolKey(order->idToken), ScriptToString(offer->ownerAddress))); } - - targetHeight = ::ChainActive().Height() + 1; } CDataStream metadata(DfTxMarker, SER_NETWORK, PROTOCOL_VERSION); @@ -735,6 +742,9 @@ UniValue icxsubmitexthtlc(const JSONRPCRequest& request) { CScript authScript; { LOCK(cs_main); + + targetHeight = ::ChainActive().Height() + 1; + auto offer = pcustomcsview->GetICXMakeOfferByCreationTx(submitexthtlc.offerTx); if (!offer) throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("offerTx (%s) does not exist",submitexthtlc.offerTx.GetHex()));\ @@ -751,8 +761,6 @@ UniValue icxsubmitexthtlc(const JSONRPCRequest& request) { { authScript = order->ownerAddress; } - - targetHeight = ::ChainActive().Height() + 1; } CDataStream metadata(DfTxMarker, SER_NETWORK, PROTOCOL_VERSION); @@ -1196,7 +1204,7 @@ UniValue icxlistorders(const JSONRPCRequest& request) { size_t limit = 50; std::string tokenSymbol, chain; uint256 orderTxid; - bool closed = false; + bool closed = false, offers = false; RPCTypeCheck(request.params, {UniValue::VOBJ}, false); if (request.params.size() > 0) @@ -1204,7 +1212,11 @@ UniValue icxlistorders(const JSONRPCRequest& request) { UniValue byObj = request.params[0].get_obj(); if (!byObj["token"].isNull()) tokenSymbol = trim_ws(byObj["token"].getValStr()); if (!byObj["chain"].isNull()) chain = trim_ws(byObj["chain"].getValStr()); - if (!byObj["orderTx"].isNull()) orderTxid = uint256S(byObj["orderTx"].getValStr()); + if (!byObj["orderTx"].isNull()) + { + orderTxid = uint256S(byObj["orderTx"].getValStr()); + offers = true; + } if (!byObj["limit"].isNull()) limit = (size_t) byObj["limit"].get_int64(); if (!byObj["closed"].isNull()) closed = byObj["closed"].get_bool(); } @@ -1226,7 +1238,7 @@ UniValue icxlistorders(const JSONRPCRequest& request) { prefix = idToken; auto orderkeylambda = [&](CICXOrderView::OrderKey const & key, uint8_t status) { - if (key.first != prefix) + if (key.first != prefix || !limit) return (false); auto order = pcustomcsview->GetICXOrderByCreationTx(key.second); if (order) @@ -1234,7 +1246,7 @@ UniValue icxlistorders(const JSONRPCRequest& request) { ret.pushKVs(icxOrderToJSON(*order, status)); limit--; } - return limit != 0; + return true; }; if (closed) @@ -1244,10 +1256,10 @@ UniValue icxlistorders(const JSONRPCRequest& request) { return ret; } - else if (!orderTxid.IsNull()) + else if (offers) { auto offerkeylambda = [&](CICXOrderView::TxidPairKey const & key, uint8_t status) { - if (key.first != orderTxid) + if (key.first != orderTxid || !limit) return (false); auto offer = pcustomcsview->GetICXMakeOfferByCreationTx(key.second); if (offer) @@ -1255,7 +1267,7 @@ UniValue icxlistorders(const JSONRPCRequest& request) { ret.pushKVs(icxMakeOfferToJSON(*offer, status)); limit--; } - return limit != 0; + return true; }; if (closed) pcustomcsview->ForEachICXMakeOfferClose(offerkeylambda, orderTxid); @@ -1266,13 +1278,15 @@ UniValue icxlistorders(const JSONRPCRequest& request) { } auto orderlambda = [&](CICXOrderView::OrderKey const & key, uint8_t status) { + if (!limit) + return false; auto order = pcustomcsview->GetICXOrderByCreationTx(key.second); if (order) { ret.pushKVs(icxOrderToJSON(*order, status)); limit--; } - return limit != 0; + return true; }; if (closed) @@ -1291,8 +1305,7 @@ UniValue icxlisthtlcs(const JSONRPCRequest& request) { { {"offerTx",RPCArg::Type::STR, RPCArg::Optional::NO, "Offer txid for which to list all HTLCS"}, {"limit", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, "Maximum number of orders to return (default: 20)"}, - {"refunded", RPCArg::Type::BOOL, RPCArg::Optional::OMITTED, "Display refunded HTLC (default: false)"}, - {"claimed", RPCArg::Type::BOOL, RPCArg::Optional::OMITTED, "Display claimed HTLCs (default: false)"}, + {"closed", RPCArg::Type::BOOL, RPCArg::Optional::OMITTED, "Display also claimed, expired and refunded HTLCs (default: false)"}, }, }, @@ -1333,7 +1346,7 @@ UniValue icxlisthtlcs(const JSONRPCRequest& request) { ret.pushKV("WARNING", "ICX and Atomic Swap are experimental features. You might end up losing your funds. USE IT AT YOUR OWN RISK."); auto dfchtlclambda = [&](CICXOrderView::TxidPairKey const & key, uint8_t status) { - if (key.first != offerTxid) + if (key.first != offerTxid || !limit) return false; auto dfchtlc = pcustomcsview->GetICXSubmitDFCHTLCByCreationTx(key.second); if (dfchtlc) @@ -1341,10 +1354,10 @@ UniValue icxlisthtlcs(const JSONRPCRequest& request) { ret.pushKVs(icxSubmitDFCHTLCToJSON(*dfchtlc,status)); limit--; } - return limit != 0; + return true; }; auto exthtlclambda = [&](CICXOrderView::TxidPairKey const & key, uint8_t status) { - if (key.first != offerTxid) + if (key.first != offerTxid || !limit) return false; auto exthtlc = pcustomcsview->GetICXSubmitEXTHTLCByCreationTx(key.second); if (exthtlc) @@ -1352,11 +1365,11 @@ UniValue icxlisthtlcs(const JSONRPCRequest& request) { ret.pushKVs(icxSubmitEXTHTLCToJSON(*exthtlc, status)); limit--; } - return limit != 0; + return true; }; pcustomcsview->ForEachICXClaimDFCHTLC([&](CICXOrderView::TxidPairKey const & key, uint8_t status) { - if (key.first != offerTxid) + if (key.first != offerTxid || !limit) return false; auto claimdfchtlc = pcustomcsview->GetICXClaimDFCHTLCByCreationTx(key.second); if (claimdfchtlc) @@ -1364,7 +1377,7 @@ UniValue icxlisthtlcs(const JSONRPCRequest& request) { ret.pushKVs(icxClaimDFCHTLCToJSON(*claimdfchtlc)); limit--; } - return limit != 0; + return true; }, offerTxid); if (closed) diff --git a/src/masternodes/rpc_masternodes.cpp b/src/masternodes/rpc_masternodes.cpp index 7f791c68c4a..ae1136d48ab 100644 --- a/src/masternodes/rpc_masternodes.cpp +++ b/src/masternodes/rpc_masternodes.cpp @@ -3,7 +3,7 @@ #include // Here (but not a class method) just by similarity with other '..ToJSON' -UniValue mnToJSON(uint256 const & nodeId, CMasternode const& node, bool verbose, const std::set> mnIds, const CWallet* pwallet) +UniValue mnToJSON(uint256 const & nodeId, CMasternode const& node, bool verbose, const std::set>& mnIds, const CWallet* pwallet) { UniValue ret(UniValue::VOBJ); if (!verbose) { @@ -36,9 +36,11 @@ UniValue mnToJSON(uint256 const & nodeId, CMasternode const& node, bool verbose, } obj.pushKV("localMasternode", localMasternode); + auto currentHeight = ChainActive().Height(); + uint16_t timelock = pcustomcsview->GetTimelock(nodeId, node, currentHeight); + // Only get targetMultiplier for active masternodes if (node.IsActive()) { - auto currentHeight = ChainActive().Height(); auto usedHeight = currentHeight <= Params().GetConsensus().EunosHeight ? node.creationHeight : currentHeight; auto stakerBlockTime = pcustomcsview->GetMasternodeLastBlockTime(node.operatorAuthAddress, usedHeight); // No record. No stake blocks or post-fork createmastnode TX, use fork time. @@ -47,7 +49,13 @@ UniValue mnToJSON(uint256 const & nodeId, CMasternode const& node, bool verbose, stakerBlockTime = std::min(GetTime() - block->GetBlockTime(), Params().GetConsensus().pos.nStakeMaxAge); } } - obj.pushKV("targetMultiplier", pos::CalcCoinDayWeight(Params().GetConsensus(), GetTime(), stakerBlockTime ? *stakerBlockTime : 0).getdouble()); + + obj.pushKV("targetMultiplier", pos::CalcCoinDayWeight(Params().GetConsensus(), GetTime(), timelock, + stakerBlockTime ? *stakerBlockTime : 0).getdouble()); + } + + if (timelock) { + obj.pushKV("timelock", strprintf("%d years", timelock / 52)); } /// @todo add unlock height and|or real resign height @@ -87,6 +95,10 @@ UniValue createmasternode(const JSONRPCRequest& request) }, }, }, + {"timelock", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Defaults to no timelock period so masternode can be resigned once active. To set a timelock period\n" + "specify either FIVEYEARTIMELOCK or TENYEARTIMELOCK to create a masternode that cannot be resigned for\n" + "five or ten years and will have 1.5x or 2.0 the staking power respectively. Be aware that this means\n" + "that you cannot spend the collateral used to create a masternode for whatever period is specified."}, }, RPCResult{ "\"hash\" (string) The hex-encoded hash of broadcasted transaction\n" @@ -111,10 +123,30 @@ UniValue createmasternode(const JSONRPCRequest& request) } std::string ownerAddress = request.params[0].getValStr(); - std::string operatorAddress = request.params.size() > 1 ? request.params[1].getValStr() : ownerAddress; + std::string operatorAddress = request.params.size() > 1 && !request.params[1].getValStr().empty() ? request.params[1].getValStr() : ownerAddress; CTxDestination ownerDest = DecodeDestination(ownerAddress); // type will be checked on apply/create CTxDestination operatorDest = DecodeDestination(operatorAddress); + bool eunosPaya; + { + LOCK(cs_main); + eunosPaya = ::ChainActive().Tip()->height >= Params().GetConsensus().EunosPayaHeight; + } + + // Get timelock if any + uint16_t timelock{0}; + if (!request.params[3].isNull()) { + if (!eunosPaya) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Timelock cannot be specified before EunosPaya hard fork"); + } + std::string timelockStr = request.params[3].getValStr(); + if (timelockStr == "FIVEYEARTIMELOCK") { + timelock = 5 * 52; + } else if (timelockStr == "TENYEARTIMELOCK") { + timelock = 10 * 52; + } + } + // check type here cause need operatorAuthKey. all other validation (for owner for ex.) in further apply/create if (operatorDest.which() != 1 && operatorDest.which() != 4) { throw JSONRPCError(RPC_INVALID_PARAMETER, "operatorAddress (" + operatorAddress + ") does not refer to a P2PKH or P2WPKH address"); @@ -130,6 +162,10 @@ UniValue createmasternode(const JSONRPCRequest& request) metadata << static_cast(CustomTxType::CreateMasternode) << static_cast(operatorDest.which()) << operatorAuthKey; + if (eunosPaya) { + metadata << timelock; + } + CScript scriptMeta; scriptMeta << OP_RETURN << ToByteVector(metadata); diff --git a/src/miner.cpp b/src/miner.cpp index 520a790fa36..359dd8cbe6a 100644 --- a/src/miner.cpp +++ b/src/miner.cpp @@ -681,6 +681,7 @@ namespace pos { CBlockIndex* tip; int64_t height; boost::optional stakerBlockTime; + uint16_t timelock; { LOCK(cs_main); @@ -708,6 +709,7 @@ namespace pos { height = tip->height + 1; creationHeight = int64_t(nodePtr->creationHeight); blockTime = std::max(tip->GetMedianTimePast() + 1, GetAdjustedTime()); + timelock = pcustomcsview->GetTimelock(masternodeID, *nodePtr, height); stakerBlockTime = pcustomcsview->GetMasternodeLastBlockTime(args.operatorID, height); // No record. No stake blocks or post-fork createmastnode TX, use fork time. @@ -747,8 +749,8 @@ namespace pos { blockTime = ((uint32_t)currentTime - t); - if (pos::CheckKernelHash(stakeModifier, nBits, creationHeight, blockTime, height, masternodeID, - chainparams.GetConsensus(), stakerBlockTime ? *stakerBlockTime : 0)) + if (pos::CheckKernelHash(stakeModifier, nBits, creationHeight, blockTime, height, masternodeID, chainparams.GetConsensus(), + stakerBlockTime ? *stakerBlockTime : 0, timelock)) { LogPrint(BCLog::STAKING, "MakeStake: kernel found\n"); @@ -770,8 +772,8 @@ namespace pos { blockTime = ((uint32_t)searchTime + t); - if (pos::CheckKernelHash(stakeModifier, nBits, creationHeight, blockTime, height, masternodeID, - chainparams.GetConsensus(), stakerBlockTime ? *stakerBlockTime : 0)) + if (pos::CheckKernelHash(stakeModifier, nBits, creationHeight, blockTime, height, masternodeID, chainparams.GetConsensus(), + stakerBlockTime ? *stakerBlockTime : 0, timelock)) { LogPrint(BCLog::STAKING, "MakeStake: kernel found\n"); @@ -782,7 +784,7 @@ namespace pos { boost::this_thread::yield(); // give a slot to other threads } } - }); + }, height); if (!found) { return Status::stakeWaiting; @@ -836,9 +838,14 @@ namespace pos { } template - void Staker::withSearchInterval(F&& f) { - // Mine up to max future minus 5 second buffer - nFutureTime = GetAdjustedTime() + (MAX_FUTURE_BLOCK_TIME_DAKOTACRESCENT - 5); + void Staker::withSearchInterval(F&& f, int64_t height) { + if (height >= Params().GetConsensus().EunosPayaHeight) { + // Mine up to max future minus 1 second buffer + nFutureTime = GetAdjustedTime() + (MAX_FUTURE_BLOCK_TIME_EUNOSPAYA - 1); // 29 seconds + } else { + // Mine up to max future minus 5 second buffer + nFutureTime = GetAdjustedTime() + (MAX_FUTURE_BLOCK_TIME_DAKOTACRESCENT - 5); // 295 seconds + } if (nFutureTime > nLastCoinStakeSearchTime) { f(GetAdjustedTime(), nLastCoinStakeSearchTime, nFutureTime); diff --git a/src/miner.h b/src/miner.h index ae7b875dfc6..b20cc9adf96 100644 --- a/src/miner.h +++ b/src/miner.h @@ -253,7 +253,7 @@ namespace pos { private: template - void withSearchInterval(F&& f); + void withSearchInterval(F&& f, int64_t height); }; } diff --git a/src/pos.cpp b/src/pos.cpp index 2ef6591c749..55263f6abfd 100644 --- a/src/pos.cpp +++ b/src/pos.cpp @@ -60,6 +60,7 @@ bool ContextualCheckProofOfStake(const CBlockHeader& blockHeader, const Consensu uint256 masternodeID; int64_t creationHeight; boost::optional stakerBlockTime; + uint16_t timelock; { // check that block minter exists and active at the height of the block AssertLockHeld(cs_main); @@ -73,6 +74,7 @@ bool ContextualCheckProofOfStake(const CBlockHeader& blockHeader, const Consensu return false; } creationHeight = int64_t(nodePtr->creationHeight); + timelock = mnView->GetTimelock(masternodeID, *nodePtr, blockHeader.height); auto usedHeight = blockHeader.height <= params.EunosHeight ? creationHeight : blockHeader.height; stakerBlockTime = mnView->GetMasternodeLastBlockTime(nodePtr->operatorAuthAddress, usedHeight); @@ -83,8 +85,10 @@ bool ContextualCheckProofOfStake(const CBlockHeader& blockHeader, const Consensu } } } + // checking PoS kernel is faster, so check it first - if (!CheckKernelHash(blockHeader.stakeModifier, blockHeader.nBits, creationHeight, blockHeader.GetBlockTime(), blockHeader.height, masternodeID, params, stakerBlockTime ? *stakerBlockTime : 0)) { + if (!CheckKernelHash(blockHeader.stakeModifier, blockHeader.nBits, creationHeight, blockHeader.GetBlockTime(),blockHeader.height, + masternodeID, params, stakerBlockTime ? *stakerBlockTime : 0, timelock)) { return false; } diff --git a/src/pos_kernel.cpp b/src/pos_kernel.cpp index c71f57720b0..76580b8edff 100644 --- a/src/pos_kernel.cpp +++ b/src/pos_kernel.cpp @@ -16,15 +16,19 @@ namespace pos { return Hash(ss.begin(), ss.end()); } - arith_uint256 CalcCoinDayWeight(const Consensus::Params& params, const int64_t coinstakeTime, const int64_t stakersBlockTime) + arith_uint256 CalcCoinDayWeight(const Consensus::Params& params, const int64_t coinstakeTime, const uint16_t timelock, const int64_t stakersBlockTime) { - // Default to min age - int64_t nTimeTx{params.pos.nStakeMinAge}; - - // If staker has provided a previous block time use that to avoid DB lookup. - - nTimeTx = std::min(coinstakeTime - stakersBlockTime, params.pos.nStakeMaxAge); + // Increase stake time by freezer multiplier + int64_t freezerTime{coinstakeTime - stakersBlockTime}; + if (timelock) { + // Timelock in weeks, 260 or 520, divide into 52 weeks to get 5 or 10 years. + // Divide that by 10 to get half or whole extra freezer time added. + // 5 years fives 1.5x bonus and 10 years gives 2x bonus. + freezerTime += timelock / 52.0 / 10 * freezerTime; + } + // Calculate max age and limit to max allowed if above it. + int64_t nTimeTx = std::min(freezerTime, params.pos.nStakeMaxAge); // Raise time to min age if below it. nTimeTx = std::max(nTimeTx, params.pos.nStakeMinAge); @@ -36,26 +40,26 @@ namespace pos { } bool - CheckKernelHash(const uint256& stakeModifier, uint32_t nBits, int64_t height, int64_t coinstakeTime, uint64_t blockHeight, const uint256& masternodeID, const Consensus::Params& params, const int64_t stakersBlockTime) { + CheckKernelHash(const uint256& stakeModifier, uint32_t nBits, int64_t creationHeight, int64_t coinstakeTime, uint64_t blockHeight, + const uint256& masternodeID, const Consensus::Params& params, const int64_t stakersBlockTime, const uint16_t timelock) { // Base target arith_uint256 targetProofOfStake; targetProofOfStake.SetCompact(nBits); - const auto hashProofOfStake = UintToArith256(CalcKernelHash(stakeModifier, height, coinstakeTime, masternodeID, params)); + const auto hashProofOfStake = UintToArith256(CalcKernelHash(stakeModifier, creationHeight, coinstakeTime, masternodeID, params)); // New difficulty calculation to make staking easier the longer it has // been since a masternode staked a block. if (blockHeight >= static_cast(Params().GetConsensus().DakotaCrescentHeight)) { - auto coinDayWeight = CalcCoinDayWeight(params, coinstakeTime, stakersBlockTime); - + auto coinDayWeight = CalcCoinDayWeight(params, coinstakeTime, timelock, stakersBlockTime); // Increase target by coinDayWeight. - return (hashProofOfStake / static_cast( GetMnCollateralAmount( static_cast(height) ) ) ) <= targetProofOfStake * coinDayWeight; + return (hashProofOfStake / static_cast( GetMnCollateralAmount( static_cast(creationHeight) ) ) ) <= targetProofOfStake * coinDayWeight; } // Now check if proof-of-stake hash meets target protocol - return (hashProofOfStake / static_cast( GetMnCollateralAmount( static_cast(height) ) ) ) <= targetProofOfStake; + return (hashProofOfStake / static_cast( GetMnCollateralAmount( static_cast(creationHeight) ) ) ) <= targetProofOfStake; } uint256 ComputeStakeModifier(const uint256& prevStakeModifier, const CKeyID& key) { diff --git a/src/pos_kernel.h b/src/pos_kernel.h index a530a39d54e..a73abfbefaa 100644 --- a/src/pos_kernel.h +++ b/src/pos_kernel.h @@ -22,10 +22,11 @@ namespace pos { uint256 CalcKernelHash(const uint256& stakeModifier, int64_t height, int64_t coinstakeTime, const uint256& masternodeID, const Consensus::Params& params); // Calculate target multiplier - arith_uint256 CalcCoinDayWeight(const Consensus::Params& params, const int64_t coinstakeTime, const int64_t stakersBlockTime = 0); + arith_uint256 CalcCoinDayWeight(const Consensus::Params& params, const int64_t coinstakeTime, const uint16_t timelock, const int64_t stakersBlockTime); /// Check whether stake kernel meets hash target - bool CheckKernelHash(const uint256& stakeModifier, uint32_t nBits, int64_t height, int64_t coinstakeTime, uint64_t blockHeight, const uint256& masternodeID, const Consensus::Params& params, const int64_t stakersBlockTime = 0); + bool CheckKernelHash(const uint256& stakeModifier, uint32_t nBits, int64_t creationHeight, int64_t coinstakeTime, uint64_t blockHeight, + const uint256& masternodeID, const Consensus::Params& params, const int64_t stakersBlockTime, const uint16_t timelock); /// Stake Modifier (hash modifier of proof-of-stake) uint256 ComputeStakeModifier(const uint256& prevStakeModifier, const CKeyID& key); diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index ce0899faf47..19c2ff1e26a 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -246,7 +246,8 @@ static UniValue getmininginfo(const JSONRPCRequest& request) LOCK(cs_main); UniValue obj(UniValue::VOBJ); - obj.pushKV("blocks", (int)::ChainActive().Height()); + int height = static_cast(::ChainActive().Height()); + obj.pushKV("blocks", height); if (BlockAssembler::m_last_block_weight) obj.pushKV("currentblockweight", *BlockAssembler::m_last_block_weight); if (BlockAssembler::m_last_block_num_txs) obj.pushKV("currentblocktx", *BlockAssembler::m_last_block_num_txs); obj.pushKV("difficulty", (double)GetDifficulty(::ChainActive().Tip())); @@ -288,6 +289,8 @@ static UniValue getmininginfo(const JSONRPCRequest& request) subObj.pushKV("lastblockcreationattempt", (lastBlockCreationAttemptTs != 0) ? FormatISO8601DateTime(lastBlockCreationAttemptTs) : "0"); } + const auto timelock = pcustomcsview->GetTimelock(mnId.second, *nodePtr, height); + // Get targetMultiplier if node is active if (nodePtr->IsActive()) { auto currentHeight = ChainActive().Height(); @@ -299,7 +302,12 @@ static UniValue getmininginfo(const JSONRPCRequest& request) stakerBlockTime = std::min(GetTime() - block->GetBlockTime(), Params().GetConsensus().pos.nStakeMaxAge); } } - subObj.pushKV("targetMultiplier", pos::CalcCoinDayWeight(Params().GetConsensus(), GetTime(), stakerBlockTime ? *stakerBlockTime : 0).getdouble()); + subObj.pushKV("targetMultiplier", pos::CalcCoinDayWeight(Params().GetConsensus(), GetTime(), timelock, + stakerBlockTime ? *stakerBlockTime : 0).getdouble()); + } + + if (timelock) { + obj.pushKV("timelock", strprintf("%d years", timelock / 52)); } mnArr.push_back(subObj); diff --git a/src/test/anchor_tests.cpp b/src/test/anchor_tests.cpp index ebd829f9545..6380c125f02 100644 --- a/src/test/anchor_tests.cpp +++ b/src/test/anchor_tests.cpp @@ -11,7 +11,7 @@ struct SpvTestingSetup : public TestingSetup { SpvTestingSetup() - : TestingSetup(CBaseChainParams::REGTEST) + : TestingSetup(CBaseChainParams::MAIN) { spv::pspv = MakeUnique(); } @@ -22,6 +22,16 @@ struct SpvTestingSetup : public TestingSetup { } }; +// Generate keys and populate team +void createTeams(std::vector& signers, CAnchorData::CTeam& team) { + for (int i{0}; i < 5; ++i) { + CKey key; + key.MakeNewKey(true); + signers.push_back(key); + team.insert(key.GetPubKey().GetID()); + } +} + BOOST_FIXTURE_TEST_SUITE(anchor_tests, SpvTestingSetup) BOOST_AUTO_TEST_CASE(anchor_order_logic) @@ -348,13 +358,7 @@ BOOST_AUTO_TEST_CASE(Test_AnchorConfirmationOrder) std::vector signers; CAnchorData::CTeam team; - // Generate keys and populate team - for (int i{0}; i < 5; ++i) { - CKey key; - key.MakeNewKey(true); - signers.push_back(key); - team.insert(key.GetPubKey().GetID()); - } + createTeams(signers, team); // Create confirm data CAnchorConfirmData confirm{uint256S(std::string(64, '9')), 0, 0, CKeyID(), 1}; @@ -381,10 +385,60 @@ BOOST_AUTO_TEST_CASE(Test_AnchorConfirmationOrder) auto result = panchorAwaitingConfirms->GetQuorumFor(team); // First result that meets quorum is return, no others. - BOOST_CHECK_EQUAL(result.size(), 1); + BOOST_CHECK_EQUAL(result.size(), 4); // Expect to get lowset BTC height first, not lowest TX hash which would be block 16,000. BOOST_CHECK_EQUAL(result[0].btcTxHeight, 1000); } +BOOST_AUTO_TEST_CASE(Test_AnchorFinalMsgCount) +{ + // Team and private keys + std::vector signers; + CAnchorData::CTeam team; + + createTeams(signers, team); + + // Create confirm data + CAnchorConfirmData confirm{uint256S(std::string(64, '9')), 0, 0, CKeyID(), 1}; + CAnchorConfirmDataPlus confirmPlus{confirm}; + CAnchorFinalizationMessagePlus finalMsg{confirmPlus}; + + for (int i{0}; i < 4 && i < signers.size(); ++i) { + CAnchorConfirmMessage confirmMsg{confirmPlus}; + signers[i < 3 ? i : i - 1].SignCompact(confirmMsg.GetSignHash(), confirmMsg.signature); + finalMsg.sigs.push_back(confirmMsg.signature); + } + + // Double sig should excluded + BOOST_CHECK_EQUAL(CheckSigs(finalMsg.GetSignHash(), finalMsg.sigs, team), 3); +} + + +BOOST_AUTO_TEST_CASE(Test_AnchorMsgCount) +{ + // Team and private keys + std::vector signers; + CAnchorData::CTeam team; + + createTeams(signers, team); + + // Create confirm data + uint256 blockHash{uint256S(std::string(64, '9'))}; + CAnchorData data{blockHash, 0, blockHash, CAnchorData::CTeam{}}; + CAnchor anchor{data}; + + for (int i{0}; i < 4 && i < signers.size(); ++i) { + CAnchorAuthMessage authMsg{data}; + authMsg.SignWithKey(signers[i < 3 ? i : i - 1]); + anchor.sigs.push_back(authMsg.GetSignature()); + } + + // Double sig should included + BOOST_CHECK_EQUAL(anchor.CheckAuthSigs(team, 0), true); + + // Double sig should excluded + BOOST_CHECK_EQUAL(anchor.CheckAuthSigs(team, std::numeric_limits::max()), false); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/blockencodings_tests.cpp b/src/test/blockencodings_tests.cpp index 6c99372f1f9..5623d8f35e5 100644 --- a/src/test/blockencodings_tests.cpp +++ b/src/test/blockencodings_tests.cpp @@ -88,7 +88,7 @@ static CBlock BuildBlockTestCase() { assert(!mutated); block.nTime = 0; - while (!pos::CheckKernelHash(block.stakeModifier, block.nBits, creationHeight, (int64_t) block.nTime, block.height, masternodeID, Params().GetConsensus(), stakerBlockTime ? *stakerBlockTime : 0)) block.nTime++; + while (!pos::CheckKernelHash(block.stakeModifier, block.nBits, creationHeight, (int64_t) block.nTime, block.height, masternodeID, Params().GetConsensus(), stakerBlockTime ? *stakerBlockTime : 0, 0)) block.nTime++; // while (!CheckProofOfWork(block.GetHash(), block.nBits, Params().GetConsensus())) ++block.nNonce; std::shared_ptr pblock = std::make_shared(std::move(block)); diff --git a/src/test/mn_blocktime_tests.cpp b/src/test/mn_blocktime_tests.cpp index 43fb61409c4..55c2271f025 100644 --- a/src/test/mn_blocktime_tests.cpp +++ b/src/test/mn_blocktime_tests.cpp @@ -22,7 +22,7 @@ BOOST_AUTO_TEST_CASE(retrieve_last_time) uint256 mnId = uint256S("1111111111111111111111111111111111111111111111111111111111111111"); CCustomCSView mnview(*pcustomcsview.get()); - mnview.CreateMasternode(mnId, mn); + mnview.CreateMasternode(mnId, mn, 0); // Add time records mnview.SetMasternodeLastBlockTime(minter, 100, 1000); diff --git a/src/test/pos_tests.cpp b/src/test/pos_tests.cpp index cd0c0e6e83b..c8bb30c4540 100644 --- a/src/test/pos_tests.cpp +++ b/src/test/pos_tests.cpp @@ -61,10 +61,10 @@ BOOST_AUTO_TEST_CASE(calc_kernel) pos::CalcKernelHash(stakeModifier, 1, coinstakeTime, mnID, Params().GetConsensus())); uint32_t target = 0x1effffff; - BOOST_CHECK(pos::CheckKernelHash(stakeModifier, target, 1, coinstakeTime, 0, mnID, Params().GetConsensus())); + BOOST_CHECK(pos::CheckKernelHash(stakeModifier, target, 1, coinstakeTime, 0, mnID, Params().GetConsensus(), 0, 0)); uint32_t unattainableTarget = 0x00ffffff; - BOOST_CHECK(!pos::CheckKernelHash(stakeModifier, unattainableTarget, 1, coinstakeTime, 0, mnID, Params().GetConsensus())); + BOOST_CHECK(!pos::CheckKernelHash(stakeModifier, unattainableTarget, 1, coinstakeTime, 0, mnID, Params().GetConsensus(), 0, 0)); // CKey key; // key.MakeNewKey(true); // Need to use compressed keys in segwit or the signing will fail diff --git a/src/validation.cpp b/src/validation.cpp index 4989251350b..68018347070 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -1783,7 +1783,9 @@ DisconnectResult CChainState::DisconnectBlock(const CBlock& block, const CBlockI // Remove burn balance transfers if (pindex->nHeight == Params().GetConsensus().EunosHeight) { - uint32_t lastTxOut; + // Make sure to initialize lastTxOut, otherwise it never finds the block and + // ends up looping through uninitialized garbage value. + uint32_t lastTxOut = 0; auto shouldContinueToNextAccountHistory = [&lastTxOut, block](AccountHistoryKey const & key, CLazySerialize valueLazy) -> bool { if (key.owner != Params().GetConsensus().burnAddress) { @@ -2627,6 +2629,8 @@ bool CChainState::ConnectBlock(const CBlock& block, CValidationState& state, CBl // close expired orders, refund all expired DFC HTLCs at this block height if (pindex->nHeight >= chainparams.GetConsensus().EunosHeight) { + bool isPreEunosPaya = pindex->nHeight < chainparams.GetConsensus().EunosPayaHeight; + cache.ForEachICXOrderExpire([&](CICXOrderView::StatusKey const & key, uint8_t status) { if (static_cast(key.first) != pindex->nHeight) @@ -2671,8 +2675,8 @@ bool CChainState::ConnectBlock(const CBlock& block, CValidationState& state, CBl CScript txidAddr(offer->creationTx.begin(),offer->creationTx.end()); CTokenAmount takerFee{DCT_ID{0}, offer->takerFee}; - if ((order->orderType == CICXOrder::TYPE_INTERNAL && !cache.HasICXSubmitDFCHTLCOpen(offer->creationTx)) || - (order->orderType == CICXOrder::TYPE_EXTERNAL && !cache.HasICXSubmitEXTHTLCOpen(offer->creationTx))) + if ((order->orderType == CICXOrder::TYPE_INTERNAL && !cache.ExistedICXSubmitDFCHTLC(offer->creationTx, isPreEunosPaya)) || + (order->orderType == CICXOrder::TYPE_EXTERNAL && !cache.ExistedICXSubmitEXTHTLC(offer->creationTx, isPreEunosPaya))) { auto res = cache.SubBalance(txidAddr,takerFee); if (!res) @@ -2710,7 +2714,7 @@ bool CChainState::ConnectBlock(const CBlock& block, CValidationState& state, CBl if (status == CICXSubmitDFCHTLC::STATUS_EXPIRED && order->orderType == CICXOrder::TYPE_INTERNAL) { - if (!cache.HasICXSubmitEXTHTLCOpen(dfchtlc->offerTx)) + if (!cache.ExistedICXSubmitEXTHTLC(dfchtlc->offerTx, isPreEunosPaya)) { CTokenAmount makerDeposit{DCT_ID{0}, offer->takerFee}; cache.CalculateOwnerRewards(order->ownerAddress,pindex->nHeight); @@ -2765,7 +2769,7 @@ bool CChainState::ConnectBlock(const CBlock& block, CValidationState& state, CBl if (status == CICXSubmitEXTHTLC::STATUS_EXPIRED && order->orderType == CICXOrder::TYPE_EXTERNAL) { - if (!cache.HasICXSubmitDFCHTLCOpen(exthtlc->offerTx)) + if (!cache.ExistedICXSubmitDFCHTLC(exthtlc->offerTx, isPreEunosPaya)) { CTokenAmount makerDeposit{DCT_ID{0}, offer->takerFee}; cache.CalculateOwnerRewards(order->ownerAddress,pindex->nHeight); @@ -4291,6 +4295,11 @@ static bool ContextualCheckBlockHeader(const CBlockHeader& block, CValidationSta return state.Invalid(ValidationInvalidReason::BLOCK_INVALID_HEADER, false, REJECT_INVALID, "time-too-old", strprintf("block's timestamp is too early. Block time: %d Min time: %d", block.GetBlockTime(), pindexPrev->GetMedianTimePast())); // Check timestamp + if (Params().NetworkIDString() != CBaseChainParams::REGTEST && block.height >= static_cast(consensusParams.EunosPayaHeight)) { + if (block.GetBlockTime() > GetTime() + MAX_FUTURE_BLOCK_TIME_EUNOSPAYA) + return state.Invalid(ValidationInvalidReason::BLOCK_TIME_FUTURE, false, REJECT_INVALID, "time-too-new", strprintf("block timestamp too far in the future. Block time: %d Max time: %d", block.GetBlockTime(), GetTime() + MAX_FUTURE_BLOCK_TIME_EUNOSPAYA)); + } + if (block.GetBlockTime() > nAdjustedTime + MAX_FUTURE_BLOCK_TIME) return state.Invalid(ValidationInvalidReason::BLOCK_TIME_FUTURE, false, REJECT_INVALID, "time-too-new", "block timestamp too far in the future"); diff --git a/src/version.h b/src/version.h index ed2ae05c2fe..548fa98c893 100644 --- a/src/version.h +++ b/src/version.h @@ -9,7 +9,7 @@ * network protocol versioning */ -static const int PROTOCOL_VERSION = 70017; +static const int PROTOCOL_VERSION = 70018; //! initial proto version, to be increased after version/verack negotiation static const int INIT_PROTO_VERSION = 209; diff --git a/test/functional/feature_burn_address.py b/test/functional/feature_burn_address.py index 8697a823706..bb2e45b48db 100755 --- a/test/functional/feature_burn_address.py +++ b/test/functional/feature_burn_address.py @@ -33,7 +33,7 @@ def run_test(self): # Check create masternode burn fee result = self.nodes[0].listburnhistory() - assert_equal(result[0]['owner'][0:16], "6a1a446654784301") # OP_RETURN data + assert_equal(result[0]['owner'][4:14], "4466547843") # OP_RETURN data DfTxC assert_equal(result[0]['txn'], 2) assert_equal(result[0]['type'], 'CreateMasternode') assert_equal(result[0]['amounts'][0], '1.00000000@DFI') diff --git a/test/functional/feature_icx_orderbook.py b/test/functional/feature_icx_orderbook.py index 45f8ad7ac82..5ac2b24da9b 100755 --- a/test/functional/feature_icx_orderbook.py +++ b/test/functional/feature_icx_orderbook.py @@ -104,7 +104,7 @@ def run_test(self): assert_equal(result["ICX_TAKERFEE_PER_BTC"], Decimal('0.001')) - # Open and close an order + # DFI/BTC Open and close an order orderTx = self.nodes[0].icx_createorder({ 'tokenFrom': idDFI, 'chainTo': "BTC", @@ -199,6 +199,75 @@ def run_test(self): assert_equal(order[orderTx]["status"], "CLOSED") assert_equal(order[orderTx]["type"], "INTERNAL") + # BTC/DFI Open and close an order + orderTx = self.nodes[0].icx_createorder({ + 'chainFrom': "BTC", + 'tokenTo': idDFI, + 'ownerAddress': accountDFI, + 'amountFrom': 2, + 'orderPrice':100})["txid"] + + self.nodes[0].generate(1) + self.sync_blocks() + + beforeOffer = self.nodes[1].getaccount(accountBTC, {}, True)[idDFI] + + offerTx = self.nodes[1].icx_makeoffer({ + 'orderTx': orderTx, + 'amount': 10, + 'ownerAddress': accountBTC, + 'receivePubkey': '037f9563f30c609b19fd435a19b8bde7d6db703012ba1aba72e9f42a87366d1941'})["txid"] + + self.nodes[1].generate(1) + self.sync_blocks() + + assert_equal(self.nodes[1].getaccount(accountBTC, {}, True)[idDFI], beforeOffer - Decimal('0.01000000')) + + offer = self.nodes[0].icx_listorders({"orderTx": orderTx}) + + assert_equal(len(offer), 2) + + # Close offer + closeOrder = self.nodes[1].icx_closeoffer(offerTx)["txid"] + rawCloseOrder = self.nodes[1].getrawtransaction(closeOrder, 1) + authTx = self.nodes[1].getrawtransaction(rawCloseOrder['vin'][0]['txid'], 1) + found = False + for vout in authTx['vout']: + if 'addresses' in vout['scriptPubKey'] and vout['scriptPubKey']['addresses'][0] == accountBTC: + found = True + assert(found) + + self.nodes[1].generate(1) + self.sync_blocks() + + assert_equal(self.nodes[1].getaccount(accountBTC, {}, True)[idDFI], beforeOffer) + + offer = self.nodes[0].icx_listorders({"orderTx": orderTx}) + + assert_equal(len(offer), 1) + + # Check order exist + order = self.nodes[0].icx_listorders() + assert_equal(len(order), 2) + + # Close order + closeOrder = self.nodes[0].icx_closeorder(orderTx)["txid"] + rawCloseOrder = self.nodes[0].getrawtransaction(closeOrder, 1) + authTx = self.nodes[0].getrawtransaction(rawCloseOrder['vin'][0]['txid'], 1) + found = False + for vout in authTx['vout']: + if 'addresses' in vout['scriptPubKey'] and vout['scriptPubKey']['addresses'][0] == accountDFI: + found = True + assert(found) + + self.nodes[0].generate(1) + self.sync_blocks() + + order = self.nodes[0].icx_listorders() + + assert_equal(len(order), 1) + + # DFI/BTC scenario # Open an order @@ -249,15 +318,14 @@ def run_test(self): assert_equal(offer[offerTx]["amount"], Decimal('0.10000000')) assert_equal(offer[offerTx]["ownerAddress"], accountBTC) assert_equal(offer[offerTx]["takerFee"], Decimal('0.01000000')) - assert_equal(offer[offerTx]["expireHeight"], self.nodes[0].getblockchaininfo()["blocks"] + 10) + assert_equal(offer[offerTx]["expireHeight"], self.nodes[0].getblockchaininfo()["blocks"] + 20) beforeDFCHTLC = self.nodes[0].getaccount(accountDFI, {}, True)[idDFI] dfchtlcTx = self.nodes[0].icx_submitdfchtlc({ 'offerTx': offerTx, 'amount': 10, - 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - 'timeout': 500})["txid"] + 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220'})["txid"] self.nodes[0].generate(1) self.sync_blocks() @@ -280,7 +348,7 @@ def run_test(self): assert_equal(htlcs[dfchtlcTx]["amount"], Decimal('10.00000000')) assert_equal(htlcs[dfchtlcTx]["amountInEXTAsset"], Decimal('0.10000000')) assert_equal(htlcs[dfchtlcTx]["hash"], '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220') - assert_equal(htlcs[dfchtlcTx]["timeout"], 500) + assert_equal(htlcs[dfchtlcTx]["timeout"], 1440) exthtlcTx = self.nodes[1].icx_submitexthtlc({ 'offerTx': offerTx, @@ -288,7 +356,7 @@ def run_test(self): 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 'htlcScriptAddress': '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', 'ownerPubkey': '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', - 'timeout': 15})["txid"] + 'timeout': 24})["txid"] self.nodes[1].generate(1) self.sync_blocks() @@ -304,7 +372,7 @@ def run_test(self): assert_equal(htlcs[exthtlcTx]["hash"], '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220') assert_equal(htlcs[exthtlcTx]["htlcScriptAddress"], '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N') assert_equal(htlcs[exthtlcTx]["ownerPubkey"], '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252') - assert_equal(htlcs[exthtlcTx]["timeout"], 15) + assert_equal(htlcs[exthtlcTx]["timeout"], 24) beforeClaim0 = self.nodes[0].getaccount(accountDFI, {}, True)[idDFI] beforeClaim1 = self.nodes[1].getaccount(accountBTC, {}, True)[idDFI] @@ -370,14 +438,13 @@ def run_test(self): assert_equal(offer[offerTx]["amount"], Decimal('0.10000000')) assert_equal(offer[offerTx]["ownerAddress"], accountBTC) assert_equal(offer[offerTx]["takerFee"], Decimal('0.01000000')) - assert_equal(offer[offerTx]["expireHeight"], self.nodes[0].getblockchaininfo()["blocks"] + 10) + assert_equal(offer[offerTx]["expireHeight"], self.nodes[0].getblockchaininfo()["blocks"] + 20) dfchtlcTx = self.nodes[0].icx_submitdfchtlc({ 'offerTx': offerTx, 'amount': 5, - 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - 'timeout': 500})["txid"] + 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220'})["txid"] self.nodes[0].generate(1) self.sync_blocks() @@ -398,7 +465,7 @@ def run_test(self): assert_equal(htlcs[dfchtlcTx]["amount"], Decimal('5.00000000')) assert_equal(htlcs[dfchtlcTx]["amountInEXTAsset"], Decimal('0.05000000')) assert_equal(htlcs[dfchtlcTx]["hash"], '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220') - assert_equal(htlcs[dfchtlcTx]["timeout"], 500) + assert_equal(htlcs[dfchtlcTx]["timeout"], 1440) exthtlcTx = self.nodes[1].icx_submitexthtlc({ 'offerTx': offerTx, @@ -406,7 +473,7 @@ def run_test(self): 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 'htlcScriptAddress': '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', 'ownerPubkey': '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', - 'timeout': 15})["txid"] + 'timeout': 24})["txid"] self.nodes[1].generate(1) self.sync_blocks() @@ -422,7 +489,7 @@ def run_test(self): assert_equal(htlcs[exthtlcTx]["hash"], '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220') assert_equal(htlcs[exthtlcTx]["htlcScriptAddress"], '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N') assert_equal(htlcs[exthtlcTx]["ownerPubkey"], '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252') - assert_equal(htlcs[exthtlcTx]["timeout"], 15) + assert_equal(htlcs[exthtlcTx]["timeout"], 24) beforeClaim0 = self.nodes[0].getaccount(accountDFI, {}, True)[idDFI] beforeClaim1 = self.nodes[1].getaccount(accountBTC, {}, True)[idDFI] @@ -508,7 +575,7 @@ def run_test(self): assert_equal(offer[offerTx]["ownerAddress"], accountBTC) assert_equal(offer[offerTx]["receivePubkey"], '037f9563f30c609b19fd435a19b8bde7d6db703012ba1aba72e9f42a87366d1941') assert_equal(offer[offerTx]["takerFee"], Decimal('0.30000000')) - assert_equal(offer[offerTx]["expireHeight"], self.nodes[0].getblockchaininfo()["blocks"] + 10) + assert_equal(offer[offerTx]["expireHeight"], self.nodes[0].getblockchaininfo()["blocks"] + 20) exthtlcTx = self.nodes[0].icx_submitexthtlc({ @@ -517,11 +584,13 @@ def run_test(self): 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 'htlcScriptAddress': '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', 'ownerPubkey': '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', - 'timeout': 30})["txid"] + 'timeout': 72})["txid"] self.nodes[0].generate(1) self.sync_blocks() + assert_equal(self.nodes[1].getaccount(accountBTC, {}, True)[idDFI], beforeOffer - Decimal('0.20000000')) + # Check burn assert_equal(self.nodes[0].getburninfo()['tokens'][0], "0.43000000@DFI") result = self.nodes[0].listburnhistory() @@ -540,13 +609,12 @@ def run_test(self): assert_equal(htlcs[exthtlcTx]["hash"], '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220') assert_equal(htlcs[exthtlcTx]["htlcScriptAddress"], '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N') assert_equal(htlcs[exthtlcTx]["ownerPubkey"], '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252') - assert_equal(htlcs[exthtlcTx]["timeout"], 30) + assert_equal(htlcs[exthtlcTx]["timeout"], 72) dfchtlcTx = self.nodes[1].icx_submitdfchtlc({ 'offerTx': offerTx, 'amount': 2000, - 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - 'timeout': 400})["txid"] + 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220'})["txid"] self.nodes[1].generate(1) self.sync_blocks() @@ -560,7 +628,7 @@ def run_test(self): assert_equal(htlcs[dfchtlcTx]["amount"], Decimal('2000.00000000')) assert_equal(htlcs[dfchtlcTx]["amountInEXTAsset"], Decimal('2.00000000')) assert_equal(htlcs[dfchtlcTx]["hash"], '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220') - assert_equal(htlcs[dfchtlcTx]["timeout"], 400) + assert_equal(htlcs[dfchtlcTx]["timeout"], 480) beforeClaim = self.nodes[0].getaccount(accountDFI, {}, True)[idDFI] @@ -593,6 +661,85 @@ def run_test(self): order = self.nodes[0].icx_listorders({"closed": True}) assert_equal(order[orderTx]["status"], 'FILLED') + + # DFI/BTC partial offer acceptance + orderTx = self.nodes[0].icx_createorder({ + 'tokenFrom': idDFI, + 'chainTo': "BTC", + 'ownerAddress': accountDFI, + 'receivePubkey': '037f9563f30c609b19fd435a19b8bde7d6db703012ba1aba72e9f42a87366d1941', + 'amountFrom': 15, + 'orderPrice':0.01})["txid"] + + self.nodes[0].generate(1) + self.sync_blocks() + + beforeOffer = self.nodes[1].getaccount(accountBTC, {}, True)[idDFI] + + offerTx = self.nodes[1].icx_makeoffer({ + 'orderTx': orderTx, + 'amount': 1, + 'ownerAddress': accountBTC})["txid"] + + self.nodes[1].generate(1) + self.sync_blocks() + + assert_equal(self.nodes[1].getaccount(accountBTC, {}, True)[idDFI], beforeOffer - Decimal('0.10000000')) + + offer = self.nodes[0].icx_listorders({"orderTx": orderTx}) + + assert_equal(offer[offerTx]["orderTx"], orderTx) + assert_equal(offer[offerTx]["amount"], Decimal('1.00000000')) + assert_equal(offer[offerTx]["ownerAddress"], accountBTC) + assert_equal(offer[offerTx]["takerFee"], Decimal('0.10000000')) + + dfchtlcTx = self.nodes[0].icx_submitdfchtlc({ + 'offerTx': offerTx, + 'amount': 15, + 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220'})["txid"] + + self.nodes[0].generate(1) + self.sync_blocks() + + assert_equal(self.nodes[1].getaccount(accountBTC, {}, True)[idDFI], beforeOffer - Decimal('0.01500000')) + + # Check burn + assert_equal(self.nodes[0].getburninfo()['tokens'][0], "0.46000000@DFI") + result = self.nodes[0].listburnhistory() + assert_equal(result[0]['owner'], burn_address) + assert_equal(result[0]['type'], 'ICXSubmitDFCHTLC') + assert_equal(result[0]['amounts'][0], '0.03000000@DFI') + + offer = self.nodes[0].icx_listorders({"orderTx": orderTx}) + assert_equal(offer[offerTx]["takerFee"], Decimal('0.01500000')) + + + exthtlcTx = self.nodes[1].icx_submitexthtlc({ + 'offerTx': offerTx, + 'amount': 0.15, + 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', + 'htlcScriptAddress': '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', + 'ownerPubkey': '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', + 'timeout': 24})["txid"] + + self.nodes[1].generate(1) + self.sync_blocks() + + claimTx = self.nodes[1].icx_claimdfchtlc({ + 'dfchtlcTx': dfchtlcTx, + 'seed': 'f75a61ad8f7a6e0ab701d5be1f5d4523a9b534571e4e92e0c4610c6a6784ccef'})["txid"] + + self.nodes[1].generate(1) + self.sync_blocks() + + # Make sure offer and order are now closed + offer = self.nodes[0].icx_listorders({"orderTx": orderTx}) + assert_equal(len(offer), 1) + order = self.nodes[0].icx_listorders() + assert_equal(len(offer), 1) + order = self.nodes[0].icx_listorders({"closed": True}) + assert_equal(order[orderTx]["status"], 'FILLED') + # DFI/BTC scenario expiration test # Open an order @@ -625,7 +772,7 @@ def run_test(self): offer = self.nodes[0].icx_listorders({"orderTx": orderTxDFI}) assert_equal(len(offer), 2) - self.nodes[1].generate(10) + self.nodes[1].generate(20) assert_equal(self.nodes[1].getaccount(accountBTC, {}, True)[idDFI], beforeOffer) @@ -645,8 +792,7 @@ def run_test(self): dfchtlcTx = self.nodes[0].icx_submitdfchtlc({ 'offerTx': offerTx, 'amount': 10, - 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - 'timeout': 500})["txid"] + 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220'})["txid"] self.nodes[0].generate(1) assert_equal(self.nodes[0].getaccount(accountDFI, {}, True)[idDFI], beforeDFCHTLC - Decimal('0.01000000')) @@ -698,7 +844,7 @@ def run_test(self): offer = self.nodes[0].icx_listorders({"orderTx": orderTxBTC}) assert_equal(len(offer), 2) - self.nodes[1].generate(10) + self.nodes[1].generate(20) self.sync_blocks() assert_equal(self.nodes[1].getaccount(accountBTC, {}, True)[idDFI], beforeOffer) @@ -725,7 +871,7 @@ def run_test(self): 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 'htlcScriptAddress': '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', 'ownerPubkey': '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', - 'timeout': 30})["txid"] + 'timeout': 72})["txid"] self.nodes[0].generate(1) @@ -773,8 +919,7 @@ def run_test(self): dfchtlcTx = self.nodes[0].icx_submitdfchtlc({ 'offerTx': offerTx, 'amount': 10, - 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - 'timeout': 500})["txid"] + 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220'})["txid"] self.nodes[0].generate(1) self.sync_blocks() @@ -784,7 +929,7 @@ def run_test(self): 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 'htlcScriptAddress': '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', 'ownerPubkey': '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', - 'timeout': 15})["txid"] + 'timeout': 24})["txid"] self.nodes[1].generate(1) self.sync_blocks() diff --git a/test/functional/feature_icx_orderbook_errors.py b/test/functional/feature_icx_orderbook_errors.py index 4df0da64081..4a6f6a3615f 100755 --- a/test/functional/feature_icx_orderbook_errors.py +++ b/test/functional/feature_icx_orderbook_errors.py @@ -226,7 +226,7 @@ def run_test(self): 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 'htlcScriptAddress': '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', 'ownerPubkey': '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', - 'timeout': 15}) + 'timeout': 24}) except JSONRPCException as e: errorString = e.error['message'] assert("offer ("+ offerTx + ") needs to have dfc htlc submitted first, but no dfc htlc found!" in errorString) @@ -236,8 +236,7 @@ def run_test(self): self.nodes[0].icx_submitdfchtlc({ 'offerTx': offerTx, 'amount': 1, - 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - 'timeout': 500}) + 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220'}) except JSONRPCException as e: errorString = e.error['message'] assert("amount 0.00000000 is less than 0.00100000" in errorString) @@ -250,8 +249,7 @@ def run_test(self): self.nodes[0].icx_submitdfchtlc({ 'offerTx': offerTx, 'amount': 2, - 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - 'timeout': 500}) + 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220'}) except JSONRPCException as e: errorString = e.error['message'] assert("amount must be lower or equal the offer one" in errorString) @@ -262,16 +260,15 @@ def run_test(self): 'offerTx': offerTx, 'amount': 1, 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - 'timeout': 499}) + 'timeout': 1439}) except JSONRPCException as e: errorString = e.error['message'] - assert("timeout must be greater than 499" in errorString) + assert("timeout must be greater than 1439" in errorString) dfchtlcTx = self.nodes[0].icx_submitdfchtlc({ 'offerTx': offerTx, 'amount': 1, - 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - 'timeout': 500})["txid"] + 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220'})["txid"] self.nodes[0].generate(1) self.sync_blocks() @@ -281,8 +278,7 @@ def run_test(self): self.nodes[0].icx_submitdfchtlc({ 'offerTx': offerTx, 'amount': 1, - 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - 'timeout': 500}) + 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220'}) except JSONRPCException as e: errorString = e.error['message'] assert("dfc htlc already submitted" in errorString) @@ -296,10 +292,10 @@ def run_test(self): 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 'htlcScriptAddress': '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', 'ownerPubkey': '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', - 'timeout': 15}) + 'timeout': 24}) except JSONRPCException as e: errorString = e.error['message'] - assert("amount 100000 must be equal to calculated dfchtlc amount 1000000" in errorString) + assert("amount must be equal to calculated dfchtlc amount" in errorString) # more amount try: @@ -309,10 +305,10 @@ def run_test(self): 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 'htlcScriptAddress': '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', 'ownerPubkey': '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', - 'timeout': 15}) + 'timeout': 24}) except JSONRPCException as e: errorString = e.error['message'] - assert("amount 10000000 must be equal to calculated dfchtlc amount 1000000" in errorString) + assert("amount must be equal to calculated dfchtlc amount" in errorString) # different hash try: @@ -322,7 +318,7 @@ def run_test(self): 'hash': '957fc0fd643f605b2938e0000000029fd70bd35b2162a21d978c41e5241a5220', 'htlcScriptAddress': '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', 'ownerPubkey': '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', - 'timeout': 15}) + 'timeout': 24}) except JSONRPCException as e: errorString = e.error['message'] assert("Invalid hash, external htlc hash is different than dfc htlc hash" in errorString) @@ -335,10 +331,10 @@ def run_test(self): 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 'htlcScriptAddress': '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', 'ownerPubkey': '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', - 'timeout': 10}) + 'timeout': 23}) except JSONRPCException as e: errorString = e.error['message'] - assert("timeout must be greater than 14" in errorString) + assert("timeout must be greater than 23" in errorString) # timeout more than expiration of dfc htlc try: @@ -348,7 +344,7 @@ def run_test(self): 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 'htlcScriptAddress': '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', 'ownerPubkey': '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', - 'timeout': 32}) + 'timeout': 73}) except JSONRPCException as e: errorString = e.error['message'] assert("timeout must be less than expiration period of 1st htlc in DFC blocks" in errorString) @@ -359,7 +355,7 @@ def run_test(self): 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 'htlcScriptAddress': '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', 'ownerPubkey': '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', - 'timeout': 15})["txid"] + 'timeout': 24})["txid"] self.nodes[1].generate(1) self.sync_blocks() @@ -493,8 +489,7 @@ def run_test(self): self.nodes[1].icx_submitdfchtlc({ 'offerTx': offerTx, 'amount': 1, - 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - 'timeout': 500}) + 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220'}) except JSONRPCException as e: errorString = e.error['message'] assert("offer ("+ offerTx + ") needs to have ext htlc submitted first, but no external htlc found" in errorString) @@ -507,7 +502,7 @@ def run_test(self): 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 'htlcScriptAddress': '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', 'ownerPubkey': '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', - 'timeout': 30}) + 'timeout': 72}) except JSONRPCException as e: errorString = e.error['message'] assert("amount must be lower or equal the offer one" in errorString) @@ -520,10 +515,10 @@ def run_test(self): 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 'htlcScriptAddress': '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', 'ownerPubkey': '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', - 'timeout': 29}) + 'timeout': 71}) except JSONRPCException as e: errorString = e.error['message'] - assert("timeout must be greater than 29" in errorString) + assert("timeout must be greater than 71" in errorString) self.nodes[0].icx_submitexthtlc({ 'offerTx': offerTx, @@ -531,7 +526,7 @@ def run_test(self): 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 'htlcScriptAddress': '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', 'ownerPubkey': '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', - 'timeout': 30})["txid"] + 'timeout': 72})["txid"] self.nodes[0].generate(1) self.sync_blocks() @@ -544,7 +539,7 @@ def run_test(self): 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', 'htlcScriptAddress': '13sJQ9wBWh8ssihHUgAaCmNWJbBAG5Hr9N', 'ownerPubkey': '036494e7c9467c8c7ff3bf29e841907fb0fa24241866569944ea422479ec0e6252', - 'timeout': 30}) + 'timeout': 72}) except JSONRPCException as e: errorString = e.error['message'] assert("ext htlc already submitted" in errorString) @@ -554,8 +549,7 @@ def run_test(self): self.nodes[1].icx_submitdfchtlc({ 'offerTx': offerTx, 'amount': 0.5, - 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - 'timeout': 400}) + 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220'}) except JSONRPCException as e: errorString = e.error['message'] assert("amount must be equal to calculated exthtlc amount" in errorString) @@ -565,8 +559,7 @@ def run_test(self): self.nodes[1].icx_submitdfchtlc({ 'offerTx': offerTx, 'amount': 2, - 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - 'timeout': 400}) + 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220'}) except JSONRPCException as e: errorString = e.error['message'] assert("amount must be equal to calculated exthtlc amount" in errorString) @@ -577,10 +570,10 @@ def run_test(self): 'offerTx': offerTx, 'amount': 1, 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - 'timeout': 249}) + 'timeout': 479}) except JSONRPCException as e: errorString = e.error['message'] - assert("timeout must be greater than 249" in errorString) + assert("timeout must be greater than 479" in errorString) # timeout more than expiration of ext htlc try: @@ -588,7 +581,7 @@ def run_test(self): 'offerTx': offerTx, 'amount': 1, 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - 'timeout': 500}) + 'timeout': 1441}) except JSONRPCException as e: errorString = e.error['message'] assert("timeout must be less than expiration period of 1st htlc in DFI blocks" in errorString) @@ -596,8 +589,7 @@ def run_test(self): dfchtlcTx = self.nodes[1].icx_submitdfchtlc({ 'offerTx': offerTx, 'amount': 1, - 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220', - 'timeout': 450})["txid"] + 'hash': '957fc0fd643f605b2938e0631a61529fd70bd35b2162a21d978c41e5241a5220'})["txid"] self.nodes[1].generate(1) self.sync_blocks() diff --git a/test/functional/feature_longterm_lockin.py b/test/functional/feature_longterm_lockin.py new file mode 100644 index 00000000000..0632e0eb777 --- /dev/null +++ b/test/functional/feature_longterm_lockin.py @@ -0,0 +1,166 @@ +#!/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 the masternodes timelock""" + +import time + +from test_framework.test_framework import DefiTestFramework + +from test_framework.authproxy import JSONRPCException +from test_framework.util import assert_equal + +class MasternodesTimelockTest (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', '-dakotacrescentheight=1', '-eunosheight=140']] + + def run_test(self): + + self.nodes[0].generate(101) + + collateral = self.nodes[0].getnewaddress("", "legacy") + collateral5 = self.nodes[0].getnewaddress("", "legacy") + collateral10 = self.nodes[0].getnewaddress("", "legacy") + + # Try to set time lock before EunosPaya + try: + self.nodes[0].createmasternode(collateral5, "", [], "FIVEYEARTIMELOCK") + except JSONRPCException as e: + errorString = e.error['message'] + assert("Timelock cannot be specified before EunosPaya hard fork" in errorString) + + # Generate to hard fork + self.nodes[0].generate(39) + + # Create MNs with locked funds + nodeid = self.nodes[0].createmasternode(collateral) + nodeid5 = self.nodes[0].createmasternode(collateral5, "", [], "FIVEYEARTIMELOCK") + nodeid10 = self.nodes[0].createmasternode(collateral10, "", [], "TENYEARTIMELOCK") + self.nodes[0].generate(1) + + # Check state and timelock length + result = self.nodes[0].getmasternode(nodeid) + assert_equal(result[nodeid]['state'], 'PRE_ENABLED') + assert_equal('timelock' not in result[nodeid], True) + result5 = self.nodes[0].getmasternode(nodeid5) + assert_equal(result5[nodeid5]['state'], 'PRE_ENABLED') + assert_equal(result5[nodeid5]['timelock'], '5 years') + result10 = self.nodes[0].getmasternode(nodeid10) + assert_equal(result10[nodeid10]['state'], 'PRE_ENABLED') + assert_equal(result10[nodeid10]['timelock'], '10 years') + + # Activate masternodes + self.nodes[0].generate(20) + + # Check all multipliers are set to 1 + result = self.nodes[0].getmasternode(nodeid) + assert_equal(result[nodeid]['targetMultiplier'], 1) + result5 = self.nodes[0].getmasternode(nodeid5) + assert_equal(result5[nodeid5]['targetMultiplier'], 1) + result10 = self.nodes[0].getmasternode(nodeid10) + assert_equal(result10[nodeid10]['targetMultiplier'], 1) + + # Time travel a day + self.nodes[0].set_mocktime(int(time.time()) + (24 * 60 * 60)) + self.nodes[0].generate(1) + + # Check all multipliers have increased + result = self.nodes[0].getmasternode(nodeid) + assert_equal(result[nodeid]['targetMultiplier'], 4) + result5 = self.nodes[0].getmasternode(nodeid5) + assert_equal(result5[nodeid5]['targetMultiplier'], 6) + result10 = self.nodes[0].getmasternode(nodeid10) + assert_equal(result10[nodeid10]['targetMultiplier'], 8) + + # Time travel a week + self.nodes[0].set_mocktime(int(time.time()) + (7 * 24 * 60 * 60)) + self.nodes[0].generate(1) + + # Check all multipliers have increased + result = self.nodes[0].getmasternode(nodeid) + assert_equal(result[nodeid]['targetMultiplier'], 28) + result5 = self.nodes[0].getmasternode(nodeid5) + assert_equal(result5[nodeid5]['targetMultiplier'], 42) + result10 = self.nodes[0].getmasternode(nodeid10) + assert_equal(result10[nodeid10]['targetMultiplier'], 56) + + # Time travel 11 days, max for freezer users + self.nodes[0].set_mocktime(int(time.time()) + (11 * 24 * 60 * 60)) + self.nodes[0].generate(1) + + # Check all multipliers have increased + result = self.nodes[0].getmasternode(nodeid) + assert_equal(result[nodeid]['targetMultiplier'], 44) + result5 = self.nodes[0].getmasternode(nodeid5) + assert_equal(result5[nodeid5]['targetMultiplier'], 57) + result10 = self.nodes[0].getmasternode(nodeid10) + assert_equal(result10[nodeid10]['targetMultiplier'], 57) + + # Let's try and resign the MNs + try: + self.nodes[0].resignmasternode(nodeid5) + except JSONRPCException as e: + errorString = e.error['message'] + assert("Trying to resign masternode before timelock expiration" in errorString) + + try: + self.nodes[0].resignmasternode(nodeid10) + except JSONRPCException as e: + errorString = e.error['message'] + assert("Trying to resign masternode before timelock expiration" in errorString) + + # Time travel five years + self.nodes[0].set_mocktime(int(time.time()) + (5 * 365 * 24 * 60 * 60)) + + # Generate enough future blocks to create average future time + self.nodes[0].generate(41) + + # Check state + result5 = self.nodes[0].getmasternode(nodeid5) + assert_equal(result5[nodeid5]['state'], 'ENABLED') + + # Timelock should no longer be present + assert_equal('timelock' not in result5[nodeid5], True) + + # Resign 5 year MN + self.nodes[0].resignmasternode(nodeid5) + + # Try to resign 10 year MN + try: + self.nodes[0].resignmasternode(nodeid10) + except JSONRPCException as e: + errorString = e.error['message'] + assert("Trying to resign masternode before timelock expiration" in errorString) + + # Generate enough blocks to confirm resignation + self.nodes[0].generate(41) + result5 = self.nodes[0].getmasternode(nodeid5) + assert_equal(result5[nodeid5]['state'], 'RESIGNED') + + # Time travel ten years + self.nodes[0].set_mocktime(int(time.time()) + (10 * 365 * 24 * 60 * 60)) + + # Generate enough future blocks to create average future time + self.nodes[0].generate(41) + + # Check state + result10 = self.nodes[0].getmasternode(nodeid10) + assert_equal(result10[nodeid10]['state'], 'ENABLED') + + # Timelock should no longer be present + assert_equal('timelock' not in result10[nodeid10], True) + + # Resign 10 year MN + self.nodes[0].resignmasternode(nodeid10) + + # Generate enough blocks to confirm resignation + self.nodes[0].generate(41) + result10 = self.nodes[0].getmasternode(nodeid10) + assert_equal(result10[nodeid10]['state'], 'RESIGNED') + +if __name__ == '__main__': + MasternodesTimelockTest().main() diff --git a/test/functional/rpc_mn_basic.py b/test/functional/rpc_mn_basic.py index 62d5c0bb961..7212396911c 100755 --- a/test/functional/rpc_mn_basic.py +++ b/test/functional/rpc_mn_basic.py @@ -182,8 +182,19 @@ def run_test(self): mnTx = self.nodes[0].createmasternode(self.nodes[0].getnewaddress("", "legacy")) self.nodes[0].generate(1) assert_equal(self.nodes[0].listmasternodes({}, False)[mnTx], "PRE_ENABLED") + + # Try and resign masternode while still in PRE_ENABLED + try: + self.nodes[0].resignmasternode(mnTx) + except JSONRPCException as e: + errorString = e.error['message'] + assert("state is not 'ENABLED'" in errorString) + + # Check end of PRE_ENABLED range self.nodes[0].generate(19) assert_equal(self.nodes[0].listmasternodes({}, False)[mnTx], "PRE_ENABLED") + + # Move ahead to ENABLED state self.nodes[0].generate(1) assert_equal(self.nodes[0].listmasternodes({}, False)[mnTx], "ENABLED") diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index c1a38a56944..dcbee34d741 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -222,6 +222,7 @@ 'feature_dersig.py', 'feature_cltv.py', 'rpc_uptime.py', + 'feature_longterm_lockin.py', 'wallet_resendwallettransactions.py', 'feature_custom_poolreward.py', 'wallet_fallbackfee.py',