Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6708af4
test: check for false-positives in rpc_scanblocks.py
theStack Oct 19, 2022
0cf634a
test: add rpc_scanblocks.py to test runner list
claude Jun 18, 2025
441e10f
test: add helper for `register{,_evo,_legacy}` calls
kwvg Jun 16, 2025
84f00c1
test: add helper for `register_fund{,_evo,_legacy}` calls
kwvg Jun 16, 2025
32986e3
test: add helper for `revoke` calls
kwvg Jun 11, 2025
bcd74eb
test: add helper for `update_registrar{,_legacy}` calls
kwvg Jun 11, 2025
b570377
test: add helper for `update_service{,_evo}` calls
kwvg Jun 16, 2025
6925cc0
rpc: extend the ability to withhold submission to all ProTxes
kwvg Jun 11, 2025
9b20d14
test: validate that the 'submit' argument works as intended
kwvg Jun 15, 2025
2e593cd
test: allow `assert_raises_rpc_error` with helper functions
kwvg Jun 12, 2025
d60ef22
refactor: use helper GetDataFromUnlockTxes to get credit pool amount
knst May 5, 2025
1d7b223
perf: use GetAncestor() to jump blocks back for CreditPool
knst May 5, 2025
2750a69
refactor: combine GetDataFromUnlockTxes and GetBlockForCreditPool to …
knst May 5, 2025
03ef4ae
perf: cache block data for credit pool for calculation asset unlock l…
knst May 5, 2025
4685392
fmt: apply clang-format suggestions
knst May 5, 2025
edbac7c
refactor: use CreditPoolPeriodBlocks for block_data_cache
knst May 27, 2025
affaddc
fix: typo in error message for GetDataFromUnlockTx
knst May 27, 2025
ea497bd
refactor: use if statement feature
knst Jun 2, 2025
c6a9c61
feat: bail out if GetTxPayload failed
knst Jun 2, 2025
9fa4012
test: simplify wait_for_quorum_list
knst May 12, 2025
34786c9
test: reduce spamming quorum list to logs while waiting
knst May 12, 2025
f58c4e4
test: enforce 1s delay for feature_mnehf test
knst May 12, 2025
9018704
test: enforce 1 second delay for wait_for_sporks helper
knst May 12, 2025
38edf2e
test: reduce delay in wait_until from 0.5s to 0.05s
knst May 12, 2025
8b6b501
test: add BIP158 false-positive element check in rpc_scanblocks.py
claude Jun 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/release-notes-6720.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Updated RPCs
------------

* A new optional field `submit` has been introduced to the `protx revoke`, `protx update_registrar`, `protx update_service` RPCs. It behaves identically to `submit` in `protx register` or `protx register_fund`.
109 changes: 56 additions & 53 deletions src/evo/creditpool.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
#include <deploymentstatus.h>
#include <logging.h>
#include <node/blockstorage.h>
#include <util/irange.h>
#include <validation.h>

#include <algorithm>
Expand Down Expand Up @@ -47,29 +46,63 @@ static bool GetDataFromUnlockTx(const CTransaction& tx, CAmount& toUnlock, uint6
}

