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
255 changes: 131 additions & 124 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ sc-consensus = { version = "0.50.0", default-features = false }
sc-executor = { version = "0.43.0", default-features = false }
sc-network = { version = "0.51.0", default-features = false }
sc-network-common = { version = "0.49.0", default-features = false }
sc-network-sync = { version = "0.50.0", default-features = false }
sc-network-types = { version = "0.17.0", default-features = false }
sc-offchain = { version = "46.0.0", default-features = false }
sc-service = { version = "0.52.0", default-features = false }
Expand Down
461 changes: 224 additions & 237 deletions EXTERNAL_MINER_PROTOCOL.md

Large diffs are not rendered by default.

2 changes: 0 additions & 2 deletions client/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ log = { workspace = true, default-features = true }
names = { workspace = true, default-features = false }
qp-dilithium-crypto = { workspace = true, features = ["full_crypto", "serde", "std"] }
qp-rusty-crystals-dilithium = { workspace = true }
qp-rusty-crystals-hdwallet = { workspace = true }
rand = { workspace = true, default-features = true }
regex = { workspace = true }
rpassword = { workspace = true }
Expand All @@ -46,7 +45,6 @@ serde_json = { workspace = true, default-features = true }
sp-blockchain = { workspace = true, default-features = true }
sp-core = { workspace = true, default-features = true }
sp-keyring = { workspace = true, default-features = true }
sp-keystore = { workspace = true, default-features = true }
sp-panic-handler = { workspace = true, default-features = true }
sp-runtime = { workspace = true, default-features = true }
sp-version = { workspace = true, default-features = true }
Expand Down
6 changes: 0 additions & 6 deletions client/consensus/qpow/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@ prometheus-endpoint = { workspace = true, default-features = true }
sc-client-api = { workspace = true, default-features = false }
sc-consensus = { workspace = true }
sc-service = { workspace = true, default-features = false }
scale-info = { workspace = true, default-features = false }
sha2.workspace = true
sha3.workspace = true
sp-api = { workspace = true, default-features = false }
sp-block-builder = { workspace = true, default-features = true }
sp-blockchain = { workspace = true, default-features = false }
Expand All @@ -37,9 +34,6 @@ default = ["std"]
std = [
"codec/std",
"primitive-types/std",
"scale-info/std",
"sha2/std",
"sha3/std",
"sp-api/std",
"sp-consensus-pow/std",
"sp-consensus-qpow/std",
Expand Down
35 changes: 35 additions & 0 deletions client/consensus/qpow/src/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ use sc_consensus::{BlockImportParams, BoxBlockImport, StateAction, StorageChange
use sp_api::ProvideRuntimeApi;
use sp_consensus::{BlockOrigin, Proposal};
use sp_consensus_pow::{Seal, POW_ENGINE_ID};
use sp_consensus_qpow::QPoWApi;
use sp_runtime::{
traits::{Block as BlockT, Header as HeaderT},
AccountId32, DigestItem,
Expand Down Expand Up @@ -82,6 +83,7 @@ impl<Block, AC, L, Proof> MiningHandle<Block, AC, L, Proof>
where
Block: BlockT<Hash = H256>,
AC: ProvideRuntimeApi<Block>,
AC::Api: QPoWApi<Block>,
L: sc_consensus::JustificationSyncLink<Block>,
{
fn increment_version(&self) {
Expand Down Expand Up @@ -133,6 +135,39 @@ where
self.build.lock().as_ref().map(|b| b.metadata.clone())
}

/// Verify a seal without consuming the build.
///
/// Returns `true` if the seal is valid for the current block, `false` otherwise.
/// Returns `false` if there's no current build.
pub fn verify_seal(&self, seal: &Seal) -> bool {
let build = self.build.lock();
let build = match build.as_ref() {
Some(b) => b,
None => return false,
};

// Convert seal to nonce [u8; 64]
let nonce: [u8; 64] = match seal.as_slice().try_into() {
Ok(arr) => arr,
Err(_) => {
warn!(target: LOG_TARGET, "Seal does not have exactly 64 bytes");
return false;
},
};

let pre_hash = build.metadata.pre_hash.0;
let best_hash = build.metadata.best_hash;

// Verify using runtime API
match self.client.runtime_api().verify_nonce_local_mining(best_hash, pre_hash, nonce) {
Ok(valid) => valid,
Err(e) => {
warn!(target: LOG_TARGET, "Runtime API error verifying seal: {:?}", e);
false
},
}
}

/// Submit a mined seal. The seal will be validated again. Returns true if the submission is
/// successful.
#[allow(clippy::await_holding_lock)]
Expand Down
1 change: 0 additions & 1 deletion client/network/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ libp2p-identity = { workspace = true, features = ["dilithium"] }
linked_hash_set = { workspace = true }
log = { workspace = true, default-features = true }
mockall = { workspace = true }
once_cell = { workspace = true }
parking_lot = { workspace = true, default-features = true }
partial_sort = { workspace = true }
pin-project = { workspace = true }
Expand Down
2 changes: 2 additions & 0 deletions miner-api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ version = "0.0.3"

[dependencies]
serde = { workspace = true, features = ["alloc", "derive"] }
serde_json = { workspace = true, features = ["std"] }
tokio = { workspace = true, features = ["io-util"] }
75 changes: 69 additions & 6 deletions miner-api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
use serde::{Deserialize, Serialize};
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};

/// Maximum message size (16 MB) to prevent memory exhaustion attacks.
pub const MAX_MESSAGE_SIZE: u32 = 16 * 1024 * 1024;

/// Status codes returned in API responses.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
Expand All @@ -13,18 +17,74 @@ pub enum ApiResponseStatus {
Error,
}

/// Request payload sent from Node to Miner (`/mine` endpoint).
/// QUIC protocol messages exchanged between node and miner.
///
/// The protocol is:
/// - Miner sends `Ready` immediately after connecting to establish the stream
/// - Node sends `NewJob` to submit a mining job (implicitly cancels any previous job)
/// - Miner sends `JobResult` when mining completes
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum MinerMessage {
/// Miner → Node: Sent immediately after connecting to establish the stream.
/// This is required because QUIC streams are lazily initialized.
Ready,

/// Node → Miner: Submit a new mining job.
/// If a job is already running, it will be cancelled and replaced.
NewJob(MiningRequest),

/// Miner → Node: Mining result (completed, failed, or cancelled).
JobResult(MiningResult),
}

/// Write a length-prefixed JSON message to an async writer.
///
/// Wire format: 4-byte big-endian length prefix followed by JSON payload.
pub async fn write_message<W: AsyncWrite + Unpin>(
writer: &mut W,
msg: &MinerMessage,
) -> std::io::Result<()> {
let json = serde_json::to_vec(msg)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
let len = json.len() as u32;
writer.write_all(&len.to_be_bytes()).await?;
writer.write_all(&json).await?;
Ok(())
}

/// Read a length-prefixed JSON message from an async reader.
///
/// Wire format: 4-byte big-endian length prefix followed by JSON payload.
/// Returns an error if the message exceeds MAX_MESSAGE_SIZE.
pub async fn read_message<R: AsyncRead + Unpin>(reader: &mut R) -> std::io::Result<MinerMessage> {
let mut len_buf = [0u8; 4];
reader.read_exact(&mut len_buf).await?;
let len = u32::from_be_bytes(len_buf);

if len > MAX_MESSAGE_SIZE {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Message size {} exceeds maximum {}", len, MAX_MESSAGE_SIZE),
));
}

let mut buf = vec![0u8; len as usize];
reader.read_exact(&mut buf).await?;
serde_json::from_slice(&buf)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
}

/// Request payload sent from Node to Miner.
///
/// The miner will choose its own random starting nonce, enabling multiple
/// miners to work on the same job without coordination.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct MiningRequest {
pub job_id: String,
/// Hex encoded header hash (32 bytes -> 64 chars, no 0x prefix)
pub mining_hash: String,
/// Distance threshold (u64 as string)
/// Distance threshold (U512 as decimal string)
pub distance_threshold: String,
/// Hex encoded start nonce (U512 -> 128 chars, no 0x prefix)
pub nonce_start: String,
/// Hex encoded end nonce (U512 -> 128 chars, no 0x prefix)
pub nonce_end: String,
}

