From 5e4a64ee49cbec19401018b07170b576e1be7195 Mon Sep 17 00:00:00 2001 From: pasta Date: Sun, 1 Nov 2026 13:45:11 +0000 Subject: [PATCH 1/4] params%test: add a script that writes the BRR payment corpus brr_payments.py reads the block value and masternode payment of the first and last block of each reallocation period over established RPCs and writes brr_payments.json5. contrib stays out of the published crate. --- MODULE.bazel.lock | 2 +- pkgs/params/Cargo.toml | 3 + pkgs/params/contrib/brr_payments.py | 332 ++++++++++++++++++++++++++ pkgs/params/corpus/brr_payments.json5 | 86 +++++++ 4 files changed, 422 insertions(+), 1 deletion(-) create mode 100755 pkgs/params/contrib/brr_payments.py create mode 100644 pkgs/params/corpus/brr_payments.json5 diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 0106cfcb4..e0372a598 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -508,7 +508,7 @@ "FILE:@@//pkgs/types/Cargo.toml 799d7b380252085f067daf6e89b8cc4853683cbf10495e97116abf3da8473143", "FILE:@@//pkgs/types/marker/Cargo.toml f942e6618c2cf5807e8186e69795f08a641bc25ca4fe7babb835c1970a181d26", "FILE:@@//meta/dev/Cargo.toml 6ce2554a5f2715d749e17db39c665ba4a0a4b06f0be435f6c96a602a26ce29bd", - "FILE:@@//pkgs/params/Cargo.toml e41ec68cf9db3ca5acec94c30c3718483cdba9a3f92cdf75ecc35973a3fa580c", + "FILE:@@//pkgs/params/Cargo.toml e6a2f385a46ef872afd059ac2e7d69847034d9c32a5113f264f9821215cbce69", "FILE:@@//pkgs/pow/Cargo.toml 3f33e4320c8dc8836ac9655cbadd5e9cacee225b30950e309f265a66d5c29512", "FILE:@@//pkgs/primitives/Cargo.toml 7693a4e45c573f346126d8420e66b6cdfff3513d02234448737c03cfb4c9096a", "FILE:@@//pkgs/pkc/Cargo.toml a16f6907ab4048fbe3aa7423f49aaa615ec5ecdc1faabf13ee4e958593cea8cf", diff --git a/pkgs/params/Cargo.toml b/pkgs/params/Cargo.toml index 1f493344b..7c2ca6eda 100644 --- a/pkgs/params/Cargo.toml +++ b/pkgs/params/Cargo.toml @@ -1,6 +1,9 @@ [package] name = "dash-params" version = "0.0.0" +exclude = [ + "contrib", +] readme = "README.md" authors.workspace = true edition.workspace = true diff --git a/pkgs/params/contrib/brr_payments.py b/pkgs/params/contrib/brr_payments.py new file mode 100755 index 000000000..846c59e5b --- /dev/null +++ b/pkgs/params/contrib/brr_payments.py @@ -0,0 +1,332 @@ +#!/usr/bin/env python3 +# coding: latin-1 + +# +# Copyright (c) 2026-present, The Dash Core developers +# SPDX-License-Identifier: MIT +# See the accompanying file LICENSE or https://opensource.org/license/MIT +# + +"""Write the block reward reallocation payment corpus. + +Reads blocks from nodes of the reference implementation over JSON-RPC and +writes `pkgs/params/corpus/brr_payments.json5`. For the first and last block +of each block reward reallocation period before `v20` activates, a row holds +the block value and the masternode payment from it, keyed by the chain the +node reports. A node needs `-txindex` and the block and undo data of the +heights read, but no connections. + +Only established RPCs are read: + +- `getblockchaininfo`, for the chain and its tip; +- `getdeploymentinfo`, for the `realloc` and `v20` heights; +- `getgovernanceinfo`, for the superblock cycle; +- `getblockhash` and `getblockstats`, whose subsidy plus fees is the block + value; +- `masternode payments`, for the masternode payment; +- `getblock`, for the coinbase, which must pay each masternode payee; +- `getnetworkinfo`, for the node version. + +Each node is given by its RPC address and cookie file. Nodes are reached on +loopback only. Heights above a chain's `--max-height`, by default the node's +tip, are left out, and a `--max-height` above the tip is refused. `--release` +names the release archive the nodes run, whose SHA-256 goes into the header. +Without `--output` the corpus is written, which needs a node on `main` and +one on `test`. Other sets of chains need `--output`. + +Usage: + + ./pkgs/params/contrib/brr_payments.py \\ + --node 127.0.0.1:9998 ~/.dashcore/.cookie \\ + --node 127.0.0.1:19998 ~/.dashcore/testnet3/.cookie \\ + --max-height main 1877608 \\ + --release dashcore-24.0.0-rc.5-x86_64-linux-gnu.tar.gz +""" + +from __future__ import annotations + +import argparse +import base64 +import hashlib +import http.client +import ipaddress +import json +import sys +from pathlib import Path +from typing import TypeVar + +T = TypeVar("T") + +# Where the corpus is written by default. +CORPUS = ( + Path(__file__).resolve().parent.parent / "corpus" / "brr_payments.json5" +) + +# The chains the corpus holds, as nodes name them. +CORPUS_CHAINS = ["main", "test"] + +# Reallocation periods with a share of their own; the last share then holds. +PERIODS = 19 + +# Superblock cycles in a reallocation period. +CYCLES_PER_PERIOD = 3 + +# The highest mainnet height written. +MAINNET_MAX_HEIGHT = 2_077_000 + +# Seconds to wait for a reply. +RPC_TIMEOUT = 600 + + +class RpcError(Exception): + """A failed call, or a reply without the expected members.""" + + +def is_loopback(host: str) -> bool: + """Whether *host* is `localhost` or a loopback address.""" + if host == "localhost": + return True + # An IPv6 address in a host:port pair comes in brackets. + if host.startswith("[") and host.endswith("]"): + host = host[1:-1] + try: + return ipaddress.ip_address(host).is_loopback + except ValueError: + return False + + +class Node: + """A JSON-RPC client of one node, authenticated with its cookie.""" + + def __init__(self, address: str, cookie: Path) -> None: + host, _, port = address.rpartition(":") + if not host or not port.isdigit(): + raise RpcError(f"{address!r} is not a host:port address") + if not is_loopback(host): + hint = "reach a remote node through a tunnel" + raise RpcError(f"{host} is not a loopback address; {hint}") + self.host = host + self.port = int(port) + token = base64.b64encode(cookie.read_bytes().strip()).decode() + self.headers = { + "Authorization": f"Basic {token}", + "Content-Type": "application/json", + } + + def call(self, method: str, *params: object) -> object: + """Return the result of RPC *method* with *params*.""" + body = json.dumps({"id": 0, "method": method, "params": list(params)}) + conn = http.client.HTTPConnection(self.host, self.port, timeout=RPC_TIMEOUT) + try: + conn.request("POST", "/", body, self.headers) + response = conn.getresponse() + data = response.read() + except http.client.HTTPException as err: + raise RpcError(f"{method}: {err!r}") from err + finally: + conn.close() + if response.status == http.client.UNAUTHORIZED: + raise RpcError(f"{method}: the cookie was refused") + # The node sends an error with another status and a JSON body naming it, + # so the body is decoded before the status is checked. + try: + reply = json.loads(data) + except ValueError: + reply = None + error = reply.get("error") if isinstance(reply, dict) else None + if error is not None: + raise RpcError(f"{method}: {error!r:.300}") + if response.status != http.client.OK or not isinstance(reply, dict): + raise RpcError(f"{method}: HTTP {response.status}, {data[:300]!r}") + return reply.get("result") + + +def field(obj: object, key: str, kind: type[T]) -> T: + """Return member *key* of the JSON object *obj*, checked to be a *kind*.""" + value = obj.get(key) if isinstance(obj, dict) else None + # Python reads a JSON boolean as an int, so an int member refuses one. + if not isinstance(value, kind) or (isinstance(value, bool) and kind is int): + raise RpcError(f"{key!r} is not a {kind.__name__} in {obj!r:.300}") + return value + + +def heights( + node: Node, chain: str, tip: int, max_height: int | None +) -> list[int]: + """Return the first and last height of each period the corpus covers.""" + deployments = field(node.call("getdeploymentinfo"), "deployments", dict) + brr = field(field(deployments, "realloc", dict), "height", int) + v20 = field(field(deployments, "v20", dict), "height", int) + cycle = field(node.call("getgovernanceinfo"), "superblockcycle", int) + # The first period starts with the first superblock cycle after `realloc`. + start = brr - brr % cycle + cycle + period = CYCLES_PER_PERIOD * cycle + if max_height is not None and max_height > tip: + raise RpcError(f"--max-height {max_height} is above the {chain} tip {tip}") + top = tip if max_height is None else max_height + found = [] + for index in range(PERIODS): + first = start + index * period + found += [h for h in (first, first + period - 1) if h < v20 and h <= top] + if chain == "main" and found and found[-1] > MAINNET_MAX_HEIGHT: + raise RpcError( + f"refusing mainnet height {found[-1]} above {MAINNET_MAX_HEIGHT}" + ) + return found + + +def payment(node: Node, height: int) -> dict[str, int]: + """Return the corpus row of the block at *height*.""" + block_hash = node.call("getblockhash", height) + stats = node.call("getblockstats", block_hash, ["subsidy", "totalfee"]) + value = field(stats, "subsidy", int) + field(stats, "totalfee", int) + blocks = node.call("masternode", "payments", block_hash, 1) + if not isinstance(blocks, list) or len(blocks) != 1: + raise RpcError(f"masternode payments {height}: {blocks!r:.300}") + block = blocks[0] + if ( + field(block, "height", int) != height + or field(block, "blockhash", str) != block_hash + ): + raise RpcError(f"masternode payments {height}: {block!r:.300}") + txs = field(node.call("getblock", block_hash, 2), "tx", list) + if not txs: + raise RpcError(f"getblock {height}: no transactions") + outputs = { + ( + field(field(out, "scriptPubKey", dict), "hex", str), + field(out, "valueSat", int), + ) + for out in field(txs[0], "vout", list) + } + for masternode in field(block, "masternodes", list): + for payee in field(masternode, "payees", list): + output = (field(payee, "script", str), field(payee, "amount", int)) + if output not in outputs: + raise RpcError(f"the coinbase at {height} does not pay {output}") + paid = field(block, "amount", int) + return {"height": height, "block_value": value, "payment": paid} + + +def render( + chains: dict[str, list[dict[str, int]]], + version: str, + release: Path | None, +) -> str: + """Return the corpus text, a comment header and then the rows by chain.""" + lines = [ + "//", + "// Block reward reallocation payments. Generated by", + "// pkgs/params/contrib/brr_payments.py with the reference implementation", + f"// (dashpay/dash {version}).", + ] + if release is not None: + digest = hashlib.sha256(release.read_bytes()).hexdigest() + lines += [ + "//", + f"// Release {release.name}, SHA-256", + f"// {digest}.", + ] + lines += [ + "//", + "// Each row is the block at the first or last height of a reallocation", + "// period. Its block value is the subsidy plus the fees `getblockstats`", + "// reports, and its payment the amount `masternode payments` reports,", + "// which the block's coinbase pays to each payee.", + "//", + "// Heights by chain", + "//", + ] + for chain, rows in chains.items(): + first, last = rows[0]["height"], rows[-1]["height"] + lines.append(f"// {chain} {first} to {last}") + lines += ["//", "", "{", ' "payments": {'] + for index, (chain, rows) in enumerate(chains.items()): + lines.append(f" {json.dumps(chain)}: [") + lines += [ + f" {json.dumps(row)}{',' if n + 1 < len(rows) else ''}" + for n, row in enumerate(rows) + ] + lines.append(" ]" + ("," if index + 1 < len(chains) else "")) + lines += [" }", "}", ""] + return "\n".join(lines) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description=__doc__.splitlines()[0] if __doc__ else None + ) + parser.add_argument( + "--node", + nargs=2, + action="append", + required=True, + metavar=("HOST:PORT", "COOKIE"), + help="RPC address and cookie file of a node, once per chain", + ) + parser.add_argument( + "--max-height", + nargs=2, + action="append", + default=[], + metavar=("CHAIN", "HEIGHT"), + help="highest height to read on CHAIN, by default the node's tip", + ) + parser.add_argument( + "--release", + type=Path, + help="release archive the nodes run, recorded with its SHA-256", + ) + parser.add_argument( + "--output", + type=Path, + help="file to write, by default the corpus, which needs main and test", + ) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + max_heights: dict[str, int] = {} + for chain, height in args.max_height: + if chain in max_heights: + raise RpcError(f"--max-height given twice for chain {chain}") + max_heights[chain] = int(height) + chains: dict[str, list[dict[str, int]]] = {} + versions = set() + for address, cookie in args.node: + node = Node(address, Path(cookie).expanduser()) + info = node.call("getblockchaininfo") + chain = field(info, "chain", str) + if chain in chains: + raise RpcError(f"two nodes on chain {chain}") + tip = field(info, "blocks", int) + found = heights(node, chain, tip, max_heights.pop(chain, None)) + if not found: + raise RpcError(f"no heights to read on chain {chain}") + chains[chain] = [payment(node, height) for height in found] + versions.add(field(node.call("getnetworkinfo"), "buildversion", str)) + if max_heights: + raise RpcError(f"no node on chain {', '.join(max_heights)}") + output = args.output + if output is None: + if sorted(chains) != CORPUS_CHAINS: + need = " and ".join(CORPUS_CHAINS) + hint = "give --output to write other chains" + raise RpcError(f"the corpus needs nodes on {need}; {hint}") + output = CORPUS + if len(versions) != 1: + raise RpcError(f"nodes run different versions {sorted(versions)}") + version = versions.pop() + release = args.release + if release is not None and version.removeprefix("v") not in release.name: + raise RpcError(f"{release.name} is not the archive of {version}") + text = render(dict(sorted(chains.items())), version, release) + output.write_text(text, encoding="latin-1") + + +if __name__ == "__main__": + try: + main() + except (RpcError, OSError, ValueError) as err: + sys.exit(f"brr_payments: {err}") diff --git a/pkgs/params/corpus/brr_payments.json5 b/pkgs/params/corpus/brr_payments.json5 new file mode 100644 index 000000000..c1ea27390 --- /dev/null +++ b/pkgs/params/corpus/brr_payments.json5 @@ -0,0 +1,86 @@ +// +// Block reward reallocation payments. Generated by +// pkgs/params/contrib/brr_payments.py with the reference implementation +// (dashpay/dash v24.0.0-rc.5). +// +// Release dashcore-24.0.0-rc.5-x86_64-linux-gnu.tar.gz, SHA-256 +// 54cc952f79e4733d20b950c93b9c0824ae4410acf9a4972cd9361e2a107cb62d. +// +// Each row is the block at the first or last height of a reallocation +// period. Its block value is the subsidy plus the fees `getblockstats` +// reports, and its payment the amount `masternode payments` reports, +// which the block's coinbase pays to each payee. +// +// Heights by chain +// +// main 1379128 to 1877608 +// test 387504 to 388871 +// + +{ + "payments": { + "main": [ + {"height": 1379128, "block_value": 288576259, "payment": 148039620}, + {"height": 1428975, "block_value": 288495578, "payment": 147998231}, + {"height": 1428976, "block_value": 288577883, "payment": 151791966}, + {"height": 1478823, "block_value": 268117299, "payment": 141029699}, + {"height": 1478824, "block_value": 267867322, "payment": 142773282}, + {"height": 1528671, "block_value": 267894452, "payment": 142787742}, + {"height": 1528672, "block_value": 268208197, "payment": 144832426}, + {"height": 1578519, "block_value": 267927413, "payment": 144680803}, + {"height": 1578520, "block_value": 267897963, "payment": 146272287}, + {"height": 1628367, "block_value": 268152374, "payment": 146411196}, + {"height": 1628368, "block_value": 267912600, "payment": 147887755}, + {"height": 1678215, "block_value": 267867322, "payment": 147862761}, + {"height": 1678216, "block_value": 269473302, "payment": 150096629}, + {"height": 1728063, "block_value": 248734168, "payment": 138544931}, + {"height": 1728064, "block_value": 248786793, "payment": 139818177}, + {"height": 1777911, "block_value": 248796865, "payment": 139823838}, + {"height": 1777912, "block_value": 248829482, "payment": 141086316}, + {"height": 1827759, "block_value": 248744314, "payment": 141038026}, + {"height": 1827760, "block_value": 248831722, "payment": 142331744}, + {"height": 1877607, "block_value": 248744767, "payment": 142282006}, + {"height": 1877608, "block_value": 248982117, "payment": 143662681} + ], + "test": [ + {"height": 387504, "block_value": 2089286402, "payment": 1071803924}, + {"height": 387575, "block_value": 2089286854, "payment": 1071804156}, + {"height": 387576, "block_value": 2089287776, "payment": 1098965370}, + {"height": 387647, "block_value": 2089285715, "payment": 1098964286}, + {"height": 387648, "block_value": 2089287318, "payment": 1113590140}, + {"height": 387719, "block_value": 2089285715, "payment": 1113589286}, + {"height": 387720, "block_value": 2089285715, "payment": 1128214286}, + {"height": 387791, "block_value": 2089287547, "payment": 1128215275}, + {"height": 387792, "block_value": 2089286631, "payment": 1140750500}, + {"height": 387863, "block_value": 2089285715, "payment": 1140750000}, + {"height": 387864, "block_value": 2089288463, "payment": 1153287231}, + {"height": 387935, "block_value": 2089285715, "payment": 1153285714}, + {"height": 387936, "block_value": 2089285715, "payment": 1163732143}, + {"height": 388007, "block_value": 2089287318, "payment": 1163733036}, + {"height": 388008, "block_value": 2089285715, "payment": 1174178571}, + {"height": 388079, "block_value": 2089287089, "payment": 1174179344}, + {"height": 388080, "block_value": 2089286631, "payment": 1184625519}, + {"height": 388151, "block_value": 2089285944, "payment": 1184625130}, + {"height": 388152, "block_value": 2089285715, "payment": 1195071428}, + {"height": 388223, "block_value": 2089288005, "payment": 1195072738}, + {"height": 388224, "block_value": 2089285944, "payment": 1205517989}, + {"height": 388295, "block_value": 2089286173, "payment": 1205518121}, + {"height": 388296, "block_value": 2089286402, "payment": 1215964685}, + {"height": 388367, "block_value": 2089289379, "payment": 1215966418}, + {"height": 388368, "block_value": 2089286402, "payment": 1222232545}, + {"height": 388439, "block_value": 2089285715, "payment": 1222232143}, + {"height": 388440, "block_value": 2089285715, "payment": 1228500000}, + {"height": 388511, "block_value": 2089289150, "payment": 1228502020}, + {"height": 388512, "block_value": 2089291440, "payment": 1234771241}, + {"height": 388583, "block_value": 2089290066, "payment": 1234770429}, + {"height": 388584, "block_value": 2089285715, "payment": 1241035714}, + {"height": 388655, "block_value": 2089285715, "payment": 1241035714}, + {"height": 388656, "block_value": 2089291440, "payment": 1247306989}, + {"height": 388727, "block_value": 2089285715, "payment": 1247303571}, + {"height": 388728, "block_value": 2089285715, "payment": 1251482143}, + {"height": 388799, "block_value": 2089285715, "payment": 1251482143}, + {"height": 388800, "block_value": 2089285944, "payment": 1253571566}, + {"height": 388871, "block_value": 2089291440, "payment": 1253574864} + ] + } +} From 66a83061f0cf240526849787a94a2fd3447646a5 Mon Sep 17 00:00:00 2001 From: pasta Date: Sun, 1 Nov 2026 13:45:11 +0000 Subject: [PATCH 2/4] params%feat: add `BRR_PERIODS_PERMILLE`, the BRR masternode shares The masternode share of the block value in each block reward reallocation period, checked against every brr_payments.json5 row. The rows cover all 19 shares. --- pkgs/params/src/consensus.rs | 11 +++++ pkgs/params/src/lib.rs | 2 +- pkgs/params/tests/brr_payments.rs | 75 +++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 pkgs/params/tests/brr_payments.rs diff --git a/pkgs/params/src/consensus.rs b/pkgs/params/src/consensus.rs index e3c97bca9..36ef5017b 100644 --- a/pkgs/params/src/consensus.rs +++ b/pkgs/params/src/consensus.rs @@ -16,6 +16,17 @@ use dash_types::type_id::Unencodable; /// Block height paired with its block hash. pub type Checkpoint = (BlockHeight, Hash256); +/// The masternode share of the block value, in permille, for each block reward +/// reallocation period of three superblock cycles, until +/// [`ConsensusParams::v20_height`]. +/// +/// The first period starts with the first superblock cycle after +/// [`ConsensusParams::brr_height`], and the last share then holds. The shares +/// are the same on every network. +pub const BRR_PERIODS_PERMILLE: [u16; 19] = [ + 513, 526, 533, 540, 546, 552, 557, 562, 567, 572, 577, 582, 585, 588, 591, 594, 597, 599, 600, +]; + /// Buried deployment positions with hardcoded activation heights. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[cfg_attr(feature = "codec", derive(Unencodable))] diff --git a/pkgs/params/src/lib.rs b/pkgs/params/src/lib.rs index 2bab02bd2..4007da24d 100644 --- a/pkgs/params/src/lib.rs +++ b/pkgs/params/src/lib.rs @@ -29,7 +29,7 @@ use dash_types::type_id::Unencodable; pub use addr::AddrParams; pub use chain::{ChainParams, ChainTxData, MessageStart}; -pub use consensus::{BuriedDeployment, Checkpoint, ConsensusParams}; +pub use consensus::{BuriedDeployment, Checkpoint, ConsensusParams, BRR_PERIODS_PERMILLE}; pub use deployment::{Bip9Deployment, Bip9Deployments}; pub use genesis::GenesisParams; pub use llmq::{LlmqParams, LlmqType}; diff --git a/pkgs/params/tests/brr_payments.rs b/pkgs/params/tests/brr_payments.rs new file mode 100644 index 000000000..9d72f7f16 --- /dev/null +++ b/pkgs/params/tests/brr_payments.rs @@ -0,0 +1,75 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! Block reward reallocation shares against the reference implementation's +//! masternode payments. + +use dash_dev::Corpus; +use dash_params::{ConsensusParams, Network, BRR_PERIODS_PERMILLE}; +use rstest::rstest; +use serde::Deserialize; + +use std::collections::{BTreeMap, BTreeSet}; + +/// A block's value and the masternode payment from it, in duffs. +#[derive(Debug, Deserialize)] +struct Payment { + height: u32, + block_value: u64, + payment: u64, +} + +/// The chains the corpus holds rows for. +const NETWORKS: [Network; 2] = [Network::Main, Network::Testnet3]; + +/// The rows of `network`. +fn payments(network: Network) -> Vec { + let mut chains: BTreeMap> = + Corpus::open(env!("CARGO_MANIFEST_DIR"), "brr_payments").value("payments"); + let names: BTreeSet<&str> = NETWORKS.iter().map(|network| network.chain().network_id).collect(); + assert_eq!(chains.keys().map(String::as_str).collect::>(), names); + chains.remove(network.chain().network_id).unwrap_or_default() +} + +/// The index into [`BRR_PERIODS_PERMILLE`] of the share at `height`. +fn period(consensus: &ConsensusParams, height: u32) -> usize { + let cycle = consensus.superblock_cycle.unsigned_abs(); + let brr = consensus.brr_height.to_u32(); + // The first period starts with the first superblock cycle after + // `brr_height`, and the last share then holds. + let start = brr - brr % cycle + cycle; + assert!(height >= start, "{height} is before the first period"); + (((height - start) / (3 * cycle)) as usize).min(BRR_PERIODS_PERMILLE.len() - 1) +} + +#[rstest] +#[case::mainnet(Network::Main)] +#[case::testnet(Network::Testnet3)] +fn payments_match_vectors(#[case] network: Network) { + let consensus = network.consensus(); + let rows = payments(network); + assert!(!rows.is_empty()); + for row in rows { + let height = row.height; + assert!(height < consensus.v20_height.to_u32(), "{height}"); + let share = u64::from(BRR_PERIODS_PERMILLE[period(consensus, height)]); + assert_eq!(row.payment, row.block_value * share / 1000, "{height}"); + } +} + +#[test] +fn vectors_cover_every_share() { + let covered: BTreeSet = NETWORKS + .into_iter() + .flat_map(|network| { + let consensus = network.consensus(); + payments(network) + .into_iter() + .map(move |row| period(consensus, row.height)) + }) + .collect(); + assert_eq!(covered.len(), BRR_PERIODS_PERMILLE.len()); +} From c0266f71379d442167d4086994cd2ca848143647 Mon Sep 17 00:00:00 2001 From: pasta Date: Sun, 1 Nov 2026 13:45:11 +0000 Subject: [PATCH 3/4] params%doc: update changelog --- pkgs/params/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkgs/params/CHANGELOG.md b/pkgs/params/CHANGELOG.md index c17db652a..6147e62f0 100644 --- a/pkgs/params/CHANGELOG.md +++ b/pkgs/params/CHANGELOG.md @@ -9,6 +9,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - Initial release. +### Added + +- `BRR_PERIODS_PERMILLE`, the masternode share of the block value in each block reward reallocation period. + ### Changed - **`ChainParams` drops the runtime-policy fields** `prune_after_height`, `assumed_blockchain_size_gb`, From cc213259849453b4ca5c9f8087ff8babbfc09b75 Mon Sep 17 00:00:00 2001 From: pasta Date: Mon, 2 Nov 2026 04:40:49 +0000 Subject: [PATCH 4/4] params%doc: trim the BRR test and corpus script docs The test doc names the chains it checks, and the script docstring says what the corpus holds and why. --- pkgs/params/contrib/brr_payments.py | 28 ++++------------------------ pkgs/params/tests/brr_payments.rs | 3 +-- 2 files changed, 5 insertions(+), 26 deletions(-) diff --git a/pkgs/params/contrib/brr_payments.py b/pkgs/params/contrib/brr_payments.py index 846c59e5b..3b993c32c 100755 --- a/pkgs/params/contrib/brr_payments.py +++ b/pkgs/params/contrib/brr_payments.py @@ -9,30 +9,10 @@ """Write the block reward reallocation payment corpus. -Reads blocks from nodes of the reference implementation over JSON-RPC and -writes `pkgs/params/corpus/brr_payments.json5`. For the first and last block -of each block reward reallocation period before `v20` activates, a row holds -the block value and the masternode payment from it, keyed by the chain the -node reports. A node needs `-txindex` and the block and undo data of the -heights read, but no connections. - -Only established RPCs are read: - -- `getblockchaininfo`, for the chain and its tip; -- `getdeploymentinfo`, for the `realloc` and `v20` heights; -- `getgovernanceinfo`, for the superblock cycle; -- `getblockhash` and `getblockstats`, whose subsidy plus fees is the block - value; -- `masternode payments`, for the masternode payment; -- `getblock`, for the coinbase, which must pay each masternode payee; -- `getnetworkinfo`, for the node version. - -Each node is given by its RPC address and cookie file. Nodes are reached on -loopback only. Heights above a chain's `--max-height`, by default the node's -tip, are left out, and a `--max-height` above the tip is refused. `--release` -names the release archive the nodes run, whose SHA-256 goes into the header. -Without `--output` the corpus is written, which needs a node on `main` and -one on `test`. Other sets of chains need `--output`. +For the first and last block of each block reward reallocation period before +`v20`, the corpus holds the block value and the masternode payment from it, on +mainnet and testnet. The params tests check `BRR_PERIODS_PERMILLE` against +these payments. Each node needs `-txindex`. Usage: diff --git a/pkgs/params/tests/brr_payments.rs b/pkgs/params/tests/brr_payments.rs index 9d72f7f16..e99f8b773 100644 --- a/pkgs/params/tests/brr_payments.rs +++ b/pkgs/params/tests/brr_payments.rs @@ -4,8 +4,7 @@ // See the accompanying file LICENSE or https://opensource.org/license/MIT // -//! Block reward reallocation shares against the reference implementation's -//! masternode payments. +//! Block reward reallocation shares against mainnet and testnet. use dash_dev::Corpus; use dash_params::{ConsensusParams, Network, BRR_PERIODS_PERMILLE};