Skip to content

Commit

Permalink
feat!: add an implementation of DIP 0027 Credit Asset Locks (#5026)
Browse files Browse the repository at this point in the history
## 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>
  • Loading branch information
knst and UdjinM6 authored Jul 24, 2023
1 parent 3c65626 commit 8a0e681
Show file tree
Hide file tree
Showing 37 changed files with 2,165 additions and 32 deletions.
6 changes: 6 additions & 0 deletions src/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down Expand Up @@ -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 \
Expand Down Expand Up @@ -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 \
Expand Down Expand Up @@ -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 \
Expand Down
1 change: 1 addition & 0 deletions src/Makefile.test.include
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
4 changes: 4 additions & 0 deletions src/bloom.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
4 changes: 4 additions & 0 deletions src/chainparams.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ class CMainParams : public CChainParams {
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;
Expand Down Expand Up @@ -431,6 +432,7 @@ class CTestNetParams : public CChainParams {
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;
Expand Down Expand Up @@ -595,6 +597,7 @@ class CDevNetParams : public CChainParams {
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);
Expand Down Expand Up @@ -859,6 +862,7 @@ class CRegTestParams : public CChainParams {
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);
Expand Down
1 change: 1 addition & 0 deletions src/consensus/params.h
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 9 additions & 4 deletions src/consensus/tx_check.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions src/consensus/tx_verify.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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",
Expand Down
15 changes: 15 additions & 0 deletions src/core_write.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

#include <spentindex.h>

#include <evo/assetlocktx.h>
#include <evo/cbtx.h>
#include <evo/mnhftx.h>
#include <evo/providertx.h>
Expand Down Expand Up @@ -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())
Expand Down
205 changes: 205 additions & 0 deletions src/evo/assetlocktx.cpp
Original file line number Diff line number Diff line change
@@ -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());
}
Loading

0 comments on commit 8a0e681

Please sign in to comment.