namespace {
struct UnlockDataPerBlock {
CAmount unlocked{0};
std::unordered_set<uint64_t> indexes;
};
struct CreditPoolDataPerBlock {
CAmount credit_pool{0};
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)
static std::optional<CreditPoolDataPerBlock> GetCreditDataFromBlock(const gsl::not_null<const CBlockIndex*> block_index,
const Consensus::Params& consensusParams)
{
UnlockDataPerBlock blockData;
// There's no CbTx before DIP0003 activation
if (!DeploymentActiveAt(*block_index, Params().GetConsensus(), Consensus::DEPLOYMENT_DIP0003)) {
return std::nullopt;
}

CreditPoolDataPerBlock blockData;

static Mutex cache_mutex;
static unordered_lru_cache<uint256, CreditPoolDataPerBlock, StaticSaltedHasher> block_data_cache GUARDED_BY(
cache_mutex){static_cast<size_t>(Params().CreditPoolPeriodBlocks()) * 2};
if (LOCK(cache_mutex); block_data_cache.get(block_index->GetBlockHash(), blockData)) {
return blockData;
}

CBlock block;
if (!ReadBlockFromDisk(block, block_index, consensusParams)) {
throw std::runtime_error("failed-getcbforblock-read");
}

for (CTransactionRef tx : vtx) {
if (block.vtx.empty() || block.vtx[0]->vExtraPayload.empty() || !block.vtx[0]->IsSpecialTxVersion()) {
LogPrintf("%s: ERROR: empty CbTx for CreditPool at height=%d\n", __func__, block_index->nHeight);
return std::nullopt;
}


if (const auto opt_cbTx = GetTxPayload<CCbTx>(block.vtx[0]->vExtraPayload); opt_cbTx) {
blockData.credit_pool = opt_cbTx->creditPoolBalance;
} else {
LogPrintf("%s: WARNING: No valid CbTx at height=%d\n", __func__, block_index->nHeight);
return std::nullopt;
}
for (CTransactionRef tx : block.vtx) {
if (!tx->IsSpecialTxVersion() || 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()));
throw std::runtime_error(strprintf("%s: GetDataFromUnlockTx failed: %s", __func__, tx_state.ToString()));
}
blockData.unlocked += unlocked;
blockData.indexes.insert(index);
}

LOCK(cache_mutex);
block_data_cache.insert(block_index->GetBlockHash(), blockData);
return blockData;
}

Expand Down Expand Up @@ -112,32 +145,11 @@ void CCreditPoolManager::AddToCache(const uint256& block_hash, int height, const
}
}

static std::optional<CBlock> GetBlockForCreditPool(const gsl::not_null<const CBlockIndex*> block_index,
const Consensus::Params& consensusParams)
{
// There's no CbTx before DIP0003 activation
if (!DeploymentActiveAt(*block_index, Params().GetConsensus(), Consensus::DEPLOYMENT_DIP0003)) {
return std::nullopt;
}

CBlock block;
if (!ReadBlockFromDisk(block, block_index, consensusParams)) {
throw std::runtime_error("failed-getcbforblock-read");
}

if (block.vtx.empty() || block.vtx[0]->vExtraPayload.empty() || !block.vtx[0]->IsSpecialTxVersion()) {
LogPrintf("%s: ERROR: empty CbTx for CreditPool at height=%d\n", __func__, block_index->nHeight);
return std::nullopt;
}

return block;
}