/// Response payload for job submission (`/mine`) and cancellation (`/cancel`).
Expand All @@ -48,4 +108,7 @@ pub struct MiningResult {
pub work: Option<String>,
pub hash_count: u64,
pub elapsed_time: f64,
/// Miner ID assigned by the node (set server-side, not by the miner).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub miner_id: Option<u64>,
}
9 changes: 6 additions & 3 deletions node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,17 @@ pallet-transaction-payment-rpc.default-features = true
pallet-transaction-payment-rpc.workspace = true
prometheus.workspace = true
qp-dilithium-crypto = { workspace = true }
qp-rusty-crystals-dilithium.workspace = true
qp-rusty-crystals-hdwallet.workspace = true
qpow-math.workspace = true
quantus-miner-api = { workspace = true }
quantus-runtime.workspace = true
quinn = "0.10"
rand = { workspace = true, default-features = false, features = [
"alloc",
"getrandom",
] }
reqwest = { workspace = true, default-features = false, features = ["json"] }
rcgen = "0.11"
rustls = { version = "0.21", default-features = false, features = ["dangerous_configuration", "quic"] }
sc-basic-authorship.default-features = true
sc-basic-authorship.workspace = true
sc-cli.default-features = true
Expand All @@ -56,6 +57,8 @@ sc-executor.default-features = true
sc-executor.workspace = true
sc-network.default-features = true
sc-network.workspace = true
sc-network-sync.default-features = true
sc-network-sync.workspace = true
sc-offchain.default-features = true
sc-offchain.workspace = true
sc-service.default-features = true
Expand Down Expand Up @@ -93,7 +96,6 @@ sp-timestamp.workspace = true
substrate-frame-rpc-system.default-features = true
substrate-frame-rpc-system.workspace = true
tokio-util.workspace = true
uuid.workspace = true

