From b0b24c50f359cd1b5b87d6315f0d2d127ddba1f1 Mon Sep 17 00:00:00 2001 From: dskvr Date: Sat, 14 Sep 2024 11:05:54 +0200 Subject: [PATCH] actual concurrency --- demo/index.html | 3 ++ demo/main.js | 100 ++++++++++++++++++++++++++++++++++++++++-------- demo/worker.js | 39 ++++++++++++++----- src/lib.rs | 56 ++++++++++++++++++++++----- 4 files changed, 163 insertions(+), 35 deletions(-) diff --git a/demo/index.html b/demo/index.html index 56e1f56..bf915ff 100644 --- a/demo/index.html +++ b/demo/index.html @@ -86,6 +86,9 @@

note⛏️

+

+    

+
     

Hash Rate:

0 H/s

Result:

diff --git a/demo/main.js b/demo/main.js index cd82958..9b68849 100644 --- a/demo/main.js +++ b/demo/main.js @@ -11,27 +11,51 @@ const pubkey = window.NostrTools.getPublicKey(secret); const pool = new SimplePool(); let pubs = []; -let NUM_WORKERS = navigator.hardwareConcurrency-1 || 2; // Or set a fixed number +let totalWorkers = navigator.hardwareConcurrency-1 || 2; // Or set a fixed number let workers = []; let isWorkerReady = 0; let isMining = false; const mineButton = document.getElementById('mineButton'); const eventInput = document.getElementById('eventInput'); + const difficultyInput = document.getElementById('difficulty'); -const resultOutput = document.getElementById('result'); -const hashrateOutput = document.getElementById('hashrate'); +const numberOfWorkers = document.getElementById('numberOfWorkers') + const cancelButton = document.getElementById('cancelButton'); const relayStatus = document.getElementById('relayStatus'); + +const minersBestPowOutput = document.getElementById('minersBestPow'); +const overallBestPowOutput = document.getElementById('overallBestPow'); +const hashrateOutput = document.getElementById('hashrate'); + +const resultOutput = document.getElementById('result'); const neventOutput = document.getElementById('neventOutput'); -const numberOfWorkers = document.getElementById('numberOfWorkers'); -numberOfWorkers.value = NUM_WORKERS; + +numberOfWorkers.value = totalWorkers; numberOfWorkers.max = navigator.hardwareConcurrency || 3; +minersBestPowOutput.style.display = 'none'; +overallBestPowOutput.style.display = 'none'; +neventOutput.style.display = 'none'; +relayStatus.style.display = 'none'; + let workerHashRates = {}; +let minersBestPow +let overallBestPow + +function resetBestPow() { + minersBestPow = {}; + overallBestPow = { + bestPow: 0, + nonce: 0, + hash: '', + workerId: null, + }; +} -for (let i = 0; i < NUM_WORKERS; i++) { +for (let i = 0; i < totalWorkers; i++) { const worker = new Worker('./worker.js', { type: 'module' }); worker.onmessage = handleWorkerMessage; worker.postMessage({ type: 'init', id: i }); @@ -39,15 +63,36 @@ for (let i = 0; i < NUM_WORKERS; i++) { } function handleWorkerMessage(e) { - const { type, data, error, hashRate, workerId } = e.data; + const { type, data, error, hashRate, workerId, bestPowData:bestPowDataMap } = e.data; if (type === 'progress') { + workerHashRates[workerId] = hashRate; const totalHashRate = Object.values(workerHashRates).reduce((a, b) => a + b, 0); hashrateOutput.textContent = `${(totalHashRate / 1000).toFixed(2)} kH/s`; + + if (bestPowDataMap?.size > 0) { + const bestPowData = Object.fromEntries(bestPowDataMap); + const { best_pow, nonce, hash } = bestPowData; + minersBestPow[workerId] = { + bestPow: best_pow, + nonce, + hash, + }; + if (best_pow > overallBestPow.bestPow) { + overallBestPow = { + bestPow: best_pow, + nonce, + hash, + workerId, + }; + } + updateBestPowDisplay(); + } + } else if (type === 'ready') { isWorkerReady++; - if (isWorkerReady === NUM_WORKERS) { + if (isWorkerReady === totalWorkers) { console.log('All workers are ready.'); mineButton.disabled = false; resultOutput.textContent = 'Workers are ready. You can start mining.'; @@ -61,6 +106,7 @@ ${JSON.stringify(data, null, 2)} } else { try { resultOutput.textContent = JSON.stringify(data, null, 2); + neventOutput.style.display = 'block'; publishEvent(data.event); cancelOtherWorkers(workerId); } catch (e) { @@ -91,15 +137,31 @@ function cancelOtherWorkers(excludeWorkerId) { }); } +function updateBestPowDisplay() { + // Update the UI to display each miner's best PoW + let minersPowInfo = ''; + for (const [workerId, powData] of Object.entries(minersBestPow)) { + minersPowInfo += `Miner #${workerId}: Best PoW ${powData.bestPow} (Nonce: ${powData.nonce}, Hash: ${powData.hash})\n`; + } + minersBestPowOutput.textContent = minersPowInfo; + // Update the UI to display the overall best PoW + if (overallBestPow.workerId !== null) { + overallBestPowOutput.textContent = `Overall Best PoW: ${overallBestPow.bestPow} by Miner #${overallBestPow.workerId} (Nonce: ${overallBestPow.nonce}, Hash: ${overallBestPow.hash})` + } +} mineButton.addEventListener('click', () => { if (isMining) return; + resetBestPow(); + minersBestPowOutput.style.display = 'block'; + overallBestPowOutput.style.display = 'block'; + const content = eventInput.value.trim(); const nostrEvent = generateEvent(content); const difficulty = parseInt(difficultyInput.value, 10); - const NUM_WORKERS = parseInt(numberOfWorkers.value, 10); + const totalWorkers = parseInt(numberOfWorkers.value, 10); relayStatus.textContent = ''; neventOutput.textContent = ''; @@ -116,7 +178,7 @@ mineButton.addEventListener('click', () => { return; } - if (isWorkerReady < NUM_WORKERS) { + if (isWorkerReady < totalWorkers) { alert('Workers are not ready yet. Please wait.'); return; } @@ -129,12 +191,15 @@ mineButton.addEventListener('click', () => { hashrateOutput.textContent = '0 H/s'; isMining = true; + console.log('main: event:', event) + workers.forEach((worker, index) => { worker.postMessage({ type: 'mine', event, difficulty: difficulty, workerId: index, + totalWorkers, }); }); }); @@ -210,6 +275,7 @@ const publishEvent = async (ev) => { if (!isGood) throw new Error('Event is not valid'); pubs = pool.publish(RELAYS, ev); await Promise.allSettled(pubs); + relayStatus.style.display = ''; showRelayStatus(); console.log('Event published successfully.'); neventOutput.textContent = generateNEvent(ev); @@ -262,19 +328,19 @@ const showRelayStatus = () => { // const neventOutput = document.getElementById('neventOutput'); // const numberOfWorkers = document.getElementById('numberOfWorkers'); -// let NUM_WORKERS = navigator.hardwareConcurrency || 4; // Or set a fixed number +// let totalWorkers = navigator.hardwareConcurrency || 4; // Or set a fixed number // let workers = []; // let isWorkerReady = 0; // let isMining = false; // let pubs = []; -// numberOfWorkers.value = NUM_WORKERS; +// numberOfWorkers.value = totalWorkers; // const MOVING_AVERAGE_WINDOW = 2; // let recentHashRates = []; -// for (let i = 0; i < NUM_WORKERS; i++) { +// for (let i = 0; i < totalWorkers; i++) { // const worker = new Worker('./worker.js', { type: 'module' }); // worker.onmessage = handleWorkerMessage; // worker.postMessage({ type: 'init' }); @@ -286,14 +352,14 @@ const showRelayStatus = () => { // if (type === 'progress') { // recentHashRates.push(averageHashRate); -// if (recentHashRates.length > MOVING_AVERAGE_WINDOW * NUM_WORKERS) { +// if (recentHashRates.length > MOVING_AVERAGE_WINDOW * totalWorkers) { // recentHashRates.shift(); // } // const totalHashRate = recentHashRates.reduce((a, b) => a + b, 0); // hashrateOutput.textContent = `${(totalHashRate / 1000).toFixed(2)} kH/s`; // } else if (type === 'ready') { // isWorkerReady++; -// if (isWorkerReady === NUM_WORKERS) { +// if (isWorkerReady === totalWorkers) { // console.log('All workers are ready.'); // mineButton.disabled = false; // resultOutput.textContent = 'Workers are ready. You can start mining.'; @@ -339,7 +405,7 @@ const showRelayStatus = () => { // const content = eventInput.value.trim(); // const nostrEvent = generateEvent(content); // const difficulty = parseInt(difficultyInput.value, 10); -// NUM_WORKERS = parseInt(numberOfWorkers.value, 10); +// totalWorkers = parseInt(numberOfWorkers.value, 10); // relayStatus.textContent = ''; // neventOutput.textContent = ''; @@ -356,7 +422,7 @@ const showRelayStatus = () => { // return; // } -// if (isWorkerReady < NUM_WORKERS) { +// if (isWorkerReady < totalWorkers) { // alert('Workers are not ready yet. Please wait.'); // return; // } diff --git a/demo/worker.js b/demo/worker.js index cf29e13..1a2672c 100644 --- a/demo/worker.js +++ b/demo/worker.js @@ -14,27 +14,48 @@ async function initWasm() { } } -function reportProgress(hashRate) { - postMessage({ type: 'progress', hashRate, workerId }); -} + function shouldCancel() { return miningCancelled; } self.onmessage = async function (e) { - const { type, event, difficulty, id } = e.data; + const { type, event, difficulty, workerId, totalWorkers } = e.data; + + function reportProgress(hashRate, bestPowData) { + + + const message = { + type: 'progress', + hashRate, + workerId, + }; + + if (bestPowData && bestPowData !== null) { + message.bestPowData = bestPowData; + } + + postMessage(message); + } + if (type === 'init') { - workerId = id; initWasm(); } else if (type === 'mine' && !mining) { miningCancelled = false; // Reset cancellation flag mining = true; try { - if (typeof event !== 'string') { - throw new Error('Event must be a stringified JSON.'); - } - const minedResult = mine_event(event, difficulty, reportProgress, shouldCancel); + const startNonce = BigInt(workerId); + const nonceStep = BigInt(totalWorkers); + + const minedResult = mine_event( + event, + difficulty, + startNonce.toString(), + nonceStep.toString(), + reportProgress, + shouldCancel + ); postMessage({ type: 'result', data: minedResult, workerId }); } catch (error) { if (error.message !== 'Mining cancelled.') { diff --git a/src/lib.rs b/src/lib.rs index f05c5d6..c338bf5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ use wasm_bindgen::prelude::*; use web_sys::console; use console_error_panic_hook; use js_sys::Function; +use serde_wasm_bindgen; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct NostrEvent { @@ -85,10 +86,12 @@ pub fn main_js() { pub fn mine_event( event_json: &str, difficulty: u32, + start_nonce_str: &str, + nonce_step_str: &str, report_progress: JsValue, should_cancel: JsValue, ) -> JsValue { - + console::log_1(&format!("event_json: {}", event_json).into()); let mut event: NostrEvent = match serde_json::from_str(event_json) { Ok(e) => e, Err(err) => { @@ -113,7 +116,11 @@ pub fn mine_event( } } if nonce_index.is_none() { - event.tags.push(vec!["nonce".to_string(), "0".to_string(), difficulty.to_string()]); + event.tags.push(vec![ + "nonce".to_string(), + "0".to_string(), + difficulty.to_string(), + ]); nonce_index = Some(event.tags.len() - 1); } @@ -129,13 +136,20 @@ pub fn mine_event( }; let start_time = js_sys::Date::now(); - let mut nonce: u64 = 0; + let start_nonce: u64 = start_nonce_str.parse().unwrap_or(0); + let nonce_step: u64 = nonce_step_str.parse().unwrap_or(1); + + let mut nonce: u64 = start_nonce; let mut total_hashes: u64 = 0; let report_interval = 200_000; let mut last_report_time = start_time; let should_cancel = should_cancel.dyn_into::().ok(); + let mut best_pow: u32 = 0; + let mut best_nonce: u64 = 0; + let mut best_hash_bytes: Vec = Vec::new(); + loop { if let Some(index) = nonce_index { if let Some(tag) = event.tags.get_mut(index) { @@ -157,7 +171,30 @@ pub fn mine_event( let pow = get_pow(&hash_bytes); - total_hashes += 1; + if pow > best_pow { + best_pow = pow; + best_nonce = nonce; + best_hash_bytes = hash_bytes.clone(); + + let best_pow_data = serde_json::json!({ + "best_pow": best_pow, + "nonce": best_nonce.to_string(), + "hash": hex::encode(&best_hash_bytes), + }); + + report_progress + .call2( + &JsValue::NULL, + &JsValue::from_f64(0.0), + &serde_wasm_bindgen::to_value(&best_pow_data).unwrap(), + ) + .unwrap_or_else(|err| { + console::log_1( + &format!("Error calling progress callback: {:?}", err).into(), + ); + JsValue::NULL + }); + } if pow >= difficulty { let event_hash = hex::encode(&hash_bytes); @@ -176,10 +213,11 @@ pub fn mine_event( return serde_wasm_bindgen::to_value(&result).unwrap_or(JsValue::NULL); } - nonce += 1; + nonce = nonce.wrapping_add(nonce_step); + total_hashes += 1; if let Some(ref should_cancel) = should_cancel { - if nonce % 10_000 == 0 { + if total_hashes % 10_000 == 0 { let cancel = should_cancel.call0(&JsValue::NULL).unwrap_or(JsValue::FALSE); if cancel.is_truthy() { console::log_1(&"Mining cancelled.".into()); @@ -191,13 +229,13 @@ pub fn mine_event( } } - if nonce % report_interval == 0 { + if total_hashes % report_interval == 0 { let current_time = js_sys::Date::now(); let elapsed_time = (current_time - last_report_time) / 1000.0; if elapsed_time > 0.0 { let hash_rate = (report_interval as f64) / elapsed_time; report_progress - .call1(&JsValue::NULL, &hash_rate.into()) + .call2(&JsValue::NULL, &hash_rate.into(), &JsValue::NULL) .unwrap_or_else(|err| { console::log_1( &format!("Error calling progress callback: {:?}", err).into(), @@ -208,6 +246,7 @@ pub fn mine_event( } } + // Uncomment if you wish to log nonce progress // if nonce % report_interval == 0 { // console::log_1(&format!("Checked nonce up to: {}", nonce).into()); // } @@ -215,7 +254,6 @@ pub fn mine_event( } - #[cfg(test)] mod tests { use super::*;