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);
+    }
+}