Line data Source code
1 : // Copyright (c) 2023-2025 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 :
8 : #include <llmq/commitment.h>
9 : #include <llmq/quorumsman.h>
10 : #include <llmq/signhash.h>
11 :
12 : #include <chainparams.h>
13 : #include <consensus/params.h>
14 : #include <consensus/validation.h>
15 : #include <deploymentstatus.h>
16 : #include <logging.h>
17 : #include <node/blockstorage.h>
18 : #include <tinyformat.h>
19 : #include <util/ranges_set.h>
20 :
21 : #include <algorithm>
22 :
23 : using node::BlockManager;
24 :
25 :
26 : /**
27 : * Asset Lock Transaction
28 : */
29 15 : bool CheckAssetLockTx(const CTransaction& tx, TxValidationState& state)
30 : {
31 15 : if (tx.nType != TRANSACTION_ASSET_LOCK) {
32 1 : return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-type");
33 : }
34 :
35 14 : CAmount returnAmount{0};
36 35 : for (const CTxOut& txout : tx.vout) {
37 25 : const CScript& script = txout.scriptPubKey;
38 25 : if (script.empty() || script[0] != OP_RETURN) continue;
39 :
40 14 : if (script.size() != 2 || script[1] != 0) return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-non-empty-return");
41 :
42 13 : if (txout.nValue == 0 || !MoneyRange(txout.nValue)) return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-opreturn-outofrange");
43 :
44 : // Should be only one OP_RETURN
45 11 : if (returnAmount > 0) return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-multiple-return");
46 10 : returnAmount = txout.nValue;
47 : }
48 :
49 10 : if (returnAmount == 0) return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-no-return");
50 :
51 9 : const auto opt_assetLockTx = GetTxPayload<CAssetLockPayload>(tx);
52 9 : if (!opt_assetLockTx.has_value()) {
53 0 : return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-payload");
54 : }
55 :
56 9 : if (opt_assetLockTx->getVersion() == 0 || opt_assetLockTx->getVersion() > CAssetLockPayload::CURRENT_VERSION) {
57 0 : return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-version");
58 : }
59 :
60 9 : if (opt_assetLockTx->getCreditOutputs().empty()) {
61 0 : return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-emptycreditoutputs");
62 : }
63 :
64 9 : CAmount creditOutputsAmount = 0;
65 20 : for (const CTxOut& out : opt_assetLockTx->getCreditOutputs()) {
66 15 : if (out.nValue == 0 || !MoneyRange(out.nValue) || !MoneyRange(creditOutputsAmount + out.nValue)) {
67 3 : return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-credit-outofrange");
68 : }
69 :
70 12 : creditOutputsAmount += out.nValue;
71 12 : if (!out.scriptPubKey.IsPayToPublicKeyHash()) {
72 1 : return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-pubKeyHash");
73 : }
74 : }
75 5 : if (creditOutputsAmount != returnAmount) {
76 3 : return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetlocktx-creditamount");
77 : }
78 :
79 2 : return true;
80 15 : }
81 :
82 0 : std::string CAssetLockPayload::ToString() const
83 : {
84 0 : std::string outputs{"["};
85 0 : for (const CTxOut& tx: creditOutputs) {
86 0 : outputs.append(tx.ToString());
87 0 : outputs.append(",");
88 : }
89 0 : outputs.back() = ']';
90 0 : return strprintf("CAssetLockPayload(nVersion=%d,creditOutputs=%s)", nVersion, outputs.c_str());
91 0 : }
92 :
93 : /**
94 : * Asset Unlock Transaction (withdrawals)
95 : */
96 :
97 : const std::string ASSETUNLOCK_REQUESTID_PREFIX = "plwdtx";
98 :
99 0 : bool CAssetUnlockPayload::VerifySig(const llmq::CQuorumManager& qman, const uint256& msgHash, gsl::not_null<const CBlockIndex*> pindexTip, TxValidationState& state) const
100 : {
101 : // That quourm hash must be active at `requestHeight`,
102 : // and at the quorumHash must be active in either the current or previous quorum cycle
103 : // and the sig must validate against that specific quorumHash.
104 :
105 :
106 0 : Consensus::LLMQType llmqType = Params().GetConsensus().llmqTypePlatform;
107 :
108 0 : const auto& llmq_params_opt = Params().GetLLMQ(llmqType);
109 0 : assert(llmq_params_opt.has_value());
110 :
111 : // We check all active quorums + 1 the latest inactive
112 0 : const int quorums_to_scan = llmq_params_opt->signingActiveQuorumCount + 1;
113 0 : const auto quorums = qman.ScanQuorums(llmqType, pindexTip, quorums_to_scan);
114 :
115 0 : if (bool isActive = std::any_of(quorums.begin(), quorums.end(), [&](const auto &q) { return q->qc->quorumHash == quorumHash; }); !isActive) {
116 0 : return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-assetunlock-too-old-quorum");
117 : }
118 :
119 0 : if (static_cast<uint32_t>(pindexTip->nHeight) < requestedHeight || pindexTip->nHeight >= getHeightToExpiry()) {
120 0 : LogPrint(BCLog::CREDITPOOL, "Asset unlock tx %d with requested height %d could not be accepted on height: %d\n",
121 : index, requestedHeight, pindexTip->nHeight);
122 0 : return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-assetunlock-too-late");
123 : }
124 :
125 0 : const auto quorum = qman.GetQuorum(llmqType, quorumHash);
126 : // quorum must be valid at this point. Let's check and throw error just in case
127 0 : if (!quorum) {
128 0 : LogPrintf("%s: ERROR! No quorum for credit pool found for hash=%s\n", __func__, quorumHash.ToString());
129 0 : return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-assetunlock-quorum-internal-error");
130 : }
131 :
132 0 : const uint256 requestId = ::SerializeHash(std::make_pair(ASSETUNLOCK_REQUESTID_PREFIX, index));
133 :
134 0 : if (const llmq::SignHash signHash(llmqType, quorum->qc->quorumHash, requestId, msgHash);
135 0 : quorumSig.VerifyInsecure(quorum->qc->quorumPublicKey, signHash.Get())) {
136 0 : return true;
137 : }
138 :
139 0 : return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-assetunlock-not-verified");
140 0 : }
141 :
142 11 : bool CheckAssetUnlockTx(const BlockManager& blockman, const llmq::CQuorumManager& qman, const CTransaction& tx, gsl::not_null<const CBlockIndex*> pindexPrev, const std::optional<CRangesSet>& indexes, TxValidationState& state)
143 : {
144 : // Some checks depends from blockchain status also, such as `known indexes` and `withdrawal limits`
145 : // They are omitted here and done by CCreditPool
146 11 : if (tx.nType != TRANSACTION_ASSET_UNLOCK) {
147 1 : return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetunlocktx-type");
148 : }
149 :
150 10 : if (!tx.vin.empty()) {
151 1 : return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetunlocktx-have-input");
152 : }
153 :
154 9 : if (tx.vout.size() > CAssetUnlockPayload::MAXIMUM_WITHDRAWALS) {
155 1 : return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetunlocktx-too-many-outs");
156 : }
157 :
158 8 : const auto opt_assetUnlockTx = GetTxPayload<CAssetUnlockPayload>(tx);
159 8 : if (!opt_assetUnlockTx) {
160 0 : return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetunlocktx-payload");
161 : }
162 8 : auto& assetUnlockTx = *opt_assetUnlockTx;
163 :
164 8 : if (assetUnlockTx.getVersion() == 0 || assetUnlockTx.getVersion() > CAssetUnlockPayload::CURRENT_VERSION) {
165 3 : return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetunlocktx-version");
166 : }
167 :
168 5 : if (indexes != std::nullopt && indexes->Contains(assetUnlockTx.getIndex())) {
169 1 : return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-assetunlock-duplicated-index");
170 : }
171 :
172 8 : if (LOCK(::cs_main); blockman.LookupBlockIndex(assetUnlockTx.getQuorumHash()) == nullptr) {
173 4 : return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-assetunlock-quorum-hash");
174 : }
175 :
176 : // Copy transaction except `quorumSig` field to calculate hash
177 0 : CMutableTransaction tx_copy(tx);
178 0 : const CAssetUnlockPayload payload_copy{assetUnlockTx.getVersion(), assetUnlockTx.getIndex(), assetUnlockTx.getFee(), assetUnlockTx.getRequestedHeight(), assetUnlockTx.getQuorumHash(), CBLSSignature{}};
179 0 : SetTxPayload(tx_copy, payload_copy);
180 :
181 0 : uint256 msgHash = tx_copy.GetHash();
182 :
183 0 : return assetUnlockTx.VerifySig(qman, msgHash, pindexPrev, state);
184 11 : }
185 :
186 0 : bool GetAssetUnlockFee(const CTransaction& tx, CAmount& txfee, TxValidationState& state)
187 : {
188 0 : const auto opt_assetUnlockTx = GetTxPayload<CAssetUnlockPayload>(tx);
189 0 : if (!opt_assetUnlockTx) {
190 0 : return state.Invalid(TxValidationResult::TX_BAD_SPECIAL, "bad-assetunlocktx-payload");
191 : }
192 0 : const CAmount txfee_aux = opt_assetUnlockTx->getFee();
193 0 : if (txfee_aux == 0 || !MoneyRange(txfee_aux)) {
194 0 : return state.Invalid(TxValidationResult::TX_CONSENSUS, "bad-txns-assetunlock-fee-outofrange");
195 : }
196 0 : txfee = txfee_aux;
197 0 : return true;
198 0 : }
199 :
200 0 : std::string CAssetUnlockPayload::ToString() const
201 : {
202 0 : return strprintf("CAssetUnlockPayload(nVersion=%d,index=%d,fee=%d.%08d,requestedHeight=%d,quorumHash=%d,quorumSig=%s",
203 0 : nVersion, index, fee / COIN, fee % COIN, requestedHeight, quorumHash.GetHex(), quorumSig.ToString().c_str());
204 0 : }
|