[build-dependencies]
qp-wormhole-circuit-builder.workspace = true
Expand All @@ -115,6 +117,7 @@ std = [
"serde_json/std",
"sp-consensus-qpow/std",
]
tx-logging = [] # Enable transaction pool logging for debugging
# Dependencies that are only required if runtime benchmarking should be build.
runtime-benchmarks = [
"frame-benchmarking-cli/runtime-benchmarks",
Expand Down
7 changes: 4 additions & 3 deletions node/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ pub struct Cli {
#[arg(long, value_name = "REWARDS_ADDRESS")]
pub rewards_address: Option<String>,

/// Specify the URL of an external QPoW miner service
#[arg(long, value_name = "EXTERNAL_MINER_URL")]
pub external_miner_url: Option<String>,
/// Port to listen for external miner connections (e.g., 9833).
/// When set, the node will wait for miners to connect instead of mining locally.
#[arg(long, value_name = "PORT")]
pub miner_listen_port: Option<u16>,

/// Enable peer sharing via RPC endpoint
#[arg(long)]
Expand Down
4 changes: 1 addition & 3 deletions node/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -480,9 +480,7 @@ pub fn run() -> sc_cli::Result<()> {
quantus_runtime::opaque::Block,
<quantus_runtime::opaque::Block as sp_runtime::traits::Block>::Hash,
>,
>(
config, rewards_account, cli.external_miner_url.clone(), cli.enable_peer_sharing
)
>(config, rewards_account, cli.miner_listen_port, cli.enable_peer_sharing)
.map_err(sc_cli::Error::Service)
})
},
Expand Down
110 changes: 0 additions & 110 deletions node/src/external_miner_client.rs

This file was deleted.

Loading
Loading