Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
697 changes: 697 additions & 0 deletions MERGE_MINING.md

Large diffs are not rendered by default.

67 changes: 67 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,73 @@ POOL_ADDRESSES=pool-a.example.com:20000,pool-b.example.com:20000 \
./dmnd-client -l info -d 250T --tp-address="127.0.0.1:8336"
```

### 4.3 RSK merge mining

See [MERGE_MINING.md](MERGE_MINING.md) for the proxy design, safety model, complete bridge HTTP
contract, RskJ proof format, byte-order rules, and a bridge conformance checklist.

RSK merge mining requires Job Declaration mode (`--tp-address`), a Template Provider that honors
the advertised coinbase-output allowance, RskJ, and the `demand-rsk-op-return-bridge` service from
the adjacent DEMAND repository. The client reserves 100 additional serialized coinbase-output
bytes when it advertises capacity to the Template Provider. If that reservation or an RSK
operation fails, the original Bitcoin template and Bitcoin share paths remain authoritative.

Before an RSK proof can be queued, the client also enforces RskJ's raw-coinbase rule: the desired
payload must start at the last raw `RSKBLOCK:` marker and no more than 128 bytes may follow its hash
in the witness-stripped coinbase. It validates the prospective outputs plus locktime before
publishing the job, including markers that could be assembled across serialized field boundaries.

Configure a non-empty secret on the DMND client and run it with the Template Provider:

```sh
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"
```

RskJ's HTTP RPC configuration must enable both merge-mining work and the miner server:

```text
-Drpc.modules.mnr.enabled=true -Dminer.server.enabled=true
```

From `../demand/rust-backend`, run the bridge with the same secret:

```sh
DMND_CLIENT_API_SECRET=<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 \
RSK_RPC_URL=http://127.0.0.1:4444 \
cargo run -p demand-rsk-op-return-bridge
```

The companion bridge interoperates with this proxy but currently relies on the proxy and RskJ for
some proof-coherence validation, and its pending proof retry queue has no hard item cap. See the
production validation and bounded-state requirements in
[MERGE_MINING.md](MERGE_MINING.md#5-byte-order-and-proof-validation) when building or hardening a
bridge across a separate trust boundary.

The bridge posts each atomic RSK payload/target pair to the first endpoint and destructively polls
proof candidates from the second. The client keeps at most 32 proof candidates in memory and drops
the oldest candidate on overflow; no merge-mining queue operation blocks Bitcoin share handling.
Merge mining never lowers a miner's normal Bitcoin difficulty. The bounded MM observer evaluates
every authenticated, structurally valid share the miner submits before normal Bitcoin-difficulty
filtering. If the RSK target is easier than the miner's assigned target, however, the ASIC does not
report every RSK-only hash and those unreported hashes cannot be observed. This intentional
stability-first policy avoids increasing miner traffic or changing Bitcoin difficulty.

Run the bridge and DMND client as separate supervised processes. A bridge crash or restart must not
restart the DMND client. Whenever the DMND client process restarts, restart the bridge after the
client API is healthy so the bridge reposts the current RSK work. Pool or Job Declaration
reconnects inside the same DMND client process retain the desired pair. Synchronize the client and
bridge hosts to UTC with NTP or chrony because proof expiration uses the client's
`observed_at_unix_ts` value.

The merge-mining endpoints use a shared secret in request data and do not provide TLS. Keep the API
on a trusted network or place it behind an authenticated TLS reverse proxy and firewall; do not
expose port 3001 directly to the Internet.

## 5. Connect Your Miners

With Bitcoin Core, sv2-tp, and the DMND Client all running, point your ASICs at the machine running the DMND Client:
Expand Down
33 changes: 27 additions & 6 deletions src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use axum::{
};
use routes::Api;
use stats::StatsSender;
use tracing::{error, info};

// Holds shared state (like the router) that so that it can be accessed in all routes.
#[derive(Clone)]
Expand Down Expand Up @@ -53,6 +54,14 @@ pub(crate) async fn start(
};
let app = AxumRouter::new()
.route("/api/health", get(Api::health_check))
.route(
"/api/coinbase/op-return",
post(crate::merge_mining::set_pair_api),
)
.route(
"/api/merge-mining/found-job",
get(crate::merge_mining::poll_found_job_api),
)
.route("/api/tx/submit/{tx}", post(Api::send_tx_to_bitcoind))
.route(
"/api/tx/prioritized",
Expand All @@ -66,10 +75,22 @@ pub(crate) async fn start(
.with_state(state);

let api_server_port = crate::config::Configuration::api_server_port();
let api_server_addr = format!("0.0.0.0:{api_server_port}");
let listener = tokio::net::TcpListener::bind(api_server_addr)
.await
.expect("Invalid server address");
println!("API Server listening on port {api_server_port}");
axum::serve(listener, app).await.unwrap();
let api_bind_address =
std::env::var("API_BIND_ADDRESS").unwrap_or_else(|_| "0.0.0.0".to_string());
let api_server_addr = format!("{api_bind_address}:{api_server_port}");
loop {
let listener = match tokio::net::TcpListener::bind(&api_server_addr).await {
Ok(listener) => listener,
Err(error) => {
error!(%error, %api_server_addr, "API server could not bind; mining remains active");
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
continue;
}
};
info!(%api_server_addr, "API server listening");
if let Err(error) = axum::serve(listener, app.clone()).await {
error!(%error, "API server stopped; mining remains active");
}
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
}
8 changes: 8 additions & 0 deletions src/jd_client/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ pub enum Error {
Uint256Conversion(ParseIntError),
Infallible(std::convert::Infallible),
Unrecoverable,
UnsupportedCoinbaseOutputCount {
count: usize,
max: usize,
},
TaskManagerFailed,
JdClientMutexCorrupted,

Expand Down Expand Up @@ -68,6 +72,10 @@ impl fmt::Display for Error {
VecToSlice32(ref e) => write!(f, "Standard Error: `{e:?}`"),
Infallible(ref e) => write!(f, "Infallible Error:`{e:?}`"),
Unrecoverable => write!(f, "Unrecoverable Error"),
UnsupportedCoinbaseOutputCount { count, max } => write!(
f,
"Coinbase transaction has {count} outputs; at most {max} outputs are supported"
),
JdClientMutexCorrupted => write!(f, "JdClient mutex Corrupted"),
TaskManagerFailed => write!(f, "Failed to add Task in JdClient TaskManager"),

Expand Down
21 changes: 13 additions & 8 deletions src/jd_client/job_declarator/message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use roles_logic_sv2::{
};
pub type SendTo = SendTo_<JobDeclaration<'static>, ()>;
use roles_logic_sv2::errors::Error;
use tracing::warn;

impl ParseServerJobDeclarationMessages for JobDeclarator {
fn handle_allocate_mining_job_token_success(
Expand All @@ -30,21 +31,25 @@ impl ParseServerJobDeclarationMessages for JobDeclarator {

fn handle_declare_mining_job_error(
&mut self,
_message: DeclareMiningJobError,
message: DeclareMiningJobError,
) -> Result<SendTo, Error> {
// TODO consider using declarative names instead of setting states
super::super::IS_CUSTOM_JOB_SET.store(true, std::sync::atomic::Ordering::Release);
Ok(SendTo::None(None))
Ok(SendTo::None(Some(JobDeclaration::DeclareMiningJobError(
message.into_static(),
))))
}

fn handle_provide_missing_transactions(
&mut self,
message: ProvideMissingTransactions,
) -> Result<SendTo, Error> {
let tx_list = self
.last_declare_mining_jobs_sent
.get(&message.request_id)
.ok_or(Error::UnknownRequestId(message.request_id))?
let Some(last_declare) = self.last_declare_mining_jobs_sent.get(&message.request_id) else {
warn!(
request_id = message.request_id,
"ignoring missing-transactions request for an unknown declaration"
);
return Ok(SendTo::None(None));
};
let tx_list = last_declare
.clone()
.ok_or(Error::JDSMissingTransactions)?
.tx_list
Expand Down
Loading
Loading