Merge #6115: backport: v21.0.x rc.2

db828177bf Merge #6106: feat: create new composite quorum-command platformsign (pasta)
a45e6df58b Merge #6104: fix: adjust incorrect parameter description that says there is a default that doesn't exist (pasta)
7330982631 Merge #6100: feat: make whitelist works with composite commands for platform needs (pasta)
9998ffd92b Merge #6096: feat: split type of error in submitchainlock - return enum in CL verifying code (pasta)
cdf7a25012 Merge #6095: fix: createwallet to require 'load_on_startup' for descriptor wallets (pasta)
c1c2c55690 Merge #6092: fix: mixing for partially unlocked descriptor wallets (pasta)
117548660d Merge #6073: feat: add logging for RPC HTTP requests: command, user, http-code, time of running (pasta)

Pull request description:

  ## Issue being fixed or feature implemented
  Backports a set of 6 PRs needed in rc.2

  ## What was done?
  Backported PRs with labels

  ## How Has This Been Tested?

  ## Breaking Changes

  ## Checklist:
    _Go over all the following points, and put an `x` in all the boxes that apply._
  - [ ] I have performed a self-review of my own code
  - [ ] I have commented my code, particularly in hard-to-understand areas
  - [ ] I have added or updated relevant unit/integration/functional/e2e tests
  - [ ] I have made corresponding changes to the documentation
  - [x] I have assigned this pull request to a milestone _(for repository code-owners and collaborators only)_

ACKs for top commit:
  kwvg:
    LGTM, utACK db828177bf
  UdjinM6:
    utACK db828177bf

Tree-SHA512: 1b242c5db04bd5873ef622543bc2a25e29567f15962c677ea51ff05cb784291968d18f419bf611c206b912e8f15d687208ae75af33aab89038b6f0167d99c4bf
This commit is contained in:
pasta 2024-07-15 17:00:11 -05:00
commit b0b6ba11c1
No known key found for this signature in database
GPG Key ID: 52527BEDABE87984
23 changed files with 250 additions and 123 deletions

View File

@ -35,6 +35,7 @@ Descriptor Wallet should be created.
Without those options being set, a Legacy Wallet will be created instead.
#### `IsMine` Semantics
`IsMine` refers to the function used to determine whether a script belongs to the wallet.
@ -117,3 +118,4 @@ descriptors with private keys for now as explained earlier.
## RPC changes
- `createwallet` has changed list of arguments: `createwallet "wallet_name" ( disable_private_keys blank "passphrase" avoid_reuse descriptors load_on_startup )`
`load_on_startup` used to be an argument 5 but now has a number 6.
- `createwallet` requires specifying the `load_on_startup` flag when creating descriptor wallets due to breaking changes in v21.

View File

@ -0,0 +1,9 @@
## Remote Procedure Call (RPC) Changes
### Improved support of composite commands
Dash Core's composite commands such as `quorum list` or `bls generate` now are compatible with a whitelist feature.
For example, whitelist `getblockcount,quorumlist` will let to call commands `getblockcount`, `quorum list`, but not `quorum sign`
Note, that adding simple `quorum` in whitelist will allow to use all kind of `quorum...` commands, such as `quorum`, `quorum list`, `quorum sign`, etc

View File

@ -0,0 +1,6 @@
## Remote Procedure Call (RPC) Changes
### The new RPCs are:
- `quorum signplatform` This RPC is added for Platform needs. This composite command let to limit quorum type for signing by platform. It is equivalent of `quorum sign <platform type>`.

View File

@ -25,7 +25,7 @@ void SetupChainParamsBaseOptions(ArgsManager& argsman)
argsman.AddArg("-highsubsidyblocks=<n>", "The number of blocks with a higher than normal subsidy to mine at the start of a chain. Block after that height will have fixed subsidy base. (default: 0, devnet-only)", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS);
argsman.AddArg("-highsubsidyfactor=<n>", "The factor to multiply the normal block subsidy by while in the highsubsidyblocks window of a chain (default: 1, devnet-only)", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS);
argsman.AddArg("-llmqchainlocks=<quorum name>", "Override the default LLMQ type used for ChainLocks. Allows using ChainLocks with smaller LLMQs. (default: llmq_devnet, devnet-only)", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS);
argsman.AddArg("-llmqdevnetparams=<size>:<threshold>", "Override the default LLMQ size for the LLMQ_DEVNET quorum (default: 3:2, devnet-only)", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS);
argsman.AddArg("-llmqdevnetparams=<size>:<threshold>", "Override the default LLMQ size for the LLMQ_DEVNET quorum (devnet-only)", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS);
argsman.AddArg("-llmqinstantsenddip0024=<quorum name>", "Override the default LLMQ type used for InstantSendDIP0024. (default: llmq_devnet_dip0024, devnet-only)", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS);
argsman.AddArg("-llmqplatform=<quorum name>", "Override the default LLMQ type used for Platform. (default: llmq_devnet_platform, devnet-only)", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS);
argsman.AddArg("-llmqmnhf=<quorum name>", "Override the default LLMQ type used for EHF. (default: llmq_devnet, devnet-only)", ArgsManager::ALLOW_ANY, OptionsCategory::CHAINPARAMS);

View File

