|
|
@@ -1,4 +1,6 @@
|
|
|
use std::str::FromStr;
|
|
|
+use std::sync::atomic::{AtomicUsize, Ordering};
|
|
|
+use std::sync::Arc;
|
|
|
|
|
|
use kernels::miner::KERNEL;
|
|
|
use nockapp::kernel::checkpoint::JamPaths;
|
|
|
@@ -11,7 +13,25 @@ use nockapp::noun::{AtomExt, NounExt};
|
|
|
use nockvm::noun::{Atom, D, T};
|
|
|
use nockvm_macros::tas;
|
|
|
use tempfile::tempdir;
|
|
|
-use tracing::{instrument, warn};
|
|
|
+use tracing::{debug, info, instrument, warn};
|
|
|
+
|
|
|
+/// Goldilocks prime: p = 2^64 - 2^32 + 1
|
|
|
+const GOLDILOCKS_PRIME: u64 = 18446744069414584321;
|
|
|
+
|
|
|
+/// Convert an atom to tip5 digest format [a b c d e]
|
|
|
+/// This mirrors the Hoon function atom-to-digest:tip5:zeke
|
|
|
+fn atom_to_digest(nonce_slab: &mut NounSlab, buffer: u64) -> nockvm::noun::Noun {
|
|
|
+ let p = GOLDILOCKS_PRIME;
|
|
|
+
|
|
|
+ let (q1, a) = (buffer / p, buffer % p);
|
|
|
+ let (q2, b) = (q1 / p, q1 % p);
|
|
|
+ let (q3, c) = (q2 / p, q2 % p);
|
|
|
+ let (e, d) = (q3 / p, q3 % p);
|
|
|
+
|
|
|
+ debug!("Converting nonce {} to digest: [{} {} {} {} {}]", buffer, a, b, c, d, e);
|
|
|
+
|
|
|
+ T(nonce_slab, &[D(a), D(b), D(c), D(d), D(e)])
|
|
|
+}
|
|
|
|
|
|
pub enum MiningWire {
|
|
|
Mined,
|
|
|
@@ -46,39 +66,97 @@ pub struct MiningKeyConfig {
|
|
|
pub share: u64,
|
|
|
pub m: u64,
|
|
|
pub keys: Vec<String>,
|
|
|
+ /// Maximum number of concurrent mining attempts (default: num_cpus::get())
|
|
|
+ pub max_concurrent_attempts: Option<usize>,
|
|
|
}
|
|
|
|
|
|
impl FromStr for MiningKeyConfig {
|
|
|
type Err = String;
|
|
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
|
- // Expected format: "share,m:key1,key2,key3"
|
|
|
+ // Expected format: "share,m:key1,key2,key3" or "share,m,max_concurrent:key1,key2,key3"
|
|
|
let parts: Vec<&str> = s.split(':').collect();
|
|
|
if parts.len() != 2 {
|
|
|
- return Err("Invalid format. Expected 'share,m:key1,key2,key3'".to_string());
|
|
|
+ return Err("Invalid format. Expected 'share,m:key1,key2,key3' or 'share,m,max_concurrent:key1,key2,key3'".to_string());
|
|
|
}
|
|
|
|
|
|
let share_m: Vec<&str> = parts[0].split(',').collect();
|
|
|
- if share_m.len() != 2 {
|
|
|
- return Err("Invalid share,m format".to_string());
|
|
|
+ if share_m.len() < 2 || share_m.len() > 3 {
|
|
|
+ return Err("Invalid share,m format. Expected 'share,m' or 'share,m,max_concurrent'".to_string());
|
|
|
}
|
|
|
|
|
|
let share = share_m[0].parse::<u64>().map_err(|e| e.to_string())?;
|
|
|
let m = share_m[1].parse::<u64>().map_err(|e| e.to_string())?;
|
|
|
+
|
|
|
+ let max_concurrent_attempts = if share_m.len() == 3 {
|
|
|
+ Some(share_m[2].parse::<usize>().map_err(|e| format!("Invalid max_concurrent value: {}", e))?)
|
|
|
+ } else {
|
|
|
+ None
|
|
|
+ };
|
|
|
+
|
|
|
let keys: Vec<String> = parts[1].split(',').map(String::from).collect();
|
|
|
|
|
|
- Ok(MiningKeyConfig { share, m, keys })
|
|
|
+ Ok(MiningKeyConfig {
|
|
|
+ share,
|
|
|
+ m,
|
|
|
+ keys,
|
|
|
+ max_concurrent_attempts,
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/// Mining configuration extracted from CLI arguments
|
|
|
+#[derive(Debug, Clone)]
|
|
|
+pub struct MiningConfig {
|
|
|
+ pub key_configs: Vec<MiningKeyConfig>,
|
|
|
+ pub max_concurrent_attempts: usize,
|
|
|
+}
|
|
|
+
|
|
|
+impl MiningConfig {
|
|
|
+ pub fn from_cli(
|
|
|
+ mining_pubkey: Option<String>,
|
|
|
+ mining_key_adv: Option<Vec<MiningKeyConfig>>,
|
|
|
+ max_concurrent_attempts: Option<usize>,
|
|
|
+ ) -> Option<Self> {
|
|
|
+ let key_configs = if let Some(pubkey) = mining_pubkey {
|
|
|
+ vec![MiningKeyConfig {
|
|
|
+ share: 1,
|
|
|
+ m: 1,
|
|
|
+ keys: vec![pubkey],
|
|
|
+ max_concurrent_attempts: None,
|
|
|
+ }]
|
|
|
+ } else if let Some(configs) = mining_key_adv {
|
|
|
+ configs
|
|
|
+ } else {
|
|
|
+ return None;
|
|
|
+ };
|
|
|
+
|
|
|
+ // Determine the maximum concurrent attempts
|
|
|
+ let max_concurrent = max_concurrent_attempts
|
|
|
+ .or_else(|| {
|
|
|
+ // Use the first config's max_concurrent_attempts if specified
|
|
|
+ key_configs.first()?.max_concurrent_attempts
|
|
|
+ })
|
|
|
+ .unwrap_or_else(|| {
|
|
|
+ // Default to number of logical CPUs
|
|
|
+ num_cpus::get()
|
|
|
+ });
|
|
|
+
|
|
|
+ Some(MiningConfig {
|
|
|
+ key_configs,
|
|
|
+ max_concurrent_attempts: max_concurrent,
|
|
|
+ })
|
|
|
}
|
|
|
}
|
|
|
|
|
|
pub fn create_mining_driver(
|
|
|
- mining_config: Option<Vec<MiningKeyConfig>>,
|
|
|
+ mining_config: Option<MiningConfig>,
|
|
|
mine: bool,
|
|
|
init_complete_tx: Option<tokio::sync::oneshot::Sender<()>>,
|
|
|
) -> IODriverFn {
|
|
|
Box::new(move |mut handle| {
|
|
|
Box::pin(async move {
|
|
|
- let Some(configs) = mining_config else {
|
|
|
+ let Some(config) = mining_config else {
|
|
|
enable_mining(&handle, false).await?;
|
|
|
|
|
|
if let Some(tx) = init_complete_tx {
|
|
|
@@ -90,14 +168,16 @@ pub fn create_mining_driver(
|
|
|
|
|
|
return Ok(());
|
|
|
};
|
|
|
- if configs.len() == 1
|
|
|
- && configs[0].share == 1
|
|
|
- && configs[0].m == 1
|
|
|
- && configs[0].keys.len() == 1
|
|
|
+
|
|
|
+ // Set up mining keys
|
|
|
+ if config.key_configs.len() == 1
|
|
|
+ && config.key_configs[0].share == 1
|
|
|
+ && config.key_configs[0].m == 1
|
|
|
+ && config.key_configs[0].keys.len() == 1
|
|
|
{
|
|
|
- set_mining_key(&handle, configs[0].keys[0].clone()).await?;
|
|
|
+ set_mining_key(&handle, config.key_configs[0].keys[0].clone()).await?;
|
|
|
} else {
|
|
|
- set_mining_key_advanced(&handle, configs).await?;
|
|
|
+ set_mining_key_advanced(&handle, config.key_configs).await?;
|
|
|
}
|
|
|
enable_mining(&handle, mine).await?;
|
|
|
|
|
|
@@ -111,8 +191,14 @@ pub fn create_mining_driver(
|
|
|
if !mine {
|
|
|
return Ok(());
|
|
|
}
|
|
|
- let mut next_attempt: Option<NounSlab> = None;
|
|
|
- let mut current_attempt: tokio::task::JoinSet<()> = tokio::task::JoinSet::new();
|
|
|
+
|
|
|
+ info!("Starting mining driver with {} max concurrent attempts", config.max_concurrent_attempts);
|
|
|
+
|
|
|
+ // Track active mining attempts
|
|
|
+ let active_attempts = Arc::new(AtomicUsize::new(0));
|
|
|
+ let mut current_attempts: tokio::task::JoinSet<()> = tokio::task::JoinSet::new();
|
|
|
+ let mut candidate_queue: Vec<(NounSlab, u64)> = Vec::new();
|
|
|
+ let base_nonce = Arc::new(AtomicUsize::new(0));
|
|
|
|
|
|
loop {
|
|
|
tokio::select! {
|
|
|
@@ -132,27 +218,57 @@ pub fn create_mining_driver(
|
|
|
slab.copy_into(effect_cell.tail());
|
|
|
slab
|
|
|
};
|
|
|
- if !current_attempt.is_empty() {
|
|
|
- next_attempt = Some(candidate_slab);
|
|
|
- } else {
|
|
|
+
|
|
|
+ info!("Received new mining candidate, preparing {} concurrent attempts",
|
|
|
+ config.max_concurrent_attempts);
|
|
|
+
|
|
|
+ base_nonce.store(0, Ordering::Relaxed);
|
|
|
+
|
|
|
+ current_attempts.abort_all();
|
|
|
+ candidate_queue.clear();
|
|
|
+ active_attempts.store(0, Ordering::Relaxed);
|
|
|
+
|
|
|
+ let nonce_range_size = 10000;
|
|
|
+
|
|
|
+ for thread_id in 0..config.max_concurrent_attempts {
|
|
|
let (cur_handle, attempt_handle) = handle.dup();
|
|
|
handle = cur_handle;
|
|
|
- current_attempt.spawn(mining_attempt(candidate_slab, attempt_handle));
|
|
|
+ let attempt_counter = Arc::clone(&active_attempts);
|
|
|
+ let _nonce_counter = Arc::clone(&base_nonce);
|
|
|
+ attempt_counter.fetch_add(1, Ordering::Relaxed);
|
|
|
+
|
|
|
+ let candidate_copy = {
|
|
|
+ let mut slab = NounSlab::new();
|
|
|
+ slab.copy_into(unsafe { *candidate_slab.root() });
|
|
|
+ slab
|
|
|
+ };
|
|
|
+
|
|
|
+ let nonce_start = thread_id * nonce_range_size;
|
|
|
+
|
|
|
+ info!("Starting mining thread {} with nonce range {}-{}",
|
|
|
+ thread_id + 1,
|
|
|
+ nonce_start,
|
|
|
+ nonce_start + nonce_range_size - 1);
|
|
|
+
|
|
|
+ current_attempts.spawn(mining_attempt_with_nonce_range(
|
|
|
+ candidate_copy,
|
|
|
+ attempt_handle,
|
|
|
+ attempt_counter,
|
|
|
+ nonce_start as u64,
|
|
|
+ nonce_range_size as u64,
|
|
|
+ thread_id,
|
|
|
+ ));
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
- mining_attempt_res = current_attempt.join_next(), if !current_attempt.is_empty() => {
|
|
|
+ mining_attempt_res = current_attempts.join_next(), if !current_attempts.is_empty() => {
|
|
|
if let Some(Err(e)) = mining_attempt_res {
|
|
|
warn!("Error during mining attempt: {e:?}");
|
|
|
}
|
|
|
- let Some(candidate_slab) = next_attempt else {
|
|
|
- continue;
|
|
|
- };
|
|
|
- next_attempt = None;
|
|
|
- let (cur_handle, attempt_handle) = handle.dup();
|
|
|
- handle = cur_handle;
|
|
|
- current_attempt.spawn(mining_attempt(candidate_slab, attempt_handle));
|
|
|
-
|
|
|
+
|
|
|
+ debug!("Mining attempt completed, active attempts: {}/{}",
|
|
|
+ active_attempts.load(Ordering::Relaxed),
|
|
|
+ config.max_concurrent_attempts);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -160,35 +276,158 @@ pub fn create_mining_driver(
|
|
|
})
|
|
|
}
|
|
|
|
|
|
-pub async fn mining_attempt(candidate: NounSlab, handle: NockAppHandle) -> () {
|
|
|
+async fn mining_attempt_with_nonce_range(
|
|
|
+ candidate: NounSlab,
|
|
|
+ handle: NockAppHandle,
|
|
|
+ attempt_counter: Arc<AtomicUsize>,
|
|
|
+ nonce_start: u64,
|
|
|
+ nonce_range_size: u64,
|
|
|
+ thread_id: usize,
|
|
|
+) -> () {
|
|
|
+ // Ensure we decrement the counter when this attempt finishes
|
|
|
+ let _guard = CounterGuard::new(Arc::clone(&attempt_counter));
|
|
|
+
|
|
|
+ debug!("Mining attempt starting");
|
|
|
+ mining_attempt_with_nonce(candidate, handle, nonce_start, nonce_range_size, thread_id).await;
|
|
|
+ debug!("Mining attempt finished");
|
|
|
+}
|
|
|
+
|
|
|
+/// RAII guard to ensure attempt counter is properly decremented
|
|
|
+struct CounterGuard {
|
|
|
+ counter: Arc<AtomicUsize>,
|
|
|
+}
|
|
|
+
|
|
|
+impl CounterGuard {
|
|
|
+ fn new(counter: Arc<AtomicUsize>) -> Self {
|
|
|
+ Self { counter }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl Drop for CounterGuard {
|
|
|
+ fn drop(&mut self) {
|
|
|
+ let prev = self.counter.fetch_sub(1, Ordering::Relaxed);
|
|
|
+ debug!("Mining attempt counter decremented: {} -> {}", prev, prev - 1);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+pub async fn mining_attempt_with_nonce(
|
|
|
+ candidate: NounSlab,
|
|
|
+ handle: NockAppHandle,
|
|
|
+ nonce_start: u64,
|
|
|
+ nonce_range_size: u64,
|
|
|
+ thread_id: usize,
|
|
|
+) -> () {
|
|
|
+ let attempt_start = std::time::Instant::now();
|
|
|
+ debug!("Thread {}: Creating temporary directory for mining attempt with nonce range {}-{}",
|
|
|
+ thread_id, nonce_start, nonce_start + nonce_range_size - 1);
|
|
|
+
|
|
|
let snapshot_dir =
|
|
|
tokio::task::spawn_blocking(|| tempdir().expect("Failed to create temporary directory"))
|
|
|
.await
|
|
|
.expect("Failed to create temporary directory");
|
|
|
+
|
|
|
+ debug!("Thread {}: Producing prover hot state", thread_id);
|
|
|
let hot_state = zkvm_jetpack::hot::produce_prover_hot_state();
|
|
|
let snapshot_path_buf = snapshot_dir.path().to_path_buf();
|
|
|
let jam_paths = JamPaths::new(snapshot_dir.path());
|
|
|
- // Spawns a new std::thread for this mining attempt
|
|
|
+
|
|
|
+ debug!("Thread {}: Loading mining kernel with hot state", thread_id);
|
|
|
+ let kernel_start = std::time::Instant::now();
|
|
|
let kernel =
|
|
|
Kernel::load_with_hot_state_huge(snapshot_path_buf, jam_paths, KERNEL, &hot_state, false)
|
|
|
.await
|
|
|
.expect("Could not load mining kernel");
|
|
|
- let effects_slab = kernel
|
|
|
- .poke(MiningWire::Candidate.to_wire(), candidate)
|
|
|
- .await
|
|
|
- .expect("Could not poke mining kernel with candidate");
|
|
|
- for effect in effects_slab.to_vec() {
|
|
|
- let Ok(effect_cell) = (unsafe { effect.root().as_cell() }) else {
|
|
|
- drop(effect);
|
|
|
- continue;
|
|
|
+
|
|
|
+ debug!("Thread {}: Kernel loaded in {:?}", thread_id, kernel_start.elapsed());
|
|
|
+
|
|
|
+ // 在nonce范围内逐个尝试挖矿
|
|
|
+ for nonce_offset in 0..nonce_range_size {
|
|
|
+ let current_nonce = nonce_start + nonce_offset;
|
|
|
+
|
|
|
+ debug!("Thread {}: Attempting nonce {} ({}/{} in range)",
|
|
|
+ thread_id, current_nonce, nonce_offset + 1, nonce_range_size);
|
|
|
+
|
|
|
+ // 构建包含当前nonce的候选数据结构
|
|
|
+ let nonce_candidate = {
|
|
|
+ let mut nonce_slab = NounSlab::new();
|
|
|
+
|
|
|
+ // 调试:打印原始候选数据的结构
|
|
|
+ debug!("Thread {}: Original candidate structure: {:?}",
|
|
|
+ thread_id, unsafe { candidate.root() });
|
|
|
+
|
|
|
+ // 解析原始候选数据: [length block-commitment original-nonce]
|
|
|
+ let original_cell = unsafe { candidate.root().as_cell() }
|
|
|
+ .expect("Candidate should be a cell");
|
|
|
+
|
|
|
+ debug!("Thread {}: Original cell head: {:?}, tail: {:?}",
|
|
|
+ thread_id, original_cell.head(), original_cell.tail());
|
|
|
+
|
|
|
+ let length = original_cell.head();
|
|
|
+ let tail = original_cell.tail();
|
|
|
+
|
|
|
+ // tail应该是 [block-commitment original-nonce]
|
|
|
+ let tail_cell = tail.as_cell()
|
|
|
+ .expect("Candidate tail should be a cell containing [block-commitment nonce]");
|
|
|
+
|
|
|
+ debug!("Thread {}: Tail cell head (block-commitment): {:?}, tail (original-nonce): {:?}",
|
|
|
+ thread_id, tail_cell.head(), tail_cell.tail());
|
|
|
+
|
|
|
+ let block_commitment = tail_cell.head();
|
|
|
+ // 注意:我们忽略原始的nonce (tail_cell.tail()),使用新的current_nonce
|
|
|
+
|
|
|
+ // 创建当前nonce的tip5 digest格式
|
|
|
+ let nonce_digest = atom_to_digest(&mut nonce_slab, current_nonce);
|
|
|
+
|
|
|
+ // 构建符合miner.hoon期望的候选结构: [length block-commitment nonce]
|
|
|
+ let nonce_candidate = T(
|
|
|
+ &mut nonce_slab,
|
|
|
+ &[
|
|
|
+ length, // length
|
|
|
+ block_commitment, // block-commitment
|
|
|
+ nonce_digest, // nonce (tip5 digest格式)
|
|
|
+ ],
|
|
|
+ );
|
|
|
+
|
|
|
+ debug!("Thread {}: Built new candidate with nonce {} replacing original nonce",
|
|
|
+ thread_id, current_nonce);
|
|
|
+
|
|
|
+ nonce_slab.set_root(nonce_candidate);
|
|
|
+ nonce_slab
|
|
|
};
|
|
|
- if effect_cell.head().eq_bytes("command") {
|
|
|
- handle
|
|
|
- .poke(MiningWire::Mined.to_wire(), effect)
|
|
|
- .await
|
|
|
- .expect("Could not poke nockchain with mined PoW");
|
|
|
+
|
|
|
+ debug!("Thread {}: Poking mining kernel with nonce {}", thread_id, current_nonce);
|
|
|
+ let poke_start = std::time::Instant::now();
|
|
|
+
|
|
|
+ let effects_slab = kernel
|
|
|
+ .poke(MiningWire::Candidate.to_wire(), nonce_candidate)
|
|
|
+ .await
|
|
|
+ .expect("Could not poke mining kernel");
|
|
|
+
|
|
|
+ debug!("Thread {}: Mining poke completed in {:?} for nonce {}",
|
|
|
+ thread_id, poke_start.elapsed(), current_nonce);
|
|
|
+
|
|
|
+ // 检查是否找到了有效的证明
|
|
|
+ for effect in effects_slab.to_vec() {
|
|
|
+ let Ok(effect_cell) = (unsafe { effect.root().as_cell() }) else {
|
|
|
+ drop(effect);
|
|
|
+ continue;
|
|
|
+ };
|
|
|
+ if effect_cell.head().eq_bytes("command") {
|
|
|
+ info!("Thread {}: Mining SUCCESS! Found valid proof with nonce {} in {:?}",
|
|
|
+ thread_id, current_nonce, attempt_start.elapsed());
|
|
|
+ handle
|
|
|
+ .poke(MiningWire::Mined.to_wire(), effect)
|
|
|
+ .await
|
|
|
+ .expect("Could not poke nockchain with mined PoW");
|
|
|
+ return;
|
|
|
+ }
|
|
|
}
|
|
|
+
|
|
|
+ debug!("Thread {}: Nonce {} did not produce valid proof", thread_id, current_nonce);
|
|
|
}
|
|
|
+
|
|
|
+ debug!("Thread {}: Exhausted nonce range {}-{} without success in {:?}",
|
|
|
+ thread_id, nonce_start, nonce_start + nonce_range_size - 1, attempt_start.elapsed());
|
|
|
}
|
|
|
|
|
|
#[instrument(skip(handle, pubkey))]
|