diff --git a/demo/main.js b/demo/main.js index 0ec1f5e..f58732f 100644 --- a/demo/main.js +++ b/demo/main.js @@ -4,50 +4,46 @@ const RELAYS = ['wss://nostr.bitcoiner.social', 'wss://nostr.mom', 'wss://nos.lo const Relay = window.NostrTools.Relay; const SimplePool = window.NostrTools.SimplePool; -const pool = new SimplePool(); - const secret = window.NostrTools.generateSecretKey(); 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 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 numberOfWorkers = document.getElementById('numberOfWorkers'); const hashrateOutput = document.getElementById('hashrate'); const cancelButton = document.getElementById('cancelButton'); const relayStatus = document.getElementById('relayStatus'); const neventOutput = document.getElementById('neventOutput'); - -let NUM_WORKERS = navigator.hardwareConcurrency || 4; // Or set a fixed number -let workers = []; -let isWorkerReady = 0; -let isMining = false; - -let pubs = []; +const numberOfWorkers = document.getElementById('numberOfWorkers'); numberOfWorkers.value = NUM_WORKERS; +numberOfWorkers.max = navigator.hardwareConcurrency || 3; -const MOVING_AVERAGE_WINDOW = 2; -let recentHashRates = []; +let workerHashRates = {}; for (let i = 0; i < NUM_WORKERS; i++) { const worker = new Worker('./worker.js', { type: 'module' }); worker.onmessage = handleWorkerMessage; - worker.postMessage({ type: 'init' }); + worker.postMessage({ type: 'init', id: i }); workers.push(worker); } function handleWorkerMessage(e) { - const { type, data, error, averageHashRate, workerId } = e.data; + const { type, data, error, hashRate, workerId } = e.data; if (type === 'progress') { - recentHashRates.push(averageHashRate); - if (recentHashRates.length > MOVING_AVERAGE_WINDOW * NUM_WORKERS) { - recentHashRates.shift(); - } - const totalHashRate = recentHashRates.reduce((a, b) => a + b, 0); + workerHashRates[workerId] = hashRate; + const totalHashRate = Object.values(workerHashRates).reduce((a, b) => a + b, 0); hashrateOutput.textContent = `${(totalHashRate / 1000).toFixed(2)} kH/s`; } else if (type === 'ready') { isWorkerReady++; @@ -75,11 +71,13 @@ ${JSON.stringify(data, null, 2)} hashrateOutput.textContent = '0 H/s'; mineButton.disabled = false; isMining = false; + workerHashRates = {}; // Reset hash rates after mining } else if (type === 'error') { resultOutput.textContent = `Error: ${error}`; hashrateOutput.textContent = '0 H/s'; mineButton.disabled = false; isMining = false; + workerHashRates = {}; // Reset hash rates on error } } @@ -97,12 +95,12 @@ mineButton.addEventListener('click', () => { const content = eventInput.value.trim(); const nostrEvent = generateEvent(content); const difficulty = parseInt(difficultyInput.value, 10); - NUM_WORKERS = parseInt(numberOfWorkers.value, 10); + const NUM_WORKERS = parseInt(numberOfWorkers.value, 10); relayStatus.textContent = ''; neventOutput.textContent = ''; resultOutput.textContent = ''; - recentHashRates = []; + workerHashRates = {}; if (!content) { alert('Please enter content for the Nostr event.'); @@ -145,6 +143,7 @@ cancelButton.addEventListener('click', () => { hashrateOutput.textContent = '0 H/s'; mineButton.disabled = false; isMining = false; + workerHashRates = {}; // Reset hash rates after cancellation } }); @@ -232,3 +231,239 @@ const showRelayStatus = () => { }); }); } + + +// const CLIENT = 'notemine'; +// const TOPIC = 'notemine'; +// const RELAYS = ['wss://nostr.bitcoiner.social', 'wss://nostr.mom', 'wss://nos.lol', 'wss://powrelay.xyz', 'wss://labour.fiatjaf.com/', 'wss://nostr.lu.ke', 'wss://140.f7z.io']; + +// const Relay = window.NostrTools.Relay; +// const SimplePool = window.NostrTools.SimplePool; +// const pool = new SimplePool(); + + +// const secret = window.NostrTools.generateSecretKey(); +// const pubkey = window.NostrTools.getPublicKey(secret); + +// 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 cancelButton = document.getElementById('cancelButton'); +// const relayStatus = document.getElementById('relayStatus'); +// const neventOutput = document.getElementById('neventOutput'); +// const numberOfWorkers = document.getElementById('numberOfWorkers'); + +// let NUM_WORKERS = navigator.hardwareConcurrency || 4; // Or set a fixed number +// let workers = []; +// let isWorkerReady = 0; +// let isMining = false; + +// let pubs = []; + +// numberOfWorkers.value = NUM_WORKERS; + +// const MOVING_AVERAGE_WINDOW = 2; +// let recentHashRates = []; + +// for (let i = 0; i < NUM_WORKERS; i++) { +// const worker = new Worker('./worker.js', { type: 'module' }); +// worker.onmessage = handleWorkerMessage; +// worker.postMessage({ type: 'init' }); +// workers.push(worker); +// } + +// function handleWorkerMessage(e) { +// const { type, data, error, averageHashRate, workerId } = e.data; + +// if (type === 'progress') { +// recentHashRates.push(averageHashRate); +// if (recentHashRates.length > MOVING_AVERAGE_WINDOW * NUM_WORKERS) { +// 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) { +// console.log('All workers are ready.'); +// mineButton.disabled = false; +// resultOutput.textContent = 'Workers are ready. You can start mining.'; +// } +// } else if (type === 'result') { +// if (data.error) { +// resultOutput.textContent = ` +// Error: ${data.error} +// ${JSON.stringify(data, null, 2)} +// `; +// } else { +// try { +// resultOutput.textContent = JSON.stringify(data, null, 2); +// publishEvent(data.event); +// cancelOtherWorkers(workerId); +// } catch (e) { +// console.error('Error publishing event:', e); +// resultOutput.textContent = `Error publishing event: ${e.message}`; +// } +// } +// hashrateOutput.textContent = '0 H/s'; +// mineButton.disabled = false; +// isMining = false; +// } else if (type === 'error') { +// resultOutput.textContent = `Error: ${error}`; +// hashrateOutput.textContent = '0 H/s'; +// mineButton.disabled = false; +// isMining = false; +// } +// } + +// function cancelOtherWorkers(excludeWorkerId) { +// workers.forEach((worker, index) => { +// if (index !== excludeWorkerId) { +// worker.postMessage({ type: 'cancel' }); +// } +// }); +// } + +// mineButton.addEventListener('click', () => { +// if (isMining) return; + +// const content = eventInput.value.trim(); +// const nostrEvent = generateEvent(content); +// const difficulty = parseInt(difficultyInput.value, 10); +// NUM_WORKERS = parseInt(numberOfWorkers.value, 10); + +// relayStatus.textContent = ''; +// neventOutput.textContent = ''; +// resultOutput.textContent = ''; +// recentHashRates = []; + +// if (!content) { +// alert('Please enter content for the Nostr event.'); +// return; +// } + +// if (isNaN(difficulty) || difficulty <= 0) { +// alert('Please enter a valid difficulty.'); +// return; +// } + +// if (isWorkerReady < NUM_WORKERS) { +// alert('Workers are not ready yet. Please wait.'); +// return; +// } + +// const event = JSON.stringify(nostrEvent); + +// mineButton.disabled = true; +// resultOutput.textContent = 'Mining in progress...'; +// hashrateOutput.textContent = '0 H/s'; +// isMining = true; + +// workers.forEach((worker, index) => { +// worker.postMessage({ +// type: 'mine', +// event, +// difficulty: difficulty, +// workerId: index, +// }); +// }); +// }); + +// cancelButton.addEventListener('click', () => { +// if (isMining) { +// workers.forEach(worker => { +// worker.postMessage({ type: 'cancel' }); +// }); +// resultOutput.textContent = 'Mining cancellation requested.'; +// hashrateOutput.textContent = '0 H/s'; +// mineButton.disabled = false; +// isMining = false; +// } +// }); + +// const getPow = (hex) => { +// let count = 0; + +// for (let i = 0; i < hex.length; i++) { +// const nibble = parseInt(hex[i], 16); +// if (nibble === 0) { +// count += 4; +// } else { +// count += Math.clz32(nibble) - 28; +// break; +// } +// } + +// return count; +// } + +// const verifyPow = (event) => { +// const hash = window.NostrTools.getEventHash(event); +// const count = getPow(hash); +// const nonceTag = event.tags.find(tag => tag[0] === 'nonce'); +// if (!nonceTag || nonceTag.length < 3) { +// return 0; +// } +// const targetDifficulty = parseInt(nonceTag[2], 10); +// return Math.min(count, targetDifficulty); +// } + +// const generateEvent = (content) => { +// return { +// pubkey, +// kind: 1, +// tags: [['t', TOPIC], ['client', CLIENT]], +// content, +// } +// } + +// const generateNEvent = (event) => { +// const { id, pubkey: author } = event; +// const pointer = { id, pubkey, relays: RELAYS }; +// return window.NostrTools.nip19.neventEncode(pointer); +// } + +// const publishEvent = async (ev) => { +// const pow = verifyPow(ev); + +// if (!pow || getPow(ev.id) < pow) { +// resultOutput.textContent = `Error: Invalid POW ${pow}`; +// return; +// } +// console.log('Publishing event:', ev); +// try { +// ev = window.NostrTools.finalizeEvent(ev, secret); +// let isGood = window.NostrTools.verifyEvent(ev); +// if (!isGood) throw new Error('Event is not valid'); +// pubs = pool.publish(RELAYS, ev); +// await Promise.allSettled(pubs); +// showRelayStatus(); +// console.log('Event published successfully.'); +// neventOutput.textContent = generateNEvent(ev); +// } catch (error) { +// console.error('Error publishing event:', error); +// resultOutput.textContent = `Error publishing event: ${error.message}`; +// } +// }; + +// let settledCount = 0; + +// const showRelayStatus = () => { +// const settled = Array(pubs.length).fill(false); +// const intervalId = setInterval(() => { +// settledCount = settled.filter(Boolean).length; +// relayStatus.textContent = `Published to ${settledCount}/${pubs.length} relays.`; +// if (settledCount === pubs.length) { +// clearInterval(intervalId); +// } +// }, 100); + +// pubs.forEach((pub, index) => { +// pub.finally(() => { +// relayStatus.textContent = `Published to all relays [${RELAYS.join(', ')}]`; +// settled[index] = true; +// }); +// }); +// } diff --git a/demo/worker.js b/demo/worker.js index 2d61cd6..27a1444 100644 --- a/demo/worker.js +++ b/demo/worker.js @@ -2,28 +2,26 @@ import init, { mine_event } from './pkg/notemine.js'; let wasm; let mining = false; +let workerId; async function initWasm() { try { wasm = await init({}); - postMessage({ type: 'ready' }); + postMessage({ type: 'ready', workerId }); } catch (error) { - postMessage({ type: 'error', error: `WASM initialization failed: ${error.message}` }); + postMessage({ type: 'error', error: `WASM initialization failed: ${error.message}`, workerId }); } } -initWasm(); - -function reportProgress(averageHashRate) { - postMessage({ type: 'progress', averageHashRate, workerId }); +function reportProgress(hashRate) { + postMessage({ type: 'progress', hashRate, workerId }); } -let workerId; - self.onmessage = async function (e) { const { type, event, difficulty, id } = e.data; if (type === 'init') { workerId = id; + initWasm(); } else if (type === 'mine' && !mining) { mining = true; try { @@ -39,5 +37,7 @@ self.onmessage = async function (e) { } } else if (type === 'cancel' && mining) { console.log('Mining cancellation requested.'); + // Implement cancellation logic if possible + mining = false; } }; diff --git a/src/lib.rs b/src/lib.rs index ab2c4bb..59a5214 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use serde_wasm_bindgen::to_value; +use serde_json::to_string; use sha2::{Digest, Sha256}; use wasm_bindgen::prelude::*; @@ -24,6 +24,13 @@ pub struct MinedResult { pub khs: f64, } +fn serialize_u64_as_number<S>(x: &u64, s: S) -> Result<S::Ok, S::Error> +where + S: serde::Serializer, +{ + s.serialize_u64(*x) +} + #[derive(Serialize)] struct HashableEvent<'a>( u32, @@ -35,13 +42,6 @@ struct HashableEvent<'a>( &'a str, ); -fn serialize_u64_as_number<S>(x: &u64, s: S) -> Result<S::Ok, S::Error> -where - S: serde::Serializer, -{ - s.serialize_u64(*x) -} - #[inline] fn get_event_hash(event: &NostrEvent) -> Vec<u8> { let hashable_event = HashableEvent( @@ -53,13 +53,11 @@ fn get_event_hash(event: &NostrEvent) -> Vec<u8> { &event.content, ); - let serialized_str = match serde_json::to_string(&hashable_event) { + let serialized_str = match to_string(&hashable_event) { Ok(s) => s, Err(_) => return vec![], }; - println!("Serialized event: {}", serialized_str); - let hash_bytes = Sha256::digest(serialized_str.as_bytes()).to_vec(); hash_bytes } @@ -93,7 +91,7 @@ pub fn mine_event( Ok(e) => e, Err(err) => { console::log_1(&format!("JSON parsing error: {}", err).into()); - return to_value(&serde_json::json!({ + return serde_wasm_bindgen::to_value(&serde_json::json!({ "error": format!("Invalid event JSON: {}", err) })) .unwrap_or(JsValue::NULL); @@ -121,16 +119,13 @@ pub fn mine_event( Ok(func) => func, Err(_) => { console::log_1(&"Failed to convert report_progress to Function".into()); - return to_value(&serde_json::json!({ + return serde_wasm_bindgen::to_value(&serde_json::json!({ "error": "Invalid progress callback." })) .unwrap_or(JsValue::NULL); } }; - const MOVING_AVERAGE_WINDOW: usize = 5; - let mut recent_hash_rates: Vec<f64> = Vec::with_capacity(MOVING_AVERAGE_WINDOW); - let start_time = js_sys::Date::now(); let mut nonce: u64 = 0; let mut total_hashes: u64 = 0; @@ -151,7 +146,7 @@ pub fn mine_event( let hash_bytes = get_event_hash(&event); if hash_bytes.is_empty() { console::log_1(&"Failed to compute event hash.".into()); - return to_value(&serde_json::json!({ + return serde_wasm_bindgen::to_value(&serde_json::json!({ "error": "Failed to compute event hash." })) .unwrap_or(JsValue::NULL); @@ -175,19 +170,18 @@ pub fn mine_event( }; console::log_1(&format!("Mined successfully with nonce: {}", nonce).into()); - return to_value(&result).unwrap_or(JsValue::NULL); + return serde_wasm_bindgen::to_value(&result).unwrap_or(JsValue::NULL); } nonce += 1; if nonce % report_interval == 0 { let current_time = js_sys::Date::now(); - let elapsed_time = (current_time - last_report_time) / 1000.0; // seconds + let elapsed_time = (current_time - last_report_time) / 1000.0; if elapsed_time > 0.0 { - let hash_rate = report_interval as f64; // Number of hashes - // Send both hash count and elapsed time + let hash_rate = (report_interval as f64) / elapsed_time; report_progress - .call2(&JsValue::NULL, &hash_rate.into(), &elapsed_time.into()) + .call1(&JsValue::NULL, &hash_rate.into()) .unwrap_or_else(|err| { console::log_1( &format!("Error calling progress callback: {:?}", err).into(), @@ -201,10 +195,19 @@ pub fn mine_event( if nonce % 100_000 == 0 { console::log_1(&format!("Checked nonce up to: {}", nonce).into()); } + + if nonce >= 10_000_000 { + console::log_1(&"Reached maximum nonce limit without finding a valid hash".into()); + return serde_wasm_bindgen::to_value(&serde_json::json!({ + "error": "Reached maximum nonce limit without finding a valid hash" + })) + .unwrap_or(JsValue::NULL); + } } } + #[cfg(test)] mod tests { use super::*;