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:
Konstantin Akimov 2023-07-24 23:39:38 +07:00 committed by GitHub
parent 3c65626609
commit 8a0e681cea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 2165 additions and 32 deletions

View File

@ -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 \

View File

@ -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 \

View File

@ -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");

View File

@ -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);

View File

@ -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

View File

@ -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)

View File

@ -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",

View File

@ -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
View 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
View 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

View File

@ -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)

View File

@ -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
View 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
View 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

View File

@ -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();

View File

@ -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

View File

@ -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();

View File

@ -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++)

View File

@ -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));

View File

@ -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 */

View File

@ -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>

View File

@ -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;

View File

@ -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 */

View File

@ -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

View 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()

View File

@ -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();

View File

@ -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());

View File

@ -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();

View File

@ -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
View 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
View 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

View File

@ -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());
}

View 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()

View File

@ -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

View File

@ -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")

View File

@ -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

View File

@ -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"