CCreditPool CCreditPoolManager::ConstructCreditPool(const gsl::not_null<const CBlockIndex*> block_index,
CCreditPool prev, const Consensus::Params& consensusParams)
{
std::optional<CBlock> block = GetBlockForCreditPool(block_index, consensusParams);
if (!block) {
std::optional<CreditPoolDataPerBlock> opt_block_data = GetCreditDataFromBlock(block_index, consensusParams);
if (!opt_block_data) {
// If reading of previous block is not successfully, but
// prev contains credit pool related data, something strange happened
if (prev.locked != 0) {
Expand All @@ -151,63 +163,54 @@ CCreditPool CCreditPoolManager::ConstructCreditPool(const gsl::not_null<const CB
AddToCache(block_index->GetBlockHash(), block_index->nHeight, emptyPool);
return emptyPool;
}
CAmount locked = [&, func=__func__]() {
const auto opt_cbTx = GetTxPayload<CCbTx>(block->vtx[0]->vExtraPayload);
if (!opt_cbTx) {
throw std::runtime_error(strprintf("%s: failed-getcreditpool-cbtx-payload", func));
}
return opt_cbTx->creditPoolBalance;
}();
const CreditPoolDataPerBlock& blockData{*opt_block_data};

// We use here sliding window with Params().CreditPoolPeriodBlocks 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);
CRangesSet 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-duplicated", __func__));
}

const CBlockIndex* distant_block_index = block_index;
for ([[maybe_unused]] auto _ : irange::range(Params().CreditPoolPeriodBlocks())) {
distant_block_index = distant_block_index->pprev;
if (distant_block_index == nullptr) break;
}
const CBlockIndex* distant_block_index{
block_index->GetAncestor(block_index->nHeight - Params().CreditPoolPeriodBlocks())};
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;
if (std::optional<CreditPoolDataPerBlock> distant_block{GetCreditDataFromBlock(distant_block_index, consensusParams)};
distant_block) {
distantUnlocked = distant_block->unlocked;
}
}

CAmount currentLimit = locked;
CAmount currentLimit = blockData.credit_pool;
const CAmount latelyUnlocked = prev.latelyUnlocked + blockData.unlocked - distantUnlocked;
if (DeploymentActiveAt(*block_index, Params().GetConsensus(), Consensus::DEPLOYMENT_WITHDRAWALS)) {
currentLimit = std::min(currentLimit, LimitAmountV22);
} else {
// Unlock limits in pre-v22 are max(100, min(.10 * assetlockpool, 1000)) inside window
if (currentLimit + latelyUnlocked > LimitAmountLow) {
currentLimit = std::max(LimitAmountLow, locked / 10) - latelyUnlocked;
currentLimit = std::max(LimitAmountLow, blockData.credit_pool / 10) - latelyUnlocked;
if (currentLimit < 0) currentLimit = 0;
}
currentLimit = std::min(currentLimit, LimitAmountHigh - latelyUnlocked);
}

if (currentLimit != 0 || latelyUnlocked > 0 || locked > 0) {
if (currentLimit != 0 || latelyUnlocked > 0 || blockData.credit_pool > 0) {
LogPrint(BCLog::CREDITPOOL, /* Continued */
"CCreditPoolManager: asset unlock limits on height: %d locked: %d.%08d limit: %d.%08d "
"unlocked-in-window: %d.%08d\n",
block_index->nHeight, locked / COIN, locked % COIN, currentLimit / COIN, currentLimit % COIN,
latelyUnlocked / COIN, latelyUnlocked % COIN);
block_index->nHeight, blockData.credit_pool / COIN, blockData.credit_pool % COIN, currentLimit / COIN,
currentLimit % COIN, latelyUnlocked / COIN, latelyUnlocked % COIN);
}

if (currentLimit < 0) {
throw std::runtime_error(
strprintf("Negative limit for CreditPool: %d.%08d\n", currentLimit / COIN, currentLimit % COIN));
}

CCreditPool pool{locked, currentLimit, latelyUnlocked, indexes};
CCreditPool pool{blockData.credit_pool, currentLimit, latelyUnlocked, indexes};
AddToCache(block_index->GetBlockHash(), block_index->nHeight, pool);
return pool;

Expand Down
66 changes: 49 additions & 17 deletions src/rpc/evo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ static void SignSpecialTxPayloadByHash(const CMutableTransaction& tx, SpecialTxP
payload.sig = key.Sign(hash, use_legacy);
}

static std::string SignAndSendSpecialTx(const JSONRPCRequest& request, CChainstateHelper& chain_helper, const ChainstateManager& chainman, const CMutableTransaction& tx, bool fSubmit = true)
static std::string SignAndSendSpecialTx(const JSONRPCRequest& request, CChainstateHelper& chain_helper, const ChainstateManager& chainman, const CMutableTransaction& tx, bool fSubmit)
{
{
LOCK(cs_main);
Expand Down Expand Up @@ -551,9 +551,9 @@ static RPCHelpMan protx_register_fund_evo()
},
{
RPCResult{"if \"submit\" is not set or set to true",
RPCResult::Type::STR_HEX, "txid", "The transaction id"},
RPCResult::Type::STR_HEX, "txid", "The transaction id"},
RPCResult{"if \"submit\" is set to false",
RPCResult::Type::STR_HEX, "hex", "The serialized signed ProTx in hex format"},
RPCResult::Type::STR_HEX, "hex", "The serialized signed ProTx in hex format"},
},
RPCExamples{
HelpExampleCli("protx", "register_fund_evo \"" + EXAMPLE_ADDRESS[0] + "\" \"1.2.3.4:1234\" \"" + EXAMPLE_ADDRESS[1] + "\" \"93746e8731c57f87f79b3620a7982924e2931717d49540a85864bd543de11c43fb868fd63e501a1db37e19ed59ae6db4\" \"" + EXAMPLE_ADDRESS[1] + "\" 0 \"" + EXAMPLE_ADDRESS[0] + "\" \"f2dbd9b0a1f541a7c44d34a58674d0262f5feca5\" 22821 22822")},
Expand Down Expand Up @@ -590,9 +590,9 @@ static RPCHelpMan protx_register_evo()
},
{
RPCResult{"if \"submit\" is not set or set to true",
RPCResult::Type::STR_HEX, "txid", "The transaction id"},
RPCResult::Type::STR_HEX, "txid", "The transaction id"},
RPCResult{"if \"submit\" is set to false",
RPCResult::Type::STR_HEX, "hex", "The serialized signed ProTx in hex format"},
RPCResult::Type::STR_HEX, "hex", "The serialized signed ProTx in hex format"},
},
RPCExamples{
HelpExampleCli("protx", "register_evo \"0123456701234567012345670123456701234567012345670123456701234567\" 0 \"1.2.3.4:1234\" \"" + EXAMPLE_ADDRESS[1] + "\" \"93746e8731c57f87f79b3620a7982924e2931717d49540a85864bd543de11c43fb868fd63e501a1db37e19ed59ae6db4\" \"" + EXAMPLE_ADDRESS[1] + "\" 0 \"" + EXAMPLE_ADDRESS[0] + "\" \"f2dbd9b0a1f541a7c44d34a58674d0262f5feca5\" 22821 22822")},
Expand Down Expand Up @@ -897,7 +897,7 @@ static RPCHelpMan protx_register_submit()
ptx.vchSig = opt_vchSig.value();

