Skip to content

Commit 8a0e681

Browse files
knstUdjinM6
andauthored
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>
1 parent 3c65626 commit 8a0e681

37 files changed

+2165
-32
lines changed

src/Makefile.am

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,10 @@ BITCOIN_CORE_H = \
169169
cuckoocache.h \
170170
ctpl_stl.h \
171171
cxxtimer.hpp \
172+
evo/assetlocktx.h \
172173
evo/dmn_types.h \
173174
evo/cbtx.h \
175+
evo/creditpool.h \
174176
evo/deterministicmns.h \
175177
evo/dmnstate.h \
176178
evo/evodb.h \
@@ -320,6 +322,7 @@ BITCOIN_CORE_H = \
320322
util/underlying.h \
321323
util/serfloat.h \
322324
util/settings.h \
325+
util/skip_set.h \
323326
util/sock.h \
324327
util/string.h \
325328
util/time.h \
@@ -384,7 +387,9 @@ libbitcoin_server_a_SOURCES = \
384387
consensus/tx_verify.cpp \
385388
dbwrapper.cpp \
386389
dsnotificationinterface.cpp \
390+
evo/assetlocktx.cpp \
387391
evo/cbtx.cpp \
392+
evo/creditpool.cpp \
388393
evo/deterministicmns.cpp \
389394
evo/dmnstate.cpp \
390395
evo/evodb.cpp \
@@ -731,6 +736,7 @@ libbitcoin_util_a_SOURCES = \
731736
util/message.cpp \
732737
util/moneystr.cpp \
733738
util/settings.cpp \
739+
util/skip_set.cpp \
734740
util/spanparsing.cpp \
735741
util/strencodings.cpp \
736742
util/time.cpp \

src/Makefile.test.include

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ BITCOIN_TESTS =\
9595
test/dip0020opcodes_tests.cpp \
9696
test/descriptor_tests.cpp \
9797
test/dynamic_activation_thresholds_tests.cpp \
98+
test/evo_assetlocks_tests.cpp \
9899
test/evo_deterministicmns_tests.cpp \
99100
test/evo_instantsend_tests.cpp \
100101
test/evo_simplifiedmns_tests.cpp \

src/bloom.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,10 @@ bool CBloomFilter::CheckSpecialTransactionMatchesAndUpdate(const CTransaction &t
194194
case(TRANSACTION_MNHF_SIGNAL):
195195
// No additional checks for this transaction types
196196
return false;
197+
case(TRANSACTION_ASSET_LOCK):
198+
case(TRANSACTION_ASSET_UNLOCK):
199+
// TODO asset lock/unlock bloom?
200+
return false;
197201
}
198202

199203
LogPrintf("Unknown special transaction type in Bloom filter check.\n");