@ -374,7 +374,7 @@ bool CheckCbTxBestChainlock(const CBlock& block, const CBlockIndex* pindex,
return true;
}
uint256 curBlockCoinbaseCLBlockHash = pindex->GetAncestor(curBlockCoinbaseCLHeight)->GetBlockHash();
if (!chainlock_handler.VerifyChainLock(llmq::CChainLockSig(curBlockCoinbaseCLHeight, curBlockCoinbaseCLBlockHash, opt_cbTx->bestCLSignature))) {
if (chainlock_handler.VerifyChainLock(llmq::CChainLockSig(curBlockCoinbaseCLHeight, curBlockCoinbaseCLBlockHash, opt_cbTx->bestCLSignature)) != llmq::VerifyRecSigStatus::Valid) {
return state.Invalid(BlockValidationResult::BLOCK_CONSENSUS, "bad-cbtx-invalid-clsig");
}
} else if (opt_cbTx->bestCLHeightDiff != 0) {

View File

@ -72,7 +72,47 @@ static std::vector<std::vector<std::string>> g_rpcauth;
static std::map<std::string, std::set<std::string>> g_rpc_whitelist;
static bool g_rpc_whitelist_default = false;
static void JSONErrorReply(HTTPRequest* req, const UniValue& objError, const UniValue& id)
extern std::vector<std::string> g_external_usernames;
class RpcHttpRequest
{
public:
HTTPRequest* m_req;
int64_t m_startTime;
int m_status{0};
std::string user;
std::string command;
RpcHttpRequest(HTTPRequest* req) :
m_req{req},
m_startTime{GetTimeMicros()}
{}
~RpcHttpRequest()
{
const bool is_external = find(g_external_usernames.begin(), g_external_usernames.end(), user) != g_external_usernames.end();
LogPrint(BCLog::BENCHMARK, "HTTP RPC request handled: user=%s command=%s external=%s status=%d elapsed_time_ms=%d\n", user, command, is_external, m_status, (GetTimeMicros() - m_startTime) / 1000);
}
bool send_reply(int status, const std::string& response = "")
{
m_status = status;
m_req->WriteReply(status, response);
return m_status == HTTP_OK;
}
};
static bool whitelisted(JSONRPCRequest jreq)
{
if (g_rpc_whitelist[jreq.authUser].count(jreq.strMethod)) return true;
// check for composite command after
if (!jreq.params.isArray() || jreq.params.empty()) return false;
if (!jreq.params[0].isStr()) return false;
return g_rpc_whitelist[jreq.authUser].count(jreq.strMethod + jreq.params[0].get_str());
}
static bool JSONErrorReply(RpcHttpRequest& rpcRequest, const UniValue& objError, const UniValue& id)
{
// Send error reply from json-rpc error object
int nStatus = HTTP_INTERNAL_SERVER_ERROR;
@ -88,8 +128,8 @@ static void JSONErrorReply(HTTPRequest* req, const UniValue& objError, const Uni
std::string strReply = JSONRPCReply(NullUniValue, objError, id);
req->WriteHeader("Content-Type", "application/json");
req->WriteReply(nStatus, strReply);
rpcRequest.m_req->WriteHeader("Content-Type", "application/json");
return rpcRequest.send_reply(nStatus, strReply);
}
//This function checks username and password against -rpcauth
@ -146,24 +186,25 @@ static bool RPCAuthorized(const std::string& strAuth, std::string& strAuthUserna
return multiUserAuthorized(strUserPass);
}
static bool HTTPReq_JSONRPC(const CoreContext& context, HTTPRequest* req, bool external = false)
static bool HTTPReq_JSONRPC(const CoreContext& context, HTTPRequest* req)
{
RpcHttpRequest rpcRequest(req);
// JSONRPC handles only POST
if (req->GetRequestMethod() != HTTPRequest::POST) {
req->WriteReply(HTTP_BAD_METHOD, "JSONRPC server handles only POST requests");
return false;
return rpcRequest.send_reply(HTTP_BAD_METHOD, "JSONRPC server handles only POST requests");
}
// Check authorization
std::pair<bool, std::string> authHeader = req->GetHeader("authorization");
if (!authHeader.first) {
req->WriteHeader("WWW-Authenticate", WWW_AUTH_HEADER_DATA);
req->WriteReply(HTTP_UNAUTHORIZED);
return false;
return rpcRequest.send_reply(HTTP_UNAUTHORIZED);
}
JSONRPCRequest jreq(context);
jreq.peerAddr = req->GetPeer().ToString();
if (!RPCAuthorized(authHeader.second, jreq.authUser)) {
if (!RPCAuthorized(authHeader.second, rpcRequest.user)) {
LogPrintf("ThreadRPCServer incorrect password attempt from %s\n", jreq.peerAddr);
/* Deter brute-forcing
@ -172,9 +213,9 @@ static bool HTTPReq_JSONRPC(const CoreContext& context, HTTPRequest* req, bool e
UninterruptibleSleep(std::chrono::milliseconds{250});
req->WriteHeader("WWW-Authenticate", WWW_AUTH_HEADER_DATA);
req->WriteReply(HTTP_UNAUTHORIZED);
return false;
return rpcRequest.send_reply(HTTP_UNAUTHORIZED);
}
jreq.authUser = rpcRequest.user;
try {
// Parse request
@ -189,16 +230,16 @@ static bool HTTPReq_JSONRPC(const CoreContext& context, HTTPRequest* req, bool e
bool user_has_whitelist = g_rpc_whitelist.count(jreq.authUser);
if (!user_has_whitelist && g_rpc_whitelist_default) {
LogPrintf("RPC User %s not allowed to call any methods\n", jreq.authUser);
req->WriteReply(HTTP_FORBIDDEN);
return false;
return rpcRequest.send_reply(HTTP_FORBIDDEN);
// singleton request
} else if (valRequest.isObject()) {
jreq.parse(valRequest);
if (user_has_whitelist && !g_rpc_whitelist[jreq.authUser].count(jreq.strMethod)) {
rpcRequest.command = jreq.strMethod;
if (user_has_whitelist && !whitelisted(jreq)) {
LogPrintf("RPC User %s not allowed to call method %s\n", jreq.authUser, jreq.strMethod);
req->WriteReply(HTTP_FORBIDDEN);
return false;
return rpcRequest.send_reply(HTTP_FORBIDDEN);
}
UniValue result = tableRPC.execute(jreq);
@ -215,10 +256,9 @@ static bool HTTPReq_JSONRPC(const CoreContext& context, HTTPRequest* req, bool e
const UniValue& request = valRequest[reqIdx].get_obj();
// Parse method
std::string strMethod = find_value(request, "method").get_str();
if (!g_rpc_whitelist[jreq.authUser].count(strMethod)) {
if (!whitelisted(jreq)) {
LogPrintf("RPC User %s not allowed to call method %s\n", jreq.authUser, strMethod);
req->WriteReply(HTTP_FORBIDDEN);
return false;
return rpcRequest.send_reply(HTTP_FORBIDDEN);
}
}
}
@ -229,15 +269,13 @@ static bool HTTPReq_JSONRPC(const CoreContext& context, HTTPRequest* req, bool e
throw JSONRPCError(RPC_PARSE_ERROR, "Top-level object parse error");
req->WriteHeader("Content-Type", "application/json");
req->WriteReply(HTTP_OK, strReply);
return rpcRequest.send_reply(HTTP_OK, strReply);
} catch (const UniValue& objError) {
JSONErrorReply(req, objError, jreq.id);
return false;
return JSONErrorReply(rpcRequest, objError, jreq.id);
} catch (const std::exception& e) {
JSONErrorReply(req, JSONRPCError(RPC_PARSE_ERROR, e.what()), jreq.id);
return false;
return JSONErrorReply(rpcRequest, JSONRPCError(RPC_PARSE_ERROR, e.what()), jreq.id);
}
return true;
assert(false);
}
static bool InitRPCAuthentication()

View File

@ -154,8 +154,8 @@ static struct evhttp* eventHTTP = nullptr;
static std::vector<CSubNet> rpc_allow_subnets;
//! Work queue for handling longer requests off the event loop thread
static std::unique_ptr<WorkQueue<HTTPClosure>> g_work_queue{nullptr};
//! List of 'external' RPC users
static std::vector<std::string> g_external_usernames;
//! List of 'external' RPC users (global variable, used by httprpc)
std::vector<std::string> g_external_usernames;
//! Handlers for (sub)paths
static std::vector<HTTPPathHandler> pathHandlers;
//! Bound listening sockets

View File

@ -19,6 +19,7 @@
#include <txmempool.h>
#include <util/thread.h>
#include <util/time.h>
#include <util/underlying.h>
#include <validation.h>
#include <validationinterface.h>
@ -130,8 +131,8 @@ PeerMsgRet CChainLocksHandler::ProcessNewChainLock(const NodeId from, const llmq
}
}
if (!VerifyChainLock(clsig)) {
LogPrint(BCLog::CHAINLOCKS, "CChainLocksHandler::%s -- invalid CLSIG (%s), peer=%d\n", __func__, clsig.ToString(), from);
if (const auto ret = VerifyChainLock(clsig); ret != VerifyRecSigStatus::Valid) {
LogPrint(BCLog::CHAINLOCKS, "CChainLocksHandler::%s -- invalid CLSIG (%s), status=%d peer=%d\n", __func__, clsig.ToString(), ToUnderlying(ret), from);
if (from != -1) {
return tl::unexpected{10};
}
@ -551,10 +552,12 @@ bool CChainLocksHandler::HasChainLock(int nHeight, const uint256& blockHash) con
return InternalHasChainLock(nHeight, blockHash);
}
bool CChainLocksHandler::VerifyChainLock(const CChainLockSig& clsig) const
VerifyRecSigStatus CChainLocksHandler::VerifyChainLock(const CChainLockSig& clsig) const
{
const auto llmqType = Params().GetConsensus().llmqTypeChainLocks;
const uint256 nRequestId = ::SerializeHash(std::make_pair(llmq::CLSIG_REQUESTID_PREFIX, clsig.getHeight()));
return llmq::VerifyRecoveredSig(llmqType, m_chainstate.m_chain, qman, clsig.getHeight(), nRequestId, clsig.getBlockHash(), clsig.getSig());
}

View File

@ -9,6 +9,7 @@
#include <crypto/common.h>
#include <llmq/signing.h>
#include <llmq/quorums.h>
#include <net.h>
#include <net_types.h>
#include <primitives/block.h>
@ -114,7 +115,7 @@ public:
bool HasChainLock(int nHeight, const uint256& blockHash) const EXCLUSIVE_LOCKS_REQUIRED(!cs);
bool HasConflictingChainLock(int nHeight, const uint256& blockHash) const EXCLUSIVE_LOCKS_REQUIRED(!cs);
bool VerifyChainLock(const CChainLockSig& clsig) const;
VerifyRecSigStatus VerifyChainLock(const CChainLockSig& clsig) const;
bool IsTxSafeForMining(const uint256& txid) const EXCLUSIVE_LOCKS_REQUIRED(!cs);

View File

@ -1169,7 +1169,7 @@ CQuorumCPtr SelectQuorumForSigning(const Consensus::LLMQParams& llmq_params, con
}
}
bool VerifyRecoveredSig(Consensus::LLMQType llmqType, const CChain& active_chain, const CQuorumManager& qman,
VerifyRecSigStatus VerifyRecoveredSig(Consensus::LLMQType llmqType, const CChain& active_chain, const CQuorumManager& qman,
int signedAtHeight, const uint256& id, const uint256& msgHash, const CBLSSignature& sig,
const int signOffset)
{
@ -1177,10 +1177,11 @@ bool VerifyRecoveredSig(Consensus::LLMQType llmqType, const CChain& active_chain
assert(llmq_params_opt.has_value());
auto quorum = SelectQuorumForSigning(llmq_params_opt.value(), active_chain, qman, id, signedAtHeight, signOffset);
if (!quorum) {
return false;
return VerifyRecSigStatus::NoQuorum;
}
uint256 signHash = BuildSignHash(llmqType, quorum->qc->quorumHash, id, msgHash);
return sig.VerifyInsecure(quorum->qc->quorumPublicKey, signHash);
const bool ret = sig.VerifyInsecure(quorum->qc->quorumPublicKey, signHash);
return ret ? VerifyRecSigStatus::Valid : VerifyRecSigStatus::Invalid;
}
} // namespace llmq

View File

@ -36,6 +36,13 @@ using CDeterministicMNCPtr = std::shared_ptr<const CDeterministicMN>;
namespace llmq
{
enum class VerifyRecSigStatus
{
NoQuorum,
Invalid,
Valid,
};
class CDKGSessionManager;
class CQuorumBlockProcessor;
@ -298,7 +305,7 @@ CQuorumCPtr SelectQuorumForSigning(const Consensus::LLMQParams& llmq_params, con
const uint256& selectionHash, int signHeight = -1 /*chain tip*/, int signOffset = SIGN_HEIGHT_OFFSET);
// Verifies a recovered sig that was signed while the chain tip was at signedAtTip
bool VerifyRecoveredSig(Consensus::LLMQType llmqType, const CChain& active_chain, const CQuorumManager& qman,
VerifyRecSigStatus VerifyRecoveredSig(Consensus::LLMQType llmqType, const CChain& active_chain, const CQuorumManager& qman,
int signedAtHeight, const uint256& id, const uint256& msgHash, const CBLSSignature& sig,
int signOffset = SIGN_HEIGHT_OFFSET);
} // namespace llmq

View File

@ -578,6 +578,30 @@ PeerMsgRet CSigningManager::ProcessMessage(const CNode& pfrom, const std::string
return {};
}
static bool PreVerifyRecoveredSig(const CQuorumManager& quorum_manager, const CRecoveredSig& recoveredSig, bool& retBan)
{
retBan = false;
auto llmqType = recoveredSig.getLlmqType();
if (!Params().GetLLMQ(llmqType).has_value()) {
retBan = true;
return false;
}
CQuorumCPtr quorum = quorum_manager.GetQuorum(llmqType, recoveredSig.getQuorumHash());
if (!quorum) {
LogPrint(BCLog::LLMQ, "CSigningManager::%s -- quorum %s not found\n", __func__,
recoveredSig.getQuorumHash().ToString());
return false;
}
if (!IsQuorumActive(llmqType, quorum_manager, quorum->qc->quorumHash)) {
return false;
}
return true;
}
PeerMsgRet CSigningManager::ProcessMessageRecoveredSig(const CNode& pfrom, const std::shared_ptr<const CRecoveredSig>& recoveredSig)
{
{
@ -614,30 +638,6 @@ PeerMsgRet CSigningManager::ProcessMessageRecoveredSig(const CNode& pfrom, const
return {};
}
bool CSigningManager::PreVerifyRecoveredSig(const CQuorumManager& quorum_manager, const CRecoveredSig& recoveredSig, bool& retBan)
{
retBan = false;
auto llmqType = recoveredSig.getLlmqType();
if (!Params().GetLLMQ(llmqType).has_value()) {
retBan = true;
return false;
}
CQuorumCPtr quorum = quorum_manager.GetQuorum(llmqType, recoveredSig.getQuorumHash());
if (!quorum) {
LogPrint(BCLog::LLMQ, "CSigningManager::%s -- quorum %s not found\n", __func__,
recoveredSig.getQuorumHash().ToString());
return false;
}
if (!IsQuorumActive(llmqType, quorum_manager, quorum->qc->quorumHash)) {
return false;
}
return true;
}
void CSigningManager::CollectPendingRecoveredSigsToVerify(
size_t maxUniqueSessions,
std::unordered_map<NodeId, std::list<std::shared_ptr<const CRecoveredSig>>>& retSigShares,

View File

@ -201,7 +201,6 @@ public:
private:
PeerMsgRet ProcessMessageRecoveredSig(const CNode& pfrom, const std::shared_ptr<const CRecoveredSig>& recoveredSig);
static bool PreVerifyRecoveredSig(const CQuorumManager& quorum_manager, const CRecoveredSig& recoveredSig, bool& retBan);
void CollectPendingRecoveredSigsToVerify(size_t maxUniqueSessions,
std::unordered_map<NodeId, std::list<std::shared_ptr<const CRecoveredSig>>>& retSigShares,

View File

@ -427,42 +427,27 @@ static RPCHelpMan quorum_memberof()
};
}
static RPCHelpMan quorum_sign()
{
return RPCHelpMan{"quorum sign",
"Threshold-sign a message\n",
{
{"llmqType", RPCArg::Type::NUM, RPCArg::Optional::NO, "LLMQ type."},
{"id", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Request id."},
{"msgHash", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Message hash."},
{"quorumHash", RPCArg::Type::STR_HEX, /* default */ "", "The quorum identifier."},
{"submit", RPCArg::Type::BOOL, /* default */ "true", "Submits the signature share to the network if this is true. "
"Returns an object containing the signature share if this is false."},
},
RPCResults{},
RPCExamples{""},
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
static UniValue quorum_sign_helper(const JSONRPCRequest& request, Consensus::LLMQType llmqType)
{
const NodeContext& node = EnsureAnyNodeContext(request.context);
const ChainstateManager& chainman = EnsureChainman(node);
const LLMQContext& llmq_ctx = EnsureLLMQContext(node);
const Consensus::LLMQType llmqType{static_cast<Consensus::LLMQType>(ParseInt32V(request.params[0], "llmqType"))};
const auto llmq_params_opt = Params().GetLLMQ(llmqType);
if (!llmq_params_opt.has_value()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "invalid LLMQ type");
}
const uint256 id(ParseHashV(request.params[1], "id"));
const uint256 msgHash(ParseHashV(request.params[2], "msgHash"));
const uint256 id(ParseHashV(request.params[0], "id"));
const uint256 msgHash(ParseHashV(request.params[1], "msgHash"));
uint256 quorumHash;
if (!request.params[3].isNull() && !request.params[3].get_str().empty()) {
quorumHash = ParseHashV(request.params[3], "quorumHash");
if (!request.params[2].isNull() && !request.params[2].get_str().empty()) {
quorumHash = ParseHashV(request.params[2], "quorumHash");
}
bool fSubmit{true};
if (!request.params[4].isNull()) {
fSubmit = ParseBoolV(request.params[4], "submit");
if (!request.params[3].isNull()) {
fSubmit = ParseBoolV(request.params[3], "submit");
}
if (fSubmit) {
return llmq_ctx.sigman->AsyncSignIfMember(llmqType, *llmq_ctx.shareman, id, msgHash, quorumHash);
@ -496,10 +481,69 @@ static RPCHelpMan quorum_sign()
return obj;
}
}
static RPCHelpMan quorum_sign()
{
return RPCHelpMan{"quorum sign",
"Threshold-sign a message\n",
{
{"llmqType", RPCArg::Type::NUM, RPCArg::Optional::NO, "LLMQ type."},
{"id", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Request id."},
{"msgHash", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Message hash."},
{"quorumHash", RPCArg::Type::STR_HEX, /* default */ "", "The quorum identifier."},
{"submit", RPCArg::Type::BOOL, /* default */ "true", "Submits the signature share to the network if this is true. "
"Returns an object containing the signature share if this is false."},
},
RPCResults{},
RPCExamples{""},
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{
const Consensus::LLMQType llmqType{static_cast<Consensus::LLMQType>(ParseInt32V(request.params[0], "llmqType"))};
JSONRPCRequest new_request{request};
new_request.params.setArray();
for (unsigned int i = 1; i < request.params.size(); ++i) {
new_request.params.push_back(request.params[i]);
}
return quorum_sign_helper(new_request, llmqType);
},
};
}
static RPCHelpMan quorum_platformsign()
{
return RPCHelpMan{"quorum platformsign",
"Threshold-sign a message. It signs messages only for platform quorums\n",
{
{"id", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Request id."},
{"msgHash", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "Message hash."},
{"quorumHash", RPCArg::Type::STR_HEX, /* default */ "", "The quorum identifier."},
{"submit", RPCArg::Type::BOOL, /* default */ "true", "Submits the signature share to the network if this is true. "
"Returns an object containing the signature share if this is false."},
},
RPCResults{},
RPCExamples{""},
[&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue
{
const Consensus::LLMQType llmqType{Params().GetConsensus().llmqTypePlatform};
return quorum_sign_helper(request, llmqType);
},
};
}
static bool VerifyRecoveredSigLatestQuorums(const Consensus::LLMQParams& llmq_params, const CChain& active_chain, const llmq::CQuorumManager& qman,
int signHeight, const uint256& id, const uint256& msgHash, const CBLSSignature& sig)
{
// First check against the current active set, if it fails check against the last active set
for (int signOffset : {0, llmq_params.dkgInterval}) {
if (llmq::VerifyRecoveredSig(llmq_params.type, active_chain, qman, signHeight, id, msgHash, sig, signOffset) == llmq::VerifyRecSigStatus::Valid) {
return true;
}
}
return false;
}
static RPCHelpMan quorum_verify()
{
return RPCHelpMan{"quorum verify",
@ -545,10 +589,7 @@ static RPCHelpMan quorum_verify()
if (!request.params[5].isNull()) {
signHeight = ParseInt32V(request.params[5], "signHeight");
}
// First check against the current active set, if it fails check against the last active set
int signOffset{llmq_params_opt->dkgInterval};
return llmq::VerifyRecoveredSig(llmqType, chainman.ActiveChain(), *llmq_ctx.qman, signHeight, id, msgHash, sig, 0) ||
llmq::VerifyRecoveredSig(llmqType, chainman.ActiveChain(), *llmq_ctx.qman, signHeight, id, msgHash, sig, signOffset);
return VerifyRecoveredSigLatestQuorums(*llmq_params_opt, chainman.ActiveChain(), *llmq_ctx.qman, signHeight, id, msgHash, sig);
}
uint256 quorumHash(ParseHashV(request.params[4], "quorumHash"));
@ -958,7 +999,7 @@ static RPCHelpMan verifychainlock()
}
const LLMQContext& llmq_ctx = EnsureLLMQContext(node);
return llmq_ctx.clhandler->VerifyChainLock(llmq::CChainLockSig(nBlockHeight, nBlockHash, sig));
return llmq_ctx.clhandler->VerifyChainLock(llmq::CChainLockSig(nBlockHeight, nBlockHash, sig)) == llmq::VerifyRecSigStatus::Valid;
},
};
}
@ -1030,12 +1071,9 @@ static RPCHelpMan verifyislock()
const LLMQContext& llmq_ctx = EnsureLLMQContext(node);
auto llmqType = Params().GetConsensus().llmqTypeDIP0024InstantSend;
// First check against the current active set, if it fails check against the last active set
const auto llmq_params_opt = Params().GetLLMQ(llmqType);
CHECK_NONFATAL(llmq_params_opt.has_value());
const int signOffset{llmq_params_opt->dkgInterval};
return llmq::VerifyRecoveredSig(llmqType, chainman.ActiveChain(), *llmq_ctx.qman, signHeight, id, txid, sig, 0) ||
llmq::VerifyRecoveredSig(llmqType, chainman.ActiveChain(), *llmq_ctx.qman, signHeight, id, txid, sig, signOffset);
return VerifyRecoveredSigLatestQuorums(*llmq_params_opt, chainman.ActiveChain(), *llmq_ctx.qman, signHeight, id, txid, sig);
},
};
}
@ -1060,7 +1098,8 @@ static RPCHelpMan submitchainlock()
if (nBlockHeight <= 0) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "invalid block height");
}
const LLMQContext& llmq_ctx = EnsureLLMQContext(EnsureAnyNodeContext(request.context));
const NodeContext& node = EnsureAnyNodeContext(request.context);
const LLMQContext& llmq_ctx = EnsureLLMQContext(node);
const int32_t bestCLHeight = llmq_ctx.clhandler->GetBestChainLock().getHeight();
if (nBlockHeight <= bestCLHeight) return bestCLHeight;
@ -1070,8 +1109,15 @@ static RPCHelpMan submitchainlock()
}
auto clsig = llmq::CChainLockSig(nBlockHeight, nBlockHash, sig);
if (!llmq_ctx.clhandler->VerifyChainLock(clsig)) {
const auto clsig{llmq::CChainLockSig(nBlockHeight, nBlockHash, sig)};
const llmq::VerifyRecSigStatus ret{llmq_ctx.clhandler->VerifyChainLock(clsig)};
if (ret == llmq::VerifyRecSigStatus::NoQuorum) {
LOCK(cs_main);
const ChainstateManager& chainman = EnsureChainman(node);
const CBlockIndex* pIndex{chainman.ActiveChain().Tip()};
throw JSONRPCError(RPC_MISC_ERROR, strprintf("No quorum found. Current tip height: %d hash: %s\n", pIndex->nHeight, pIndex->GetBlockHash().ToString()));
}
if (ret != llmq::VerifyRecSigStatus::Valid) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "invalid signature");
}
@ -1096,6 +1142,7 @@ static const CRPCCommand commands[] =
{ "evo", "quorum", "dkgstatus", &quorum_dkgstatus, {"detail_level"} },
{ "evo", "quorum", "memberof", &quorum_memberof, {"proTxHash", "scanQuorumsCount"} },
{ "evo", "quorum", "sign", &quorum_sign, {"llmqType", "id", "msgHash", "quorumHash", "submit"} },
{ "evo", "quorum", "platformsign", &quorum_platformsign, {"id", "msgHash", "quorumHash", "submit"} },
{ "evo", "quorum", "verify", &quorum_verify, {"llmqType", "id", "msgHash", "signature", "quorumHash", "signHeight"} },
{ "evo", "quorum", "hasrecsig", &quorum_hasrecsig, {"llmqType", "id", "msgHash"} },
{ "evo", "quorum", "getrecsig", &quorum_getrecsig, {"llmqType", "id", "msgHash"} },

View File

@ -3032,6 +3032,9 @@ static RPCHelpMan createwallet()
#ifndef USE_SQLITE
throw JSONRPCError(RPC_WALLET_ERROR, "Compiled without sqlite support (required for descriptor wallets)");
#endif
if (request.params[6].isNull()) {
throw JSONRPCError(RPC_INVALID_PARAMETER, "The createwallet RPC requires specifying the 'load_on_startup' flag when creating descriptor wallets. Dash Core v21 introduced this requirement due to breaking changes in the createwallet RPC.");
}
flags |= WALLET_FLAG_DESCRIPTORS;
warnings.emplace_back(Untranslated("Wallet is an experimental descriptor wallet"));
}

View File

@ -331,7 +331,7 @@ void LegacyScriptPubKeyMan::MarkUnusedAddresses(WalletBatch &batch, const CScrip
void LegacyScriptPubKeyMan::UpgradeKeyMetadata()
{
LOCK(cs_KeyStore); // mapKeyMetadata
if (m_storage.IsLocked() || m_storage.IsWalletFlagSet(WALLET_FLAG_KEY_ORIGIN_METADATA) || !IsHDEnabled()) {
if (m_storage.IsLocked(false) || m_storage.IsWalletFlagSet(WALLET_FLAG_KEY_ORIGIN_METADATA) || !IsHDEnabled()) {
return;
}
@ -1901,7 +1901,7 @@ void DescriptorScriptPubKeyMan::ReturnDestination(int64_t index, bool internal,
std::map<CKeyID, CKey> DescriptorScriptPubKeyMan::GetKeys() const
{
AssertLockHeld(cs_desc_man);
if (m_storage.HasEncryptionKeys() && !m_storage.IsLocked()) {
if (m_storage.HasEncryptionKeys() && !m_storage.IsLocked(true)) {
KeyMap keys;
for (auto key_pair : m_map_crypted_keys) {
const CPubKey& pubkey = key_pair.second.first;
@ -2014,7 +2014,7 @@ bool DescriptorScriptPubKeyMan::AddDescriptorKeyWithDB(WalletBatch& batch, const
}
if (m_storage.HasEncryptionKeys()) {
if (m_storage.IsLocked()) {
if (m_storage.IsLocked(true)) {
return false;
}
@ -2423,7 +2423,7 @@ bool DescriptorScriptPubKeyMan::GetDescriptorString(std::string& out) const
void DescriptorScriptPubKeyMan::UpgradeDescriptorCache()
{
LOCK(cs_desc_man);
if (m_storage.IsLocked() || m_storage.IsWalletFlagSet(WALLET_FLAG_LAST_HARDENED_XPUB_CACHED)) {
if (m_storage.IsLocked(false) || m_storage.IsWalletFlagSet(WALLET_FLAG_LAST_HARDENED_XPUB_CACHED)) {
return;
}

View File

@ -40,7 +40,7 @@ public:
virtual void SetMinVersion(enum WalletFeature, WalletBatch* = nullptr) = 0;
virtual const CKeyingMaterial& GetEncryptionKey() const = 0;
virtual bool HasEncryptionKeys() const = 0;
virtual bool IsLocked(bool fForMixing = false) const = 0;
virtual bool IsLocked(bool fForMixing) const = 0;
// for LegacyScriptPubKeyMan::TopUpInner needs:
virtual void UpdateProgress(const std::string&, int) = 0;

View File

@ -5459,21 +5459,11 @@ bool CWallet::IsLocked(bool fForMixing) const
{
if (!IsCrypted())
return false;
bool result;
{
LOCK(cs_wallet);
result = vMasterKey.empty();
}
// fForMixing fOnlyMixingAllowed return
// ---------------------------------------
// true true result
// true false result
// false true true
// false false result
if(!fForMixing && fOnlyMixingAllowed) return true;
return result;
LOCK(cs_wallet);
return vMasterKey.empty();
}
bool CWallet::Lock(bool fAllowMixing)

View File

@ -119,7 +119,7 @@ class AssetLocksTest(DashTestFramework):
unlock_tx.calc_sha256()
msgHash = format(unlock_tx.sha256, '064x')
recsig = self.get_recovered_sig(request_id, msgHash, llmq_type=llmq_type_test)
recsig = self.get_recovered_sig(request_id, msgHash, llmq_type=llmq_type_test, use_platformsign=True)
unlockTx_payload.quorumSig = bytearray.fromhex(recsig["sig"])
unlock_tx.vExtraPayload = unlockTx_payload.serialize()

View File

@ -84,11 +84,14 @@ class LLMQChainLocksTest(DashTestFramework):
block = self.nodes[0].getblock(self.nodes[0].getblockhash(h))
assert block['chainlock']
# Update spork to SPORK_19_CHAINLOCKS_ENABLED and test its behaviour
self.log.info(f"Test submitchainlock for too high block")
assert_raises_rpc_error(-1, f"No quorum found. Current tip height: {self.nodes[1].getblockcount()}", self.nodes[1].submitchainlock, '0000000000000000000000000000000000000000000000000000000000000000', 'a5c69505b5744524c9ed6551d8a57dc520728ea013496f46baa8a73df96bfd3c86e474396d747a4af11aaef10b17dbe80498b6a2fe81938fe917a3fedf651361bfe5367c800d23d3125820e6ee5b42189f0043be94ce27e73ea13620c9ef6064', self.nodes[1].getblockcount() + 300)
self.log.info("Update spork to SPORK_19_CHAINLOCKS_ENABLED and test its behaviour")
self.nodes[0].sporkupdate("SPORK_19_CHAINLOCKS_ENABLED", 1)
self.wait_for_sporks_same()
# Generate new blocks and verify that they are not chainlocked
self.log.info("Generate new blocks and verify that they are not chainlocked")
previous_block_hash = self.nodes[0].getbestblockhash()
for _ in range(2):
block_hash = self.nodes[0].generate(1)[0]

View File

@ -12,7 +12,9 @@ from test_framework.util import (
assert_equal,
str_to_b64str
)
import json
import http.client
import re
import urllib.parse
def rpccall(node, user, method):
@ -20,7 +22,17 @@ def rpccall(node, user, method):
headers = {"Authorization": "Basic " + str_to_b64str('{}:{}'.format(user[0], user[3]))}
conn = http.client.HTTPConnection(url.hostname, url.port)
conn.connect()
conn.request('POST', '/', '{"method": "' + method + '"}', headers)
# composite commands are presented without space in whitelist
# but space can't be ommitted when using CLI/http rpc
# for sack of test, substitute missing space for quorum composite command
params = []
if re.match(r"^quorum[^ ]", method):
params = [method[6:]]
method = "quorum"
query = {"method" : method, "params" : params}
conn.request('POST', '/', json.dumps(query), headers)
resp = conn.getresponse()
conn.close()
return resp
@ -39,7 +51,8 @@ class RPCWhitelistTest(BitcoinTestFramework):
# 3 => Password Plaintext
self.users = [
["user1", "50358aa884c841648e0700b073c32b2e$b73e95fff0748cc0b517859d2ca47d9bac1aa78231f3e48fa9222b612bd2083e", "getbestblockhash,getblockcount,", "12345"],
["user2", "8650ba41296f62092377a38547f361de$4620db7ba063ef4e2f7249853e9f3c5c3592a9619a759e3e6f1c63f2e22f1d21", "getblockcount", "54321"]
["user2", "8650ba41296f62092377a38547f361de$4620db7ba063ef4e2f7249853e9f3c5c3592a9619a759e3e6f1c63f2e22f1d21", "getblockcount", "54321"],
["platform-user", "8650ba41296f62092377a38547f361de$4620db7ba063ef4e2f7249853e9f3c5c3592a9619a759e3e6f1c63f2e22f1d21", "getblockcount,quorumlist", "54321"],
]
# For exceptions
self.strange_users = [
@ -55,7 +68,7 @@ class RPCWhitelistTest(BitcoinTestFramework):
["strangedude5", "d12c6e962d47a454f962eb41225e6ec8$2dd39635b155536d3c1a2e95d05feff87d5ba55f2d5ff975e6e997a836b717c9", ":getblockcount,getblockcount", "s7R4nG3R7H1nGZ"]
]
# These commands shouldn't be allowed for any user to test failures
self.never_allowed = ["getnetworkinfo"]
self.never_allowed = ["getnetworkinfo", "quorum sign"]
with open(os.path.join(get_datadir_path(self.options.tmpdir, 0), "dash.conf"), 'a', encoding='utf8') as f:
f.write("\nrpcwhitelistdefault=0\n")
for user in self.users:

View File

@ -2033,12 +2033,15 @@ class DashTestFramework(BitcoinTestFramework):
return True
wait_until_helper(check_recovered_sig, timeout=timeout, sleep=1)
def get_recovered_sig(self, rec_sig_id, rec_sig_msg_hash, llmq_type=100):
def get_recovered_sig(self, rec_sig_id, rec_sig_msg_hash, llmq_type=100, use_platformsign=False):
# Note: recsigs aren't relayed to regular nodes by default,
# make sure to pick a mn as a node to query for recsigs.
try:
quorumHash = self.mninfo[0].node.quorum("selectquorum", llmq_type, rec_sig_id)["quorumHash"]
for mn in self.mninfo:
if use_platformsign:
mn.node.quorum("platformsign", rec_sig_id, rec_sig_msg_hash, quorumHash)
else:
mn.node.quorum("sign", llmq_type, rec_sig_id, rec_sig_msg_hash, quorumHash)
self.wait_for_recovered_sig(rec_sig_id, rec_sig_msg_hash, llmq_type, 10)
return self.mninfo[0].node.quorum("getrecsig", llmq_type, rec_sig_id, rec_sig_msg_hash)

View File

@ -704,6 +704,8 @@ class RPCOverloadWrapper():
def createwallet(self, wallet_name, disable_private_keys=None, blank=None, passphrase='', avoid_reuse=None, descriptors=None, load_on_startup=None):
if descriptors is None:
descriptors = self.descriptors
if descriptors is not None and load_on_startup is None:
load_on_startup = False
return self.__getattr__('createwallet')(wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors, load_on_startup)
def importprivkey(self, privkey, label=None, rescan=None):