SetTxPayload(tx, ptx);
return SignAndSendSpecialTx(request, chain_helper, chainman, tx);
return SignAndSendSpecialTx(request, chain_helper, chainman, tx, /*fSubmit=*/true);
},
};
}
Expand All @@ -915,9 +915,13 @@ static RPCHelpMan protx_update_service()
GetRpcArg("operatorKey"),
GetRpcArg("operatorPayoutAddress"),
GetRpcArg("feeSourceAddress"),
GetRpcArg("submit"),
},
RPCResult{
RPCResult::Type::STR_HEX, "txid", "The transaction id"
{
RPCResult{"if \"submit\" is not set or set to true",
RPCResult::Type::STR_HEX, "txid", "The transaction id"},
RPCResult{"if \"submit\" is set to false",
RPCResult::Type::STR_HEX, "hex", "The serialized signed ProTx in hex format"},
},
RPCExamples{
HelpExampleCli("protx", "update_service \"0123456701234567012345670123456701234567012345670123456701234567\" \"1.2.3.4:1234\" 5a2e15982e62f1e0b7cf9783c64cf7e3af3f90a52d6c40f6f95d624c0b1621cd")
Expand Down Expand Up @@ -947,9 +951,14 @@ static RPCHelpMan protx_update_service_evo()
GetRpcArg("platformHTTPPort"),
GetRpcArg("operatorPayoutAddress"),
GetRpcArg("feeSourceAddress"),
GetRpcArg("submit"),
},
{
RPCResult{"if \"submit\" is not set or set to true",
RPCResult::Type::STR_HEX, "txid", "The transaction id"},
RPCResult{"if \"submit\" is set to false",
RPCResult::Type::STR_HEX, "hex", "The serialized signed ProTx in hex format"},
},
RPCResult{
RPCResult::Type::STR_HEX, "txid", "The transaction id"},
RPCExamples{
HelpExampleCli("protx", "update_service_evo \"0123456701234567012345670123456701234567012345670123456701234567\" \"1.2.3.4:1234\" \"5a2e15982e62f1e0b7cf9783c64cf7e3af3f90a52d6c40f6f95d624c0b1621cd\" \"f2dbd9b0a1f541a7c44d34a58674d0262f5feca5\" 22821 22822")},
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
Expand Down Expand Up @@ -1055,14 +1064,19 @@ static UniValue protx_update_service_common_wrapper(const JSONRPCRequest& reques
}
}

