Build Your Own Blocks with Stratum V2 and Merge Mine Rootstock

Build Your Own Blocks with Stratum V2 and Merge Mine Rootstock

Earn RSK (rBTC) rewards from the same hashing, paid straight to an address you control.

You already build your own blocks, so the coinbase is yours. This guide turns on RSK merge mining, so that coinbase also commits a Rootstock block and earns you rBTC on the same work. DMND is the first pool to put that RSK commitment in the miner's hands instead of the pool's.

Before you start

This assumes you already have the build-your-own-blocks stack running (setup guide here): your node, the Template Provider, and the DMND Client. On top of that stack you need:

  • A synchronized RskJ node on mainnet, release VETIVER-9.0.3, with the mnr RPC module and the miner server enabled and the node fully synced. There is no testnet pool endpoint, so RSK merge mining runs on mainnet.
  • A DMND Client build with merge-mining support. The published v0.3.28 release predates the feature and will not work, so until a release ships with it merged, build and pin the reviewed commit b1d46b306f1415770e2dba9232aa47d6ca335999 (package 0.3.29) or a later reviewed revision that keeps the same contract.
  • The official Template Provider (sv2-tp), which provides the extra coinbase-output capacity the RSK marker needs.
  • A Rust 2024 toolchain to build the bridge.
  • Both hosts synchronized to UTC, because the bridge expires jobs using the client's timestamps and clock drift makes jobs look stale.
  • A process supervisor (systemd, Docker Compose) that restarts the bridge on its own, independently of the client and the pool.

Step 1: Configure the DMND Client

The bridge talks to the client over a small protected API, so bind that API to loopback and give it a strong, dedicated secret. The client's default API bind is not loopback, so set it explicitly:

API_BIND_ADDRESS=127.0.0.1 \
API_SECRET=<shared-secret> \
TOKEN=<DMND-token> \
./dmnd-client -l info -d 250T --tp-address="127.0.0.1:8336"

This is the same client run line from the setup guide with the merge-mining API switched on. The API defaults to port 3001 (--api-server-port, short form -s). The secret you set here has to match the one you give the bridge in Step 3.

Verify: the client is running in Job Declaration mode, declaring templates to the pool, with the API listening on 127.0.0.1:3001.

Step 2: Configure RskJ

The setting that matters most is where you get paid. rBTC rewards go to an RSK address configured on your node, so for a live mainnet deployment, set an explicit address you control:

reward.address = "<your-RSK-address>"

The alternative, miner.coinbase.secret, is a passphrase RskJ uses to derive an address into a local wallet; RSK does not recommend it for production, so treat it as testing only. This setting is separate from the client API secret and does not need to match it. Rewards arrive through RSK's Reward Manager (REMASC) after a maturity delay, not the instant a block is found, so an empty balance right after your first block is not a failure. Think of RSK as a bonus on the same work, not a second full income stream.

Beyond the payout address, RskJ has to expose HTTP RPC with the mnr module and the miner server on. The example below binds the RPC listener to loopback and runs on mainnet. Treat it as a starting point: for a real deployment, follow the official RskJ node docs and manage node configuration through a protected service setup rather than ad hoc command lines.

docker run --rm \
  -e RSKJ_SYS_PROPS='-Drpc.providers.web.http.bind_address=127.0.0.1 \
    -Drpc.modules.mnr.version=1.0 -Drpc.modules.mnr.enabled=true \
    -Dminer.server.enabled=true -Dminer.reward.address=<your-RSK-address>' \
  rsksmart/rskj:VETIVER-9.0.3

Three settings carry the work here:

  • rpc.modules.mnr.enabled=true exposes mnr_getWork, mnr_submitBitcoinBlock, and mnr_submitBitcoinBlockPartialMerkle.
  • miner.server.enabled=true is required for mnr_getWork.
  • RSK_RPC_URL, set on the bridge in the next step, must point at this listener, normally http://127.0.0.1:4444.

Leave the bind address on loopback rather than 0.0.0.0.

Verify: the node is synced and mnr_getWork responds over the RPC listener.

Step 3: Build and run the bridge

Build the bridge from the DEMAND monorepo's rust-backend directory:

cargo build --release -p demand-rsk-op-return-bridge

Then run it with its environment configured through your supervisor:

RSK_RPC_URL=http://127.0.0.1:4444 \
DMND_CLIENT_API_SECRET=<strong-random-shared-secret> \
DMND_CLIENT_OP_RETURN_URL=http://127.0.0.1:3001/api/coinbase/op-return \
DMND_CLIENT_FOUND_JOB_URL=http://127.0.0.1:3001/api/merge-mining/found-job \
./target/release/demand-rsk-op-return-bridge