src/chainparams.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ class CMainParams : public CChainParams {
250250
consensus.llmqTypeDIP0024InstantSend = Consensus::LLMQType::LLMQ_60_75;
251251
consensus.llmqTypePlatform = Consensus::LLMQType::LLMQ_100_67;
252252
consensus.llmqTypeMnhf = Consensus::LLMQType::LLMQ_400_85;
253+
consensus.llmqTypeAssetLocks = Consensus::LLMQType::LLMQ_400_85;
253254

254255
fDefaultConsistencyChecks = false;
255256
fRequireStandard = true;
@@ -431,6 +432,7 @@ class CTestNetParams : public CChainParams {
431432
consensus.llmqTypeDIP0024InstantSend = Consensus::LLMQType::LLMQ_60_75;
432433
consensus.llmqTypePlatform = Consensus::LLMQType::LLMQ_25_67;
433434
consensus.llmqTypeMnhf = Consensus::LLMQType::LLMQ_50_60;
435+
consensus.llmqTypeAssetLocks = Consensus::LLMQType::LLMQ_50_60;
434436

435437
fDefaultConsistencyChecks = false;
436438
fRequireStandard = false;
@@ -595,6 +597,7 @@ class CDevNetParams : public CChainParams {
595597
consensus.llmqTypeDIP0024InstantSend = Consensus::LLMQType::LLMQ_60_75;
596598
consensus.llmqTypePlatform = Consensus::LLMQType::LLMQ_100_67;
597599
consensus.llmqTypeMnhf = Consensus::LLMQType::LLMQ_50_60;
600+
consensus.llmqTypeAssetLocks = Consensus::LLMQType::LLMQ_50_60;
598601

599602
UpdateDevnetLLMQChainLocksFromArgs(args);
600603
UpdateDevnetLLMQInstantSendFromArgs(args);
@@ -859,6 +862,7 @@ class CRegTestParams : public CChainParams {
859862
consensus.llmqTypeDIP0024InstantSend = Consensus::LLMQType::LLMQ_TEST_DIP0024;
860863
consensus.llmqTypePlatform = Consensus::LLMQType::LLMQ_TEST_PLATFORM;
861864
consensus.llmqTypeMnhf = Consensus::LLMQType::LLMQ_TEST;
865+
consensus.llmqTypeAssetLocks = Consensus::LLMQType::LLMQ_TEST;
862866

863867
UpdateLLMQTestParametersFromArgs(args, Consensus::LLMQType::LLMQ_TEST);
864868
UpdateLLMQTestParametersFromArgs(args, Consensus::LLMQType::LLMQ_TEST_INSTANTSEND);

src/consensus/params.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ struct Params {
126126
LLMQType llmqTypeDIP0024InstantSend{LLMQType::LLMQ_NONE};
127127
LLMQType llmqTypePlatform{LLMQType::LLMQ_NONE};
128128
LLMQType llmqTypeMnhf{LLMQType::LLMQ_NONE};
129+
LLMQType llmqTypeAssetLocks{LLMQType::LLMQ_NONE};
129130
};
130131
} // namespace Consensus
131132

src/consensus/tx_check.cpp

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,20 @@
1212

1313
bool CheckTransaction(const CTransaction& tx, TxValidationState& state)
1414
{
15-
bool allowEmptyTxInOut = false;
15+
bool allowEmptyTxIn = false;
16+
bool allowEmptyTxOut = false;
1617
if (tx.nType == TRANSACTION_QUORUM_COMMITMENT) {
17-
allowEmptyTxInOut = true;
18+
allowEmptyTxIn = true;
19+
allowEmptyTxOut = true;
20+
}
21+
if (tx.nType == TRANSACTION_ASSET_UNLOCK) {
22+
allowEmptyTxIn = true;
1823
}
1924

2025
// Basic checks that don't depend on any context
21-
if (!allowEmptyTxInOut && tx.vin.empty())
26+
if (!allowEmptyTxIn && tx.vin.empty())
2227
return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-txns-vin-empty");
23-
if (!allowEmptyTxInOut && tx.vout.empty())
28+
if (!allowEmptyTxOut && tx.vout.empty())
2429
return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-txns-vout-empty");
2530
// Size limits
2631
if (::GetSerializeSize(tx, PROTOCOL_VERSION) > MAX_LEGACY_BLOCK_SIZE)

src/consensus/tx_verify.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <primitives/transaction.h>
99
#include <script/interpreter.h>
1010
#include <consensus/validation.h>
11+
#include <evo/assetlocktx.h>
1112

1213
// TODO remove the following dependencies
1314
#include <chain.h>
@@ -160,6 +161,11 @@ unsigned int GetTransactionSigOpCount(const CTransaction& tx, const CCoinsViewCa
160161

161162
bool Consensus::CheckTxInputs(const CTransaction& tx, TxValidationState& state, const CCoinsViewCache& inputs, int nSpendHeight, CAmount& txfee)
162163
{
164+
165+
if (bool isAssetUnlockTx = (tx.nVersion == 3 && tx.nType == TRANSACTION_ASSET_UNLOCK); isAssetUnlockTx) {
166+
return GetAssetUnlockFee(tx, txfee, state);
167+
}
168+
163169
// are the actual inputs available?
164170
if (!inputs.HaveInputs(tx)) {
165171
return state.Invalid(TxValidationResult::TX_MISSING_INPUTS, "bad-txns-inputs-missingorspent",

src/core_write.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
#include <spentindex.h>
1818

19+
#include <evo/assetlocktx.h>
1920
#include <evo/cbtx.h>
2021
#include <evo/mnhftx.h>
2122
#include <evo/providertx.h>
@@ -310,6 +311,20 @@ void TxToUniv(const CTransaction& tx, const uint256& hashBlock, UniValue& entry,
310311
mnhfTx.ToJson(obj);
311312
entry.pushKV("mnhfTx", obj);
312313
}
314+
} else if (tx.nType == TRANSACTION_ASSET_LOCK) {
315+
CAssetLockPayload assetLockTx;
316+
if (!GetTxPayload(tx, assetLockTx)) {
317+
UniValue obj;
318+
assetLockTx.ToJson(obj);
319+
entry.pushKV("assetLockTx", obj);
320+
}
321+
} else if (tx.nType == TRANSACTION_ASSET_UNLOCK) {
322+
CAssetUnlockPayload assetUnlockTx;
323+
if (!GetTxPayload(tx, assetUnlockTx)) {
324+
UniValue obj;
325+
assetUnlockTx.ToJson(obj);
326+
entry.pushKV("assetUnlockTx", obj);
327+
}
313328
}
314329

315330
if (!hashBlock.IsNull())

src/evo/assetlocktx.cpp

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
// Copyright (c) 2023 The Dash Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#include <evo/assetlocktx.h>
6+
#include <evo/specialtx.h>
7+
#include <evo/creditpool.h>
8+
9+
#include <consensus/params.h>
10+
11+
#include <chainparams.h>
12+
#include <logging.h>
13+
#include <validation.h>
14+
15+
#include <llmq/commitment.h>
16+
#include <llmq/signing.h>
17+
#include <llmq/utils.h>
18+
#include <llmq/quorums.h>
19+
20+
#include <algorithm>
21+
22+
/**
23+
* Common code for Asset Lock and Asset Unlock
24+
*/
25+
bool CheckAssetLockUnlockTx(const CTransaction& tx, const CBlockIndex* pindexPrev, const CCreditPool& creditPool, TxValidationState& state)
26+
{
27+
switch (tx.nType) {
28+
case TRANSACTION_ASSET_LOCK:
29+
return CheckAssetLockTx(tx, state);
30+
case TRANSACTION_ASSET_UNLOCK:
31+
return CheckAssetUnlockTx(tx, pindexPrev, creditPool, state);
32+
default:
33+
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-not-asset-locks-at-all");
34+
}
35+
}
36+
37+
/**
38+
* Asset Lock Transaction
39+
*/
40+
bool CheckAssetLockTx(const CTransaction& tx, TxValidationState& state)
41+
{
42+
if (tx.nType != TRANSACTION_ASSET_LOCK) {
43+
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-type");
44+
}
45+
46+
CAmount returnAmount{0};
47+
for (const CTxOut& txout : tx.vout) {
48+
const CScript& script = txout.scriptPubKey;
49+
if (script.empty() || script[0] != OP_RETURN) continue;
50+
51+
if (script.size() != 2 || script[1] != 0) return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-non-empty-return");
52+
53+
if (txout.nValue == 0 || !MoneyRange(txout.nValue)) return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-opreturn-outofrange");
54+
55+
// Should be only one OP_RETURN
56+
if (returnAmount > 0) return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-multiple-return");
57+
returnAmount = txout.nValue;
58+
}
59+
60+
if (returnAmount == 0) return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-no-return");
61+
62+
CAssetLockPayload assetLockTx;
63+
if (!GetTxPayload(tx, assetLockTx)) {
64+
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-payload");
65+
}
66+
67+
if (assetLockTx.getVersion() == 0 || assetLockTx.getVersion() > CAssetLockPayload::CURRENT_VERSION) {
68+
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-version");
69+
}
70+
71+
if (assetLockTx.getCreditOutputs().empty()) {
72+
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-emptycreditoutputs");
73+
}
74+
75+
CAmount creditOutputsAmount = 0;
76+
for (const CTxOut& out : assetLockTx.getCreditOutputs()) {
77+
if (out.nValue == 0 || !MoneyRange(out.nValue) || !MoneyRange(creditOutputsAmount + out.nValue)) {
78+
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-credit-outofrange");
79+
}
80+
81+
creditOutputsAmount += out.nValue;
82+
if (!out.scriptPubKey.IsPayToPublicKeyHash()) {
83+
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-pubKeyHash");
84+
}
85+
}
86+
if (creditOutputsAmount != returnAmount) {
87+
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-creditamount");
88+
}
89+
90+
return true;
91+
}
92+
93+
std::string CAssetLockPayload::ToString() const
94+
{
95+
std::string outputs{"["};
96+
for (const CTxOut& tx: creditOutputs) {
97+
outputs.append(tx.ToString());
98+
outputs.append(",");
99+
}
100+
outputs.back() = ']';
101+
return strprintf("CAssetLockPayload(nVersion=%d,creditOutputs=%s)", nVersion, outputs.c_str());
102+
}
103+
104+
/**
105+
* Asset Unlock Transaction (withdrawals)
106+
*/
107+
108+
const std::string ASSETUNLOCK_REQUESTID_PREFIX = "plwdtx";
109+
110+
bool CAssetUnlockPayload::VerifySig(const uint256& msgHash, const CBlockIndex* pindexTip, TxValidationState& state) const
111+
{
112+
// That quourm hash must be active at `requestHeight`,
113+
// and at the quorumHash must be active in either the current or previous quorum cycle
114+
// and the sig must validate against that specific quorumHash.
115+
116+
Consensus::LLMQType llmqType = Params().GetConsensus().llmqTypeAssetLocks;
117+
118+
// We check at most 2 quorums
119+
const auto quorums = llmq::quorumManager->ScanQuorums(llmqType, pindexTip, 2);
120+
bool isActive = std::any_of(quorums.begin(), quorums.end(), [&](const auto &q) { return q->qc->quorumHash == quorumHash; });
121+
122+
if (!isActive) {
123+
return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-assetunlock-not-active-quorum");
124+
}
125+
126+
if (pindexTip->nHeight < requestedHeight || pindexTip->nHeight >= getHeightToExpiry()) {
127+
LogPrintf("Asset unlock tx %d with requested height %d could not be accepted on height: %d\n",
128+
index, requestedHeight, pindexTip->nHeight);
129+
return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-assetunlock-too-late");
130+
}
131+
132+
const auto quorum = llmq::quorumManager->GetQuorum(llmqType, quorumHash);
133+
assert(quorum);
134+
135+
const uint256 requestId = ::SerializeHash(std::make_pair(ASSETUNLOCK_REQUESTID_PREFIX, index));
136+
137+
const uint256 signHash = llmq::utils::BuildSignHash(llmqType, quorum->qc->quorumHash, requestId, msgHash);
138+
if (quorumSig.VerifyInsecure(quorum->qc->quorumPublicKey, signHash)) {
139+
return true;
140+
}
141+
142+
return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-assetunlock-not-verified");
143+
}
144+
145+
bool CheckAssetUnlockTx(const CTransaction& tx, const CBlockIndex* pindexPrev, const CCreditPool& creditPool, TxValidationState& state)
146+
{
147+
if (tx.nType != TRANSACTION_ASSET_UNLOCK) {
148+
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetunlocktx-type");
149+
}
150+
151+
if (!tx.vin.empty()) {
152+
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetunlocktx-have-input");
153+
}
154+
155+
if (tx.vout.size() > CAssetUnlockPayload::MAXIMUM_WITHDRAWALS) {
156+
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetunlocktx-too-many-outs");
157+
}
158+
159+
CAssetUnlockPayload assetUnlockTx;
160+
if (!GetTxPayload(tx, assetUnlockTx)) {
161+
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetunlocktx-payload");
162+
}
163+
164+
if (assetUnlockTx.getVersion() == 0 || assetUnlockTx.getVersion() > CAssetUnlockPayload::CURRENT_VERSION) {
165+
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetunlocktx-version");
166+
}
167+
168+
if (creditPool.indexes.Contains(assetUnlockTx.getIndex())) {
169+
return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-assetunlock-duplicated-index");
170+
}
171+
172+
const CBlockIndex* pindexQuorum = WITH_LOCK(cs_main, return g_chainman.m_blockman.LookupBlockIndex(assetUnlockTx.getQuorumHash()));
173+
if (!pindexQuorum) {
174+
return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-assetunlock-quorum-hash");
175+
}
176+
177+
// Copy transaction except `quorumSig` field to calculate hash
178+
CMutableTransaction tx_copy(tx);
179+
const CAssetUnlockPayload payload_copy{assetUnlockTx.getVersion(), assetUnlockTx.getIndex(), assetUnlockTx.getFee(), assetUnlockTx.getRequestedHeight(), assetUnlockTx.getQuorumHash(), CBLSSignature{}};
180+
SetTxPayload(tx_copy, payload_copy);
181+
182+
uint256 msgHash = tx_copy.GetHash();
183+
184+
return assetUnlockTx.VerifySig(msgHash, pindexPrev, state);
185+
}
186+
187+
bool GetAssetUnlockFee(const CTransaction& tx, CAmount& txfee, TxValidationState& state)
188+
{
189+
CAssetUnlockPayload assetUnlockTx;
190+
if (!GetTxPayload(tx, assetUnlockTx)) {
191+
return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetunlocktx-payload");
192+
}
193+
const CAmount txfee_aux = assetUnlockTx.getFee();
194+
if (txfee_aux == 0 || !MoneyRange(txfee_aux)) {
195+
return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-txns-assetunlock-fee-outofrange");
196+
}
197+
txfee = txfee_aux;
198+
return true;
199+
}
200+
201+
std::string CAssetUnlockPayload::ToString() const
202+
{
203+
return strprintf("CAssetUnlockPayload(nVersion=%d,index=%d,fee=%d.%08d,requestedHeight=%d,quorumHash=%d,quorumSig=%s",
204+
nVersion, index, fee / COIN, fee % COIN, requestedHeight, quorumHash.GetHex(), quorumSig.ToString().c_str());
205+
}

0 commit comments

Comments
 (0)