diff --git a/docs/protocol-v0.md b/docs/protocol-v0.md index 1cf928e..aa441f9 100644 --- a/docs/protocol-v0.md +++ b/docs/protocol-v0.md @@ -158,7 +158,7 @@ A "block transfer" message is sent back in response to a "block request" message | protocol version = 0 (BE) | 2 | | capable version = 0 (BE) | 2 | | type = 7 (BE) | 1 | -| padding (nullbytes) | 220 | +| padding (nullbytes) | 252 | The node should answer to a "Mining task request" with a "Mining task response" @@ -173,6 +173,7 @@ The node should answer to a "Mining task request" with a "Mining task response" | previous hash | 32 | | timestamp (unix time in seconds, BE) | 8 | | difficulty sum (BE) | 32 | +| threshold | 32 | The node tells the miner "timestamp", "previous hash", "difficulty sum" and "transaction" for the new block. diff --git a/list-block-messages.py b/list-block-messages.py index e4674c5..de837cd 100755 --- a/list-block-messages.py +++ b/list-block-messages.py @@ -23,7 +23,7 @@ def main(): if len(block) != 292: break timestamp = int.from_bytes(block[244:252], "big") - time_info = time.strftime("%d.%m.%Y %H:%M.%S", time.localtime(timestamp)) + time_info = time.strftime("%d.%m.%Y %H:%M:%S", time.localtime(timestamp)) message = prepare_message(block[148:180]) print(f"[{time_info}] {message}") except FileNotFoundError: diff --git a/miner/.gitignore b/miner/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/miner/.gitignore @@ -0,0 +1 @@ +/target diff --git a/miner/Cargo.lock b/miner/Cargo.lock new file mode 100644 index 0000000..950c5e4 --- /dev/null +++ b/miner/Cargo.lock @@ -0,0 +1,385 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anstream" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "base64" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "carrotcoin_miner" +version = "0.1.0" +dependencies = [ + "base64", + "clap", + "num_cpus", + "rand", + "sha2", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "949626d00e063efc93b6dca932419ceb5432f99769911c0b995f7e884c778813" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90239a040c80f5e14809ca132ddc4176ab33d5e17e49691793296e3fcb34d72f" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "strsim" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" + +[[package]] +name = "syn" +version = "2.0.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" diff --git a/miner/Cargo.toml b/miner/Cargo.toml new file mode 100644 index 0000000..66d810f --- /dev/null +++ b/miner/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "carrotcoin_miner" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +base64 = "0.22.0" +clap = { version = "4.5.3", features = ["derive"] } +num_cpus = "1.16.0" +rand = "0.8.5" +sha2 = "0.10.8" diff --git a/miner/src/main.rs b/miner/src/main.rs new file mode 100644 index 0000000..ee13375 --- /dev/null +++ b/miner/src/main.rs @@ -0,0 +1,198 @@ +use base64::prelude::*; +use clap::Parser; +use rand::Rng; +use sha2::Digest; +use sha2::Sha256; +use std::io::ErrorKind; +use std::net::Ipv6Addr; +use std::net::UdpSocket; +use std::sync::mpsc; +use std::sync::mpsc::TryRecvError; +use std::sync::Arc; +use std::sync::Mutex; +use std::thread; +use std::time::Duration; +use std::time::Instant; + +/// Carrotcoin miner +#[derive(Parser)] +struct Args { + /// Wallet address for the mining reward + #[arg(short, long)] + wallet: String, + + /// Message to encode in each block (maximum 32 bytes) + #[arg(short, long)] + message: Option, + + /// Number of threads to start + #[arg(short, long)] + thread_count: Option, +} + +struct Task { + prefix: Vec, + threshold: [u8; 32], + work_counter: usize, +} + +fn run_mining_worker(task: Arc>, results: mpsc::Sender>) { + let mut rng = rand::thread_rng(); + loop { + let mut hasher = Sha256::new(); + let mut lock_guard = task.lock().unwrap(); + let prefix = lock_guard.prefix.clone(); + hasher.update(&prefix); + let threshold = lock_guard.threshold.clone(); + lock_guard.work_counter += 1; + drop(lock_guard); + let start = rng.gen_range(0..((1u64 << 50) - 1)) << 14; + for nonce in start..(start + (1 << 14)) { + let mut hasher_cloned = hasher.clone(); + let nonce_bytes = nonce.to_be_bytes(); + hasher_cloned.update(nonce_bytes); + let result_hash: [u8; 32] = hasher_cloned.finalize().into(); + if result_hash[0] == 0 && result_hash <= threshold { + let mut block = Vec::with_capacity(292); + block.extend_from_slice(&prefix); + block.extend_from_slice(&nonce_bytes); + results.send(block).unwrap(); + } + } + } +} + +fn start_threads(task: &Arc>, results: &mpsc::Sender>, thread_count: usize) { + for _ in 0..thread_count { + let task_cloned = task.clone(); + let results_cloned = results.clone(); + thread::spawn(|| run_mining_worker(task_cloned, results_cloned)); + } +} + +fn build_message(message_string: &str) -> Option> { + let message_bytes = message_string.as_bytes(); + if message_bytes.len() > 32 { + return None; + } + let mut message_buffer = Vec::with_capacity(32); + message_buffer.extend_from_slice(&message_bytes); + message_buffer.resize(32, 0); + Some(message_buffer) +} + +fn show_statistics(duration: Duration, work_counter: usize, threshold: &[u8]) { + let duration_seconds = duration.as_secs_f64(); + let mut hash_numeric = 0f64; + for byte_value in threshold { + hash_numeric = hash_numeric * 256.0 + f64::from(*byte_value); + } + let difficulty = 2.0f64.powi(256) / hash_numeric; + let relative_difficulty = difficulty / 2.0f64.powi(28); + let hashrate = (work_counter as f64) / duration_seconds * 2.0f64.powi(14); + let hashrate_mhs = hashrate / 1e6; + let expected_reward = hashrate * 3600f64 / difficulty; + println!("difficulty: {relative_difficulty:.02}; hashrate: {hashrate_mhs:.02} Mh/s; expected reward: {expected_reward:.04} cc / hour"); +} + +fn main() { + let args = Args::parse(); + + let public_key = match BASE64_STANDARD.decode(args.wallet) { + Ok(key_bytes) if key_bytes.len() == 32 => key_bytes, + Ok(_) => { + eprintln!("The wallet address is not exactly 32 bytes, so it is invalid."); + return; + } + Err(_) => { + eprintln!("Failed to base64-decode the wallet address."); + return; + } + }; + let message = match build_message(args.message.as_deref().unwrap_or("")) { + Some(message) => message, + None => { + eprintln!("The provided block message ist too long. Must be at most 32 bytes."); + return; + } + }; + let thread_count = args.thread_count.unwrap_or_else(num_cpus::get); + + let mut last_statistic_time = Instant::now(); + let mut statistic_output_delay = Duration::from_secs(5); + + let socket = UdpSocket::bind((Ipv6Addr::UNSPECIFIED, 0)).unwrap(); + let mut threads_started = false; + socket.connect((Ipv6Addr::LOCALHOST, 62039)).unwrap(); + socket.set_nonblocking(true).unwrap(); + let task = Arc::new(Mutex::new(Task { + prefix: Vec::new(), + threshold: [0u8; 32], + work_counter: 0, + })); + let (results_tx, results_rx) = mpsc::channel(); + let mut request = vec![0u8; 257]; + request[4] = 7; + loop { + let mut recv_buffer = vec![0u8; 512]; + loop { + match socket.recv(&mut recv_buffer) { + Ok(size) => { + if size == 257 && &recv_buffer[0..5] == &[0, 0, 0, 0, 8] { + let transaction = &recv_buffer[5..153]; + let previous_hash = &recv_buffer[153..185]; + let timestamp = &recv_buffer[185..193]; + let difficulty_sum = &recv_buffer[193..225]; + let threshold = &recv_buffer[225..257]; + let mut prefix = Vec::with_capacity(292); + prefix.extend_from_slice(transaction); + prefix.extend_from_slice(&message); + prefix.extend_from_slice(&public_key); + prefix.extend_from_slice(previous_hash); + prefix.extend_from_slice(timestamp); + prefix.extend_from_slice(difficulty_sum); + let mut lock = task.lock().unwrap(); + lock.prefix = prefix; + lock.threshold = threshold.try_into().unwrap(); + let current_time = Instant::now(); + let duration = current_time - last_statistic_time; + if duration >= statistic_output_delay { + show_statistics(duration, lock.work_counter, threshold); + last_statistic_time = current_time; + lock.work_counter = 0; + statistic_output_delay = Duration::from_secs(60); + } + if !threads_started { + start_threads(&task, &results_tx, thread_count); + threads_started = true; + } + } + } + Err(e) => { + if e.kind() == ErrorKind::WouldBlock { + break; + } else if e.kind() == ErrorKind::ConnectionRefused { + eprintln!("Node is not running locally!"); + return; + } else { + panic!("{e}"); + } + } + } + } + socket.send(&request).unwrap(); + std::thread::sleep(std::time::Duration::from_secs(1)); + let mined_block = match results_rx.try_recv() { + Ok(block) => Some(block), + Err(e) if e == TryRecvError::Empty => None, + Err(e) => panic!("{e}"), + }; + if let Some(block) = mined_block { + let mut block_transfer = Vec::with_capacity(297); + block_transfer.extend_from_slice(b"\0\0\0\0\x02"); + block_transfer.extend_from_slice(&block); + socket.send(&block_transfer).unwrap(); + println!("I found a carrot! \u{1f955}"); + } + } +} diff --git a/node.py b/node.py index e85eaf3..aa99159 100755 --- a/node.py +++ b/node.py @@ -362,8 +362,8 @@ def receiver(node, b): receive_observer.publish(identifier, event_obj) elif msg_type == 7: # mining task request - if msg_len != 225: - log(f"Got a mining task request of wrong length ({msg_len} bytes from {sender}, but expected 255 bytes)") + if msg_len != 257: + log(f"Got a mining task request of wrong length ({msg_len} bytes from {sender}, but expected 257 bytes)") continue transaction = b.open_transactions.get_transaction(0) if transaction is not None: @@ -374,8 +374,8 @@ def receiver(node, b): timestamp_raw = t.to_bytes(8, "big") latest_block = b.get_latest_block() if latest_block is not None: - B_1_difficulty_sum, _ = latest_block.get_difficulty_info(0, blockchain) - B_10_difficulty_sum, B_10_timestamp = latest_block.get_difficulty_info(9, blockchain) + B_1_difficulty_sum, _ = latest_block.get_difficulty_info(0, b) + B_10_difficulty_sum, B_10_timestamp = latest_block.get_difficulty_info(9, b) D = B_1_difficulty_sum - B_10_difficulty_sum T = t - B_10_timestamp calculated_difficulty = D * 3000 // 9 // T @@ -383,13 +383,16 @@ def receiver(node, b): difficulty_sum = B_1_difficulty_sum + block_difficulty previous_hash = latest_block.own_hash else: + block_difficulty = 2**28 difficulty_sum = 2**29 previous_hash = 32 * b"\0" + threshold = (2**256 - 1) // block_difficulty response = b"\0\0\0\0\x08" + \ transaction_raw + \ previous_hash + \ timestamp_raw + \ - difficulty_sum.to_bytes(32, "big") + difficulty_sum.to_bytes(32, "big") + \ + threshold.to_bytes(32, "big") node.node_socket.sendto(response, addr) elif msg_type == 9: # payment request