Only two variables are required: RSK_RPC_URL and DMND_CLIENT_API_SECRET (which must exactly match the client's API_SECRET). Everything else has a sane default:

VariableRequiredDefaultPurpose
RSK_RPC_URLYesnoneProtected RskJ HTTP RPC listener.
DMND_CLIENT_API_SECRETYesnoneMust exactly match the client API_SECRET.
DMND_CLIENT_OP_RETURN_URLNohttp://127.0.0.1:3001/api/coinbase/op-returnWhere the RSK commitment is posted.
DMND_CLIENT_FOUND_JOB_URLNoDerived from the OP_RETURN URLWhere found jobs are polled. Set explicitly if the OP_RETURN URL is custom.
RSK_POLL_INTERVAL_SECSNo1How often to poll RskJ for work.
FOUND_JOB_POLL_INTERVAL_SECSNo1How often to poll the client for found jobs.
JOB_RETRY_INTERVAL_SECSNo5Delay between submission retries.
FOUND_JOB_MAX_AGE_SECSNo600Oldest job age still worth submitting.
DMND_CLIENT_WORK_RESYNC_INTERVAL_SECSNo60Idempotent repost of unchanged work.
MAX_PENDING_FOUND_JOBSNo256Proofs held in bridge memory.

The binary loads a local .env file if one exists. A configuration error exits with status 2; runtime RPC and HTTP errors are logged and retried, because temporary RskJ failures are expected.

Run the bridge and the client as separate supervised processes with independent restart policies. A bridge crash must not restart the client. When the client restarts, restart the bridge once the client API is healthy so it reposts the current RSK work; reconnects inside a still-running client process keep the desired pair on their own. Keep both hosts synchronized to UTC with NTP or chrony, since proof expiry uses the client's timestamp.

Security: read first

The bridge model only holds if the control plane stays private.

  • Neither the client API nor the RskJ RPC listener may face the public internet. Run RskJ, the bridge, and the client on one host with both listeners bound to 127.0.0.1; across hosts, carry the traffic over an authenticated private tunnel (TLS/mTLS or an encrypted network). A private IP by itself is not authentication.
  • API_SECRET is application authentication, not a network perimeter. It proves the caller; it does not firewall the port.
  • The found-job request carries the secret in its URL query string. Any proxy, access log, or APM in front of these services must redact full request targets and query strings. Never put the secret, tokens, or credentials in an endpoint URL.
  • Keep secrets distinct. The client API secret, the RskJ miner secret, and your mining token are three different things. Do not reuse one for another.

Verify it is working

The bridge exposes no health endpoint, so you monitor it through its process and its logs. Healthy operation shows three log lines:

  • queued RSK merge-mining payload into dmnd-client — new or resynced RSK work was accepted by the client.
  • queued found merge-mining job for RSK submission — a miner found a qualifying share (naturally rare).
  • submitted merge-mined Bitcoin block to RSK — RskJ accepted a submission.

The submission log tags a submission_mode: partial-merkle for a multi-transaction block, coinbase-only-block for a one-transaction block. Found-job and submission logs are sparse by nature, so their absence alone does not mean the bridge is broken, only that no RSK-qualified share has come up yet. Your logs may contain job IDs, template IDs, and work hashes, but never API secrets, POST authentication bodies, or full found-job request targets — sanitize before sharing.

Troubleshooting

SymptomCheck
401 Unauthorized from the clientDMND_CLIENT_API_SECRET must exactly match the client's API_SECRET.
503 Service Unavailable from the clientConfirm a non-empty API_SECRET is set on the client.
Connection refused on port 3001Check the client's API port, bind address, and local firewall. Do not fix this by exposing the port.
RskJ reports method not foundEnable the mnr module and the miner server, then restart RskJ.
Work is fetched but no RSK jobs reach minersConfirm RskJ is synced, the client is in Job Declaration mode, and its Template Provider is serving new templates.
Jobs expire immediatelyCheck UTC clock sync between hosts and FOUND_JOB_MAX_AGE_SECS.
Repeated rate-limit messagesLet the bridge cooldown finish, then review the RskJ rate-limit policy.
Repeated transport timeoutsCheck the route, service health, and proxy bypass. The client uses 5s connect / 20s request timeouts.

RUST_LOG=debug helps temporarily, but debug output carries extra work and proof metadata and must follow the same log-handling rules.


RSK merge mining is live, and it is opt-in and off by default, so turning it on is your choice. It runs against the specific reviewed builds named in the prerequisites above, and none of it goes near your Bitcoin block submission path, so if the RSK side ever pauses, your Bitcoin mining keeps running untouched.
Keep an eye on the client README. The canonical, always-current merge-mining reference lives at github.com/dmnd-pool/dmnd-client. Releases move regularly, so if a flag, port, endpoint, or commit ever differs from what you read here, the README is the source of truth.

Support is in your registration email. Join now https:
//join.dmnd.work

Subscribe to DMND

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe