diff --git a/demo/index.html b/demo/index.html
index 6f8c399..92ee385 100644
--- a/demo/index.html
+++ b/demo/index.html
@@ -72,16 +72,19 @@
⛏️ notemine
-
+
+
Hash Rate:
0 H/s
Result:
Waiting for worker to initialize...
-
+
-
+
+
+
diff --git a/demo/main.js b/demo/main.js
index 295aaaf..f5b4854 100644
--- a/demo/main.js
+++ b/demo/main.js
@@ -18,6 +18,8 @@ let pubs = []
//worker
const worker = new Worker('./worker.js', { type: 'module' });
+const pointer = {}
+
//dom
const mineButton = document.getElementById('mineButton');
const eventInput = document.getElementById('eventInput');
@@ -26,6 +28,7 @@ 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');
let isWorkerReady = false;
@@ -49,7 +52,10 @@ worker.onmessage = function (e) {
resultOutput.textContent = 'Worker is ready. You can start mining.';
} else if (type === 'result') {
if (data.error) {
- resultOutput.textContent = `Error: ${data.error}`;
+ resultOutput.textContent = ```
+Error: ${data.error}
+JSON.stringify(data, null, 2)
+ ```;
} else {
try {
resultOutput.textContent = JSON.stringify(data, null, 2);
@@ -73,6 +79,9 @@ mineButton.addEventListener('click', () => {
const nostrEvent = generateEvent(content);
const difficulty = parseInt(difficultyInput.value, 10);
+ relayStatus.textContent = ''
+ neventOutput.textContent = ''
+
if (!content) {
alert('Please enter content for the Nostr event.');
return;
@@ -110,6 +119,85 @@ cancelButton.addEventListener('click', () => {
}
});
+// 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 {
+// let leadingZeros;
+// switch (nibble) {
+// case 0:
+// leadingZeros = 4;
+// break;
+// case 1:
+// case 2:
+// case 4:
+// case 8:
+// leadingZeros = Math.clz32(nibble << 28) - 28;
+// break;
+// default:
+// leadingZeros = Math.clz32(nibble << 28) - 28;
+// break;
+// }
+// count += leadingZeros;
+// break;
+// }
+// }
+// return count;
+// }
+
+// const verifyPow = (event) => {
+// const eventCopy = { ...event };
+// delete eventCopy.id;
+// const hash = window.NostrTools.getEventHash(eventCopy);
+// let hashHex;
+// if (typeof hash === 'string') {
+// hashHex = hash;
+// } else {
+// hashHex = Array.from(new Uint8Array(hash))
+// .map(b => b.toString(16).padStart(2, '0'))
+// .join('');
+// }
+// const count = getPow(hashHex);
+// 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 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)
+ console.log(`event hash ${hash}, event id: ${event.id}`)
+ 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 {
@@ -120,7 +208,24 @@ const generateEvent = (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 hash = window.NostrTools.getEventHash(ev)
+ // const diff = parseInt(difficultyInput.value, 10);
+ // const pow = getPow(ev);
+ const pow = verifyPow(ev)
+
+ if(!pow || getPow(ev.id) < pow) {
+ console.log(pow, diff)
+ console.log('verifyPow', verifyPow(ev), 'getPow', getPow(ev.id))
+ resultOutput.textContent = `Error: Invalid POW ${pow}<${diff}`;
+ return
+ }
console.log('Publishing event:', ev);
try {
ev = window.NostrTools.finalizeEvent(ev, secret);
@@ -130,12 +235,15 @@ const publishEvent = async (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(() => {
diff --git a/src/lib.rs b/src/lib.rs
index aa552a4..ab2c4bb 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,8 +1,9 @@
use serde::{Deserialize, Serialize};
+use serde_wasm_bindgen::to_value;
+
use sha2::{Digest, Sha256};
use wasm_bindgen::prelude::*;
use web_sys::console;
-use serde_wasm_bindgen::to_value;
use console_error_panic_hook;
use js_sys::Function;
@@ -13,7 +14,7 @@ pub struct NostrEvent {
pub content: String,
pub tags: Vec>,
pub id: Option,
- pub created_at: Option,
+ pub created_at: Option,
}
#[derive(Serialize, Deserialize, Debug)]
@@ -24,47 +25,57 @@ pub struct MinedResult {
}
#[derive(Serialize)]
-struct HashableEvent<'a> {
- pub pubkey: &'a str,
- pub kind: u32,
- pub content: &'a str,
- pub tags: &'a Vec>,
- pub created_at: Option,
+struct HashableEvent<'a>(
+ u32,
+ &'a str,
+ #[serde(serialize_with = "serialize_u64_as_number")]
+ u64,
+ u32,
+ &'a Vec>,
+ &'a str,
+);
+
+fn serialize_u64_as_number(x: &u64, s: S) -> Result
+where
+ S: serde::Serializer,
+{
+ s.serialize_u64(*x)
}
#[inline]
-fn get_event_hash(event: &mut NostrEvent) -> Vec {
- event.id = None;
- let hashable_event = HashableEvent {
- pubkey: &event.pubkey,
- kind: event.kind,
- content: &event.content,
- tags: &event.tags,
- created_at: event.created_at,
- };
- let serialized = match serde_json::to_vec(&hashable_event) {
- Ok(v) => v,
+fn get_event_hash(event: &NostrEvent) -> Vec {
+ let hashable_event = HashableEvent(
+ 0u32,
+ &event.pubkey,
+ event.created_at.unwrap_or_else(|| (js_sys::Date::now() / 1000.0) as u64),
+ event.kind,
+ &event.tags,
+ &event.content,
+ );
+
+ let serialized_str = match serde_json::to_string(&hashable_event) {
+ Ok(s) => s,
Err(_) => return vec![],
};
- let mut hasher = Sha256::new();
- hasher.update(&serialized);
- hasher.finalize().to_vec()
-}
+ println!("Serialized event: {}", serialized_str);
+
+ let hash_bytes = Sha256::digest(serialized_str.as_bytes()).to_vec();
+ hash_bytes
+}
#[inline]
fn get_pow(hash_bytes: &[u8]) -> u32 {
- let mut count = 0;
- for byte in hash_bytes {
- for i in 0..8 {
- if (byte & (0x80 >> i)) == 0 {
- count += 1;
- } else {
- return count;
- }
- }
- }
- count
+ let mut count = 0;
+ for &byte in hash_bytes {
+ if byte == 0 {
+ count += 8;
+ } else {
+ count += byte.leading_zeros() as u32;
+ break;
+ }
+ }
+ count
}
#[wasm_bindgen(start)]
@@ -74,13 +85,10 @@ pub fn main_js() {
#[wasm_bindgen]
pub fn mine_event(
- event_json: &str,
- difficulty: u32,
- report_progress: JsValue,
+ event_json: &str,
+ difficulty: u32,
+ report_progress: JsValue,
) -> JsValue {
- console::log_1(&format!("Received event_json: {}", event_json).into());
- console::log_1(&format!("Received difficulty: {}", difficulty).into());
-
let mut event: NostrEvent = match serde_json::from_str(event_json) {
Ok(e) => e,
Err(err) => {
@@ -93,89 +101,91 @@ pub fn mine_event(
};
if event.created_at.is_none() {
- let current_timestamp = js_sys::Date::now() as u64 / 1000;
+ let current_timestamp = (js_sys::Date::now() / 1000.0) as u64;
event.created_at = Some(current_timestamp);
- console::log_1(&format!("Generated created_at: {}", current_timestamp).into());
}
- let mut nonce_index = None;
- for (i, tag) in event.tags.iter().enumerate() {
- if tag.len() > 0 && tag[0] == "nonce" {
- nonce_index = Some(i);
- break;
- }
- }
- if nonce_index.is_none() {
- event.tags.push(vec!["nonce".to_string(), "0".to_string(), difficulty.to_string()]);
- nonce_index = Some(event.tags.len() - 1);
- }
-
- let report_progress = match report_progress.dyn_into::() {
- Ok(func) => func,
- Err(_) => {
- console::log_1(&"Failed to convert report_progress to Function".into());
- return to_value(&serde_json::json!({
- "error": "Invalid progress callback."
- }))
- .unwrap_or(JsValue::NULL);
- }
- };
-
- let start_time = js_sys::Date::now();
- let mut nonce: u64 = 0;
- let mut total_hashes: u64 = 0;
-
- let report_interval = 100_000;
- let mut last_report_time = start_time;
-
- loop {
- if let Some(index) = nonce_index {
- if let Some(tag) = event.tags.get_mut(index) {
- if tag.len() >= 3 {
- tag[1] = nonce.to_string();
- tag[2] = difficulty.to_string();
- }
- }
- }
-
- let hash_bytes = get_event_hash(&mut event);
- if hash_bytes.is_empty() {
- console::log_1(&"Failed to compute event hash.".into());
- return to_value(&serde_json::json!({
- "error": "Failed to compute event hash."
- }))
- .unwrap_or(JsValue::NULL);
- }
-
- let pow = get_pow(&hash_bytes);
-
- total_hashes += 1;
-
- if pow >= difficulty {
- let event_hash = hex::encode(&hash_bytes);
- event.id = Some(event_hash.clone());
- let end_time = js_sys::Date::now();
- let total_time = (end_time - start_time) / 1000.0;
- let khs = (total_hashes as f64) / 1000.0 / total_time;
-
- let result = MinedResult {
- event,
- total_time,
- khs,
- };
-
- console::log_1(&format!("Mined successfully with nonce: {}", nonce).into());
- return to_value(&result).unwrap_or(JsValue::NULL);
- }
-
- nonce += 1;
-
- if nonce % report_interval == 0 {
+ let mut nonce_index = None;
+ for (i, tag) in event.tags.iter().enumerate() {
+ if tag.len() > 0 && tag[0] == "nonce" {
+ nonce_index = Some(i);
+ break;
+ }
+ }
+ if nonce_index.is_none() {
+ event.tags.push(vec!["nonce".to_string(), "0".to_string(), difficulty.to_string()]);
+ nonce_index = Some(event.tags.len() - 1);
+ }
+
+ let report_progress = match report_progress.dyn_into::() {
+ Ok(func) => func,
+ Err(_) => {
+ console::log_1(&"Failed to convert report_progress to Function".into());
+ return 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 = Vec::with_capacity(MOVING_AVERAGE_WINDOW);
+
+ let start_time = js_sys::Date::now();
+ let mut nonce: u64 = 0;
+ let mut total_hashes: u64 = 0;
+
+ let report_interval = 100_000;
+ let mut last_report_time = start_time;
+
+ loop {
+ if let Some(index) = nonce_index {
+ if let Some(tag) = event.tags.get_mut(index) {
+ if tag.len() >= 3 {
+ tag[1] = nonce.to_string();
+ tag[2] = difficulty.to_string();
+ }
+ }
+ }
+
+ 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!({
+ "error": "Failed to compute event hash."
+ }))
+ .unwrap_or(JsValue::NULL);
+ }
+
+ let pow = get_pow(&hash_bytes);
+
+ total_hashes += 1;
+
+ if pow >= difficulty {
+ let event_hash = hex::encode(&hash_bytes);
+ event.id = Some(event_hash.clone());
+ let end_time = js_sys::Date::now();
+ let total_time = (end_time - start_time) / 1000.0;
+ let khs = (total_hashes as f64) / 1000.0 / total_time;
+
+ let result = MinedResult {
+ event,
+ total_time,
+ khs,
+ };
+
+ console::log_1(&format!("Mined successfully with nonce: {}", nonce).into());
+ return 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;
+ let elapsed_time = (current_time - last_report_time) / 1000.0; // seconds
if elapsed_time > 0.0 {
- let hash_rate = report_interval as f64;
-
+ let hash_rate = report_interval as f64; // Number of hashes
+ // Send both hash count and elapsed time
report_progress
.call2(&JsValue::NULL, &hash_rate.into(), &elapsed_time.into())
.unwrap_or_else(|err| {
@@ -187,9 +197,44 @@ pub fn mine_event(
last_report_time = current_time;
}
}
-
- if nonce % 100_000 == 0 {
- console::log_1(&format!("Checked nonce up to: {}", nonce).into());
- }
- }
- }
\ No newline at end of file
+
+ if nonce % 100_000 == 0 {
+ console::log_1(&format!("Checked nonce up to: {}", nonce).into());
+ }
+ }
+}
+
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ // {
+ // "id": "bb9727a19e7ed120333e994ada9c3b6e4a360a71739f9ea33def6d69638fff30",
+ // "pubkey": "e771af0b05c8e95fcdf6feb3500544d2fb1ccd384788e9f490bb3ee28e8ed66f",
+ // "created_at": 1668680774,
+ // "kind": 1,
+ // "tags": [],
+ // "content": "hello world",
+ // "sig": "4be1dccd81428990ba56515f2e9fc2ae61c9abc61dc3d977235fd8767f52010e44d36d3c8da30755b6440ccaf888442f7cbbd7a17e34ca3ed31c5e8a33a7df11"
+ // }
+
+ #[test]
+ fn test_get_event_hash() {
+ let event = NostrEvent {
+ pubkey: "e771af0b05c8e95fcdf6feb3500544d2fb1ccd384788e9f490bb3ee28e8ed66f".to_string(),
+ kind: 1,
+ content: "hello world".to_string(),
+ tags: vec![],
+ id: None,
+ created_at: Some(1668680774),
+ };
+
+ let expected_hash = "bb9727a19e7ed120333e994ada9c3b6e4a360a71739f9ea33def6d69638fff30";
+
+ let hash_bytes = get_event_hash(&event);
+ let hash_hex = hex::encode(&hash_bytes);
+
+ assert_eq!(hash_hex, expected_hash);
+ }
+}