mirror of
https://github.com/dashpay/dash.git
synced 2024-12-24 19:42:46 +01:00
feat!: add an implementation of DIP 0027 Credit Asset Locks (#5026)
## Issue being fixed or feature implemented This is an implementation of DIP0027 "Credit Asset Locks". It's a mechanism to fluidly exchange between Dash and credits. ## What was done? This pull request includes: - Asset Lock transaction - Asset Unlock transaction (withdrawal) - Credit Pool in coinbase - Unit tests for Asset Lock/Unlock tx - New functional test `feature_asset_locks.py` RPC: currently locked amount (credit pool) is available through rpc call `getblock`. ## How Has This Been Tested? There added new unit tests for basic checks of transaction validity (asset lock/unlock). Also added new functional test "feature_asset_locks.py" that cover typical cases, but not all corner cases yet. ## Breaking Changes This feature should be activated as hard-fork because: - It adds 2 new special transaction and one of them [asset unlock tx] requires update consensus rulels - It adds new data in coinbase tx (credit pool) ## Checklist: - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have added or updated relevant unit/integration/functional/e2e tests - [ ] I have made corresponding changes to the documentation **To release DIP 0027** - [x] I have assigned this pull request to a milestone --------- Co-authored-by: UdjinM6 <UdjinM6@users.noreply.github.com>
This commit is contained in:
parent
3c65626609
commit
8a0e681cea
@ -169,8 +169,10 @@ BITCOIN_CORE_H = \
|
||||
cuckoocache.h \
|
||||
ctpl_stl.h \
|
||||
cxxtimer.hpp \
|
||||
evo/assetlocktx.h \
|
||||
evo/dmn_types.h \
|
||||
evo/cbtx.h \
|
||||
evo/creditpool.h \
|
||||
evo/deterministicmns.h \
|
||||
evo/dmnstate.h \
|
||||
evo/evodb.h \
|
||||
@ -320,6 +322,7 @@ BITCOIN_CORE_H = \
|
||||
util/underlying.h \
|
||||
util/serfloat.h \
|
||||
util/settings.h \
|
||||
util/skip_set.h \
|
||||
util/sock.h \
|
||||
util/string.h \
|
||||
util/time.h \
|
||||
@ -384,7 +387,9 @@ libbitcoin_server_a_SOURCES = \
|
||||
consensus/tx_verify.cpp \
|
||||
dbwrapper.cpp \
|
||||
dsnotificationinterface.cpp \
|
||||
evo/assetlocktx.cpp \
|
||||
evo/cbtx.cpp \
|
||||
evo/creditpool.cpp \
|
||||
evo/deterministicmns.cpp \
|
||||
evo/dmnstate.cpp \
|
||||
evo/evodb.cpp \
|
||||
@ -731,6 +736,7 @@ libbitcoin_util_a_SOURCES = \
|
||||
util/message.cpp \
|
||||
util/moneystr.cpp \
|
||||
util/settings.cpp \
|
||||
util/skip_set.cpp \
|
||||
util/spanparsing.cpp \
|
||||
util/strencodings.cpp \
|
||||
util/time.cpp \
|
||||
|
@ -95,6 +95,7 @@ BITCOIN_TESTS =\
|
||||
test/dip0020opcodes_tests.cpp \
|
||||
test/descriptor_tests.cpp \
|
||||
test/dynamic_activation_thresholds_tests.cpp \
|
||||
test/evo_assetlocks_tests.cpp \
|
||||
test/evo_deterministicmns_tests.cpp \
|
||||
test/evo_instantsend_tests.cpp \
|
||||
test/evo_simplifiedmns_tests.cpp \
|
||||
|
@ -194,6 +194,10 @@ bool CBloomFilter::CheckSpecialTransactionMatchesAndUpdate(const CTransaction &t
|
||||
case(TRANSACTION_MNHF_SIGNAL):
|
||||
// No additional checks for this transaction types
|
||||
return false;
|
||||
case(TRANSACTION_ASSET_LOCK):
|
||||
case(TRANSACTION_ASSET_UNLOCK):
|
||||
// TODO asset lock/unlock bloom?
|
||||
return false;
|
||||
}
|
||||
|
||||
LogPrintf("Unknown special transaction type in Bloom filter check.\n");
|
||||
|
@ -250,6 +250,7 @@ public:
|
||||
consensus.llmqTypeDIP0024InstantSend = Consensus::LLMQType::LLMQ_60_75;
|
||||
consensus.llmqTypePlatform = Consensus::LLMQType::LLMQ_100_67;
|
||||
consensus.llmqTypeMnhf = Consensus::LLMQType::LLMQ_400_85;
|
||||
consensus.llmqTypeAssetLocks = Consensus::LLMQType::LLMQ_400_85;
|
||||
|
||||
fDefaultConsistencyChecks = false;
|
||||
fRequireStandard = true;
|
||||
@ -431,6 +432,7 @@ public:
|
||||
consensus.llmqTypeDIP0024InstantSend = Consensus::LLMQType::LLMQ_60_75;
|
||||
consensus.llmqTypePlatform = Consensus::LLMQType::LLMQ_25_67;
|
||||
consensus.llmqTypeMnhf = Consensus::LLMQType::LLMQ_50_60;
|
||||
consensus.llmqTypeAssetLocks = Consensus::LLMQType::LLMQ_50_60;
|
||||
|
||||
fDefaultConsistencyChecks = false;
|
||||
fRequireStandard = false;
|
||||
@ -595,6 +597,7 @@ public:
|
||||
consensus.llmqTypeDIP0024InstantSend = Consensus::LLMQType::LLMQ_60_75;
|
||||
consensus.llmqTypePlatform = Consensus::LLMQType::LLMQ_100_67;
|
||||
consensus.llmqTypeMnhf = Consensus::LLMQType::LLMQ_50_60;
|
||||
consensus.llmqTypeAssetLocks = Consensus::LLMQType::LLMQ_50_60;
|
||||
|
||||
UpdateDevnetLLMQChainLocksFromArgs(args);
|
||||
UpdateDevnetLLMQInstantSendFromArgs(args);
|
||||
@ -859,6 +862,7 @@ public:
|
||||
consensus.llmqTypeDIP0024InstantSend = Consensus::LLMQType::LLMQ_TEST_DIP0024;
|
||||
consensus.llmqTypePlatform = Consensus::LLMQType::LLMQ_TEST_PLATFORM;
|
||||
consensus.llmqTypeMnhf = Consensus::LLMQType::LLMQ_TEST;
|
||||
consensus.llmqTypeAssetLocks = Consensus::LLMQType::LLMQ_TEST;
|
||||
|
||||
UpdateLLMQTestParametersFromArgs(args, Consensus::LLMQType::LLMQ_TEST);
|
||||
UpdateLLMQTestParametersFromArgs(args, Consensus::LLMQType::LLMQ_TEST_INSTANTSEND);
|
||||
|
@ -126,6 +126,7 @@ struct Params {
|
||||
LLMQType llmqTypeDIP0024InstantSend{LLMQType::LLMQ_NONE};
|
||||
LLMQType llmqTypePlatform{LLMQType::LLMQ_NONE};
|
||||
LLMQType llmqTypeMnhf{LLMQType::LLMQ_NONE};
|
||||
LLMQType llmqTypeAssetLocks{LLMQType::LLMQ_NONE};
|
||||
};
|
||||
} // namespace Consensus
|
||||
|
||||
|
@ -12,15 +12,20 @@
|
||||
|
||||
bool CheckTransaction(const CTransaction& tx, TxValidationState& state)
|
||||
{
|
||||
bool allowEmptyTxInOut = false;
|
||||
bool allowEmptyTxIn = false;
|
||||
bool allowEmptyTxOut = false;
|
||||
if (tx.nType == TRANSACTION_QUORUM_COMMITMENT) {
|
||||
allowEmptyTxInOut = true;
|
||||
allowEmptyTxIn = true;
|
||||
allowEmptyTxOut = true;
|
||||
}
|
||||
if (tx.nType == TRANSACTION_ASSET_UNLOCK) {
|
||||
allowEmptyTxIn = true;
|
||||
}
|
||||
|
||||
// Basic checks that don't depend on any context
|
||||
if (!allowEmptyTxInOut && tx.vin.empty())
|
||||
if (!allowEmptyTxIn && tx.vin.empty())
|
||||
return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-txns-vin-empty");
|
||||
if (!allowEmptyTxInOut && tx.vout.empty())
|
||||
if (!allowEmptyTxOut && tx.vout.empty())
|
||||
return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-txns-vout-empty");
|
||||
// Size limits
|
||||
if (::GetSerializeSize(tx, PROTOCOL_VERSION) > MAX_LEGACY_BLOCK_SIZE)
|
||||
|
@ -8,6 +8,7 @@
|
||||
#include <primitives/transaction.h>
|
||||
#include <script/interpreter.h>
|
||||
#include <consensus/validation.h>
|
||||
#include <evo/assetlocktx.h>
|
||||
|
||||
// TODO remove the following dependencies
|
||||
#include <chain.h>
|
||||
@ -160,6 +161,11 @@ unsigned int GetTransactionSigOpCount(const CTransaction& tx, const CCoinsViewCa
|
||||
|
||||
bool Consensus::CheckTxInputs(const CTransaction& tx, TxValidationState& state, const CCoinsViewCache& inputs, int nSpendHeight, CAmount& txfee)
|
||||
{
|
||||
|
||||
if (bool isAssetUnlockTx = (tx.nVersion == 3 && tx.nType == TRANSACTION_ASSET_UNLOCK); isAssetUnlockTx) {
|
||||
return GetAssetUnlockFee(tx, txfee, state);
|
||||
}
|
||||
|
||||
// are the actual inputs available?
|
||||
if (!inputs.HaveInputs(tx)) {
|
||||
return state.Invalid(TxValidationResult::TX_MISSING_INPUTS, "bad-txns-inputs-missingorspent",
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
#include <spentindex.h>
|
||||
|
||||
#include <evo/assetlocktx.h>
|
||||
#include <evo/cbtx.h>
|
||||
#include <evo/mnhftx.h>
|
||||
#include <evo/providertx.h>
|
||||
@ -310,6 +311,20 @@ void TxToUniv(const CTransaction& tx, const uint256& hashBlock, UniValue& entry,
|
||||
mnhfTx.ToJson(obj);
|
||||
entry.pushKV("mnhfTx", obj);
|
||||
}
|
||||
} else if (tx.nType == TRANSACTION_ASSET_LOCK) {
|
||||
CAssetLockPayload assetLockTx;
|
||||
if (!GetTxPayload(tx, assetLockTx)) {
|
||||
UniValue obj;
|
||||
assetLockTx.ToJson(obj);
|
||||
entry.pushKV("assetLockTx", obj);
|
||||
}
|
||||
} else if (tx.nType == TRANSACTION_ASSET_UNLOCK) {
|
||||
CAssetUnlockPayload assetUnlockTx;
|
||||
if (!GetTxPayload(tx, assetUnlockTx)) {
|
||||
UniValue obj;
|
||||
assetUnlockTx.ToJson(obj);
|
||||
entry.pushKV("assetUnlockTx", obj);
|
||||
}
|
||||
}
|
||||
|
||||
if (!hashBlock.IsNull())
|
||||
|
205
src/evo/assetlocktx.cpp
Normal file
205
src/evo/assetlocktx.cpp
Normal file
@ -0,0 +1,205 @@
|
||||
// Copyright (c) 2023 The Dash Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <evo/assetlocktx.h>
|
||||
#include <evo/specialtx.h>
|
||||
#include <evo/creditpool.h>
|
||||
|
||||
#include <consensus/params.h>
|
||||
|
||||
#include <chainparams.h>
|
||||
#include <logging.h>
|
||||
#include <validation.h>
|
||||
|
||||
#include <llmq/commitment.h>
|
||||
#include <llmq/signing.h>
|
||||
#include <llmq/utils.h>
|
||||
#include <llmq/quorums.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
/**
|
||||
* Common code for Asset Lock and Asset Unlock
|
||||
*/
|
||||
bool CheckAssetLockUnlockTx(const CTransaction& tx, const CBlockIndex* pindexPrev, const CCreditPool& creditPool, TxValidationState& state)
|
||||
{
|
||||
switch (tx.nType) {
|
||||
case TRANSACTION_ASSET_LOCK:
|
||||
return CheckAssetLockTx(tx, state);
|
||||
case TRANSACTION_ASSET_UNLOCK:
|
||||
return CheckAssetUnlockTx(tx, pindexPrev, creditPool, state);
|
||||
default:
|
||||
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-not-asset-locks-at-all");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset Lock Transaction
|
||||
*/
|
||||
bool CheckAssetLockTx(const CTransaction& tx, TxValidationState& state)
|
||||
{
|
||||
if (tx.nType != TRANSACTION_ASSET_LOCK) {
|
||||
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-type");
|
||||
}
|
||||
|
||||
CAmount returnAmount{0};
|
||||
for (const CTxOut& txout : tx.vout) {
|
||||
const CScript& script = txout.scriptPubKey;
|
||||
if (script.empty() || script[0] != OP_RETURN) continue;
|
||||
|
||||
if (script.size() != 2 || script[1] != 0) return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-non-empty-return");
|
||||
|
||||
if (txout.nValue == 0 || !MoneyRange(txout.nValue)) return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-opreturn-outofrange");
|
||||
|
||||
// Should be only one OP_RETURN
|
||||
if (returnAmount > 0) return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-multiple-return");
|
||||
returnAmount = txout.nValue;
|
||||
}
|
||||
|
||||
if (returnAmount == 0) return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-no-return");
|
||||
|
||||
CAssetLockPayload assetLockTx;
|
||||
if (!GetTxPayload(tx, assetLockTx)) {
|
||||
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-payload");
|
||||
}
|
||||
|
||||
if (assetLockTx.getVersion() == 0 || assetLockTx.getVersion() > CAssetLockPayload::CURRENT_VERSION) {
|
||||
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-version");
|
||||
}
|
||||
|
||||
if (assetLockTx.getCreditOutputs().empty()) {
|
||||
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-emptycreditoutputs");
|
||||
}
|
||||
|
||||
CAmount creditOutputsAmount = 0;
|
||||
for (const CTxOut& out : assetLockTx.getCreditOutputs()) {
|
||||
if (out.nValue == 0 || !MoneyRange(out.nValue) || !MoneyRange(creditOutputsAmount + out.nValue)) {
|
||||
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-credit-outofrange");
|
||||
}
|
||||
|
||||
creditOutputsAmount += out.nValue;
|
||||
if (!out.scriptPubKey.IsPayToPublicKeyHash()) {
|
||||
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-pubKeyHash");
|
||||
}
|
||||
}
|
||||
if (creditOutputsAmount != returnAmount) {
|
||||
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-creditamount");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string CAssetLockPayload::ToString() const
|
||||
{
|
||||
std::string outputs{"["};
|
||||
for (const CTxOut& tx: creditOutputs) {
|
||||
outputs.append(tx.ToString());
|
||||
outputs.append(",");
|
||||
}
|
||||
outputs.back() = ']';
|
||||
return strprintf("CAssetLockPayload(nVersion=%d,creditOutputs=%s)", nVersion, outputs.c_str());
|
||||
}
|
||||
|
||||
/**
|
||||
* Asset Unlock Transaction (withdrawals)
|
||||
*/
|
||||
|
||||
const std::string ASSETUNLOCK_REQUESTID_PREFIX = "plwdtx";
|
||||
|
||||
bool CAssetUnlockPayload::VerifySig(const uint256& msgHash, const CBlockIndex* pindexTip, TxValidationState& state) const
|
||||
{
|
||||
// That quourm hash must be active at `requestHeight`,
|
||||
// and at the quorumHash must be active in either the current or previous quorum cycle
|
||||
// and the sig must validate against that specific quorumHash.
|
||||
|
||||
Consensus::LLMQType llmqType = Params().GetConsensus().llmqTypeAssetLocks;
|
||||
|
||||
// We check at most 2 quorums
|
||||
const auto quorums = llmq::quorumManager->ScanQuorums(llmqType, pindexTip, 2);
|
||||
bool isActive = std::any_of(quorums.begin(), quorums.end(), [&](const auto &q) { return q->qc->quorumHash == quorumHash; });
|
||||
|
||||
if (!isActive) {
|
||||
return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-assetunlock-not-active-quorum");
|
||||
}
|
||||
|
||||
if (pindexTip->nHeight < requestedHeight || pindexTip->nHeight >= getHeightToExpiry()) {
|
||||
LogPrintf("Asset unlock tx %d with requested height %d could not be accepted on height: %d\n",
|
||||
index, requestedHeight, pindexTip->nHeight);
|
||||
return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-assetunlock-too-late");
|
||||
}
|
||||
|
||||
const auto quorum = llmq::quorumManager->GetQuorum(llmqType, quorumHash);
|
||||
assert(quorum);
|
||||
|
||||
const uint256 requestId = ::SerializeHash(std::make_pair(ASSETUNLOCK_REQUESTID_PREFIX, index));
|
||||
|
||||
const uint256 signHash = llmq::utils::BuildSignHash(llmqType, quorum->qc->quorumHash, requestId, msgHash);
|
||||
if (quorumSig.VerifyInsecure(quorum->qc->quorumPublicKey, signHash)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-assetunlock-not-verified");
|
||||
}
|
||||
|
||||
bool CheckAssetUnlockTx(const CTransaction& tx, const CBlockIndex* pindexPrev, const CCreditPool& creditPool, TxValidationState& state)
|
||||
{
|
||||
if (tx.nType != TRANSACTION_ASSET_UNLOCK) {
|
||||
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetunlocktx-type");
|
||||
}
|
||||
|
||||
if (!tx.vin.empty()) {
|
||||
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetunlocktx-have-input");
|
||||
}
|
||||
|
||||
if (tx.vout.size() > CAssetUnlockPayload::MAXIMUM_WITHDRAWALS) {
|
||||
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetunlocktx-too-many-outs");
|
||||
}
|
||||
|
||||
CAssetUnlockPayload assetUnlockTx;
|
||||
if (!GetTxPayload(tx, assetUnlockTx)) {
|
||||
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetunlocktx-payload");
|
||||
}
|
||||
|
||||
if (assetUnlockTx.getVersion() == 0 || assetUnlockTx.getVersion() > CAssetUnlockPayload::CURRENT_VERSION) {
|
||||
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetunlocktx-version");
|
||||
}
|
||||
|
||||
if (creditPool.indexes.Contains(assetUnlockTx.getIndex())) {
|
||||
return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-assetunlock-duplicated-index");
|
||||
}
|
||||
|
||||
const CBlockIndex* pindexQuorum = WITH_LOCK(cs_main, return g_chainman.m_blockman.LookupBlockIndex(assetUnlockTx.getQuorumHash()));
|
||||
if (!pindexQuorum) {
|
||||
return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-assetunlock-quorum-hash");
|
||||
}
|
||||
|
||||
// Copy transaction except `quorumSig` field to calculate hash
|
||||
CMutableTransaction tx_copy(tx);
|
||||
const CAssetUnlockPayload payload_copy{assetUnlockTx.getVersion(), assetUnlockTx.getIndex(), assetUnlockTx.getFee(), assetUnlockTx.getRequestedHeight(), assetUnlockTx.getQuorumHash(), CBLSSignature{}};
|
||||
SetTxPayload(tx_copy, payload_copy);
|
||||
|
||||
uint256 msgHash = tx_copy.GetHash();
|
||||
|
||||
return assetUnlockTx.VerifySig(msgHash, pindexPrev, state);
|
||||
}
|
||||
|
||||
bool GetAssetUnlockFee(const CTransaction& tx, CAmount& txfee, TxValidationState& state)
|
||||
{
|
||||
CAssetUnlockPayload assetUnlockTx;
|
||||
if (!GetTxPayload(tx, assetUnlockTx)) {
|
||||
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetunlocktx-payload");
|
||||
}
|
||||
const CAmount txfee_aux = assetUnlockTx.getFee();
|
||||
if (txfee_aux == 0 || !MoneyRange(txfee_aux)) {
|
||||
return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-txns-assetunlock-fee-outofrange");
|
||||
}
|
||||
txfee = txfee_aux;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string CAssetUnlockPayload::ToString() const
|
||||
{
|
||||
return strprintf("CAssetUnlockPayload(nVersion=%d,index=%d,fee=%d.%08d,requestedHeight=%d,quorumHash=%d,quorumSig=%s",
|
||||
nVersion, index, fee / COIN, fee % COIN, requestedHeight, quorumHash.GetHex(), quorumSig.ToString().c_str());
|
||||
}
|
173
src/evo/assetlocktx.h
Normal file
173
src/evo/assetlocktx.h
Normal file
@ -0,0 +1,173 @@
|
||||
// Copyright (c) 2023 The Dash Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef BITCOIN_EVO_ASSETLOCKTX_H
|
||||
#define BITCOIN_EVO_ASSETLOCKTX_H
|
||||
|
||||
#include <bls/bls_ies.h>
|
||||
#include <evo/specialtx.h>
|
||||
#include <primitives/transaction.h>
|
||||
|
||||
#include <key_io.h>
|
||||
#include <serialize.h>
|
||||
#include <tinyformat.h>
|
||||
#include <univalue.h>
|
||||
|
||||
class CBlockIndex;
|
||||
struct CCreditPool;
|
||||
|
||||
class CAssetLockPayload
|
||||
{
|
||||
public:
|
||||
static constexpr uint8_t CURRENT_VERSION = 1;
|
||||
static constexpr auto SPECIALTX_TYPE = TRANSACTION_ASSET_LOCK;
|
||||
|
||||
private:
|
||||
uint8_t nVersion{CURRENT_VERSION};
|
||||
std::vector<CTxOut> creditOutputs;
|
||||
|
||||
public:
|
||||
explicit CAssetLockPayload(const std::vector<CTxOut>& creditOutputs) :
|
||||
creditOutputs(creditOutputs)
|
||||
{}
|
||||
|
||||
CAssetLockPayload() = default;
|
||||
|
||||
SERIALIZE_METHODS(CAssetLockPayload, obj)
|
||||
{
|
||||
READWRITE(
|
||||
obj.nVersion,
|
||||
obj.creditOutputs
|
||||
);
|
||||
}
|
||||
|
||||
std::string ToString() const;
|
||||
|
||||
void ToJson(UniValue& obj) const
|
||||
{
|
||||
obj.clear();
|
||||
obj.setObject();
|
||||
obj.pushKV("version", int(nVersion));
|
||||
UniValue outputs;
|
||||
outputs.setArray();
|
||||
for (const CTxOut& out : creditOutputs) {
|
||||
outputs.push_back(out.ToString());
|
||||
}
|
||||
obj.pushKV("creditOutputs", outputs);
|
||||
}
|
||||
|
||||
// getters
|
||||
uint8_t getVersion() const
|
||||
{
|
||||
return nVersion;
|
||||
}
|
||||
|
||||
const std::vector<CTxOut>& getCreditOutputs() const
|
||||
{
|
||||
return creditOutputs;
|
||||
}
|
||||
};
|
||||
|
||||
class CAssetUnlockPayload
|
||||
{
|
||||
public:
|
||||
static constexpr uint8_t CURRENT_VERSION = 1;
|
||||
static constexpr auto SPECIALTX_TYPE = TRANSACTION_ASSET_UNLOCK;
|
||||
|
||||
static constexpr size_t MAXIMUM_WITHDRAWALS = 32;
|
||||
|
||||
private:
|
||||
uint8_t nVersion{CURRENT_VERSION};
|
||||
uint64_t index{0};
|
||||
uint32_t fee{0};
|
||||
uint32_t requestedHeight{0};
|
||||
uint256 quorumHash{0};
|
||||
CBLSSignature quorumSig{};
|
||||
|
||||
public:
|
||||
CAssetUnlockPayload(uint8_t nVersion, uint64_t index, uint32_t fee, uint32_t requestedHeight,
|
||||
uint256 quorumHash, CBLSSignature quorumSig) :
|
||||
nVersion(nVersion),
|
||||
index(index),
|
||||
fee(fee),
|
||||
requestedHeight(requestedHeight),
|
||||
quorumHash(quorumHash),
|
||||
quorumSig(quorumSig)
|
||||
{}
|
||||
|
||||
CAssetUnlockPayload() = default;
|
||||
|
||||
SERIALIZE_METHODS(CAssetUnlockPayload, obj)
|
||||
{
|
||||
READWRITE(
|
||||
obj.nVersion,
|
||||
obj.index,
|
||||
obj.fee,
|
||||
obj.requestedHeight,
|
||||
obj.quorumHash,
|
||||
obj.quorumSig
|
||||
);
|
||||
}
|
||||
|
||||
std::string ToString() const;
|
||||
|
||||
void ToJson(UniValue& obj) const
|
||||
{
|
||||
obj.clear();
|
||||
obj.setObject();
|
||||
obj.pushKV("version", int(nVersion));
|
||||
obj.pushKV("index", int(index));
|
||||
obj.pushKV("fee", int(fee));
|
||||
obj.pushKV("requestedHeight", int(requestedHeight));
|
||||
obj.pushKV("quorumHash", quorumHash.ToString());
|
||||
obj.pushKV("quorumSig", quorumSig.ToString());
|
||||
}
|
||||
|
||||
bool VerifySig(const uint256& msgHash, const CBlockIndex* pindexTip, TxValidationState& state) const;
|
||||
|
||||
// getters
|
||||
uint8_t getVersion() const
|
||||
{
|
||||
return nVersion;
|
||||
}
|
||||
|
||||
uint64_t getIndex() const
|
||||
{
|
||||
return index;
|
||||
}
|
||||
|
||||
uint32_t getFee() const
|
||||
{
|
||||
return fee;
|
||||
}
|
||||
|
||||
uint32_t getRequestedHeight() const
|
||||
{
|
||||
return requestedHeight;
|
||||
}
|
||||
|
||||
const uint256& getQuorumHash() const
|
||||
{
|
||||
return quorumHash;
|
||||
}
|
||||
|
||||
const CBLSSignature& getQuorumSig() const
|
||||
{
|
||||
return quorumSig;
|
||||
}
|
||||
|
||||
// used by mempool to know when possible to drop a transaction as expired
|
||||
static constexpr int HEIGHT_DIFF_EXPIRING = 48;
|
||||
int getHeightToExpiry() const
|
||||
{
|
||||
return requestedHeight + HEIGHT_DIFF_EXPIRING;
|
||||
}
|
||||
};
|
||||
|
||||
bool CheckAssetLockTx(const CTransaction& tx, TxValidationState& state);
|
||||
bool CheckAssetUnlockTx(const CTransaction& tx, const CBlockIndex* pindexPrev, const CCreditPool& creditPool, TxValidationState& state);
|
||||
bool CheckAssetLockUnlockTx(const CTransaction& tx, const CBlockIndex* pindexPrev, const CCreditPool& creditPool, TxValidationState& state);
|
||||
bool GetAssetUnlockFee(const CTransaction& tx, CAmount& txfee, TxValidationState& state);
|
||||
|
||||
#endif // BITCOIN_EVO_ASSETLOCKTX_H
|
@ -36,11 +36,11 @@ bool CheckCbTx(const CTransaction& tx, const CBlockIndex* pindexPrev, TxValidati
|
||||
return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-cbtx-version");
|
||||
}
|
||||
|
||||
if (pindexPrev && pindexPrev->nHeight + 1 != cbTx.nHeight) {
|
||||
return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-cbtx-height");
|
||||
}
|
||||
|
||||
if (pindexPrev) {
|
||||
if (pindexPrev->nHeight + 1 != cbTx.nHeight) {
|
||||
return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-cbtx-height");
|
||||
}
|
||||
|
||||
bool fDIP0008Active = pindexPrev->nHeight >= Params().GetConsensus().DIP0008Height;
|
||||
if (fDIP0008Active && cbTx.nVersion < CCbTx::CB_V19_VERSION) {
|
||||
return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-cbtx-version");
|
||||
@ -431,8 +431,9 @@ bool CalcCbTxBestChainlock(const llmq::CChainLocksHandler& chainlock_handler, co
|
||||
|
||||
std::string CCbTx::ToString() const
|
||||
{
|
||||
return strprintf("CCbTx(nVersion=%d, nHeight=%d, merkleRootMNList=%s, merkleRootQuorums=%s, bestCLHeightDiff=%d, bestCLSig=%s)",
|
||||
nVersion, nHeight, merkleRootMNList.ToString(), merkleRootQuorums.ToString(), bestCLHeightDiff, bestCLSignature.ToString());
|
||||
return strprintf("CCbTx(nVersion=%d, nHeight=%d, merkleRootMNList=%s, merkleRootQuorums=%s, bestCLHeightDiff=%d, bestCLSig=%s, assetLockedAmount=%d.%08d)",
|
||||
nVersion, nHeight, merkleRootMNList.ToString(), merkleRootQuorums.ToString(), bestCLHeightDiff, bestCLSignature.ToString(),
|
||||
assetLockedAmount / COIN, assetLockedAmount % COIN);
|
||||
}
|
||||
|
||||
std::optional<CCbTx> GetCoinbaseTx(const CBlockIndex* pindex)
|
||||
|
@ -20,6 +20,9 @@ class CQuorumBlockProcessor;
|
||||
class CChainLocksHandler;
|
||||
}// namespace llmq
|
||||
|
||||
// Forward declaration from core_io to get rid of circular dependency
|
||||
UniValue ValueFromAmount(const CAmount& amount);
|
||||
|
||||
// coinbase transaction
|
||||
class CCbTx
|
||||
{
|
||||
@ -34,6 +37,7 @@ public:
|
||||
uint256 merkleRootQuorums;
|
||||
uint32_t bestCLHeightDiff;
|
||||
CBLSSignature bestCLSignature;
|
||||
CAmount assetLockedAmount{0};
|
||||
|
||||
SERIALIZE_METHODS(CCbTx, obj)
|
||||
{
|
||||
@ -44,8 +48,10 @@ public:
|
||||
if (obj.nVersion >= CB_V20_VERSION) {
|
||||
READWRITE(COMPACTSIZE(obj.bestCLHeightDiff));
|
||||
READWRITE(obj.bestCLSignature);
|
||||
READWRITE(obj.assetLockedAmount);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
std::string ToString() const;
|
||||
@ -62,6 +68,7 @@ public:
|
||||
if (nVersion >= CB_V20_VERSION) {
|
||||
obj.pushKV("bestCLHeightDiff", static_cast<int>(bestCLHeightDiff));
|
||||
obj.pushKV("bestCLSignature", bestCLSignature.ToString());
|
||||
obj.pushKV("assetLockedAmount", ValueFromAmount(assetLockedAmount));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
302
src/evo/creditpool.cpp
Normal file
302
src/evo/creditpool.cpp
Normal file
@ -0,0 +1,302 @@
|
||||
// Copyright (c) 2023 The Dash Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <evo/creditpool.h>
|
||||
|
||||
#include <evo/assetlocktx.h>
|
||||
#include <evo/cbtx.h>
|
||||
|
||||
#include <llmq/utils.h>
|
||||
|
||||
#include <chain.h>
|
||||
#include <logging.h>
|
||||
#include <validation.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <exception>
|
||||
#include <memory>
|
||||
#include <stack>
|
||||
|
||||
static const std::string DB_CREDITPOOL_SNAPSHOT = "cpm_S";
|
||||
|
||||
std::unique_ptr<CCreditPoolManager> creditPoolManager;
|
||||
|
||||
static bool GetDataFromUnlockTx(const CTransaction& tx, CAmount& toUnlock, uint64_t& index, TxValidationState& state)
|
||||
{
|
||||
CAssetUnlockPayload assetUnlockTx;
|
||||
if (!GetTxPayload(tx, assetUnlockTx)) {
|
||||
return state.Invalid(TxValidationResult::TX_CONSENSUS, "failed-creditpool-unlock-payload");
|
||||
}
|
||||
|
||||
index = assetUnlockTx.getIndex();
|
||||
toUnlock = assetUnlockTx.getFee();
|
||||
for (const CTxOut& txout : tx.vout) {
|
||||
if (!MoneyRange(txout.nValue)) {
|
||||
return state.Invalid(TxValidationResult::TX_CONSENSUS, "failed-creditpool-unlock-txout-outofrange");
|
||||
}
|
||||
toUnlock += txout.nValue;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace {
|
||||
struct UnlockDataPerBlock {
|
||||
CAmount unlocked{0};
|
||||
std::unordered_set<uint64_t> indexes;
|
||||
};
|
||||
} // anonymous namespace
|
||||
|
||||
// it throws exception if anything went wrong
|
||||
static UnlockDataPerBlock GetDataFromUnlockTxes(const std::vector<CTransactionRef>& vtx)
|
||||
{
|
||||
UnlockDataPerBlock blockData;
|
||||
|
||||
for (CTransactionRef tx : vtx) {
|
||||
if (tx->nVersion != 3 || tx->nType != TRANSACTION_ASSET_UNLOCK) continue;
|
||||
|
||||
CAmount unlocked{0};
|
||||
TxValidationState tx_state;
|
||||
uint64_t index{0};
|
||||
if (!GetDataFromUnlockTx(*tx, unlocked, index, tx_state)) {
|
||||
throw std::runtime_error(strprintf("%s: CCreditPoolManager::GetDataFromUnlockTxes failed: %s", __func__, tx_state.ToString()));
|
||||
}
|
||||
blockData.unlocked += unlocked;
|
||||
blockData.indexes.insert(index);
|
||||
}
|
||||
return blockData;
|
||||
}
|
||||
|
||||
std::string CCreditPool::ToString() const
|
||||
{
|
||||
return strprintf("CCreditPool(locked=%lld, currentLimit=%lld, nIndexes=%lld)",
|
||||
locked, currentLimit, indexes.Size());
|
||||
}
|
||||
|
||||
std::optional<CCreditPool> CCreditPoolManager::GetFromCache(const CBlockIndex* const block_index)
|
||||
{
|
||||
if (!llmq::utils::IsV20Active(block_index)) return CCreditPool{};
|
||||
|
||||
const uint256 block_hash = block_index->GetBlockHash();
|
||||
CCreditPool pool;
|
||||
{
|
||||
LOCK(cache_mutex);
|
||||
if (creditPoolCache.get(block_hash, pool)) {
|
||||
return pool;
|
||||
}
|
||||
}
|
||||
if (block_index->nHeight % DISK_SNAPSHOT_PERIOD == 0) {
|
||||
if (evoDb.Read(std::make_pair(DB_CREDITPOOL_SNAPSHOT, block_hash), pool)) {
|
||||
LOCK(cache_mutex);
|
||||
creditPoolCache.insert(block_hash, pool);
|
||||
return pool;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void CCreditPoolManager::AddToCache(const uint256& block_hash, int height, const CCreditPool &pool)
|
||||
{
|
||||
{
|
||||
LOCK(cache_mutex);
|
||||
creditPoolCache.insert(block_hash, pool);
|
||||
}
|
||||
if (height % DISK_SNAPSHOT_PERIOD == 0) {
|
||||
evoDb.Write(std::make_pair(DB_CREDITPOOL_SNAPSHOT, block_hash), pool);
|
||||
}
|
||||
}
|
||||
|
||||
static std::optional<CBlock> GetBlockForCreditPool(const CBlockIndex* const block_index, const Consensus::Params& consensusParams)
|
||||
{
|
||||
CBlock block;
|
||||
if (!ReadBlockFromDisk(block, block_index, consensusParams)) {
|
||||
throw std::runtime_error("failed-getcbforblock-read");
|
||||
}
|
||||
// Should not fail if V20 (DIP0027) is active but it happens for RegChain (unit tests)
|
||||
if (block.vtx[0]->nVersion != 3) return std::nullopt;
|
||||
|
||||
assert(!block.vtx.empty());
|
||||
assert(block.vtx[0]->nVersion == 3);
|
||||
assert(!block.vtx[0]->vExtraPayload.empty());
|
||||
|
||||
return block;
|
||||
}
|
||||
|
||||
CCreditPool CCreditPoolManager::ConstructCreditPool(const CBlockIndex* const block_index, CCreditPool prev, const Consensus::Params& consensusParams)
|
||||
{
|
||||
std::optional<CBlock> block = GetBlockForCreditPool(block_index, consensusParams);
|
||||
if (!block) {
|
||||
// If reading of previous block is not successfully, but
|
||||
// prev contains credit pool related data, something strange happened
|
||||
assert(prev.locked == 0);
|
||||
assert(prev.indexes.Size() == 0);
|
||||
|
||||
CCreditPool emptyPool;
|
||||
AddToCache(block_index->GetBlockHash(), block_index->nHeight, emptyPool);
|
||||
return emptyPool;
|
||||
}
|
||||
CAmount locked{0};
|
||||
{
|
||||
CCbTx cbTx;
|
||||
if (!GetTxPayload(block->vtx[0]->vExtraPayload, cbTx)) {
|
||||
throw std::runtime_error(strprintf("%s: failed-getcreditpool-cbtx-payload", __func__));
|
||||
}
|
||||
locked = cbTx.assetLockedAmount;
|
||||
}
|
||||
|
||||
// We use here sliding window with LimitBlocksToTrace to determine
|
||||
// current limits for asset unlock transactions.
|
||||
// Indexes should not be duplicated since genesis block, but the Unlock Amount
|
||||
// of withdrawal transaction is limited only by this window
|
||||
UnlockDataPerBlock blockData = GetDataFromUnlockTxes(block->vtx);
|
||||
CSkipSet indexes{std::move(prev.indexes)};
|
||||
if (std::any_of(blockData.indexes.begin(), blockData.indexes.end(), [&](const uint64_t index) { return !indexes.Add(index); })) {
|
||||
throw std::runtime_error(strprintf("%s: failed-getcreditpool-index-exceed", __func__));
|
||||
}
|
||||
|
||||
const CBlockIndex* distant_block_index = block_index;
|
||||
for (size_t i = 0; i < CCreditPoolManager::LimitBlocksToTrace; ++i) {
|
||||
distant_block_index = distant_block_index->pprev;
|
||||
if (distant_block_index == nullptr) break;
|
||||
}
|
||||
CAmount distantUnlocked{0};
|
||||
if (distant_block_index) {
|
||||
if (std::optional<CBlock> distant_block = GetBlockForCreditPool(distant_block_index, consensusParams); distant_block) {
|
||||
distantUnlocked = GetDataFromUnlockTxes(distant_block->vtx).unlocked;
|
||||
}
|
||||
}
|
||||
|
||||
// Unlock limits are # max(100, min(.10 * assetlockpool, 1000)) inside window
|
||||
CAmount currentLimit = locked;
|
||||
const CAmount latelyUnlocked = prev.latelyUnlocked + blockData.unlocked - distantUnlocked;
|
||||
if (currentLimit + latelyUnlocked > LimitAmountLow) {
|
||||
currentLimit = std::max(LimitAmountLow, locked / 10) - latelyUnlocked;
|
||||
if (currentLimit < 0) currentLimit = 0;
|
||||
}
|
||||
currentLimit = std::min(currentLimit, LimitAmountHigh - latelyUnlocked);
|
||||
|
||||
assert(currentLimit >= 0);
|
||||
|
||||
if (currentLimit > 0 || latelyUnlocked > 0 || locked > 0) {
|
||||
LogPrintf("CCreditPoolManager: asset unlock limits on height: %d locked: %d.%08d limit: %d.%08d previous: %d.%08d\n", block_index->nHeight, locked / COIN, locked % COIN,
|
||||
currentLimit / COIN, currentLimit % COIN,
|
||||
latelyUnlocked / COIN, latelyUnlocked % COIN);
|
||||
}
|
||||
|
||||
CCreditPool pool{locked, currentLimit, latelyUnlocked, indexes};
|
||||
AddToCache(block_index->GetBlockHash(), block_index->nHeight, pool);
|
||||
return pool;
|
||||
|
||||
}
|
||||
|
||||
CCreditPool CCreditPoolManager::GetCreditPool(const CBlockIndex* block_index, const Consensus::Params& consensusParams)
|
||||
{
|
||||
std::stack<const CBlockIndex *> to_calculate;
|
||||
|
||||
std::optional<CCreditPool> poolTmp;
|
||||
while (!(poolTmp = GetFromCache(block_index)).has_value()) {
|
||||
to_calculate.push(block_index);
|
||||
block_index = block_index->pprev;
|
||||
}
|
||||
while (!to_calculate.empty()) {
|
||||
poolTmp = ConstructCreditPool(to_calculate.top(), *poolTmp, consensusParams);
|
||||
to_calculate.pop();
|
||||
}
|
||||
return *poolTmp;
|
||||
}
|
||||
|
||||
CCreditPoolManager::CCreditPoolManager(CEvoDB& _evoDb)
|
||||
: evoDb(_evoDb)
|
||||
{
|
||||
}
|
||||
|
||||
CCreditPoolDiff::CCreditPoolDiff(CCreditPool starter, const CBlockIndex *pindex, const Consensus::Params& consensusParams) :
|
||||
pool(std::move(starter)),
|
||||
pindex(pindex)
|
||||
{
|
||||
assert(pindex);
|
||||
}
|
||||
|
||||
bool CCreditPoolDiff::SetTarget(const CTransaction& tx, TxValidationState& state)
|
||||
{
|
||||
CCbTx cbTx;
|
||||
if (!GetTxPayload(tx, cbTx)) {
|
||||
return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-cbtx-payload");
|
||||
}
|
||||
|
||||
if (cbTx.nVersion == 3) {
|
||||
targetLocked = cbTx.assetLockedAmount;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CCreditPoolDiff::Lock(const CTransaction& tx, TxValidationState& state)
|
||||
{
|
||||
CAssetLockPayload assetLockTx;
|
||||
if (!GetTxPayload(tx, assetLockTx)) {
|
||||
return state.Invalid(TxValidationResult::TX_CONSENSUS, "failed-creditpool-lock-payload");
|
||||
}
|
||||
|
||||
for (const CTxOut& txout : tx.vout) {
|
||||
const CScript& script = txout.scriptPubKey;
|
||||
if (script.empty() || script[0] != OP_RETURN) continue;
|
||||
|
||||
sessionLocked += txout.nValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
return state.Invalid(TxValidationResult::TX_CONSENSUS, "failed-creditpool-lock-invalid");
|
||||
}
|
||||
|
||||
bool CCreditPoolDiff::Unlock(const CTransaction& tx, TxValidationState& state)
|
||||
{
|
||||
uint64_t index{0};
|
||||
CAmount toUnlock{0};
|
||||
if (!GetDataFromUnlockTx(tx, toUnlock, index, state)) {
|
||||
// state is set up inside GetDataFromUnlockTx
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sessionUnlocked + toUnlock > pool.currentLimit) {
|
||||
return state.Invalid(TxValidationResult::TX_CONSENSUS, "failed-creditpool-unlock-too-much");
|
||||
}
|
||||
|
||||
if (pool.indexes.Contains(index) || newIndexes.count(index)) {
|
||||
return state.Invalid(TxValidationResult::TX_CONSENSUS, "failed-creditpool-unlock-duplicated-index");
|
||||
}
|
||||
|
||||
if (!pool.indexes.CanBeAdded(index)) {
|
||||
return state.Invalid(TxValidationResult::TX_CONSENSUS, "failed-creditpool-unlock-cant-add");
|
||||
}
|
||||
|
||||
newIndexes.insert(index);
|
||||
sessionUnlocked += toUnlock;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CCreditPoolDiff::ProcessTransaction(const CTransaction& tx, TxValidationState& state)
|
||||
{
|
||||
if (tx.nVersion != 3) return true;
|
||||
if (tx.nType == TRANSACTION_COINBASE) return SetTarget(tx, state);
|
||||
|
||||
if (tx.nType != TRANSACTION_ASSET_LOCK && tx.nType != TRANSACTION_ASSET_UNLOCK) return true;
|
||||
|
||||
if (!CheckAssetLockUnlockTx(tx, pindex, this->pool, state)) {
|
||||
// pass the state returned by the function above
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
switch (tx.nType) {
|
||||
case TRANSACTION_ASSET_LOCK:
|
||||
return Lock(tx, state);
|
||||
case TRANSACTION_ASSET_UNLOCK:
|
||||
return Unlock(tx, state);
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
LogPrintf("%s -- failed: %s\n", __func__, e.what());
|
||||
return state.Invalid(TxValidationResult::TX_CONSENSUS, "failed-procassetlocksinblock");
|
||||
}
|
||||
}
|
139
src/evo/creditpool.h
Normal file
139
src/evo/creditpool.h
Normal file
@ -0,0 +1,139 @@
|
||||
// Copyright (c) 2023 The Dash Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef BITCOIN_EVO_CREDITPOOL_H
|
||||
#define BITCOIN_EVO_CREDITPOOL_H
|
||||
|
||||
#include <coins.h>
|
||||
|
||||
#include <evo/assetlocktx.h>
|
||||
#include <evo/evodb.h>
|
||||
|
||||
#include <saltedhasher.h>
|
||||
#include <serialize.h>
|
||||
#include <sync.h>
|
||||
#include <threadsafety.h>
|
||||
#include <unordered_lru_cache.h>
|
||||
#include <util/skip_set.h>
|
||||
|
||||
#include <optional>
|
||||
#include <unordered_set>
|
||||
|
||||
class CBlockIndex;
|
||||
class TxValidationState;
|
||||
|
||||
namespace Consensus
|
||||
{
|
||||
struct Params;
|
||||
}
|
||||
|
||||
struct CCreditPool {
|
||||
CAmount locked{0};
|
||||
|
||||
// needs for logic of limits of unlocks
|
||||
CAmount currentLimit{0};
|
||||
CAmount latelyUnlocked{0};
|
||||
CSkipSet indexes{};
|
||||
|
||||
std::string ToString() const;
|
||||
|
||||
SERIALIZE_METHODS(CCreditPool, obj)
|
||||
{
|
||||
READWRITE(
|
||||
obj.locked,
|
||||
obj.currentLimit,
|
||||
obj.latelyUnlocked,
|
||||
obj.indexes
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The class CCreditPoolDiff has 2 purposes:
|
||||
* - it helps to determine which transaction can be included in new mined block
|
||||
* within current limits for Asset Unlock transactions and filter duplicated indexes
|
||||
* - to validate Asset Unlock transaction in mined block. The standalone checks of tx
|
||||
* such as CheckSpecialTx is not able to do so because at that moment there is no full
|
||||
* information about Credit Pool limits.
|
||||
*
|
||||
* CCreditPoolDiff temporary stores new values `lockedAmount` and `indexes` while
|
||||
* limits should stay same and depends only on the previous block.
|
||||
*/
|
||||
class CCreditPoolDiff {
|
||||
private:
|
||||
const CCreditPool pool;
|
||||
std::unordered_set<uint64_t> newIndexes;
|
||||
|
||||
CAmount sessionLocked{0};
|
||||
CAmount sessionUnlocked{0};
|
||||
|
||||
// target value is used to validate CbTx. If values mismatched, block is invalid
|
||||
std::optional<CAmount> targetLocked;
|
||||
|
||||
const CBlockIndex *pindex{nullptr};
|
||||
public:
|
||||
explicit CCreditPoolDiff(CCreditPool starter, const CBlockIndex *pindex, const Consensus::Params& consensusParams);
|
||||
|
||||
/**
|
||||
* This function should be called for each Asset Lock/Unlock tx
|
||||
* to change amount of credit pool
|
||||
* @return true if transaction can be included in this block
|
||||
*/
|
||||
bool ProcessTransaction(const CTransaction& tx, TxValidationState& state);
|
||||
|
||||
CAmount GetTotalLocked() const {
|
||||
return pool.locked + sessionLocked - sessionUnlocked;
|
||||
}
|
||||
|
||||
const std::optional<CAmount>& GetTargetLocked() const {
|
||||
return targetLocked;
|
||||
}
|
||||
|
||||
std::string ToString() const {
|
||||
return strprintf("CCreditPoolDiff(target=%lld, sessionLocked=%lld, sessionUnlocked=%lld, newIndexes=%lld, pool=%s)", GetTargetLocked() ? *GetTargetLocked() : -1, sessionLocked, sessionUnlocked, newIndexes.size(), pool.ToString());
|
||||
}
|
||||
|
||||
private:
|
||||
bool SetTarget(const CTransaction& tx, TxValidationState& state);
|
||||
bool Lock(const CTransaction& tx, TxValidationState& state);
|
||||
bool Unlock(const CTransaction& tx, TxValidationState& state);
|
||||
};
|
||||
|
||||
class CCreditPoolManager
|
||||
{
|
||||
private:
|
||||
static constexpr size_t CreditPoolCacheSize = 1000;
|
||||
RecursiveMutex cache_mutex;
|
||||
unordered_lru_cache<uint256, CCreditPool, StaticSaltedHasher> creditPoolCache GUARDED_BY(cache_mutex) {CreditPoolCacheSize};
|
||||
|
||||
CEvoDB& evoDb;
|
||||
|
||||
static constexpr int DISK_SNAPSHOT_PERIOD = 576; // once per day
|
||||
|
||||
public:
|
||||
static constexpr int LimitBlocksToTrace = 576;
|
||||
static constexpr CAmount LimitAmountLow = 100 * COIN;
|
||||
static constexpr CAmount LimitAmountHigh = 1000 * COIN;
|
||||
|
||||
explicit CCreditPoolManager(CEvoDB& _evoDb);
|
||||
|
||||
~CCreditPoolManager() = default;
|
||||
|
||||
/**
|
||||
* @return CCreditPool with data or with empty depends on activation V19 at that block
|
||||
* In case if block is invalid the function GetCreditPool throws an exception
|
||||
* it can happen if there limits of withdrawal (unlock) exceed
|
||||
*/
|
||||
CCreditPool GetCreditPool(const CBlockIndex* block, const Consensus::Params& consensusParams);
|
||||
|
||||
private:
|
||||
std::optional<CCreditPool> GetFromCache(const CBlockIndex* const block_index);
|
||||
void AddToCache(const uint256& block_hash, int height, const CCreditPool& pool);
|
||||
|
||||
CCreditPool ConstructCreditPool(const CBlockIndex* block_index, CCreditPool prev, const Consensus::Params& consensusParams);
|
||||
};
|
||||
|
||||
extern std::unique_ptr<CCreditPoolManager> creditPoolManager;
|
||||
|
||||
#endif
|
@ -7,9 +7,11 @@
|
||||
#include <chainparams.h>
|
||||
#include <consensus/validation.h>
|
||||
#include <evo/cbtx.h>
|
||||
#include <evo/creditpool.h>
|
||||
#include <evo/deterministicmns.h>
|
||||
#include <evo/mnhftx.h>
|
||||
#include <evo/providertx.h>
|
||||
#include <evo/assetlocktx.h>
|
||||
#include <hash.h>
|
||||
#include <llmq/blockprocessor.h>
|
||||
#include <llmq/commitment.h>
|
||||
@ -17,7 +19,7 @@
|
||||
#include <primitives/block.h>
|
||||
#include <validation.h>
|
||||
|
||||
bool CheckSpecialTx(const CTransaction& tx, const CBlockIndex* pindexPrev, TxValidationState& state, const CCoinsViewCache& view, bool check_sigs)
|
||||
bool CheckSpecialTx(const CTransaction& tx, const CBlockIndex* pindexPrev, const CCoinsViewCache& view, const CCreditPool& creditPool, bool check_sigs, TxValidationState& state)
|
||||
{
|
||||
AssertLockHeld(cs_main);
|
||||
|
||||
@ -44,6 +46,12 @@ bool CheckSpecialTx(const CTransaction& tx, const CBlockIndex* pindexPrev, TxVal
|
||||
return llmq::CheckLLMQCommitment(tx, pindexPrev, state);
|
||||
case TRANSACTION_MNHF_SIGNAL:
|
||||
return pindexPrev->nHeight + 1 >= Params().GetConsensus().DIP0024Height && CheckMNHFTx(tx, pindexPrev, state);
|
||||
case TRANSACTION_ASSET_LOCK:
|
||||
case TRANSACTION_ASSET_UNLOCK:
|
||||
if (!llmq::utils::IsV20Active(pindexPrev)) {
|
||||
return state.Invalid(TxValidationResult::TX_CONSENSUS, "assetlocks-before-v20");
|
||||
}
|
||||
return CheckAssetLockUnlockTx(tx, pindexPrev, creditPool, state);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
LogPrintf("%s -- failed: %s\n", __func__, e.what());
|
||||
@ -60,6 +68,9 @@ bool ProcessSpecialTx(const CTransaction& tx, const CBlockIndex* pindex, TxValid
|
||||
}
|
||||
|
||||
switch (tx.nType) {
|
||||
case TRANSACTION_ASSET_LOCK:
|
||||
case TRANSACTION_ASSET_UNLOCK:
|
||||
return true; // handled per block (during cb)
|
||||
case TRANSACTION_PROVIDER_REGISTER:
|
||||
case TRANSACTION_PROVIDER_UPDATE_SERVICE:
|
||||
case TRANSACTION_PROVIDER_UPDATE_REGISTRAR:
|
||||
@ -83,6 +94,9 @@ bool UndoSpecialTx(const CTransaction& tx, const CBlockIndex* pindex)
|
||||
}
|
||||
|
||||
switch (tx.nType) {
|
||||
case TRANSACTION_ASSET_LOCK:
|
||||
case TRANSACTION_ASSET_UNLOCK:
|
||||
return true; // handled per block (during cb)
|
||||
case TRANSACTION_PROVIDER_REGISTER:
|
||||
case TRANSACTION_PROVIDER_UPDATE_SERVICE:
|
||||
case TRANSACTION_PROVIDER_UPDATE_REGISTRAR:
|
||||
@ -100,7 +114,8 @@ bool UndoSpecialTx(const CTransaction& tx, const CBlockIndex* pindex)
|
||||
}
|
||||
|
||||
bool ProcessSpecialTxsInBlock(const CBlock& block, const CBlockIndex* pindex, llmq::CQuorumBlockProcessor& quorum_block_processor, const llmq::CChainLocksHandler& chainlock_handler,
|
||||
BlockValidationState& state, const CCoinsViewCache& view, bool fJustCheck, bool fCheckCbTxMerleRoots)
|
||||
const Consensus::Params& consensusParams, const CCoinsViewCache& view, bool fJustCheck, bool fCheckCbTxMerleRoots,
|
||||
BlockValidationState& state)
|
||||
{
|
||||
AssertLockHeld(cs_main);
|
||||
|
||||
@ -113,11 +128,18 @@ bool ProcessSpecialTxsInBlock(const CBlock& block, const CBlockIndex* pindex, ll
|
||||
|
||||
int64_t nTime1 = GetTimeMicros();
|
||||
|
||||
const CCreditPool creditPool = creditPoolManager->GetCreditPool(pindex->pprev, consensusParams);
|
||||
std::optional<CCreditPoolDiff> creditPoolDiff;
|
||||
if (bool fV20Active_context = llmq::utils::IsV20Active(pindex->pprev); fV20Active_context) {
|
||||
LogPrintf("%s: CCreditPool is %s\n", __func__, creditPool.ToString());
|
||||
creditPoolDiff.emplace(creditPool, pindex->pprev, consensusParams);
|
||||
}
|
||||
|
||||
for (const auto& ptr_tx : block.vtx) {
|
||||
TxValidationState tx_state;
|
||||
// At this moment CheckSpecialTx() and ProcessSpecialTx() may fail by 2 possible ways:
|
||||
// consensus failures and "TX_BAD_SPECIAL"
|
||||
if (!CheckSpecialTx(*ptr_tx, pindex->pprev, tx_state, view, fCheckCbTxMerleRoots)) {
|
||||
if (!CheckSpecialTx(*ptr_tx, pindex->pprev, view, creditPool, fCheckCbTxMerleRoots, tx_state)) {
|
||||
assert(tx_state.GetResult() == TxValidationResult::TX_CONSENSUS || tx_state.GetResult() == TxValidationResult::TX_BAD_SPECIAL);
|
||||
return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, tx_state.GetRejectReason(),
|
||||
strprintf("Special Transaction check failed (tx hash %s) %s", ptr_tx->GetHash().ToString(), tx_state.GetDebugMessage()));
|
||||
@ -127,6 +149,21 @@ bool ProcessSpecialTxsInBlock(const CBlock& block, const CBlockIndex* pindex, ll
|
||||
return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, tx_state.GetRejectReason(),
|
||||
strprintf("Process Special Transaction failed (tx hash %s) %s", ptr_tx->GetHash().ToString(), tx_state.GetDebugMessage()));
|
||||
}
|
||||
if (creditPoolDiff != std::nullopt && !creditPoolDiff->ProcessTransaction(*ptr_tx, tx_state)) {
|
||||
assert(tx_state.GetResult() == TxValidationResult::TX_CONSENSUS || tx_state.GetResult() == TxValidationResult::TX_BAD_SPECIAL);
|
||||
return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, tx_state.GetRejectReason(),
|
||||
strprintf("Process Special Transaction failed at Credit Pool (tx hash %s) %s", ptr_tx->GetHash().ToString(), tx_state.GetDebugMessage()));
|
||||
}
|
||||
}
|
||||
if (creditPoolDiff != std::nullopt) {
|
||||
CAmount locked_proposed{0};
|
||||
if(creditPoolDiff->GetTargetLocked()) locked_proposed = *creditPoolDiff->GetTargetLocked();
|
||||
|
||||
CAmount locked_calculated = creditPoolDiff->GetTotalLocked();
|
||||
if (locked_proposed != locked_calculated) {
|
||||
LogPrintf("%s: mismatched locked amount in CbTx: %lld against re-calculated: %lld\n", __func__, locked_proposed, locked_calculated);
|
||||
return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-cbtx-assetlocked-amount");
|
||||
}
|
||||
}
|
||||
|
||||
int64_t nTime2 = GetTimeMicros();
|
||||
|
@ -12,18 +12,24 @@
|
||||
class BlockValidationState;
|
||||
class CBlock;
|
||||
class CBlockIndex;
|
||||
struct CCreditPool;
|
||||
class CCoinsViewCache;
|
||||
class TxValidationState;
|
||||
namespace llmq {
|
||||
class CQuorumBlockProcessor;
|
||||
class CChainLocksHandler;
|
||||
} // namespace llmq
|
||||
namespace Consensus {
|
||||
struct Params;
|
||||
} // namespace Consensus
|
||||
|
||||
extern RecursiveMutex cs_main;
|
||||
|
||||
bool CheckSpecialTx(const CTransaction& tx, const CBlockIndex* pindexPrev, TxValidationState& state, const CCoinsViewCache& view, bool check_sigs) EXCLUSIVE_LOCKS_REQUIRED(cs_main);
|
||||
bool CheckSpecialTx(const CTransaction& tx, const CBlockIndex* pindexPrev, const CCoinsViewCache& view, const CCreditPool& pool, bool check_sigs,
|
||||
TxValidationState& state) EXCLUSIVE_LOCKS_REQUIRED(cs_main);
|
||||
bool ProcessSpecialTxsInBlock(const CBlock& block, const CBlockIndex* pindex, llmq::CQuorumBlockProcessor& quorum_block_processor, const llmq::CChainLocksHandler& chainlock_handler,
|
||||
BlockValidationState& state, const CCoinsViewCache& view, bool fJustCheck, bool fCheckCbTxMerleRoots) EXCLUSIVE_LOCKS_REQUIRED(cs_main);
|
||||
const Consensus::Params& consensusParams, const CCoinsViewCache& view, bool fJustCheck, bool fCheckCbTxMerleRoots,
|
||||
BlockValidationState& state) EXCLUSIVE_LOCKS_REQUIRED(cs_main);
|
||||
bool UndoSpecialTxsInBlock(const CBlock& block, const CBlockIndex* pindex, llmq::CQuorumBlockProcessor& quorum_block_processor) EXCLUSIVE_LOCKS_REQUIRED(cs_main);
|
||||
|
||||
#endif // BITCOIN_EVO_SPECIALTXMAN_H
|
||||
|
@ -82,6 +82,7 @@
|
||||
#include <spork.h>
|
||||
#include <walletinitinterface.h>
|
||||
|
||||
#include <evo/creditpool.h>
|
||||
#include <evo/deterministicmns.h>
|
||||
#include <llmq/blockprocessor.h>
|
||||
#include <llmq/chainlocks.h>
|
||||
@ -352,6 +353,7 @@ void PrepareShutdown(NodeContext& node)
|
||||
}
|
||||
llmq::quorumSnapshotManager.reset();
|
||||
deterministicMNManager.reset();
|
||||
creditPoolManager.reset();
|
||||
node.evodb.reset();
|
||||
}
|
||||
for (const auto& client : node.chain_clients) {
|
||||
@ -2031,6 +2033,7 @@ bool AppInitMain(const CoreContext& context, NodeContext& node, interfaces::Bloc
|
||||
// Same logic as above with pblocktree
|
||||
deterministicMNManager.reset();
|
||||
deterministicMNManager.reset(new CDeterministicMNManager(*node.evodb, *node.connman));
|
||||
creditPoolManager.reset(new CCreditPoolManager(*node.evodb));
|
||||
llmq::quorumSnapshotManager.reset();
|
||||
llmq::quorumSnapshotManager.reset(new llmq::CQuorumSnapshotManager(*node.evodb));
|
||||
node.llmq_ctx.reset();
|
||||
|
@ -44,6 +44,8 @@ CMerkleBlock::CMerkleBlock(const CBlock& block, CBloomFilter* filter, const std:
|
||||
TRANSACTION_PROVIDER_UPDATE_REGISTRAR,
|
||||
TRANSACTION_PROVIDER_UPDATE_REVOKE,
|
||||
TRANSACTION_COINBASE,
|
||||
TRANSACTION_ASSET_LOCK,
|
||||
TRANSACTION_ASSET_UNLOCK,
|
||||
};
|
||||
|
||||
for (unsigned int i = 0; i < block.vtx.size(); i++)
|
||||
|
@ -23,6 +23,7 @@
|
||||
|
||||
#include <evo/specialtx.h>
|
||||
#include <evo/cbtx.h>
|
||||
#include <evo/creditpool.h>
|
||||
#include <evo/simplifiedmns.h>
|
||||
#include <governance/governance.h>
|
||||
#include <llmq/blockprocessor.h>
|
||||
@ -133,6 +134,7 @@ std::unique_ptr<CBlockTemplate> BlockAssembler::CreateNewBlock(const CScript& sc
|
||||
|
||||
bool fDIP0003Active_context = nHeight >= chainparams.GetConsensus().DIP0003Height;
|
||||
bool fDIP0008Active_context = nHeight >= chainparams.GetConsensus().DIP0008Height;
|
||||
bool fV20Active_context = llmq::utils::IsV20Active(pindexPrev);
|
||||
|
||||
pblock->nVersion = ComputeBlockVersion(pindexPrev, chainparams.GetConsensus());
|
||||
// Non-mainnet only: allow overriding block.nVersion with
|
||||
@ -166,7 +168,14 @@ std::unique_ptr<CBlockTemplate> BlockAssembler::CreateNewBlock(const CScript& sc
|
||||
|
||||
int nPackagesSelected = 0;
|
||||
int nDescendantsUpdated = 0;
|
||||
addPackageTxs(nPackagesSelected, nDescendantsUpdated);
|
||||
|
||||
std::optional<CCreditPoolDiff> creditPoolDiff;
|
||||
if (fV20Active_context) {
|
||||
CCreditPool creditPool = creditPoolManager->GetCreditPool(pindexPrev, chainparams.GetConsensus());
|
||||
LogPrintf("%s: CCreditPool is %s\n", __func__, creditPool.ToString());
|
||||
creditPoolDiff.emplace(std::move(creditPool), pindexPrev, chainparams.GetConsensus());
|
||||
}
|
||||
addPackageTxs(nPackagesSelected, nDescendantsUpdated, creditPoolDiff);
|
||||
|
||||
int64_t nTime1 = GetTimeMicros();
|
||||
|
||||
@ -197,7 +206,7 @@ std::unique_ptr<CBlockTemplate> BlockAssembler::CreateNewBlock(const CScript& sc
|
||||
|
||||
CCbTx cbTx;
|
||||
|
||||
if (llmq::utils::IsV20Active(pindexPrev)) {
|
||||
if (fV20Active_context) {
|
||||
cbTx.nVersion = CCbTx::CB_V20_VERSION;
|
||||
} else if (fDIP0008Active_context) {
|
||||
cbTx.nVersion = CCbTx::CB_V19_VERSION;
|
||||
@ -215,14 +224,15 @@ std::unique_ptr<CBlockTemplate> BlockAssembler::CreateNewBlock(const CScript& sc
|
||||
if (!CalcCbTxMerkleRootQuorums(*pblock, pindexPrev, quorum_block_processor, cbTx.merkleRootQuorums, state)) {
|
||||
throw std::runtime_error(strprintf("%s: CalcCbTxMerkleRootQuorums failed: %s", __func__, state.ToString()));
|
||||
}
|
||||
if (llmq::utils::IsV20Active(pindexPrev)) {
|
||||
if (fV20Active_context) {
|
||||
if (CalcCbTxBestChainlock(m_clhandler, pindexPrev, cbTx.bestCLHeightDiff, cbTx.bestCLSignature)) {
|
||||
LogPrintf("CreateNewBlock() h[%d] CbTx bestCLHeightDiff[%d] CLSig[%s]\n", nHeight, cbTx.bestCLHeightDiff, cbTx.bestCLSignature.ToString());
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// not an error
|
||||
LogPrintf("CreateNewBlock() h[%d] CbTx failed to find best CL. Inserting null CL\n", nHeight);
|
||||
}
|
||||
assert(creditPoolDiff != std::nullopt);
|
||||
cbTx.assetLockedAmount = creditPoolDiff->GetTotalLocked();
|
||||
}
|
||||
}
|
||||
|
||||
@ -383,7 +393,7 @@ void BlockAssembler::SortForBlock(const CTxMemPool::setEntries& package, std::ve
|
||||
// Each time through the loop, we compare the best transaction in
|
||||
// mapModifiedTxs with the next transaction in the mempool to decide what
|
||||
// transaction package to work on next.
|
||||
void BlockAssembler::addPackageTxs(int &nPackagesSelected, int &nDescendantsUpdated)
|
||||
void BlockAssembler::addPackageTxs(int &nPackagesSelected, int &nDescendantsUpdated, std::optional<CCreditPoolDiff>& creditPoolDiff)
|
||||
{
|
||||
AssertLockHeld(m_mempool.cs);
|
||||
|
||||
@ -440,6 +450,23 @@ void BlockAssembler::addPackageTxs(int &nPackagesSelected, int &nDescendantsUpda
|
||||
}
|
||||
}
|
||||
|
||||
if (creditPoolDiff != std::nullopt) {
|
||||
// If one transaction is skipped due to limits, it is not a reason to interrupt
|
||||
// whole process of adding transactions.
|
||||
// `state` is local here because used to log info about this specific tx
|
||||
TxValidationState state;
|
||||
|
||||
if (!creditPoolDiff->ProcessTransaction(iter->GetTx(), state)) {
|
||||
if (fUsingModified) {
|
||||
mapModifiedTx.get<ancestor_score>().erase(modit);
|
||||
failedTx.insert(iter);
|
||||
}
|
||||
LogPrintf("%s: asset-locks tx %s skipped due %s\n",
|
||||
__func__, iter->GetTx().GetHash().ToString(), state.ToString());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// We skip mapTx entries that are inBlock, and mapModifiedTx shouldn't
|
||||
// contain anything that is inBlock.
|
||||
assert(!inBlock.count(iter));
|
||||
|
@ -20,6 +20,7 @@
|
||||
class CBlockIndex;
|
||||
class CChainParams;
|
||||
class CConnman;
|
||||
class CCreditPoolDiff;
|
||||
class CGovernanceManager;
|
||||
class CScript;
|
||||
class CSporkManager;
|
||||
@ -194,7 +195,7 @@ private:
|
||||
/** Add transactions based on feerate including unconfirmed ancestors
|
||||
* Increments nPackagesSelected / nDescendantsUpdated with corresponding
|
||||
* statistics from the package selection (for logging statistics). */
|
||||
void addPackageTxs(int& nPackagesSelected, int& nDescendantsUpdated) EXCLUSIVE_LOCKS_REQUIRED(m_mempool.cs);
|
||||
void addPackageTxs(int& nPackagesSelected, int& nDescendantsUpdated, std::optional<CCreditPoolDiff>& creditPoolDiff) EXCLUSIVE_LOCKS_REQUIRED(m_mempool.cs);
|
||||
|
||||
// helper functions for addPackageTxs()
|
||||
/** Remove confirmed (inBlock) entries from given set */
|
||||
|
@ -6,6 +6,7 @@
|
||||
|
||||
#include <addrman.h>
|
||||
#include <banman.h>
|
||||
#include <evo/creditpool.h>
|
||||
#include <interfaces/chain.h>
|
||||
#include <llmq/context.h>
|
||||
#include <evo/evodb.h>
|
||||
|
@ -15,6 +15,7 @@ class BanMan;
|
||||
class CAddrMan;
|
||||
class CBlockPolicyEstimator;
|
||||
class CConnman;
|
||||
class CCreditPoolManager;
|
||||
class CScheduler;
|
||||
class CTxMemPool;
|
||||
class ChainstateManager;
|
||||
@ -56,6 +57,7 @@ struct NodeContext {
|
||||
std::function<void()> rpc_interruption_point = [] {};
|
||||
//! Dash
|
||||
std::unique_ptr<LLMQContext> llmq_ctx;
|
||||
std::unique_ptr<CCreditPoolManager> creditPoolManager;
|
||||
|
||||
std::unique_ptr<CEvoDB> evodb;
|
||||
|
||||
|
@ -22,6 +22,8 @@ enum {
|
||||
TRANSACTION_COINBASE = 5,
|
||||
TRANSACTION_QUORUM_COMMITMENT = 6,
|
||||
TRANSACTION_MNHF_SIGNAL = 7,
|
||||
TRANSACTION_ASSET_LOCK = 8,
|
||||
TRANSACTION_ASSET_UNLOCK = 9,
|
||||
};
|
||||
|
||||
/** An outpoint - a combination of a transaction hash and an index n into its vout */
|
||||
|
@ -7,6 +7,7 @@
|
||||
#include <chainparams.h>
|
||||
#include <consensus/validation.h>
|
||||
#include <core_io.h>
|
||||
#include <evo/creditpool.h>
|
||||
#include <evo/deterministicmns.h>
|
||||
#include <evo/dmn_types.h>
|
||||
#include <evo/providertx.h>
|
||||
@ -328,8 +329,10 @@ static std::string SignAndSendSpecialTx(const JSONRPCRequest& request, const CMu
|
||||
{
|
||||
LOCK(cs_main);
|
||||
|
||||
CCreditPool creditPool = creditPoolManager->GetCreditPool(::ChainActive().Tip(), Params().GetConsensus());
|
||||
|
||||
TxValidationState state;
|
||||
if (!CheckSpecialTx(CTransaction(tx), ::ChainActive().Tip(), state, ::ChainstateActive().CoinsTip(), true)) {
|
||||
if (!CheckSpecialTx(CTransaction(tx), ::ChainActive().Tip(), ::ChainstateActive().CoinsTip(), creditPool, true, state)) {
|
||||
throw std::runtime_error(state.ToString());
|
||||
}
|
||||
} // cs_main
|
||||
|
398
src/test/evo_assetlocks_tests.cpp
Normal file
398
src/test/evo_assetlocks_tests.cpp
Normal file
@ -0,0 +1,398 @@
|
||||
// Copyright (c) 2023 The Dash Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <test/util/setup_common.h>
|
||||
|
||||
#include <amount.h>
|
||||
#include <consensus/tx_check.h>
|
||||
#include <evo/assetlocktx.h>
|
||||
#include <evo/creditpool.h>
|
||||
#include <policy/settings.h>
|
||||
#include <script/script.h>
|
||||
#include <script/signingprovider.h>
|
||||
#include <validation.h> // for ::ChainActive()
|
||||
|
||||
#include <boost/test/unit_test.hpp>
|
||||
|
||||
//
|
||||
// Helper: create two dummy transactions, each with
|
||||
// two outputs. The first has 11 and 50 CENT outputs
|
||||
// paid to a TX_PUBKEY, the second 21 and 22 CENT outputs
|
||||
// paid to a TX_PUBKEYHASH.
|
||||
//
|
||||
static std::vector<CMutableTransaction>
|
||||
SetupDummyInputs(FillableSigningProvider& keystoreRet, CCoinsViewCache& coinsRet)
|
||||
{
|
||||
std::vector<CMutableTransaction> dummyTransactions;
|
||||
dummyTransactions.resize(2);
|
||||
|
||||
// Add some keys to the keystore:
|
||||
std::array<CKey, 4> key;
|
||||
{
|
||||
bool flip = true;
|
||||
for (auto& k : key) {
|
||||
k.MakeNewKey(flip);
|
||||
keystoreRet.AddKey(k);
|
||||
flip = !flip;
|
||||
}
|
||||
}
|
||||
|
||||
// Create some dummy input transactions
|
||||
dummyTransactions[0].vout.resize(2);
|
||||
dummyTransactions[0].vout[0].nValue = 11*CENT;
|
||||
dummyTransactions[0].vout[0].scriptPubKey << ToByteVector(key[0].GetPubKey()) << OP_CHECKSIG;
|
||||
dummyTransactions[0].vout[1].nValue = 50*CENT;
|
||||
dummyTransactions[0].vout[1].scriptPubKey << ToByteVector(key[1].GetPubKey()) << OP_CHECKSIG;
|
||||
AddCoins(coinsRet, CTransaction(dummyTransactions[0]), 0);
|
||||
|
||||
dummyTransactions[1].vout.resize(2);
|
||||
dummyTransactions[1].vout[0].nValue = 21*CENT;
|
||||
dummyTransactions[1].vout[0].scriptPubKey = GetScriptForDestination(PKHash(key[2].GetPubKey()));
|
||||
dummyTransactions[1].vout[1].nValue = 22*CENT;
|
||||
dummyTransactions[1].vout[1].scriptPubKey = GetScriptForDestination(PKHash(key[3].GetPubKey()));
|
||||
AddCoins(coinsRet, CTransaction(dummyTransactions[1]), 0);
|
||||
|
||||
return dummyTransactions;
|
||||
}
|
||||
|
||||
static CMutableTransaction CreateAssetLockTx(FillableSigningProvider& keystore, CCoinsViewCache& coins, CKey& key)
|
||||
{
|
||||
std::vector<CMutableTransaction> dummyTransactions = SetupDummyInputs(keystore, coins);
|
||||
|
||||
std::vector<CTxOut> creditOutputs(2);
|
||||
creditOutputs[0].nValue = 17 * CENT;
|
||||
creditOutputs[0].scriptPubKey = GetScriptForDestination(PKHash(key.GetPubKey()));
|
||||
creditOutputs[1].nValue = 13 * CENT;
|
||||
creditOutputs[1].scriptPubKey = GetScriptForDestination(PKHash(key.GetPubKey()));
|
||||
|
||||
CAssetLockPayload assetLockTx(creditOutputs);
|
||||
|
||||
CMutableTransaction tx;
|
||||
tx.nVersion = 3;
|
||||
tx.nType = TRANSACTION_ASSET_LOCK;
|
||||
SetTxPayload(tx, assetLockTx);
|
||||
|
||||
tx.vin.resize(1);
|
||||
tx.vin[0].prevout.hash = dummyTransactions[0].GetHash();
|
||||
tx.vin[0].prevout.n = 1;
|
||||
tx.vin[0].scriptSig << std::vector<unsigned char>(65, 0);
|
||||
|
||||
tx.vout.resize(2);
|
||||
tx.vout[0].nValue = 30 * CENT;
|
||||
tx.vout[0].scriptPubKey = CScript() << OP_RETURN << ParseHex("");
|
||||
|
||||
tx.vout[1].nValue = 20 * CENT;
|
||||
tx.vout[1].scriptPubKey = GetScriptForDestination(PKHash(key.GetPubKey()));
|
||||
|
||||
return tx;
|
||||
}
|
||||
|
||||
static CMutableTransaction CreateAssetUnlockTx(FillableSigningProvider& keystore, CKey& key)
|
||||
{
|
||||
int nVersion = 1;
|
||||
// just a big number bigger than uint32_t
|
||||
uint64_t index = 0x001122334455667788L;
|
||||
// big enough to overflow int32_t
|
||||
uint32_t fee = 2000'000'000u;
|
||||
// just big enough to overflow uint16_t
|
||||
uint32_t requestedHeight = 1000'000;
|
||||
uint256 quorumHash;
|
||||
CBLSSignature quorumSig;
|
||||
CAssetUnlockPayload assetUnlockTx(nVersion, index, fee, requestedHeight, quorumHash, quorumSig);
|
||||
|
||||
CMutableTransaction tx;
|
||||
tx.nVersion = 3;
|
||||
tx.nType = TRANSACTION_ASSET_UNLOCK;
|
||||
SetTxPayload(tx, assetUnlockTx);
|
||||
|
||||
tx.vin.resize(0);
|
||||
|
||||
tx.vout.resize(2);
|
||||
tx.vout[0].nValue = 10 * CENT;
|
||||
tx.vout[0].scriptPubKey = GetScriptForDestination(PKHash(key.GetPubKey()));
|
||||
|
||||
tx.vout[1].nValue = 20 * CENT;
|
||||
tx.vout[1].scriptPubKey = GetScriptForDestination(PKHash(key.GetPubKey()));
|
||||
|
||||
return tx;
|
||||
}
|
||||
|
||||
BOOST_FIXTURE_TEST_SUITE(evo_assetlocks_tests, TestChain100Setup)
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(evo_assetlock, TestChain100Setup)
|
||||
{
|
||||
|
||||
LOCK(cs_main);
|
||||
FillableSigningProvider keystore;
|
||||
CCoinsView coinsDummy;
|
||||
CCoinsViewCache coins(&coinsDummy);
|
||||
|
||||
CKey key;
|
||||
key.MakeNewKey(true);
|
||||
|
||||
const CMutableTransaction tx = CreateAssetLockTx(keystore, coins, key);
|
||||
std::string reason;
|
||||
BOOST_CHECK(IsStandardTx(CTransaction(tx), reason));
|
||||
|
||||
TxValidationState tx_state;
|
||||
std::string strTest;
|
||||
BOOST_CHECK_MESSAGE(CheckTransaction(CTransaction(tx), tx_state), strTest);
|
||||
BOOST_CHECK(tx_state.IsValid());
|
||||
|
||||
BOOST_CHECK(CheckAssetLockTx(CTransaction(tx), tx_state));
|
||||
|
||||
BOOST_CHECK(AreInputsStandard(CTransaction(tx), coins));
|
||||
|
||||
// Check version
|
||||
{
|
||||
BOOST_CHECK(tx.nVersion == 3);
|
||||
|
||||
CAssetLockPayload lockPayload;
|
||||
GetTxPayload(tx, lockPayload);
|
||||
|
||||
BOOST_CHECK(lockPayload.getVersion() == 1);
|
||||
}
|
||||
|
||||
{
|
||||
// Wrong type "Asset Unlock TX" instead "Asset Lock TX"
|
||||
CMutableTransaction txWrongType = tx;
|
||||
txWrongType.nType = TRANSACTION_ASSET_UNLOCK;
|
||||
BOOST_CHECK(!CheckAssetLockTx(CTransaction(txWrongType), tx_state));
|
||||
BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-type");
|
||||
}
|
||||
|
||||
{
|
||||
CAmount inSum = 0;
|
||||
for (const auto& vin : tx.vin) {
|
||||
inSum += coins.AccessCoin(vin.prevout).out.nValue;
|
||||
}
|
||||
|
||||
auto outSum = CTransaction(tx).GetValueOut();
|
||||
BOOST_CHECK(inSum == outSum);
|
||||
|
||||
// Outputs should not be bigger than inputs
|
||||
CMutableTransaction txBigOutput = tx;
|
||||
txBigOutput.vout[0].nValue += 1;
|
||||
BOOST_CHECK(!CheckAssetLockTx(CTransaction(txBigOutput), tx_state));
|
||||
BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-creditamount");
|
||||
|
||||
// Smaller outputs are allown
|
||||
CMutableTransaction txSmallOutput = tx;
|
||||
txSmallOutput.vout[1].nValue -= 1;
|
||||
BOOST_CHECK(CheckAssetLockTx(CTransaction(txSmallOutput), tx_state));
|
||||
}
|
||||
|
||||
const CAssetLockPayload assetLockPayload = [tx]() -> CAssetLockPayload {
|
||||
CAssetLockPayload payload;
|
||||
GetTxPayload(tx, payload);
|
||||
return payload;
|
||||
}();
|
||||
const std::vector<CTxOut> creditOutputs = assetLockPayload.getCreditOutputs();
|
||||
|
||||
{
|
||||
// Sum of credit output greater than OP_RETURN
|
||||
std::vector<CTxOut> wrongOutput = creditOutputs;
|
||||
wrongOutput[0].nValue += CENT;
|
||||
CAssetLockPayload greaterCreditsPayload(wrongOutput);
|
||||
|
||||
CMutableTransaction txGreaterCredits = tx;
|
||||
SetTxPayload(txGreaterCredits, greaterCreditsPayload);
|
||||
|
||||
BOOST_CHECK(!CheckAssetLockTx(CTransaction(txGreaterCredits), tx_state));
|
||||
BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-creditamount");
|
||||
|
||||
// Sum of credit output less than OP_RETURN
|
||||
wrongOutput[1].nValue -= 2 * CENT;
|
||||
CAssetLockPayload lessCreditsPayload(wrongOutput);
|
||||
|
||||
CMutableTransaction txLessCredits = tx;
|
||||
SetTxPayload(txLessCredits, lessCreditsPayload);
|
||||
|
||||
BOOST_CHECK(!CheckAssetLockTx(CTransaction(txLessCredits), tx_state));
|
||||
BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-creditamount");
|
||||
}
|
||||
|
||||
{
|
||||
// Credit output is out-of-range
|
||||
std::vector<CTxOut> creditOutputsOutOfRange = creditOutputs;
|
||||
creditOutputsOutOfRange[0].nValue = 0;
|
||||
CAssetLockPayload invalidOutputsPayload(creditOutputsOutOfRange);
|
||||
|
||||
CMutableTransaction txInvalidOutputs = tx;
|
||||
SetTxPayload(txInvalidOutputs, invalidOutputsPayload);
|
||||
|
||||
BOOST_CHECK(!CheckAssetLockTx(CTransaction(txInvalidOutputs), tx_state));
|
||||
BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-credit-outofrange");
|
||||
|
||||
// one of output is out of range
|
||||
creditOutputsOutOfRange[0].nValue = MAX_MONEY + 1;
|
||||
SetTxPayload(txInvalidOutputs, CAssetLockPayload{creditOutputsOutOfRange});
|
||||
BOOST_CHECK(!CheckAssetLockTx(CTransaction(txInvalidOutputs), tx_state));
|
||||
BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-credit-outofrange");
|
||||
|
||||
// sum of some of output is out of range
|
||||
creditOutputsOutOfRange[0].nValue = MAX_MONEY + 1 - creditOutputsOutOfRange[1].nValue;
|
||||
SetTxPayload(txInvalidOutputs, CAssetLockPayload{creditOutputsOutOfRange});
|
||||
BOOST_CHECK(!CheckAssetLockTx(CTransaction(txInvalidOutputs), tx_state));
|
||||
BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-credit-outofrange");
|
||||
|
||||
}
|
||||
{
|
||||
// One credit output keys is not pub key
|
||||
std::vector<CTxOut> creditOutputsNotPubkey = creditOutputs;
|
||||
creditOutputsNotPubkey[0].scriptPubKey = CScript() << OP_1;
|
||||
CAssetLockPayload notPubkeyPayload(creditOutputsNotPubkey);
|
||||
|
||||
CMutableTransaction txNotPubkey = tx;
|
||||
SetTxPayload(txNotPubkey, notPubkeyPayload);
|
||||
|
||||
BOOST_CHECK(!CheckAssetLockTx(CTransaction(txNotPubkey), tx_state));
|
||||
BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-pubKeyHash");
|
||||
|
||||
}
|
||||
|
||||
{
|
||||
// OP_RETURN must be only one, not more
|
||||
CMutableTransaction txMultipleReturn = tx;
|
||||
txMultipleReturn.vout[1].scriptPubKey = CScript() << OP_RETURN << ParseHex("");
|
||||
|
||||
BOOST_CHECK(!CheckAssetLockTx(CTransaction(txMultipleReturn), tx_state));
|
||||
BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-multiple-return");
|
||||
|
||||
}
|
||||
|
||||
{
|
||||
// zero/negative OP_RETURN
|
||||
CMutableTransaction txReturnOutOfRange = tx;
|
||||
txReturnOutOfRange.vout[0].nValue = 0;
|
||||
|
||||
BOOST_CHECK(!CheckAssetLockTx(CTransaction(txReturnOutOfRange), tx_state));
|
||||
BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-opreturn-outofrange");
|
||||
|
||||
txReturnOutOfRange.vout[0].nValue = MAX_MONEY + 1;
|
||||
|
||||
BOOST_CHECK(!CheckAssetLockTx(CTransaction(txReturnOutOfRange), tx_state));
|
||||
BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-opreturn-outofrange");
|
||||
}
|
||||
|
||||
|
||||
{
|
||||
// OP_RETURN is missing
|
||||
CMutableTransaction txNoReturn = tx;
|
||||
txNoReturn.vout[0].scriptPubKey = GetScriptForDestination(PKHash(key.GetPubKey()));
|
||||
|
||||
BOOST_CHECK(!CheckAssetLockTx(CTransaction(txNoReturn), tx_state));
|
||||
BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-no-return");
|
||||
}
|
||||
|
||||
{
|
||||
// OP_RETURN should not have any data
|
||||
CMutableTransaction txReturnData = tx;
|
||||
txReturnData.vout[0].scriptPubKey = CScript() << OP_RETURN << ParseHex("abc");
|
||||
|
||||
BOOST_CHECK(!CheckAssetLockTx(CTransaction(txReturnData), tx_state));
|
||||
BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetlocktx-non-empty-return");
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_FIXTURE_TEST_CASE(evo_assetunlock, TestChain100Setup)
|
||||
{
|
||||
LOCK(cs_main);
|
||||
FillableSigningProvider keystore;
|
||||
|
||||
CKey key;
|
||||
key.MakeNewKey(true);
|
||||
|
||||
const CMutableTransaction tx = CreateAssetUnlockTx(keystore, key);
|
||||
std::string reason;
|
||||
BOOST_CHECK(IsStandardTx(CTransaction(tx), reason));
|
||||
|
||||
TxValidationState tx_state;
|
||||
std::string strTest;
|
||||
BOOST_CHECK_MESSAGE(CheckTransaction(CTransaction(tx), tx_state), strTest);
|
||||
BOOST_CHECK(tx_state.IsValid());
|
||||
|
||||
const CBlockIndex *block_index = ::ChainActive().Tip();
|
||||
CCreditPool pool;
|
||||
BOOST_CHECK(!CheckAssetUnlockTx(CTransaction(tx), block_index, pool, tx_state));
|
||||
BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetunlock-quorum-hash");
|
||||
|
||||
{
|
||||
// Any input should be a reason to fail CheckAssetUnlockTx()
|
||||
CCoinsView coinsDummy;
|
||||
CCoinsViewCache coins(&coinsDummy);
|
||||
std::vector<CMutableTransaction> dummyTransactions = SetupDummyInputs(keystore, coins);
|
||||
|
||||
CMutableTransaction txNonemptyInput = tx;
|
||||
txNonemptyInput.vin.resize(1);
|
||||
txNonemptyInput.vin[0].prevout.hash = dummyTransactions[0].GetHash();
|
||||
txNonemptyInput.vin[0].prevout.n = 1;
|
||||
txNonemptyInput.vin[0].scriptSig << std::vector<unsigned char>(65, 0);
|
||||
|
||||
std::string reason;
|
||||
BOOST_CHECK(IsStandardTx(CTransaction(tx), reason));
|
||||
|
||||
BOOST_CHECK(!CheckAssetUnlockTx(CTransaction(txNonemptyInput), block_index, pool, tx_state));
|
||||
BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetunlocktx-have-input");
|
||||
}
|
||||
|
||||
{
|
||||
CAssetUnlockPayload unlockPayload;
|
||||
GetTxPayload(tx, unlockPayload);
|
||||
BOOST_CHECK(unlockPayload.getVersion() == 1);
|
||||
BOOST_CHECK(unlockPayload.getRequestedHeight() == 1000'000);
|
||||
BOOST_CHECK(unlockPayload.getFee() == 2000'000'000u);
|
||||
BOOST_CHECK(unlockPayload.getIndex() == 0x001122334455667788L);
|
||||
|
||||
// Wrong type "Asset Lock TX" instead "Asset Unlock TX"
|
||||
CMutableTransaction txWrongType = tx;
|
||||
txWrongType.nType = TRANSACTION_ASSET_LOCK;
|
||||
BOOST_CHECK(!CheckAssetUnlockTx(CTransaction(txWrongType), block_index, pool, tx_state));
|
||||
BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetunlocktx-type");
|
||||
|
||||
// Check version of tx and payload
|
||||
BOOST_CHECK(tx.nVersion == 3);
|
||||
for (uint8_t payload_version : {0, 1, 2, 255}) {
|
||||
CAssetUnlockPayload unlockPayload_tmp{payload_version,
|
||||
unlockPayload.getIndex(),
|
||||
unlockPayload.getFee(),
|
||||
unlockPayload.getRequestedHeight(),
|
||||
unlockPayload.getQuorumHash(),
|
||||
unlockPayload.getQuorumSig()};
|
||||
CMutableTransaction txWrongVersion = tx;
|
||||
SetTxPayload(txWrongVersion, unlockPayload_tmp);
|
||||
if (payload_version != 1) {
|
||||
BOOST_CHECK(!CheckAssetUnlockTx(CTransaction(txWrongVersion), block_index, pool, tx_state));
|
||||
BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetunlocktx-version");
|
||||
} else {
|
||||
BOOST_CHECK(!CheckAssetUnlockTx(CTransaction(txWrongVersion), block_index, pool, tx_state));
|
||||
BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetunlock-quorum-hash");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// Exactly 32 withdrawal is fine
|
||||
CMutableTransaction txManyOutputs = tx;
|
||||
int outputsLimit = 32;
|
||||
txManyOutputs.vout.resize(outputsLimit);
|
||||
for (auto& out : txManyOutputs.vout) {
|
||||
out.nValue = CENT;
|
||||
out.scriptPubKey = GetScriptForDestination(PKHash(key.GetPubKey()));
|
||||
}
|
||||
|
||||
BOOST_CHECK(!CheckAssetUnlockTx(CTransaction(txManyOutputs), block_index, pool, tx_state));
|
||||
BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetunlock-quorum-hash");
|
||||
|
||||
// Should not be more than 32 withdrawal in one transaction
|
||||
txManyOutputs.vout.resize(outputsLimit + 1);
|
||||
txManyOutputs.vout.back().nValue = CENT;
|
||||
txManyOutputs.vout.back().scriptPubKey = GetScriptForDestination(PKHash(key.GetPubKey()));
|
||||
BOOST_CHECK(!CheckAssetUnlockTx(CTransaction(txManyOutputs), block_index, pool, tx_state));
|
||||
BOOST_CHECK(tx_state.GetRejectReason() == "bad-assetunlocktx-too-many-outs");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
@ -56,6 +56,7 @@
|
||||
#include <coinjoin/coinjoin.h>
|
||||
#include <coinjoin/server.h>
|
||||
#include <evo/cbtx.h>
|
||||
#include <evo/creditpool.h>
|
||||
#include <evo/deterministicmns.h>
|
||||
#include <evo/evodb.h>
|
||||
#include <evo/specialtx.h>
|
||||
@ -139,6 +140,7 @@ BasicTestingSetup::BasicTestingSetup(const std::string& chainName, const std::ve
|
||||
connman = std::make_unique<CConnman>(0x1337, 0x1337, *m_node.addrman);
|
||||
deterministicMNManager.reset(new CDeterministicMNManager(*m_node.evodb, *connman));
|
||||
llmq::quorumSnapshotManager.reset(new llmq::CQuorumSnapshotManager(*m_node.evodb));
|
||||
creditPoolManager = std::make_unique<CCreditPoolManager>(*m_node.evodb);
|
||||
static bool noui_connected = false;
|
||||
if (!noui_connected) {
|
||||
noui_connect();
|
||||
@ -152,6 +154,7 @@ BasicTestingSetup::~BasicTestingSetup()
|
||||
connman.reset();
|
||||
llmq::quorumSnapshotManager.reset();
|
||||
deterministicMNManager.reset();
|
||||
creditPoolManager.reset();
|
||||
m_node.evodb.reset();
|
||||
|
||||
LogInstance().DisconnectTestLogger();
|
||||
@ -188,6 +191,7 @@ ChainTestingSetup::ChainTestingSetup(const std::string& chainName, const std::ve
|
||||
|
||||
deterministicMNManager.reset(new CDeterministicMNManager(*m_node.evodb, *m_node.connman));
|
||||
m_node.llmq_ctx = std::make_unique<LLMQContext>(*m_node.evodb, *m_node.mempool, *m_node.connman, *sporkManager, m_node.peerman, true, false);
|
||||
m_node.creditPoolManager = std::make_unique<CCreditPoolManager>(*m_node.evodb);
|
||||
|
||||
// Start script-checking threads. Set g_parallel_script_checks to true so they are used.
|
||||
constexpr int script_check_threads = 2;
|
||||
@ -201,6 +205,7 @@ ChainTestingSetup::~ChainTestingSetup()
|
||||
deterministicMNManager.reset();
|
||||
m_node.llmq_ctx->Interrupt();
|
||||
m_node.llmq_ctx->Stop();
|
||||
m_node.creditPoolManager.reset();
|
||||
StopScriptCheckWorkerThreads();
|
||||
GetMainSignals().FlushBackgroundCallbacks();
|
||||
GetMainSignals().UnregisterBackgroundSignalScheduler();
|
||||
|
@ -15,6 +15,7 @@
|
||||
#include <util/getuniquepath.h>
|
||||
#include <util/message.h> // For MessageSign(), MessageVerify(), MESSAGE_MAGIC
|
||||
#include <util/moneystr.h>
|
||||
#include <util/skip_set.h>
|
||||
#include <util/spanparsing.h>
|
||||
#include <util/strencodings.h>
|
||||
#include <util/string.h>
|
||||
@ -23,10 +24,12 @@
|
||||
|
||||
#include <array>
|
||||
#include <optional>
|
||||
#include <random>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <thread>
|
||||
#include <univalue.h>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#ifndef WIN32
|
||||
@ -2160,6 +2163,31 @@ BOOST_AUTO_TEST_CASE(test_Capitalize)
|
||||
BOOST_CHECK_EQUAL(Capitalize("\x00\xfe\xff"), "\x00\xfe\xff");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(test_SkipSet)
|
||||
{
|
||||
std::mt19937 gen;
|
||||
for (size_t test = 0; test < 17; ++test) {
|
||||
std::uniform_int_distribution<uint64_t> dist_value(0, (1 << test));
|
||||
size_t skip_size = test ? (1 << (test - 1)) : 1;
|
||||
CSkipSet set_1{skip_size};
|
||||
std::unordered_set<uint64_t> set_2;
|
||||
for (size_t iter = 0; iter < (1 << test) * 2; ++iter) {
|
||||
uint64_t value = dist_value(gen);
|
||||
BOOST_CHECK(set_1.Contains(value) == !!set_2.count(value));
|
||||
if (!set_1.Contains(value) && set_1.CanBeAdded(value)) {
|
||||
BOOST_CHECK(!set_1.Contains(value));
|
||||
BOOST_CHECK(set_1.Add(value));
|
||||
set_2.insert(value);
|
||||
}
|
||||
BOOST_CHECK(set_1.Contains(value) == !!set_2.count(value));
|
||||
BOOST_CHECK(set_1.Size() == set_2.size());
|
||||
}
|
||||
if (test > 4) {
|
||||
BOOST_CHECK(set_1.Size() > ((1 << test) / 4));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static std::string SpanToStr(const Span<const char>& span)
|
||||
{
|
||||
return std::string(span.begin(), span.end());
|
||||
|
@ -20,6 +20,7 @@
|
||||
#include <validationinterface.h>
|
||||
|
||||
#include <evo/specialtx.h>
|
||||
#include <evo/assetlocktx.h>
|
||||
#include <evo/providertx.h>
|
||||
#include <evo/deterministicmns.h>
|
||||
#include <llmq/instantsend.h>
|
||||
@ -451,6 +452,11 @@ void CTxMemPool::addUnchecked(const CTxMemPoolEntry &entry, setEntries &setAnces
|
||||
if (dmn->pdmnState->pubKeyOperator.Get() != CBLSPublicKey()) {
|
||||
newit->isKeyChangeProTx = true;
|
||||
}
|
||||
} else if (tx.nType == TRANSACTION_ASSET_UNLOCK) {
|
||||
CAssetUnlockPayload assetUnlockTx;
|
||||
bool ok = GetTxPayload(tx, assetUnlockTx);
|
||||
assert(ok);
|
||||
mapAssetUnlockExpiry.insert({tx.GetHash(), assetUnlockTx.getHeightToExpiry()});
|
||||
}
|
||||
}
|
||||
|
||||
@ -679,6 +685,8 @@ void CTxMemPool::removeUnchecked(txiter it, MemPoolRemovalReason reason)
|
||||
assert(false);
|
||||
}
|
||||
eraseProTxRef(proTx.proTxHash, it->GetTx().GetHash());
|
||||
} else if (it->GetTx().nType == TRANSACTION_ASSET_UNLOCK) {
|
||||
mapAssetUnlockExpiry.erase(it->GetTx().GetHash());
|
||||
}
|
||||
|
||||
totalTxSize -= it->GetTxSize();
|
||||
@ -999,6 +1007,25 @@ void CTxMemPool::removeForBlock(const std::vector<CTransactionRef>& vtx, unsigne
|
||||
blockSinceLastRollingFeeBump = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a lenght of chain is increased. Removes from mempool expired asset-unlock transactions
|
||||
*/
|
||||
void CTxMemPool::removeExpiredAssetUnlock(unsigned int nBlockHeight)
|
||||
{
|
||||
AssertLockHeld(cs);
|
||||
// items to removed should be firstly collected to independed list,
|
||||
// because removing items by `removeRecursive` changes the mapAssetUnlockExpiry
|
||||
std::vector<CTransactionRef> entries;
|
||||
for (const auto& item: mapAssetUnlockExpiry) {
|
||||
if (item.second < nBlockHeight) {
|
||||
entries.push_back(get(item.first));
|
||||
}
|
||||
}
|
||||
for (const auto& tx : entries) {
|
||||
removeRecursive(*tx, MemPoolRemovalReason::EXPIRY);
|
||||
}
|
||||
}
|
||||
|
||||
void CTxMemPool::_clear()
|
||||
{
|
||||
mapLinks.clear();
|
||||
|
@ -574,6 +574,7 @@ private:
|
||||
std::map<CKeyID, uint256> mapProTxPubKeyIDs;
|
||||
std::map<uint256, uint256> mapProTxBlsPubKeyHashes;
|
||||
std::map<COutPoint, uint256> mapProTxCollaterals;
|
||||
std::map<uint256, int /* expiry height */> mapAssetUnlockExpiry; // tx hash -> height
|
||||
|
||||
void UpdateParent(txiter entry, txiter parent, bool add) EXCLUSIVE_LOCKS_REQUIRED(cs);
|
||||
void UpdateChild(txiter entry, txiter child, bool add) EXCLUSIVE_LOCKS_REQUIRED(cs);
|
||||
@ -634,6 +635,7 @@ public:
|
||||
void removeProTxKeyChangedConflicts(const CTransaction &tx, const uint256& proTxHash, const uint256& newKeyHash) EXCLUSIVE_LOCKS_REQUIRED(cs);
|
||||
void removeProTxConflicts(const CTransaction &tx) EXCLUSIVE_LOCKS_REQUIRED(cs);
|
||||
void removeForBlock(const std::vector<CTransactionRef>& vtx, unsigned int nBlockHeight) EXCLUSIVE_LOCKS_REQUIRED(cs);
|
||||
void removeExpiredAssetUnlock(unsigned int nBlockHeight) EXCLUSIVE_LOCKS_REQUIRED(cs);
|
||||
|
||||
void clear();
|
||||
void _clear() EXCLUSIVE_LOCKS_REQUIRED(cs); //lock free
|
||||
|
53
src/util/skip_set.cpp
Normal file
53
src/util/skip_set.cpp
Normal file
@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2023 The Dash Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <util/skip_set.h>
|
||||
|
||||
#include <logging.h>
|
||||
#include <stdexcept>
|
||||
|
||||
bool CSkipSet::Add(uint64_t value)
|
||||
{
|
||||
if (Contains(value)) {
|
||||
throw std::runtime_error(strprintf("%s: trying to add an element that can't be added", __func__));
|
||||
}
|
||||
|
||||
if (auto it = skipped.find(value); it != skipped.end()) {
|
||||
skipped.erase(it);
|
||||
return true;
|
||||
}
|
||||
|
||||
assert(current_max <= value);
|
||||
if (Capacity() + value - current_max > capacity_limit) {
|
||||
LogPrintf("CSkipSet::Add failed due to capacity exceeded: requested %lld to %lld while limit is %lld\n",
|
||||
value - current_max, Capacity(), capacity_limit);
|
||||
return false;
|
||||
}
|
||||
for (uint64_t index = current_max; index < value; ++index) {
|
||||
bool insert_ret = skipped.insert(index).second;
|
||||
assert(insert_ret);
|
||||
}
|
||||
current_max = value + 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CSkipSet::CanBeAdded(uint64_t value) const
|
||||
{
|
||||
if (Contains(value)) return false;
|
||||
|
||||
if (skipped.find(value) != skipped.end()) return true;
|
||||
|
||||
if (Capacity() + value - current_max > capacity_limit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CSkipSet::Contains(uint64_t value) const
|
||||
{
|
||||
if (current_max <= value) return false;
|
||||
return skipped.find(value) == skipped.end();
|
||||
}
|
||||
|
52
src/util/skip_set.h
Normal file
52
src/util/skip_set.h
Normal file
@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2023 The Dash Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef BITCOIN_UTIL_SKIP_SET_H
|
||||
#define BITCOIN_UTIL_SKIP_SET_H
|
||||
|
||||
#include <serialize.h>
|
||||
#include <unordered_set>
|
||||
|
||||
// This data structure keeps efficiently all indexes and have a strict limit for used memory
|
||||
// So far as CCreditPool is built only in direction from parent block to child
|
||||
// there's no need to remove elements from CSkipSet ever, only add them
|
||||
class CSkipSet {
|
||||
private:
|
||||
std::unordered_set<uint64_t> skipped;
|
||||
uint64_t current_max{0};
|
||||
size_t capacity_limit;
|
||||
public:
|
||||
explicit CSkipSet(size_t capacity_limit = 10'000) :
|
||||
capacity_limit(capacity_limit)
|
||||
{}
|
||||
|
||||
/**
|
||||
* `Add` returns true if element has been added correctly and false if
|
||||
* capacity is depleted.
|
||||
*
|
||||
* `Add` should not be called if the element has been already added.
|
||||
* Use `Contains` to check if the element is here
|
||||
* Adding existing value will cause an exception
|
||||
*/
|
||||
[[nodiscard]] bool Add(uint64_t value);
|
||||
|
||||
bool CanBeAdded(uint64_t value) const;
|
||||
|
||||
bool Contains(uint64_t value) const;
|
||||
|
||||
size_t Size() const {
|
||||
return current_max - skipped.size();
|
||||
}
|
||||
size_t Capacity() const {
|
||||
return skipped.size();
|
||||
}
|
||||
|
||||
SERIALIZE_METHODS(CSkipSet, obj)
|
||||
{
|
||||
READWRITE(obj.current_max);
|
||||
READWRITE(obj.skipped);
|
||||
}
|
||||
};
|
||||
|
||||
#endif // BITCOIN_UTIL_SKIP_SET_H
|
@ -50,6 +50,7 @@
|
||||
#include <masternode/payments.h>
|
||||
#include <masternode/sync.h>
|
||||
|
||||
#include <evo/creditpool.h>
|
||||
#include <evo/evodb.h>
|
||||
#include <evo/specialtx.h>
|
||||
#include <evo/specialtxman.h>
|
||||
@ -384,7 +385,9 @@ static bool ContextualCheckTransaction(const CTransaction& tx, TxValidationState
|
||||
tx.nType != TRANSACTION_PROVIDER_UPDATE_REVOKE &&
|
||||
tx.nType != TRANSACTION_COINBASE &&
|
||||
tx.nType != TRANSACTION_QUORUM_COMMITMENT &&
|
||||
tx.nType != TRANSACTION_MNHF_SIGNAL) {
|
||||
tx.nType != TRANSACTION_MNHF_SIGNAL &&
|
||||
tx.nType != TRANSACTION_ASSET_LOCK &&
|
||||
tx.nType != TRANSACTION_ASSET_UNLOCK) {
|
||||
return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-txns-type");
|
||||
}
|
||||
if (tx.IsCoinBase() && tx.nType != TRANSACTION_COINBASE)
|
||||
@ -707,7 +710,7 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CCreditPool creditPool = creditPoolManager->GetCreditPool(::ChainActive().Tip(), chainparams.GetConsensus());
|
||||
LockPoints lp;
|
||||
m_view.SetBackend(m_viewmempool);
|
||||
|
||||
@ -822,7 +825,7 @@ bool MemPoolAccept::PreChecks(ATMPArgs& args, Workspace& ws)
|
||||
// DoS scoring a node for non-critical errors, e.g. duplicate keys because a TX is received that was already
|
||||
// mined
|
||||
// NOTE: we use UTXO here and do NOT allow mempool txes as masternode collaterals
|
||||
if (!CheckSpecialTx(tx, m_active_chainstate.m_chain.Tip(), state, m_active_chainstate.CoinsTip(), true))
|
||||
if (!CheckSpecialTx(tx, m_active_chainstate.m_chain.Tip(), m_active_chainstate.CoinsTip(), creditPool, true, state))
|
||||
return false;
|
||||
|
||||
if (m_pool.existsProviderTxConflict(tx)) {
|
||||
@ -2258,7 +2261,7 @@ bool CChainState::ConnectBlock(const CBlock& block, BlockValidationState& state,
|
||||
bool fDIP0001Active_context = pindex->nHeight >= Params().GetConsensus().DIP0001Height;
|
||||
|
||||
// MUST process special txes before updating UTXO to ensure consistency between mempool and block processing
|
||||
if (!ProcessSpecialTxsInBlock(block, pindex, *m_quorum_block_processor, *m_clhandler, state, view, fJustCheck, fScriptChecks)) {
|
||||
if (!ProcessSpecialTxsInBlock(block, pindex, *m_quorum_block_processor, *m_clhandler, m_params.GetConsensus(), view, fJustCheck, fScriptChecks, state)) {
|
||||
return error("ConnectBlock(DASH): ProcessSpecialTxsInBlock for block %s failed with %s",
|
||||
pindex->GetBlockHash().ToString(), state.ToString());
|
||||
}
|
||||
@ -2962,6 +2965,7 @@ bool CChainState::ConnectTip(BlockValidationState& state, CBlockIndex* pindexNew
|
||||
// Remove conflicting transactions from the mempool.;
|
||||
if (m_mempool) {
|
||||
m_mempool->removeForBlock(blockConnecting.vtx, pindexNew->nHeight);
|
||||
m_mempool->removeExpiredAssetUnlock(pindexNew->nHeight);
|
||||
disconnectpool.removeForBlock(blockConnecting.vtx);
|
||||
}
|
||||
// Update m_chain & related variables.
|
||||
@ -4907,7 +4911,7 @@ bool CChainState::RollforwardBlock(const CBlockIndex* pindex, CCoinsViewCache& i
|
||||
|
||||
// MUST process special txes before updating UTXO to ensure consistency between mempool and block processing
|
||||
BlockValidationState state;
|
||||
if (!ProcessSpecialTxsInBlock(block, pindex, *m_quorum_block_processor, *m_clhandler, state, inputs, false /*fJustCheck*/, false /*fScriptChecks*/)) {
|
||||
if (!ProcessSpecialTxsInBlock(block, pindex, *m_quorum_block_processor, *m_clhandler, m_params.GetConsensus(), inputs, false /*fJustCheck*/, false /*fScriptChecks*/, state)) {
|
||||
return error("RollforwardBlock(DASH): ProcessSpecialTxsInBlock for block %s failed with %s",
|
||||
pindex->GetBlockHash().ToString(), state.ToString());
|
||||
}
|
||||
|
522
test/functional/feature_asset_locks.py
Executable file
522
test/functional/feature_asset_locks.py
Executable file
@ -0,0 +1,522 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2022 The Dash Core developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import copy
|
||||
import struct
|
||||
from decimal import Decimal
|
||||
from io import BytesIO
|
||||
|
||||
from test_framework.blocktools import (
|
||||
create_block,
|
||||
create_coinbase,
|
||||
)
|
||||
from test_framework.authproxy import JSONRPCException
|
||||
from test_framework.key import ECKey
|
||||
from test_framework.messages import (
|
||||
CAssetLockTx,
|
||||
CAssetUnlockTx,
|
||||
COIN,
|
||||
COutPoint,
|
||||
CTransaction,
|
||||
CTxIn,
|
||||
CTxOut,
|
||||
FromHex,
|
||||
hash256,
|
||||
ser_string,
|
||||
)
|
||||
from test_framework.script import (
|
||||
CScript,
|
||||
OP_CHECKSIG,
|
||||
OP_DUP,
|
||||
OP_EQUALVERIFY,
|
||||
OP_HASH160,
|
||||
OP_RETURN,
|
||||
hash160,
|
||||
)
|
||||
from test_framework.test_framework import DashTestFramework
|
||||
from test_framework.util import (
|
||||
assert_equal,
|
||||
assert_greater_than,
|
||||
assert_greater_than_or_equal,
|
||||
)
|
||||
|
||||
llmq_type_test = 100
|
||||
tiny_amount = int(Decimal("0.0007") * COIN)
|
||||
blocks_in_one_day = 576
|
||||
|
||||
class AssetLocksTest(DashTestFramework):
|
||||
def set_test_params(self):
|
||||
self.set_dash_test_params(5, 3)
|
||||
|
||||
def skip_test_if_missing_module(self):
|
||||
self.skip_if_no_wallet()
|
||||
|
||||
def create_assetlock(self, coin, amount, pubkey):
|
||||
node_wallet = self.nodes[0]
|
||||
|
||||
|
||||
inputs = [CTxIn(COutPoint(int(coin["txid"], 16), coin["vout"]))]
|
||||
|
||||
credit_outputs = CTxOut(amount, CScript([OP_DUP, OP_HASH160, hash160(pubkey), OP_EQUALVERIFY, OP_CHECKSIG]))
|
||||
|
||||
lockTx_payload = CAssetLockTx(1, [credit_outputs])
|
||||
|
||||
remaining = int(COIN * coin['amount']) - tiny_amount - credit_outputs.nValue
|
||||
|
||||
tx_output_ret = CTxOut(credit_outputs.nValue, CScript([OP_RETURN, b""]))
|
||||
tx_output = CTxOut(remaining, CScript([pubkey, OP_CHECKSIG]))
|
||||
|
||||
lock_tx = CTransaction()
|
||||
lock_tx.vin = inputs
|
||||
lock_tx.vout = [tx_output, tx_output_ret] if remaining > 0 else [tx_output_ret]
|
||||
lock_tx.nVersion = 3
|
||||
lock_tx.nType = 8 # asset lock type
|
||||
lock_tx.vExtraPayload = lockTx_payload.serialize()
|
||||
|
||||
lock_tx = node_wallet.signrawtransactionwithwallet(lock_tx.serialize().hex())
|
||||
self.log.info(f"next tx: {lock_tx} payload: {lockTx_payload}")
|
||||
return FromHex(CTransaction(), lock_tx["hex"])
|
||||
|
||||
|
||||
def create_assetunlock(self, index, withdrawal, pubkey=None, fee=tiny_amount):
|
||||
node_wallet = self.nodes[0]
|
||||
mninfo = self.mninfo
|
||||
assert_greater_than(int(withdrawal), fee)
|
||||
tx_output = CTxOut(int(withdrawal) - fee, CScript([pubkey, OP_CHECKSIG]))
|
||||
|
||||
# request ID = sha256("plwdtx", index)
|
||||
request_id_buf = ser_string(b"plwdtx") + struct.pack("<Q", index)
|
||||
request_id = hash256(request_id_buf)[::-1].hex()
|
||||
|
||||
height = node_wallet.getblockcount()
|
||||
quorumHash = mninfo[0].node.quorum("selectquorum", llmq_type_test, request_id)["quorumHash"]
|
||||
unlockTx_payload = CAssetUnlockTx(
|
||||
version = 1,
|
||||
index = index,
|
||||
fee = fee,
|
||||
requestedHeight = height,
|
||||
quorumHash = int(quorumHash, 16),
|
||||
quorumSig = b'\00' * 96)
|
||||
|
||||
unlock_tx = CTransaction()
|
||||
unlock_tx.vin = []
|
||||
unlock_tx.vout = [tx_output]
|
||||
unlock_tx.nVersion = 3
|
||||
unlock_tx.nType = 9 # asset unlock type
|
||||
unlock_tx.vExtraPayload = unlockTx_payload.serialize()
|
||||
|
||||
unlock_tx.calc_sha256()
|
||||
msgHash = format(unlock_tx.sha256, '064x')
|
||||
|
||||
recsig = self.get_recovered_sig(request_id, msgHash, llmq_type=llmq_type_test)
|
||||
|
||||
unlockTx_payload.quorumSig = bytearray.fromhex(recsig["sig"])
|
||||
unlock_tx.vExtraPayload = unlockTx_payload.serialize()
|
||||
return unlock_tx
|
||||
|
||||
def get_credit_pool_amount(self, node = None, block_hash = None):
|
||||
if node is None:
|
||||
node = self.nodes[0]
|
||||
|
||||
if block_hash is None:
|
||||
block_hash = node.getbestblockhash()
|
||||
block = node.getblock(block_hash)
|
||||
return int(COIN * block['cbTx']['assetLockedAmount'])
|
||||
|
||||
def validate_credit_pool_amount(self, expected = None, block_hash = None):
|
||||
for node in self.nodes:
|
||||
locked = self.get_credit_pool_amount(node=node, block_hash=block_hash)
|
||||
if expected is None:
|
||||
expected = locked
|
||||
else:
|
||||
assert_equal(expected, locked)
|
||||
self.log.info(f"Credit pool amount matched with '{expected}'")
|
||||
return expected
|
||||
|
||||
def check_mempool_size(self):
|
||||
self.sync_mempools()
|
||||
for node in self.nodes:
|
||||
assert_equal(node.getmempoolinfo()['size'], self.mempool_size)
|
||||
|
||||
def check_mempool_result(self, result_expected, tx):
|
||||
"""Wrapper to check result of testmempoolaccept on node_0's mempool"""
|
||||
result_expected['txid'] = tx.rehash()
|
||||
|
||||
result_test = self.nodes[0].testmempoolaccept([tx.serialize().hex()])
|
||||
|
||||
assert_equal([result_expected], result_test)
|
||||
self.check_mempool_size()
|
||||
|
||||
def create_and_check_block(self, txes, expected_error = None):
|
||||
node_wallet = self.nodes[0]
|
||||
best_block_hash = node_wallet.getbestblockhash()
|
||||
best_block = node_wallet.getblock(best_block_hash)
|
||||
tip = int(best_block_hash, 16)
|
||||
height = best_block["height"] + 1
|
||||
block_time = best_block["time"] + 1
|
||||
|
||||
cbb = create_coinbase(height, dip4_activated=True, v20_activated=True)
|
||||
cbb.calc_sha256()
|
||||
block = create_block(tip, cbb, block_time, version=4)
|
||||
for tx in txes:
|
||||
block.vtx.append(tx)
|
||||
block.hashMerkleRoot = block.calc_merkle_root()
|
||||
block.solve()
|
||||
result = node_wallet.submitblock(block.serialize().hex())
|
||||
if result != expected_error:
|
||||
raise AssertionError('mining the block should have failed with error %s, but submitblock returned %s' % (expected_error, result))
|
||||
|
||||
def set_sporks(self):
|
||||
spork_enabled = 0
|
||||
spork_disabled = 4070908800
|
||||
|
||||
self.nodes[0].sporkupdate("SPORK_17_QUORUM_DKG_ENABLED", spork_enabled)
|
||||
self.nodes[0].sporkupdate("SPORK_19_CHAINLOCKS_ENABLED", spork_disabled)
|
||||
self.nodes[0].sporkupdate("SPORK_3_INSTANTSEND_BLOCK_FILTERING", spork_disabled)
|
||||
self.nodes[0].sporkupdate("SPORK_2_INSTANTSEND_ENABLED", spork_disabled)
|
||||
self.wait_for_sporks_same()
|
||||
|
||||
def ensure_tx_is_not_mined(self, tx_id):
|
||||
try:
|
||||
for node in self.nodes:
|
||||
node.gettransaction(tx_id)
|
||||
raise AssertionError("Transaction should not be mined")
|
||||
except JSONRPCException as e:
|
||||
assert "Invalid or non-wallet transaction id" in e.error['message']
|
||||
|
||||
def send_tx_simple(self, tx):
|
||||
return self.nodes[0].sendrawtransaction(hexstring=tx.serialize().hex(), maxfeerate=0)
|
||||
|
||||
def send_tx(self, tx, expected_error = None, reason = None):
|
||||
try:
|
||||
self.log.info(f"Send tx with expected_error:'{expected_error}'...")
|
||||
tx_res = self.send_tx_simple(tx)
|
||||
if expected_error is None:
|
||||
self.sync_mempools()
|
||||
return tx_res
|
||||
|
||||
# failure didn't happen, but expected:
|
||||
message = "Transaction should not be accepted"
|
||||
if reason is not None:
|
||||
message += ": " + reason
|
||||
|
||||
raise AssertionError(message)
|
||||
except JSONRPCException as e:
|
||||
assert expected_error in e.error['message']
|
||||
|
||||
def slowly_generate_batch(self, amount):
|
||||
self.log.info(f"Slowly generate {amount} blocks")
|
||||
while amount > 0:
|
||||
self.log.info(f"Generating batch of blocks {amount} left")
|
||||
next = min(10, amount)
|
||||
amount -= next
|
||||
self.bump_mocktime(next)
|
||||
self.nodes[1].generate(next)
|
||||
self.sync_all()
|
||||
|
||||
def run_test(self):
|
||||
node_wallet = self.nodes[0]
|
||||
node = self.nodes[1]
|
||||
|
||||
self.set_sporks()
|
||||
self.activate_v20()
|
||||
|
||||
self.mempool_size = 0
|
||||
|
||||
key = ECKey()
|
||||
key.generate()
|
||||
pubkey = key.get_pubkey().get_bytes()
|
||||
|
||||
self.log.info("Testing asset lock...")
|
||||
locked_1 = 10 * COIN + 141421
|
||||
locked_2 = 10 * COIN + 314159
|
||||
|
||||
coins = node_wallet.listunspent()
|
||||
coin = None
|
||||
while coin is None or COIN * coin['amount'] < locked_2:
|
||||
coin = coins.pop()
|
||||
asset_lock_tx = self.create_assetlock(coin, locked_1, pubkey)
|
||||
|
||||
self.check_mempool_result(tx=asset_lock_tx, result_expected={'allowed': True})
|
||||
self.validate_credit_pool_amount(0)
|
||||
txid_in_block = self.send_tx(asset_lock_tx)
|
||||
self.validate_credit_pool_amount(0)
|
||||
node.generate(1)
|
||||
assert_equal(self.get_credit_pool_amount(node=node_wallet), 0)
|
||||
assert_equal(self.get_credit_pool_amount(node=node), locked_1)
|
||||
self.log.info("Generate a number of blocks to ensure this is the longest chain for later in the test when we reconsiderblock")
|
||||
node.generate(12)
|
||||
self.sync_all()
|
||||
|
||||
self.validate_credit_pool_amount(locked_1)
|
||||
|
||||
# tx is mined, let's get blockhash
|
||||
self.log.info("Invalidate block with asset lock tx...")
|
||||
block_hash_1 = node_wallet.gettransaction(txid_in_block)['blockhash']
|
||||
for inode in self.nodes:
|
||||
inode.invalidateblock(block_hash_1)
|
||||
assert_equal(self.get_credit_pool_amount(node=inode), 0)
|
||||
node.generate(3)
|
||||
self.sync_all()
|
||||
self.validate_credit_pool_amount(0)
|
||||
self.log.info("Resubmit asset lock tx to new chain...")
|
||||
# NEW tx appears
|
||||
asset_lock_tx_2 = self.create_assetlock(coin, locked_2, pubkey)
|
||||
txid_in_block = self.send_tx(asset_lock_tx_2)
|
||||
node.generate(1)
|
||||
self.sync_all()
|
||||
self.validate_credit_pool_amount(locked_2)
|
||||
self.log.info("Reconsider old blocks...")
|
||||
for inode in self.nodes:
|
||||
inode.reconsiderblock(block_hash_1)
|
||||
self.validate_credit_pool_amount(locked_1)
|
||||
self.sync_all()
|
||||
|
||||
self.log.info('Mine block with incorrect credit-pool value...')
|
||||
extra_lock_tx = self.create_assetlock(coin, COIN, pubkey)
|
||||
self.create_and_check_block([extra_lock_tx], expected_error = 'bad-cbtx-assetlocked-amount')
|
||||
|
||||
self.log.info("Mine a quorum...")
|
||||
self.mine_quorum()
|
||||
self.validate_credit_pool_amount(locked_1)
|
||||
|
||||
self.log.info("Testing asset unlock...")
|
||||
asset_unlock_tx_index_too_far = self.create_assetunlock(10001, COIN, pubkey)
|
||||
tx_too_far_index = self.send_tx(asset_unlock_tx_index_too_far)
|
||||
node.generate(1)
|
||||
self.sync_all()
|
||||
self.mempool_size += 1
|
||||
self.check_mempool_size()
|
||||
self.log.info("Checking that `asset_unlock_tx_index_too_far` not mined yet...")
|
||||
self.ensure_tx_is_not_mined(tx_too_far_index)
|
||||
|
||||
self.log.info("Generating several txes by same quorum....")
|
||||
self.validate_credit_pool_amount(locked_1)
|
||||
asset_unlock_tx = self.create_assetunlock(101, COIN, pubkey)
|
||||
asset_unlock_tx_late = self.create_assetunlock(102, COIN, pubkey)
|
||||
asset_unlock_tx_too_late = self.create_assetunlock(103, COIN, pubkey)
|
||||
asset_unlock_tx_too_big_fee = self.create_assetunlock(104, COIN, pubkey, fee=int(Decimal("0.1") * COIN))
|
||||
asset_unlock_tx_zero_fee = self.create_assetunlock(105, COIN, pubkey, fee=0)
|
||||
asset_unlock_tx_duplicate_index = copy.deepcopy(asset_unlock_tx)
|
||||
asset_unlock_tx_duplicate_index.vout[0].nValue += COIN
|
||||
too_late_height = node.getblock(node.getbestblockhash())["height"] + 48
|
||||
|
||||
self.check_mempool_result(tx=asset_unlock_tx, result_expected={'allowed': True})
|
||||
self.check_mempool_result(tx=asset_unlock_tx_too_big_fee,
|
||||
result_expected={'allowed': False, 'reject-reason' : 'absurdly-high-fee'})
|
||||
self.check_mempool_result(tx=asset_unlock_tx_zero_fee,
|
||||
result_expected={'allowed': False, 'reject-reason' : 'bad-txns-assetunlock-fee-outofrange'})
|
||||
# not-verified is a correct faiure from mempool. Mempool knows nothing about CreditPool indexes and he just report that signature is not validated
|
||||
self.check_mempool_result(tx=asset_unlock_tx_duplicate_index,
|
||||
result_expected={'allowed': False, 'reject-reason' : 'bad-assetunlock-not-verified'})
|
||||
|
||||
self.log.info("Validating that we calculate payload hash correctly: ask quorum forcely by message hash...")
|
||||
asset_unlock_tx_payload = CAssetUnlockTx()
|
||||
asset_unlock_tx_payload.deserialize(BytesIO(asset_unlock_tx.vExtraPayload))
|
||||
|
||||
assert_equal(asset_unlock_tx_payload.quorumHash, int(self.mninfo[0].node.quorum("selectquorum", llmq_type_test, 'e6c7a809d79f78ea85b72d5df7e9bd592aecf151e679d6e976b74f053a7f9056')["quorumHash"], 16))
|
||||
|
||||
self.send_tx(asset_unlock_tx)
|
||||
self.mempool_size += 1
|
||||
self.check_mempool_size()
|
||||
self.validate_credit_pool_amount(locked_1)
|
||||
self.log.info("Mining one block - index '10001' can't be included in this block")
|
||||
node.generate(1)
|
||||
self.sync_all()
|
||||
self.validate_credit_pool_amount(locked_1 - COIN)
|
||||
self.mempool_size -= 1
|
||||
self.check_mempool_size()
|
||||
self.log.info("Tx should not be mined yet... mine one more block")
|
||||
node.generate(1)
|
||||
self.sync_all()
|
||||
self.mempool_size -= 1
|
||||
self.check_mempool_size()
|
||||
block_asset_unlock = node.getrawtransaction(asset_unlock_tx.rehash(), 1)['blockhash']
|
||||
|
||||
self.send_tx(asset_unlock_tx,
|
||||
expected_error = "Transaction already in block chain",
|
||||
reason = "double copy")
|
||||
|
||||
self.check_mempool_result(tx=asset_unlock_tx_duplicate_index,
|
||||
result_expected={'allowed': False, 'reject-reason' : 'bad-assetunlock-duplicated-index'})
|
||||
self.send_tx(asset_unlock_tx_duplicate_index,
|
||||
expected_error = "bad-assetunlock-duplicated-index",
|
||||
reason = "double index")
|
||||
|
||||
self.log.info("Checking tx with too far index is mined too - it is not too far anymore...")
|
||||
self.validate_credit_pool_amount(locked_1 - 2 * COIN)
|
||||
self.nodes[0].getrawtransaction(tx_too_far_index, 1)['blockhash']
|
||||
|
||||
self.log.info("Mining next quorum to check tx 'asset_unlock_tx_late' is still valid...")
|
||||
self.mine_quorum()
|
||||
self.log.info("Checking credit pool amount is same...")
|
||||
self.validate_credit_pool_amount(locked_1 - 2 * COIN)
|
||||
self.check_mempool_result(tx=asset_unlock_tx_late, result_expected={'allowed': True})
|
||||
self.log.info("Checking credit pool amount still is same...")
|
||||
self.validate_credit_pool_amount(locked_1 - 2 * COIN)
|
||||
self.send_tx(asset_unlock_tx_late)
|
||||
node.generate(1)
|
||||
self.sync_all()
|
||||
self.validate_credit_pool_amount(locked_1 - 3 * COIN)
|
||||
|
||||
self.log.info("Generating many blocks to make quorum far behind (even still active)...")
|
||||
self.slowly_generate_batch(too_late_height - node.getblock(node.getbestblockhash())["height"] - 1)
|
||||
self.check_mempool_result(tx=asset_unlock_tx_too_late, result_expected={'allowed': True})
|
||||
node.generate(1)
|
||||
self.sync_all()
|
||||
self.check_mempool_result(tx=asset_unlock_tx_too_late,
|
||||
result_expected={'allowed': False, 'reject-reason' : 'bad-assetunlock-too-late'})
|
||||
|
||||
self.log.info("Checking that two quorums later it is too late because quorum is not active...")
|
||||
self.mine_quorum()
|
||||
self.log.info("Expecting new reject-reason...")
|
||||
self.check_mempool_result(tx=asset_unlock_tx_too_late,
|
||||
result_expected={'allowed': False, 'reject-reason' : 'bad-assetunlock-not-active-quorum'})
|
||||
|
||||
block_to_reconsider = node.getbestblockhash()
|
||||
self.log.info("Test block invalidation with asset unlock tx...")
|
||||
for inode in self.nodes:
|
||||
inode.invalidateblock(block_asset_unlock)
|
||||
self.validate_credit_pool_amount(locked_1)
|
||||
self.slowly_generate_batch(50)
|
||||
self.validate_credit_pool_amount(locked_1)
|
||||
for inode in self.nodes:
|
||||
inode.reconsiderblock(block_to_reconsider)
|
||||
self.validate_credit_pool_amount(locked_1 - 3 * COIN)
|
||||
|
||||
self.log.info("Forcibly mining asset_unlock_tx_too_late and ensure block is invalid...")
|
||||
self.create_and_check_block([asset_unlock_tx_too_late], expected_error = "bad-assetunlock-not-active-quorum")
|
||||
|
||||
node.generate(1)
|
||||
self.sync_all()
|
||||
|
||||
self.validate_credit_pool_amount(locked_1 - 3 * COIN)
|
||||
self.validate_credit_pool_amount(block_hash=block_hash_1, expected=locked_1)
|
||||
|
||||
self.log.info("Checking too big withdrawal... expected to not be mined")
|
||||
asset_unlock_tx_full = self.create_assetunlock(201, 1 + self.get_credit_pool_amount(), pubkey)
|
||||
|
||||
self.log.info("Checking that transaction with exceeding amount accepted by mempool...")
|
||||
# Mempool doesn't know about the size of the credit pool
|
||||
self.check_mempool_result(tx=asset_unlock_tx_full, result_expected={'allowed': True })
|
||||
|
||||
txid_in_block = self.send_tx(asset_unlock_tx_full)
|
||||
node.generate(1)
|
||||
self.sync_all()
|
||||
|
||||
self.ensure_tx_is_not_mined(txid_in_block)
|
||||
|
||||
self.log.info("Forcibly mine asset_unlock_tx_full and ensure block is invalid...")
|
||||
self.create_and_check_block([asset_unlock_tx_full], expected_error = "failed-creditpool-unlock-too-much")
|
||||
|
||||
self.mempool_size += 1
|
||||
asset_unlock_tx_full = self.create_assetunlock(301, self.get_credit_pool_amount(), pubkey)
|
||||
self.check_mempool_result(tx=asset_unlock_tx_full, result_expected={'allowed': True })
|
||||
|
||||
txid_in_block = self.send_tx(asset_unlock_tx_full)
|
||||
node.generate(1)
|
||||
self.sync_all()
|
||||
self.log.info("Check txid_in_block was mined...")
|
||||
block = node.getblock(node.getbestblockhash())
|
||||
assert txid_in_block in block['tx']
|
||||
self.validate_credit_pool_amount(0)
|
||||
|
||||
self.log.info("After many blocks duplicated tx still should not be mined")
|
||||
self.send_tx(asset_unlock_tx_duplicate_index,
|
||||
expected_error = "bad-assetunlock-duplicated-index",
|
||||
reason = "double index")
|
||||
|
||||
self.log.info("Forcibly mine asset_unlock_tx_full and ensure block is invalid...")
|
||||
self.create_and_check_block([asset_unlock_tx_duplicate_index], expected_error = "bad-assetunlock-duplicated-index")
|
||||
|
||||
self.log.info("Fast forward to the next day to reset all current unlock limits...")
|
||||
self.slowly_generate_batch(blocks_in_one_day + 1)
|
||||
self.mine_quorum()
|
||||
|
||||
total = self.get_credit_pool_amount()
|
||||
while total <= 10_900 * COIN:
|
||||
self.log.info(f"Collecting coins in pool... Collected {total}/{10_900 * COIN}")
|
||||
coin = coins.pop()
|
||||
to_lock = int(coin['amount'] * COIN) - tiny_amount
|
||||
if to_lock > 50 * COIN:
|
||||
to_lock = 50 * COIN
|
||||
total += to_lock
|
||||
tx = self.create_assetlock(coin, to_lock, pubkey)
|
||||
self.send_tx_simple(tx)
|
||||
self.sync_mempools()
|
||||
node.generate(1)
|
||||
self.sync_all()
|
||||
credit_pool_amount_1 = self.get_credit_pool_amount()
|
||||
assert_greater_than(credit_pool_amount_1, 10_900 * COIN)
|
||||
limit_amount_1 = 1000 * COIN
|
||||
# take most of limit by one big tx for faster testing and
|
||||
# create several tiny withdrawal with exactly 1 *invalid* / causes spend above limit tx
|
||||
withdrawals = [600 * COIN, 131 * COIN, 131 * COIN, 131 * COIN, 131 * COIN]
|
||||
amount_to_withdraw_1 = sum(withdrawals)
|
||||
index = 400
|
||||
for next_amount in withdrawals:
|
||||
index += 1
|
||||
asset_unlock_tx = self.create_assetunlock(index, next_amount, pubkey)
|
||||
self.send_tx_simple(asset_unlock_tx)
|
||||
if index == 401:
|
||||
self.sync_mempools()
|
||||
node.generate(1)
|
||||
|
||||
self.sync_mempools()
|
||||
node.generate(1)
|
||||
self.sync_all()
|
||||
|
||||
new_total = self.get_credit_pool_amount()
|
||||
amount_actually_withdrawn = total - new_total
|
||||
block = node.getblock(node.getbestblockhash())
|
||||
self.log.info("Testing that we tried to withdraw more than we could...")
|
||||
assert_greater_than(amount_to_withdraw_1, amount_actually_withdrawn)
|
||||
self.log.info("Checking that we tried to withdraw more than the limit...")
|
||||
assert_greater_than(amount_to_withdraw_1, limit_amount_1)
|
||||
self.log.info("Checking we didn't actually withdraw more than allowed by the limit...")
|
||||
assert_greater_than_or_equal(limit_amount_1, amount_actually_withdrawn)
|
||||
assert_equal(amount_actually_withdrawn, 993 * COIN)
|
||||
node.generate(1)
|
||||
self.sync_all()
|
||||
self.log.info("Checking that exactly 1 tx stayed in mempool...")
|
||||
self.mempool_size = 1
|
||||
self.check_mempool_size()
|
||||
|
||||
assert_equal(new_total, self.get_credit_pool_amount())
|
||||
self.log.info("Fast forward to next day again...")
|
||||
self.slowly_generate_batch(blocks_in_one_day - 2)
|
||||
self.log.info("Checking mempool is empty now...")
|
||||
self.mempool_size = 0
|
||||
self.check_mempool_size()
|
||||
|
||||
self.log.info("Creating new asset-unlock tx. It should be mined exactly 1 block after")
|
||||
credit_pool_amount_2 = self.get_credit_pool_amount()
|
||||
limit_amount_2 = credit_pool_amount_2 // 10
|
||||
index += 1
|
||||
asset_unlock_tx = self.create_assetunlock(index, limit_amount_2, pubkey)
|
||||
self.send_tx(asset_unlock_tx)
|
||||
node.generate(1)
|
||||
self.sync_all()
|
||||
assert_equal(new_total, self.get_credit_pool_amount())
|
||||
node.generate(1)
|
||||
self.sync_all()
|
||||
new_total -= limit_amount_2
|
||||
assert_equal(new_total, self.get_credit_pool_amount())
|
||||
self.log.info("Trying to withdraw more... expecting to fail")
|
||||
index += 1
|
||||
asset_unlock_tx = self.create_assetunlock(index, COIN * 100, pubkey)
|
||||
self.send_tx(asset_unlock_tx)
|
||||
node.generate(1)
|
||||
self.sync_all()
|
||||
|
||||
self.log.info("generate many blocks to be sure that mempool is empty afterwards...")
|
||||
self.slowly_generate_batch(60)
|
||||
self.log.info("Checking that credit pool is not changed...")
|
||||
assert_equal(new_total, self.get_credit_pool_amount())
|
||||
self.check_mempool_size()
|
||||
|
||||
if __name__ == '__main__':
|
||||
AssetLocksTest().main()
|
@ -48,7 +48,7 @@ def script_BIP34_coinbase_height(height):
|
||||
return CScript([CScriptNum(height)])
|
||||
|
||||
|
||||
def create_coinbase(height, pubkey=None, dip4_activated=False):
|
||||
def create_coinbase(height, pubkey=None, dip4_activated=False, v20_activated=False):
|
||||
"""Create a coinbase transaction, assuming no miner fees.
|
||||
|
||||
If pubkey is passed in, the coinbase output will be a P2PK output;
|
||||
@ -67,7 +67,8 @@ def create_coinbase(height, pubkey=None, dip4_activated=False):
|
||||
if dip4_activated:
|
||||
coinbase.nVersion = 3
|
||||
coinbase.nType = 5
|
||||
cbtx_payload = CCbTx(2, height, 0, 0)
|
||||
cbtx_version = 3 if v20_activated else 2
|
||||
cbtx_payload = CCbTx(cbtx_version, height, 0, 0, 0)
|
||||
coinbase.vExtraPayload = cbtx_payload.serialize()
|
||||
coinbase.calc_sha256()
|
||||
return coinbase
|
||||
|
@ -1046,9 +1046,9 @@ class CMerkleBlock:
|
||||
|
||||
|
||||
class CCbTx:
|
||||
__slots__ = ("version", "height", "merkleRootMNList", "merkleRootQuorums", "bestCLHeightDiff", "bestCLSignature")
|
||||
__slots__ = ("version", "height", "merkleRootMNList", "merkleRootQuorums", "bestCLHeightDiff", "bestCLSignature", "lockedAmount")
|
||||
|
||||
def __init__(self, version=None, height=None, merkleRootMNList=None, merkleRootQuorums=None, bestCLHeightDiff=None, bestCLSignature=None):
|
||||
def __init__(self, version=None, height=None, merkleRootMNList=None, merkleRootQuorums=None, bestCLHeightDiff=None, bestCLSignature=None, lockedAmount=None):
|
||||
self.set_null()
|
||||
if version is not None:
|
||||
self.version = version
|
||||
@ -1062,6 +1062,8 @@ class CCbTx:
|
||||
self.bestCLHeightDiff = bestCLHeightDiff
|
||||
if bestCLSignature is not None:
|
||||
self.bestCLSignature = bestCLSignature
|
||||
if lockedAmount is not None:
|
||||
self.lockedAmount = lockedAmount
|
||||
|
||||
def set_null(self):
|
||||
self.version = 0
|
||||
@ -1069,6 +1071,7 @@ class CCbTx:
|
||||
self.merkleRootMNList = None
|
||||
self.bestCLHeightDiff = 0
|
||||
self.bestCLSignature = b'\x00' * 96
|
||||
self.lockedAmount = 0
|
||||
|
||||
def deserialize(self, f):
|
||||
self.version = struct.unpack("<H", f.read(2))[0]
|
||||
@ -1079,6 +1082,7 @@ class CCbTx:
|
||||
if self.version >= 3:
|
||||
self.bestCLHeightDiff = deser_compact_size(f)
|
||||
self.bestCLSignature = f.read(96)
|
||||
self.lockedAmount = struct.unpack("<q", f.read(8))[0]
|
||||
|
||||
|
||||
def serialize(self):
|
||||
@ -1091,9 +1095,87 @@ class CCbTx:
|
||||
if self.version >= 3:
|
||||
r += ser_compact_size(self.bestCLHeightDiff)
|
||||
r += self.bestCLSignature
|
||||
r += struct.pack("<q", self.lockedAmount)
|
||||
return r
|
||||
|
||||
|
||||
class CAssetLockTx:
|
||||
__slots__ = ("version", "creditOutputs")
|
||||
|
||||
def __init__(self, version=None, creditOutputs=None):
|
||||
self.set_null()
|
||||
if version is not None:
|
||||
self.version = version
|
||||
self.creditOutputs = creditOutputs if creditOutputs is not None else []
|
||||
|
||||
def set_null(self):
|
||||
self.version = 0
|
||||
self.creditOutputs = None
|
||||
|
||||
def deserialize(self, f):
|
||||
self.version = struct.unpack("<B", f.read(1))[0]
|
||||
self.creditOutputs = deser_vector(f, CTxOut)
|
||||
|
||||
def serialize(self):
|
||||
r = b""
|
||||
r += struct.pack("<B", self.version)
|
||||
r += ser_vector(self.creditOutputs)
|
||||
return r
|
||||
|
||||
def __repr__(self):
|
||||
return "CAssetLockTx(version={} creditOutputs={}" \
|
||||
.format(self.version, repr(self.creditOutputs))
|
||||
|
||||
|
||||
class CAssetUnlockTx:
|
||||
__slots__ = ("version", "index", "fee", "requestedHeight", "quorumHash", "quorumSig")
|
||||
|
||||
def __init__(self, version=None, index=None, fee=None, requestedHeight=None, quorumHash = 0, quorumSig = None):
|
||||
self.set_null()
|
||||
if version is not None:
|
||||
self.version = version
|
||||
if index is not None:
|
||||
self.index = index
|
||||
if fee is not None:
|
||||
self.fee = fee
|
||||
if requestedHeight is not None:
|
||||
self.requestedHeight = requestedHeight
|
||||
if quorumHash is not None:
|
||||
self.quorumHash = quorumHash
|
||||
if quorumSig is not None:
|
||||
self.quorumSig = quorumSig
|
||||
|
||||
def set_null(self):
|
||||
self.version = 0
|
||||
self.index = 0
|
||||
self.fee = None
|
||||
self.requestedHeight = 0
|
||||
self.quorumHash = 0
|
||||
self.quorumSig = b'\x00' * 96
|
||||
|
||||
def deserialize(self, f):
|
||||
self.version = struct.unpack("<B", f.read(1))[0]
|
||||
self.index = struct.unpack("<Q", f.read(8))[0]
|
||||
self.fee = struct.unpack("<I", f.read(4))[0]
|
||||
self.requestedHeight = struct.unpack("<I", f.read(4))[0]
|
||||
self.quorumHash = deser_uint256(f)
|
||||
self.quorumSig = f.read(96)
|
||||
|
||||
def serialize(self):
|
||||
r = b""
|
||||
r += struct.pack("<B", self.version)
|
||||
r += struct.pack("<Q", self.index)
|
||||
r += struct.pack("<I", self.fee)
|
||||
r += struct.pack("<I", self.requestedHeight)
|
||||
r += ser_uint256(self.quorumHash)
|
||||
r += self.quorumSig
|
||||
return r
|
||||
|
||||
def __repr__(self):
|
||||
return "CAssetUnlockTx(version={} index={} fee={} requestedHeight={} quorumHash={:x} quorumSig={}" \
|
||||
.format(self.version, self.index, self.fee, self.requestedHeight, self.quorumHash, self.quorumSig.hex())
|
||||
|
||||
|
||||
class CSimplifiedMNListEntry:
|
||||
__slots__ = ("proRegTxHash", "confirmedHash", "service", "pubKeyOperator", "keyIDVoting", "isValid", "nVersion", "type", "platformHTTPPort", "platformNodeID")
|
||||
|
||||
|
@ -120,6 +120,7 @@ BASE_SCRIPTS = [
|
||||
'feature_llmq_is_retroactive.py', # NOTE: needs dash_hash to pass
|
||||
'feature_llmq_dkgerrors.py', # NOTE: needs dash_hash to pass
|
||||
'feature_dip4_coinbasemerkleroots.py', # NOTE: needs dash_hash to pass
|
||||
'feature_asset_locks.py', # NOTE: needs dash_hash to pass
|
||||
# vv Tests less than 60s vv
|
||||
'p2p_sendheaders.py', # NOTE: needs dash_hash to pass
|
||||
'p2p_sendheaders_compressed.py', # NOTE: needs dash_hash to pass
|
||||
|
@ -71,6 +71,11 @@ EXPECTED_CIRCULAR_DEPENDENCIES=(
|
||||
"evo/deterministicmns -> llmq/utils -> net -> evo/deterministicmns"
|
||||
"policy/policy -> policy/settings -> policy/policy"
|
||||
"evo/specialtxman -> validation -> evo/specialtxman"
|
||||
"evo/creditpool -> validation -> evo/creditpool"
|
||||
"consensus/tx_verify -> evo/assetlocktx -> validation -> consensus/tx_verify"
|
||||
"consensus/tx_verify -> evo/assetlocktx -> llmq/quorums -> net_processing -> txmempool -> consensus/tx_verify"
|
||||
"evo/assetlocktx -> evo/creditpool -> evo/assetlocktx"
|
||||
"evo/assetlocktx -> llmq/quorums -> net_processing -> txmempool -> evo/assetlocktx"
|
||||
|
||||
"evo/simplifiedmns -> llmq/blockprocessor -> net_processing -> llmq/snapshot -> evo/simplifiedmns"
|
||||
"llmq/blockprocessor -> net_processing -> llmq/context -> llmq/blockprocessor"
|
||||
|
Loading…
Reference in New Issue
Block a user