#!/usr/bin/env python3
"""Reproduction for a dashpay/dash v24.0.0-rc.1 release-candidate report.
A participant's funding inputs are signed, with signrawtransactionwithwallet, into a shared masternode
registration whose share table has no share for that participant, and the registration is accepted.
It starts a throwaway regtest node in a temporary data directory (RPC port 29710, not listening for
peers), creates three wallets (coord, A, B) and uses only these RPC commands: createrawtransaction,
protx shared_register_prepare, protx shared_sign, protx shared_combine, signrawtransactionwithwallet,
sendrawtransaction, protx info and getaddressinfo. The node is stopped and the directory removed at
the end.
usage: DASH_BIN=/path/to/dashcore-24.0.0-rc.1/bin python3 repro_shared_register_unlisted_participant.py
Exits 0 when the behavior reproduces, 1 when it does not.
"""
import json
import os
import shutil
import subprocess
import sys
import tempfile
import time
BIN = os.environ.get("DASH_BIN", "")
DATADIR = tempfile.mkdtemp(prefix="shared-reg-repro-")
NODE = ["-regtest", f"-datadir={DATADIR}", "-rpcport=29710"]
COIN = 100_000_000
def cli(*args, wallet=None, check=True):
cmd = [os.path.join(BIN, "dash-cli"), *NODE] + ([f"-rpcwallet={wallet}"] if wallet else [])
p = subprocess.run(cmd + [a if isinstance(a, str) else json.dumps(a) for a in args], capture_output=True, text=True)
if check and p.returncode != 0:
sys.exit(f"unexpected failure of `{' '.join(map(str, args[:2]))}`: {p.stderr.strip()}")
try:
return json.loads(p.stdout) if p.returncode == 0 else p
except ValueError:
return p.stdout.strip()
def main():
subprocess.run([os.path.join(BIN, "dashd"), *NODE, "-port=29711", "-listen=0", "-daemon=1",
"-fallbackfee=0.0001", "-vbparams=v24:0:9999999999:0:4:3:3:1:0"], check=True, capture_output=True)
for _ in range(60):
if subprocess.run([os.path.join(BIN, "dash-cli"), *NODE, "getblockcount"], capture_output=True).returncode == 0:
break
time.sleep(1)
print(cli("-version").splitlines()[0])
for w in ("coord", "A", "B"):
cli("createwallet", w)
mine = cli("getnewaddress", wallet="coord")
cli("generatetoaddress", "620", mine)
print("1. v24 active:", cli("getdeploymentinfo")["deployments"]["v24"]["active"])
funds = {}
for w, amount in (("A", "601"), ("B", "401")):
addr = cli("getnewaddress", wallet=w)
txid = cli("sendtoaddress", addr, amount, wallet="coord")
funds[w] = (txid, addr)
cli("generatetoaddress", "1", mine)
inputs = []
for w, (txid, addr) in funds.items():
vout = next(o["n"] for o in cli("getrawtransaction", txid, "true")["vout"] if o["scriptPubKey"].get("address") == addr)
inputs.append({"txid": txid, "vout": vout})
change = {cli("getnewaddress", wallet="A"): 0.9999, cli("getnewaddress", wallet="B"): 0.9999}
funding = cli("createrawtransaction", inputs, change)
print("2. funding transaction spends one output of A (601 DASH) and one of B (401 DASH), change back to each")
coord = {k: cli("getnewaddress", wallet="coord") for k in ("owner", "refund", "reward")}
b = {k: cli("getnewaddress", wallet="B") for k in ("owner", "refund", "reward")}
table = [{"amount": 600 * COIN, "ownerAddress": coord["owner"], "refundAddress": coord["refund"], "rewardAddress": coord["reward"]},
{"amount": 400 * COIN, "ownerAddress": b["owner"], "refundAddress": b["refund"], "rewardAddress": b["reward"]}]
prep = cli("protx", "shared_register_prepare", funding, table, "", cli("bls", "generate")["public"],
cli("getnewaddress", wallet="coord"), "0", "100", str(10 * COIN), wallet="coord")
print("3. shared_register_prepare: the 600 DASH share names coord's keys, the 400 DASH share names B's; A has no share")
a_sign = cli("protx", "shared_sign", prep["tx"], wallet="A", check=False)
print("4. shared_sign in A's wallet:", a_sign.stderr.strip().splitlines()[-1] if hasattr(a_sign, "stderr") else "signatures returned")
sigs = cli("protx", "shared_sign", prep["tx"], wallet="coord")["signatures"] + cli("protx", "shared_sign", prep["tx"], wallet="B")["signatures"]
combined = cli("protx", "shared_combine", prep["tx"], sigs, wallet="coord")
print(" shared_sign in coord's and B's wallets, then shared_combine: combined")
a_out = cli("signrawtransactionwithwallet", combined, wallet="A")
a_input = next(v for v in cli("decoderawtransaction", a_out["hex"])["vin"] if v["txid"] == funds["A"][0])
print("5. signrawtransactionwithwallet in A's wallet: A's input signed =", bool(a_input["scriptSig"]["hex"]),
"| complete =", a_out["complete"], "(B's input still unsigned)")
b_out = cli("signrawtransactionwithwallet", a_out["hex"], wallet="B")
print(" signrawtransactionwithwallet in B's wallet: complete =", b_out["complete"])
sent = cli("sendrawtransaction", b_out["hex"], check=False)
accepted = isinstance(sent, str) and len(sent) == 64
print(" sendrawtransaction:", "accepted" if accepted else sent.stderr.strip().splitlines()[-1])
if not accepted:
return 1
cli("generatetoaddress", "1", mine)
share0 = cli("protx", "info", sent)["state"]["shares"][0]
in_coord = cli("getaddressinfo", share0["refundAddress"], wallet="coord")["ismine"]
in_a = cli("getaddressinfo", share0["refundAddress"], wallet="A")["ismine"]
print(f"6. protx info: share 0 is {share0['amount'] // COIN} DASH, refund address in coord's wallet = {in_coord}, in A's wallet = {in_a}")
reproduced = in_coord and not in_a
print("RESULT:", "reproduced" if reproduced else "not reproduced")
return 0 if reproduced else 1
if __name__ == "__main__":
if not BIN or not os.path.exists(os.path.join(BIN, "dashd")):
sys.exit("set DASH_BIN to the directory holding dashd and dash-cli (v24.0.0-rc.1)")
try:
code = main()
finally:
subprocess.run([os.path.join(BIN, "dash-cli"), *NODE, "stop"], capture_output=True)
time.sleep(3)
shutil.rmtree(DATADIR, ignore_errors=True)
sys.exit(code)
Is your feature request related to a problem? Please describe.
This is release-candidate feedback on the shared masternode registration workflow in v24.0.0-rc.1.
It is not a consensus issue. Consent works per share.
protx shared_combinerefuses a registrationthat lacks any share's consent, and the node rejects one whose consents do not match its terms
(
bad-protx-shares-sig).The gap is in the direct RPC workflow that the
protx shared_register_preparehelp describes. Aparticipant whose share is not in the table is never asked for a consent, and its funding inputs are
signed with
signrawtransactionwithwallet, which does not compare the transaction with anything theparticipant agreed to. So the transaction a participant reviews and the one whose inputs it signs can
differ while spending the same inputs.
On rc.1 regtest, with the participants in separate wallets, a table that gave participant A's 600 DASH
to owner, refund and reward addresses from the coordinator's wallet was accepted after A's wallet
signed A's funding input. A's wallet had answered
protx shared_signwith "none of the share ownerkeys were found in this wallet", and nothing at the input-signing step warned.
Describe the solution you'd like
An explicit check, within the shared registration workflow, of a participant's agreed terms before
its funding inputs are signed. For example, a mode of
protx shared_sign, or a separate RPC, thattakes the participant's agreed terms (its own share, the share table, and the early-exit period and
penalty) and refuses unless the prepared transaction matches them and the participant's inputs fund
only its contribution plus its agreed share of the fee. The
shared_register_preparehelp could thendirect each participant to run it before
signrawtransactionwithwallet.Describe alternatives you've considered
signrawtransactionwithwalletwhen it signs inputs into a shared ProRegTx whose tableholds no share with keys from this wallet. It needs no new input, but it assumes the funding wallet
also holds the share's keys, which a participant who keeps them elsewhere may not.
shared_register_preparehelp and the release notes, telling eachparticipant to compare the prepared transaction with its agreed terms before signing inputs. It is
the cheapest, and it relies on every participant doing that by hand.
equal the agreed terms, and the wallet's net outflow must equal its contribution plus its agreed fee
share), and it refuses the transaction in the reproduction below. It protects only users of that
client.
Additional context
The Qt workflow from #7701 keeps each participant's details and compares them with the transaction
before signing (
MnShareSession::adoptLockedTerms,MnShareSession::payloadMatchesEnvelope), inaddition to its amount checks (
SharedMnCreateDialog::checkOwnFunding). This report covers thedirect RPC workflow only. I have not demonstrated the same behavior through Qt.
Reproduction outline, on regtest with v24 active:
createrawtransactionspending A's and B's outputs, with a change output to each.protx shared_register_preparewith two shares, 600 DASH naming owner, refund and rewardaddresses from coord's wallet, and 400 DASH naming B's.
protx shared_signin coord's and B's wallets, thenprotx shared_combine.signrawtransactionwithwalletin A's wallet, then in B's, thensendrawtransaction.The script below runs exactly these steps on a throwaway regtest node in a temporary data directory
(RPC port 29710, no peer listening) and removes it afterwards. Run it with
DASH_BIN=/path/to/dashcore-24.0.0-rc.1/bin python3 repro_shared_register_unlisted_participant.py.repro_shared_register_unlisted_participant.py
Expected output, from the release build on 2026-09-27 (exit status 0):
I found no matching open report.
System information
Dash Core v24.0.0-rc.1, the release build
dashcore-24.0.0-rc.1-aarch64-linux-gnu.tar.gz(detachedsignature and SHA256SUMS verified), on regtest in a Debian bookworm container.