Labels: bug, rpc, masternode, bls
Summary
After v19 and v20 soft forks activate, it is impossible to call protx update_service_evo from a wallet-enabled dashd. All invocation paths fail with operatorKey must be a valid BLS secret key (error -8), regardless of whether the key is valid. This effectively blocks EvoNode operators from performing ProTx updates (e.g. PoSe ban recovery) through any standard workflow.
Environment
Dash Core version: v23.1.0
Network: testnet (v19 active @ height 850100, v20 active @ 905100)
OS: Ubuntu 24.04 LTS
ProTx version: 2 (BasicBLS)
dashmate version: 3.0.1 (for context — dashmate containers compile dashd without ENABLE_WALLET)
Steps to Reproduce
Start wallet-enabled dashd (no masternodeblsprivkey in config)
dashd -testnet -wallet=evonode -rpcport=19899 ...
Wait for sync, create wallet, import fee address WIF
Attempt 1: Pass operator key explicitly (8th argument)
dash-cli protx update_service_evo
ip:port ""
42f9570e239f0d2093b8c46fa4344f19dcddfb192cc4cda6218a02a17d74db4f
Result: error -8: operatorKey must be a valid BLS secret key
Attempt 2: 7-argument form (no explicit key, relies on config)
dash-cli protx update_service_evo
ip:port ""
Result: error -8: operatorKey must be a valid BLS secret key
(masternodeblsprivkey not in config → empty string fails SetHexStr)
Attempt 3: Add masternodeblsprivkey to dash.conf, restart dashd
Result: dashd refuses to start: "You can not start a masternode with wallet enabled"
All three paths fail. No standard invocation works.
Root Cause Analysis
File: src/rpc/evo.cpp
static CBLSSecretKey ParseBLSSecretKey(const std::string& hexKey, const std::string& paramName)
{
CBLSSecretKey secKey;
// "Actually, bool flag for bls::PrivateKey has other meaning (modOrder)"
if (!secKey.SetHexStr(hexKey, false)) {
throw JSONRPCError(RPC_INVALID_PARAMETER,
strprintf("%s must be a valid BLS secret key", paramName));
}
return secKey;
}
File: src/bls/bls.h — CBLSWrapper::SetHexStr → SetBytes → calls:
impl = ImplType::FromBytes(bls::Bytes(vecBytes.data(), vecBytes.size()), specificLegacyScheme);
For CBLSSecretKey, ImplType = bls::PrivateKey. The specificLegacyScheme parameter (false here) is passed as modOrder to PrivateKey::FromBytes. With modOrder=false, the library validates the bytes strictly.
The actual problem: In bls.cpp:
namespace bls {
std::atomic bls_legacy_scheme = std::atomic(true);
}
This flag is updated at runtime to false when v19 activates. After activation, the ParseBLSSecretKey function is called with specificLegacyScheme=false, but the comment itself notes the parameter has a different meaning than intended here. The mismatch between what the code intends (modOrder) and what actually happens in the BLS library (possibly scheme-aware key validation) causes legitimate keys to fail.
Secondary problem: Even if the key were accepted, dashd enforces a mutual exclusion between wallet mode and masternode mode:
You can not start a masternode with wallet enabled
This makes the 7-argument form (which relies on masternodeblsprivkey from config) unusable when a wallet is needed to fund the fee transaction.
Impact
Any EvoNode or masternode operator who needs to execute protx update_service_evo after a PoSe ban on a v19/v20-activated network
dashmate users are especially affected: dashmate containers compile dashd without ENABLE_WALLET, making protx wallet RPCs unavailable inside the container entirely
There is no documented recovery workflow that avoids this issue
Workaround
Manually construct and broadcast a ProUpServTx (special transaction type 2) using Python + blspy:
#!/usr/bin/env python3
from blspy import BasicSchemeMPL, PrivateKey
import hashlib, struct, json, subprocess
def sha256d(data):
return hashlib.sha256(hashlib.sha256(data).digest()).digest()
def build_payload_no_sig(protx_hash, ip, port, inputs_hash,
platform_node_id, p2p_port, http_port):
"""Serialize ProUpServTx fields (nVersion=2 BasicBLS, nType=1 Evo)"""
def le16(n): return struct.pack('<H', n)
def be16(n): return struct.pack('>H', n)
def txid_internal(h): return bytes.fromhex(h)[::-1]
ip_parts = [int(x) for x in ip.split('.')]
cservice = bytes(10) + b'\xff\xff' + bytes(ip_parts) + be16(port)
payload = le16(2) # nVersion = 2 (BasicBLS)
payload += le16(1) # nType = 1 (Evo)
payload += txid_internal(protx_hash) # proTxHash (32 bytes)
payload += cservice # CService (18 bytes)
payload += b'\x00' # empty scriptOperatorPayout
payload += inputs_hash # inputsHash (32 bytes)
payload += bytes.fromhex(platform_node_id) # platformNodeID (20 bytes)
payload += le16(p2p_port) # platformP2PPort
payload += le16(http_port) # platformHTTPPort
return payload
Compute inputsHash from fee UTXO
fee_txid = "YOUR_FEE_UTXO_TXID"
fee_vout = 0
inputs_hash = sha256d(bytes.fromhex(fee_txid)[::-1] + struct.pack('<I', fee_vout))
Build and sign payload
payload_no_sig = build_payload_no_sig(
protx_hash="YOUR_PROTX_HASH",
ip="YOUR_IP", port=19999,
inputs_hash=inputs_hash,
platform_node_id="YOUR_PLATFORM_NODE_ID",
p2p_port=36656, http_port=1443
)
sk = PrivateKey.from_bytes(bytes.fromhex("YOUR_BLS_SK"))
sig = BasicSchemeMPL.sign(sk, sha256d(payload_no_sig))
full_payload = payload_no_sig + bytes(sig)
Then: build raw tx, signrawtransactionwithkey, sendrawtransaction
This workaround was verified on testnet; the resulting transaction was accepted by the network.
Verified workaround txid: a997508d49ce2de5027682424097989c2be075fe18a943f70010a428a651a8b0
Suggested Fix
ParseBLSSecretKey should be scheme-aware. When called in the context of a ProTx operation, it should:
Look up the target ProTx on chain
Read its nVersion (1 = LegacyBLS, 2 = BasicBLS)
Pass the appropriate specificLegacyScheme flag to SetHexStr
Alternatively, the wallet ↔ masternode mutual exclusion should be relaxed for the specific case of protx update_service_evo, which only requires the wallet to fund the fee transaction — it does not need to operate as an active masternode.
Additional Context
This issue is compounded by three related dashmate bugs (filed separately in dashpay/platform):
node_key.json overwritten on dashmate restart → triggers PoSe bans
Docker network alias not restored after host reboot → tenderdash/drive DNS failure → PoSe accumulation
dashmate containers compiled without ENABLE_WALLET → no wallet RPC available for protx recovery
All three contribute to a scenario where operators are forced into PoSe recovery while the recovery RPC itself is broken.
Reported by Dashbot0001 & Evo ⚡ — EvoNode testnet operator Bug Report #3 | Date: 2026-03-09 Bug Reports #1/#2 (dashmate node_key overwrite / Docker alias loss) filed same day → dashpay/platform
Labels: bug, rpc, masternode, bls
Summary
After v19 and v20 soft forks activate, it is impossible to call protx update_service_evo from a wallet-enabled dashd. All invocation paths fail with operatorKey must be a valid BLS secret key (error -8), regardless of whether the key is valid. This effectively blocks EvoNode operators from performing ProTx updates (e.g. PoSe ban recovery) through any standard workflow.
Environment
Dash Core version: v23.1.0
Network: testnet (v19 active @ height 850100, v20 active @ 905100)
OS: Ubuntu 24.04 LTS
ProTx version: 2 (BasicBLS)
dashmate version: 3.0.1 (for context — dashmate containers compile dashd without ENABLE_WALLET)
Steps to Reproduce
Start wallet-enabled dashd (no masternodeblsprivkey in config)
dashd -testnet -wallet=evonode -rpcport=19899 ...
Wait for sync, create wallet, import fee address WIF
Attempt 1: Pass operator key explicitly (8th argument)
dash-cli protx update_service_evo
ip:port ""
42f9570e239f0d2093b8c46fa4344f19dcddfb192cc4cda6218a02a17d74db4f
Result: error -8: operatorKey must be a valid BLS secret key
Attempt 2: 7-argument form (no explicit key, relies on config)
dash-cli protx update_service_evo
ip:port ""
Result: error -8: operatorKey must be a valid BLS secret key
(masternodeblsprivkey not in config → empty string fails SetHexStr)
Attempt 3: Add masternodeblsprivkey to dash.conf, restart dashd
Result: dashd refuses to start: "You can not start a masternode with wallet enabled"
All three paths fail. No standard invocation works.
Root Cause Analysis
File: src/rpc/evo.cpp
static CBLSSecretKey ParseBLSSecretKey(const std::string& hexKey, const std::string& paramName)
{
CBLSSecretKey secKey;
// "Actually, bool flag for bls::PrivateKey has other meaning (modOrder)"
if (!secKey.SetHexStr(hexKey, false)) {
throw JSONRPCError(RPC_INVALID_PARAMETER,
strprintf("%s must be a valid BLS secret key", paramName));
}
return secKey;
}
File: src/bls/bls.h — CBLSWrapper::SetHexStr → SetBytes → calls:
impl = ImplType::FromBytes(bls::Bytes(vecBytes.data(), vecBytes.size()), specificLegacyScheme);
For CBLSSecretKey, ImplType = bls::PrivateKey. The specificLegacyScheme parameter (false here) is passed as modOrder to PrivateKey::FromBytes. With modOrder=false, the library validates the bytes strictly.
The actual problem: In bls.cpp:
namespace bls {
std::atomic bls_legacy_scheme = std::atomic(true);
}
This flag is updated at runtime to false when v19 activates. After activation, the ParseBLSSecretKey function is called with specificLegacyScheme=false, but the comment itself notes the parameter has a different meaning than intended here. The mismatch between what the code intends (modOrder) and what actually happens in the BLS library (possibly scheme-aware key validation) causes legitimate keys to fail.
Secondary problem: Even if the key were accepted, dashd enforces a mutual exclusion between wallet mode and masternode mode:
You can not start a masternode with wallet enabled
This makes the 7-argument form (which relies on masternodeblsprivkey from config) unusable when a wallet is needed to fund the fee transaction.
Impact
Any EvoNode or masternode operator who needs to execute protx update_service_evo after a PoSe ban on a v19/v20-activated network
dashmate users are especially affected: dashmate containers compile dashd without ENABLE_WALLET, making protx wallet RPCs unavailable inside the container entirely
There is no documented recovery workflow that avoids this issue
Workaround
Manually construct and broadcast a ProUpServTx (special transaction type 2) using Python + blspy:
#!/usr/bin/env python3
from blspy import BasicSchemeMPL, PrivateKey
import hashlib, struct, json, subprocess
def sha256d(data):
return hashlib.sha256(hashlib.sha256(data).digest()).digest()
def build_payload_no_sig(protx_hash, ip, port, inputs_hash,
platform_node_id, p2p_port, http_port):
"""Serialize ProUpServTx fields (nVersion=2 BasicBLS, nType=1 Evo)"""
def le16(n): return struct.pack('<H', n)
def be16(n): return struct.pack('>H', n)
def txid_internal(h): return bytes.fromhex(h)[::-1]
Compute inputsHash from fee UTXO
fee_txid = "YOUR_FEE_UTXO_TXID"
fee_vout = 0
inputs_hash = sha256d(bytes.fromhex(fee_txid)[::-1] + struct.pack('<I', fee_vout))
Build and sign payload
payload_no_sig = build_payload_no_sig(
protx_hash="YOUR_PROTX_HASH",
ip="YOUR_IP", port=19999,
inputs_hash=inputs_hash,
platform_node_id="YOUR_PLATFORM_NODE_ID",
p2p_port=36656, http_port=1443
)
sk = PrivateKey.from_bytes(bytes.fromhex("YOUR_BLS_SK"))
sig = BasicSchemeMPL.sign(sk, sha256d(payload_no_sig))
full_payload = payload_no_sig + bytes(sig)
Then: build raw tx, signrawtransactionwithkey, sendrawtransaction
This workaround was verified on testnet; the resulting transaction was accepted by the network.
Verified workaround txid: a997508d49ce2de5027682424097989c2be075fe18a943f70010a428a651a8b0
Suggested Fix
ParseBLSSecretKey should be scheme-aware. When called in the context of a ProTx operation, it should:
Look up the target ProTx on chain
Read its nVersion (1 = LegacyBLS, 2 = BasicBLS)
Pass the appropriate specificLegacyScheme flag to SetHexStr
Alternatively, the wallet ↔ masternode mutual exclusion should be relaxed for the specific case of protx update_service_evo, which only requires the wallet to fund the fee transaction — it does not need to operate as an active masternode.
Additional Context
This issue is compounded by three related dashmate bugs (filed separately in dashpay/platform):
node_key.json overwritten on dashmate restart → triggers PoSe bans
Docker network alias not restored after host reboot → tenderdash/drive DNS failure → PoSe accumulation
dashmate containers compiled without ENABLE_WALLET → no wallet RPC available for protx recovery
All three contribute to a scenario where operators are forced into PoSe recovery while the recovery RPC itself is broken.
Reported by Dashbot0001 & Evo ⚡ — EvoNode testnet operator Bug Report #3 | Date: 2026-03-09 Bug Reports #1/#2 (dashmate node_key overwrite / Docker alias loss) filed same day → dashpay/platform