bool fSubmit{true};
if (!request.params[paramIdx + 2].isNull()) {
fSubmit = ParseBoolV(request.params[paramIdx + 2], "submit");
}

FundSpecialTx(*wallet, tx, ptx, feeSource);

const bool isV19active = DeploymentActiveAfter(WITH_LOCK(cs_main, return chainman.ActiveChain().Tip();),
Params().GetConsensus(), Consensus::DEPLOYMENT_V19);
SignSpecialTxPayloadByHash(tx, ptx, keyOperator, !isV19active);
SetTxPayload(tx, ptx);

return SignAndSendSpecialTx(request, chain_helper, chainman, tx);
return SignAndSendSpecialTx(request, chain_helper, chainman, tx, fSubmit);
}

static RPCHelpMan protx_update_registrar_wrapper(const bool specific_legacy_bls_scheme)
Expand All @@ -1083,9 +1097,13 @@ static RPCHelpMan protx_update_registrar_wrapper(const bool specific_legacy_bls_
GetRpcArg("votingAddress_update"),
GetRpcArg("payoutAddress_update"),
GetRpcArg("feeSourceAddress"),
GetRpcArg("submit"),
},
RPCResult{
RPCResult::Type::STR_HEX, "txid", "The transaction id"
{
RPCResult{"if \"submit\" is not set or set to true",
RPCResult::Type::STR_HEX, "txid", "The transaction id"},
RPCResult{"if \"submit\" is set to false",
RPCResult::Type::STR_HEX, "hex", "The serialized signed ProTx in hex format"},
},
RPCExamples{
HelpExampleCli("protx", rpc_example)
Expand Down Expand Up @@ -1166,11 +1184,16 @@ static RPCHelpMan protx_update_registrar_wrapper(const bool specific_legacy_bls_
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, std::string("Invalid Dash address: ") + request.params[4].get_str());
}

bool fSubmit{true};
if (!request.params[5].isNull()) {
fSubmit = ParseBoolV(request.params[5], "submit");
}

FundSpecialTx(*wallet, tx, ptx, feeSourceDest);
SignSpecialTxPayloadByHash(tx, ptx, dmn->pdmnState->keyIDOwner, *wallet);
SetTxPayload(tx, ptx);

return SignAndSendSpecialTx(request, chain_helper, chainman, tx);
return SignAndSendSpecialTx(request, chain_helper, chainman, tx, fSubmit);
},
};
}
Expand Down Expand Up @@ -1198,9 +1221,13 @@ static RPCHelpMan protx_revoke()
GetRpcArg("operatorKey"),
GetRpcArg("reason"),
GetRpcArg("feeSourceAddress"),
GetRpcArg("submit"),
},
RPCResult{
RPCResult::Type::STR_HEX, "txid", "The transaction id"
{
RPCResult{"if \"submit\" is not set or set to true",
RPCResult::Type::STR_HEX, "txid", "The transaction id"},
RPCResult{"if \"submit\" is set to false",
RPCResult::Type::STR_HEX, "hex", "The serialized signed ProTx in hex format"},
},
RPCExamples{
HelpExampleCli("protx", "revoke \"0123456701234567012345670123456701234567012345670123456701234567\" \"072f36a77261cdd5d64c32d97bac417540eddca1d5612f416feb07ff75a8e240\"")
Expand Down Expand Up @@ -1265,10 +1292,15 @@ static RPCHelpMan protx_revoke()
throw JSONRPCError(RPC_INTERNAL_ERROR, "No payout or fee source addresses found, can't revoke");
}

bool fSubmit{true};
if (!request.params[4].isNull()) {
fSubmit = ParseBoolV(request.params[4], "submit");
}

SignSpecialTxPayloadByHash(tx, ptx, keyOperator, !isV19active);
SetTxPayload(tx, ptx);

return SignAndSendSpecialTx(request, chain_helper, chainman, tx);
return SignAndSendSpecialTx(request, chain_helper, chainman, tx, fSubmit);
},
};
}
Expand Down
Loading
Loading