diff --git a/Cargo.lock b/Cargo.lock index 547fba57..36e1d5a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -137,12 +137,6 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.10.0" @@ -178,9 +172,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "byte-slice-cast" @@ -205,7 +199,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.114", ] [[package]] @@ -228,9 +222,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.46" +version = "1.2.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" +checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" dependencies = [ "find-msvc-tools", "shlex", @@ -277,9 +271,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.51" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" dependencies = [ "clap_builder", "clap_derive", @@ -287,9 +281,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.51" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" dependencies = [ "anstream", "anstyle", @@ -306,14 +300,14 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.114", ] [[package]] name = "clap_lex" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" [[package]] name = "codespan-reporting" @@ -347,7 +341,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "once_cell", "tiny-keccak", ] @@ -404,7 +398,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.10.0", + "bitflags", "core-foundation 0.10.1", "libc", ] @@ -506,9 +500,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" [[package]] name = "digest" @@ -528,7 +522,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.114", ] [[package]] @@ -559,21 +553,17 @@ dependencies = [ name = "engine-cpu" version = "2.1.2" dependencies = [ - "anyhow", "criterion", "hex", - "log", "pow-core", "primitive-types 0.13.1", "rand 0.8.5", - "thiserror 1.0.69", ] [[package]] name = "engine-gpu" version = "2.1.2" dependencies = [ - "anyhow", "bytemuck", "criterion", "engine-cpu", @@ -581,7 +571,6 @@ dependencies = [ "futures", "hex", "log", - "metrics", "pow-core", "primitive-types 0.13.1", "qp-plonky2", @@ -590,7 +579,6 @@ dependencies = [ "qp-poseidon-core", "rand 0.8.5", "rand_chacha 0.3.1", - "thiserror 1.0.69", "wgpu", ] @@ -623,17 +611,11 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" [[package]] name = "fixed-hash" @@ -674,15 +656,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared 0.1.1", -] - [[package]] name = "foreign-types" version = "0.5.0" @@ -690,7 +663,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared 0.3.1", + "foreign-types-shared", ] [[package]] @@ -701,15 +674,9 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.114", ] -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "foreign-types-shared" version = "0.3.1" @@ -787,7 +754,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.114", ] [[package]] @@ -832,9 +799,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", @@ -893,7 +860,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 2.10.0", + "bitflags", "gpu-alloc-types", ] @@ -903,7 +870,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 2.10.0", + "bitflags", ] [[package]] @@ -924,7 +891,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" dependencies = [ - "bitflags 2.10.0", + "bitflags", "gpu-descriptor-types", "hashbrown 0.15.5", ] @@ -935,7 +902,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" dependencies = [ - "bitflags 2.10.0", + "bitflags", ] [[package]] @@ -991,9 +958,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "foldhash 0.2.0", ] @@ -1059,12 +1026,11 @@ dependencies = [ [[package]] name = "http" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -1121,19 +1087,6 @@ dependencies = [ "want", ] -[[package]] -name = "hyper-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" -dependencies = [ - "bytes", - "hyper", - "native-tls", - "tokio", - "tokio-native-tls", -] - [[package]] name = "icu_collections" version = "2.1.1" @@ -1182,9 +1135,9 @@ checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ "icu_collections", "icu_locale_core", @@ -1196,9 +1149,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" @@ -1253,25 +1206,19 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.114", ] [[package]] name = "indexmap" -version = "2.12.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.16.1", ] -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - [[package]] name = "is-terminal" version = "0.4.17" @@ -1318,9 +1265,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jni-sys" @@ -1330,9 +1277,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "js-sys" -version = "0.3.82" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" dependencies = [ "once_cell", "wasm-bindgen", @@ -1373,9 +1320,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.177" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libloading" @@ -1389,15 +1336,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" - -[[package]] -name = "linux-raw-sys" -version = "0.11.0" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "litemap" @@ -1422,9 +1363,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "malloc_buf" @@ -1447,10 +1388,10 @@ version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00c15a6f673ff72ddcc22394663290f870fb224c1bfce55734a75c414150e605" dependencies = [ - "bitflags 2.10.0", + "bitflags", "block", "core-graphics-types", - "foreign-types 0.5.0", + "foreign-types", "log", "objc", "paste", @@ -1464,8 +1405,6 @@ dependencies = [ "log", "once_cell", "prometheus", - "serde", - "serde_json", "tokio", "warp", ] @@ -1495,6 +1434,7 @@ dependencies = [ "engine-gpu", "env_logger", "log", + "metrics", "miner-service", "num_cpus", "primitive-types 0.13.1", @@ -1510,22 +1450,17 @@ dependencies = [ "crossbeam-channel", "engine-cpu", "engine-gpu", + "getrandom 0.2.17", "hex", "log", "metrics", - "miner-telemetry", "num_cpus", "pow-core", "primitive-types 0.13.1", "quantus-miner-api", "quinn", - "reqwest", "rustls 0.21.12", - "serde", - "serde_json", - "thiserror 1.0.69", "tokio", - "warp", ] [[package]] @@ -1539,15 +1474,14 @@ dependencies = [ "serde_json", "tokio", "tokio-tungstenite 0.23.1", - "url", "uuid", ] [[package]] name = "mio" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "wasi", @@ -1580,12 +1514,12 @@ checksum = "066cf25f0e8b11ee0df221219010f213ad429855f57c494f995590c861a9a7d8" dependencies = [ "arrayvec", "bit-set", - "bitflags 2.10.0", + "bitflags", "cfg-if", "cfg_aliases", "codespan-reporting", "half", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "hexf-parse", "indexmap", "libm", @@ -1594,27 +1528,10 @@ dependencies = [ "once_cell", "rustc-hash", "spirv", - "thiserror 2.0.17", + "thiserror 2.0.18", "unicode-ident", ] -[[package]] -name = "native-tls" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - [[package]] name = "ndk-sys" version = "0.6.0+11769913" @@ -1737,50 +1654,12 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" -[[package]] -name = "openssl" -version = "0.10.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "foreign-types 0.3.2", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.110", -] - [[package]] name = "openssl-probe" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" -[[package]] -name = "openssl-sys" -version = "0.9.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "ordered-float" version = "5.1.0" @@ -1931,7 +1810,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.114", ] [[package]] @@ -1986,7 +1865,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.114", ] [[package]] @@ -2052,9 +1931,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" [[package]] name = "portable-atomic-util" @@ -2078,15 +1957,9 @@ dependencies = [ name = "pow-core" version = "2.1.2" dependencies = [ - "anyhow", "hex", - "log", - "num-bigint", - "num-traits", "primitive-types 0.13.1", - "qp-poseidon-core", "qpow-math", - "thiserror 1.0.69", ] [[package]] @@ -2136,9 +2009,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -2171,7 +2044,7 @@ checksum = "39530b02faa85964bba211e030afa2d54995b403b0022f88e984c4c65679c4bc" dependencies = [ "ahash", "anyhow", - "getrandom 0.2.16", + "getrandom 0.2.17", "hashbrown 0.14.5", "itertools 0.11.0", "keccak-hash", @@ -2225,9 +2098,9 @@ dependencies = [ [[package]] name = "qp-poseidon-core" -version = "1.0.3" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52c70df221c356b3ce63afabfae623aae632c27d3e078cd20eec4348096c2d7" +checksum = "0f65766d6de64eff741c7f402002a3322f5e563d53e0e9040aeab4921ff24f2b" dependencies = [ "p3-field", "p3-goldilocks", @@ -2241,7 +2114,7 @@ dependencies = [ [[package]] name = "qpow-math" version = "0.1.0" -source = "git+https://github.com/Quantus-Network/chain.git#052cf7a361c46516b0ad294d1aa5f6f4017516be" +source = "git+https://github.com/Quantus-Network/chain.git#1c4ebb199f1653b981817663a3c508755be67d23" dependencies = [ "hex", "log", @@ -2254,10 +2127,11 @@ dependencies = [ [[package]] name = "quantus-miner-api" version = "0.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c4679baded32201e79442bd57f9d4d69b72f0f4b9111e7e6ee2428cc04f30bb" +source = "git+https://github.com/Quantus-Network/chain.git?branch=illuzen%2Fquic-miner#ba656b2d99a44298b94c1830fb105de078825165" dependencies = [ "serde", + "serde_json", + "tokio", ] [[package]] @@ -2310,9 +2184,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.42" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -2347,7 +2221,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -2367,7 +2241,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -2376,14 +2250,14 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", ] @@ -2426,7 +2300,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags", ] [[package]] @@ -2464,46 +2338,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" -[[package]] -name = "reqwest" -version = "0.11.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" -dependencies = [ - "base64", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2", - "http 0.2.12", - "http-body", - "hyper", - "hyper-tls", - "ipnet", - "js-sys", - "log", - "mime", - "native-tls", - "once_cell", - "percent-encoding", - "pin-project-lite", - "rustls-pemfile", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "system-configuration", - "tokio", - "tokio-native-tls", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "winreg", -] - [[package]] name = "ring" version = "0.16.20" @@ -2527,7 +2361,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted 0.9.0", "windows-sys 0.52.0", @@ -2554,19 +2388,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rustix" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" -dependencies = [ - "bitflags 2.10.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - [[package]] name = "rustls" version = "0.21.12" @@ -2580,13 +2401,13 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ "once_cell", "rustls-pki-types", - "rustls-webpki 0.103.8", + "rustls-webpki 0.103.9", "subtle", "zeroize", ] @@ -2614,9 +2435,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "zeroize", ] @@ -2633,9 +2454,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ "ring 0.17.14", "rustls-pki-types", @@ -2650,9 +2471,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" [[package]] name = "same-file" @@ -2700,7 +2521,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.10.0", + "bitflags", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -2750,20 +2571,20 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.114", ] [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -2797,10 +2618,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -2812,9 +2634,9 @@ checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "slotmap" -version = "1.0.7" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" dependencies = [ "version_check", ] @@ -2837,9 +2659,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" dependencies = [ "libc", "windows-sys 0.60.2", @@ -2863,7 +2685,7 @@ version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "bitflags 2.10.0", + "bitflags", ] [[package]] @@ -2909,21 +2731,15 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.110" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - [[package]] name = "synstructure" version = "0.13.2" @@ -2932,28 +2748,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", -] - -[[package]] -name = "system-configuration" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" -dependencies = [ - "core-foundation-sys", - "libc", + "syn 2.0.114", ] [[package]] @@ -2962,19 +2757,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" -[[package]] -name = "tempfile" -version = "3.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" -dependencies = [ - "fastrand", - "getrandom 0.3.4", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - [[package]] name = "termcolor" version = "1.4.1" @@ -2995,11 +2777,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -3010,18 +2792,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.114", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.114", ] [[package]] @@ -3070,9 +2852,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.48.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ "bytes", "libc", @@ -3080,7 +2862,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.1", + "socket2 0.6.2", "tokio-macros", "windows-sys 0.61.2", ] @@ -3093,17 +2875,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", -] - -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", + "syn 2.0.114", ] [[package]] @@ -3112,7 +2884,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.35", + "rustls 0.23.36", "tokio", ] @@ -3136,7 +2908,7 @@ checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd" dependencies = [ "futures-util", "log", - "rustls 0.23.35", + "rustls 0.23.36", "rustls-pki-types", "tokio", "tokio-rustls", @@ -3146,9 +2918,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -3159,18 +2931,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.3" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.23.7" +version = "0.23.10+spec-1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ "indexmap", "toml_datetime", @@ -3180,9 +2952,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" dependencies = [ "winnow", ] @@ -3195,9 +2967,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -3207,20 +2979,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.114", ] [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] @@ -3250,7 +3022,7 @@ dependencies = [ "byteorder", "bytes", "data-encoding", - "http 1.3.1", + "http 1.4.0", "httparse", "log", "rand 0.8.5", @@ -3269,11 +3041,11 @@ dependencies = [ "byteorder", "bytes", "data-encoding", - "http 1.3.1", + "http 1.4.0", "httparse", "log", "rand 0.8.5", - "rustls 0.23.35", + "rustls 0.23.36", "rustls-pki-types", "sha1", "thiserror 1.0.69", @@ -3312,9 +3084,9 @@ dependencies = [ [[package]] name = "unicase" -version = "2.8.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-ident" @@ -3324,9 +3096,9 @@ checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-width" -version = "0.1.14" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unicode-xid" @@ -3358,9 +3130,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", @@ -3388,9 +3160,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.1" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" dependencies = [ "getrandom 0.3.4", "js-sys", @@ -3398,12 +3170,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - [[package]] name = "version_check" version = "0.9.5" @@ -3466,18 +3232,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.105" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" dependencies = [ "cfg-if", "once_cell", @@ -3488,11 +3254,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.55" +version = "0.4.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -3501,9 +3268,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.105" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3511,31 +3278,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.105" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.114", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.105" +version = "0.2.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.82" +version = "0.3.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" dependencies = [ "js-sys", "wasm-bindgen", @@ -3557,14 +3324,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.4", + "webpki-roots 1.0.5", ] [[package]] name = "webpki-roots" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" dependencies = [ "rustls-pki-types", ] @@ -3576,11 +3343,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfe68bac7cde125de7a731c3400723cadaaf1703795ad3f4805f187459cd7a77" dependencies = [ "arrayvec", - "bitflags 2.10.0", + "bitflags", "cfg-if", "cfg_aliases", "document-features", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "js-sys", "log", "naga", @@ -3607,11 +3374,11 @@ dependencies = [ "arrayvec", "bit-set", "bit-vec", - "bitflags 2.10.0", + "bitflags", "bytemuck", "cfg_aliases", "document-features", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "indexmap", "log", "naga", @@ -3622,7 +3389,7 @@ dependencies = [ "raw-window-handle", "rustc-hash", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "wgpu-core-deps-apple", "wgpu-core-deps-emscripten", "wgpu-core-deps-windows-linux-android", @@ -3667,7 +3434,7 @@ dependencies = [ "arrayvec", "ash", "bit-set", - "bitflags 2.10.0", + "bitflags", "block", "bytemuck", "cfg-if", @@ -3678,7 +3445,7 @@ dependencies = [ "gpu-alloc", "gpu-allocator", "gpu-descriptor", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "js-sys", "khronos-egl", "libc", @@ -3698,7 +3465,7 @@ dependencies = [ "raw-window-handle", "renderdoc-sys", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "wasm-bindgen", "web-sys", "wgpu-types", @@ -3712,11 +3479,11 @@ version = "27.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afdcf84c395990db737f2dd91628706cb31e86d72e53482320d368e52b5da5eb" dependencies = [ - "bitflags 2.10.0", + "bitflags", "bytemuck", "js-sys", "log", - "thiserror 2.0.17", + "thiserror 2.0.18", "web-sys", ] @@ -3782,7 +3549,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.114", ] [[package]] @@ -3793,7 +3560,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.114", ] [[package]] @@ -4045,28 +3812,18 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "writeable" @@ -4108,28 +3865,28 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.114", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "71ddd76bcebeed25db614f82bf31a9f4222d3fbba300e6fb6c00afa26cbd4d9d" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "d8187381b52e32220d50b255276aa16a084ec0a9017a0ca2152a1f55c539758d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.114", ] [[package]] @@ -4149,7 +3906,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.114", "synstructure", ] @@ -4189,5 +3946,11 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.114", ] + +[[package]] +name = "zmij" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" diff --git a/Cargo.toml b/Cargo.toml index d3c533f9..8256471f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,13 +31,14 @@ num_cpus = "1.16" primitive-types = { version = "0.13.1", default-features = false } qp-poseidon-core = { version = "1.0.1", default-features = false } qpow-math = { git = "https://github.com/Quantus-Network/chain.git", package = "qpow-math", default-features = false } -quantus-miner-api = { version = "0.0.3", default-features = false } +# Point to local chain repo for development; switch back to crates.io version for release +quantus-miner-api = { git = "https://github.com/Quantus-Network/chain.git", branch = "illuzen/quic-miner" } rand = { version = "0.8.5", default-features = false } serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0.132", default-features = false } thiserror = "1" tokio = { version = "1.36", features = ["full"] } -warp = "0.3" +warp = "0.3" # Used by metrics crate for Prometheus HTTP endpoint # pow-core now delegates to qpow-math for core functionality while providing optimized mining scaffolding. # GPU backend dependencies are declared in individual engine crates (no optional deps in workspace) diff --git a/EXTERNAL_MINER_PROTOCOL.md b/EXTERNAL_MINER_PROTOCOL.md index 187d4a76..0fec0065 100644 --- a/EXTERNAL_MINER_PROTOCOL.md +++ b/EXTERNAL_MINER_PROTOCOL.md @@ -1,256 +1,235 @@ # External Miner Protocol Specification -This document defines the JSON-based HTTP protocol for communication between the Resonance Network node and an external QPoW miner service. +This document defines the QUIC-based protocol for communication between the Quantus Network node and external QPoW miner services. ## Overview -The node delegates the mining task (finding a valid nonce) to an external service. The node provides the necessary parameters (header hash, difficulty, nonce range) and the external miner searches for a valid nonce according to the QPoW rules defined in the `qpow-math` crate. The miner returns the result, including the winning nonce, when found. +The node delegates the mining task (finding a valid nonce) to external miner services over persistent QUIC connections. The node provides the necessary parameters (header hash, difficulty) and each external miner independently searches for a valid nonce according to the QPoW rules defined in the `qpow-math` crate. Miners push results back when found. -## Data Types +### Key Benefits of QUIC -See the `resonance-miner-api` crate for the canonical Rust definitions of these structures. - -- `job_id`: String (UUID recommended) - Unique identifier for a specific mining task, generated by the node. -- `mining_hash`: String (64 hex chars, no 0x) - The header hash for which to find a nonce. -- `difficulty`: String (u64 as string) - The target difficulty for the mining job. -- `nonce_start`: String (128 hex chars, no 0x) - The starting nonce value (inclusive) for the search range. -- `nonce_end`: String (128 hex chars, no 0x) - The ending nonce value (inclusive) for the search range. -- `status`: Enum (`ApiResponseStatus`) - Indicates the state or result of an API call. -- `message`: String (optional) - Provides details for `Error` status responses. -- `nonce`: String (Hex, no 0x) - Represents the `U512` value of the current or winning nonce. -- `work`: String (128 hex chars, no 0x) - Represents the winning nonce as `[u8; 64]`. This is the value the node needs for verification. -- `hash_count`: Number (u64) - Number of nonces checked by the miner for the job. -- `elapsed_time`: Number (f64) - Time in seconds the miner spent on the job. - -## Endpoints - -### 1. Submit Mining Job - -- **Endpoint:** `POST /mine` -- **Description:** The node requests the external miner to start searching for a valid nonce. -- **Request Body (`MiningRequest`):** - ```json - { - "job_id": "...", - "mining_hash": "...", - "difficulty": "...", - "nonce_start": "...", - "nonce_end": "..." - } - ``` -- **Response Body (`MiningResponse`):** - - Success (200 OK): - ```json - { - "status": "accepted", - "job_id": "..." - } - ``` - - Error (400 Bad Request - Invalid Input / 409 Conflict - Duplicate Job ID): - ```json - { - "status": "error", - "job_id": "...", - "message": "..." // e.g., "Job already exists", "Invalid mining_hash (...)" - } - ``` - -### 2. Get Job Result - -- **Endpoint:** `GET /result/{job_id}` -- **Description:** The node polls the external miner to check the status and retrieve the result. -- **Path Parameter:** - - `job_id`: String (UUID) - The ID of the job to query. -- **Response Body (`MiningResult`):** - - Job Completed (200 OK): - ```json - { - "status": "completed", - "job_id": "...", - "nonce": "...", // U512 hex value of winning nonce - "work": "...", // [u8; 64] hex value of winning nonce - "hash_count": ..., // u64 - "elapsed_time": ... // f64 seconds - } - ``` - - Job Still Running (200 OK): - ```json - { - "status": "running", - "job_id": "...", - "nonce": "...", // Current nonce being checked (U512 hex) - "work": null, - "hash_count": ..., // u64 - "elapsed_time": ... // f64 seconds - } - ``` - - Job Failed (e.g., nonce range exhausted) (200 OK): - ```json - { - "status": "failed", - "job_id": "...", - "nonce": "...", // Final nonce checked (U512 hex) - "work": null, - "hash_count": ..., // u64 - "elapsed_time": ... // f64 seconds - } - ``` - - Job Not Found (404 Not Found): - ```json - { - "status": "not_found", - "job_id": "...", - "nonce": null, - "work": null, - "hash_count": 0, - "elapsed_time": 0.0 - } - ``` - -### 3. Cancel Mining Job - -- **Endpoint:** `POST /cancel/{job_id}` -- **Description:** The node requests the external miner to stop working on a specific job. -- **Path Parameter:** - - `job_id`: String (UUID) - The ID of the job to cancel. -- **Request Body:** (Empty) -- **Response Body (`MiningResponse`): - - Success (200 OK): - ```json - { - "status": "cancelled", - "job_id": "..." - } - ``` - - Job Not Found (404 Not Found): - ```json - { - "status": "not_found", - "job_id": "..." - } - ``` +- **Lower latency**: Results are pushed immediately when found (no polling) +- **Connection resilience**: Built-in connection migration and recovery +- **Multiplexed streams**: Multiple operations on single connection +- **Built-in TLS**: Encrypted by default -## Notes +## Architecture -- All hex values (`mining_hash`, `nonce_start`, `nonce_end`, `nonce`, `work`) should be sent **without** the `0x` prefix. -- The miner must implement the validation logic defined in `qpow_math::is_valid_nonce`. -- The node relies primarily on the `work` field in the `MiningResult` (when status is `completed`) for constructing the `QPoWSeal`. +### Connection Model -# External Miner Protocol Specification +``` + ┌─────────────────────────────────┐ + │ Node │ + │ (QUIC Server on port 9833) │ + │ │ +┌──────────┐ │ Broadcasts: NewJob │ +│ Miner 1 │ ──connect───► │ Receives: JobResult │ +└──────────┘ │ │ + │ Supports multiple miners │ +┌──────────┐ │ First valid result wins │ +│ Miner 2 │ ──connect───► │ │ +└──────────┘ └─────────────────────────────────┘ + +┌──────────┐ +│ Miner 3 │ ──connect───► +└──────────┘ +``` -This document defines the JSON-based HTTP protocol for communication between the node and an external QPoW miner. +- **Node** acts as the QUIC server, listening on port 9833 (default) +- **Miners** act as QUIC clients, connecting to the node +- Single bidirectional stream per miner connection +- Connection persists across multiple mining jobs +- Multiple miners can connect simultaneously -## Overview +### Multi-Miner Operation + +When multiple miners are connected: +1. Node broadcasts the same `NewJob` to all connected miners +2. Each miner independently selects a random starting nonce +3. First miner to find a valid solution sends `JobResult` +4. Node uses the first valid result, ignores subsequent results for same job +5. New job broadcast implicitly cancels work on all miners + +### Message Types -The node delegates the mining task to an external service. The node provides the necessary parameters (mining hash, difficulty, and a nonce range) and the external miner searches for a valid nonce within that range. The miner returns the nonce and the resulting work hash when a solution is found. +The protocol uses only **two message types**: + +| Direction | Message | Description | +|-----------|---------|-------------| +| Node → Miner | `NewJob` | Submit a mining job (implicitly cancels any previous job) | +| Miner → Node | `JobResult` | Mining result (completed, failed, or cancelled) | + +### Wire Format + +Messages are length-prefixed JSON: + +``` +┌─────────────────┬─────────────────────────────────┐ +│ Length (4 bytes)│ JSON payload (MinerMessage) │ +│ big-endian u32 │ │ +└─────────────────┴─────────────────────────────────┘ +``` + +Maximum message size: 16 MB ## Data Types -- `job_id`: String (UUID recommended) - Identifier for a specific mining task. -- `mining_hash`: String (Hex-encoded, 32-byte hash, H256) - The hash derived from the block header data that the miner needs to solve. -- `difficulty`: String (Decimal representation of u64) - The target difficulty for the block. -- `nonce_start`: String (Hex-encoded, 64-byte value, U512) - The starting nonce value (inclusive). -- `nonce_end`: String (Hex-encoded, 64-byte value, U512) - The ending nonce value (inclusive). -- `nonce`: String (Hex-encoded, 64-byte value, U512) - The solution found by the miner. -- `work`: String (Hex-encoded, 32-byte hash, H256) - The hash resulting from the combination of `mining_hash` and `nonce`, meeting the difficulty requirement. -- `status`: String Enum - Indicates the state or result of an API call. - -## Endpoints - -### 1. Start Mining Job - -- **Endpoint:** `POST /mine` -- **Description:** The node requests the external miner to start searching for a valid nonce within the specified range for the given parameters. -- **Request Body (application/json):** - ```json - { - "job_id": "...", // String (UUID), generated by the node - "mining_hash": "...", // Hex String (H256) - "difficulty": "...", // String (u64 decimal) - "nonce_start": "...", // Hex String (U512 hex) - "nonce_end": "..." // Hex String (U512 hex) - } - ``` -- **Response Body (application/json):** - - Success (200 OK): - ```json - { - "status": "accepted", - "job_id": "..." // String (UUID), confirming the job ID received - } - ``` - - Error (e.g., 400 Bad Request, 500 Internal Server Error): - ```json - { - "status": "rejected", - "reason": "..." // String (Description of error) - } - ``` - -### 2. Get Job Result - -- **Endpoint:** `GET /result/{job_id}` -- **Description:** The node polls the external miner to check the status and retrieve the result of a previously submitted job. -- **Path Parameter:** - - `job_id`: String (UUID) - The ID of the job to query. -- **Response Body (application/json):** - - Solution Found (200 OK): - ```json - { - "status": "found", - "job_id": "...", // String (UUID) - "nonce": "...", // Hex String (U512 hex) - "work": "CAFEBABE01.." // Hex String (H256 hex) - } - ``` - - Still Working (200 OK): - ```json - { - "status": "working", - "job_id": "..." // String (UUID) - } - ``` - - Job Stale/Cancelled (200 OK): Indicates the job is no longer valid (e.g., the node requested cancellation or submitted work for a newer block). - ```json - { - "status": "stale", - "job_id": "..." // String (UUID) - } - ``` - - Job Not Found (404 Not Found): - ```json - { - "status": "not_found", - "job_id": "..." // String (UUID) - } - ``` - -### 3. Cancel Mining Job - -- **Endpoint:** `POST /cancel/{job_id}` -- **Description:** The node requests the external miner to stop working on a specific job. This is typically used when the node receives a new block or its mining parameters change, making the old job obsolete. -- **Path Parameter:** - - `job_id`: String (UUID) - The ID of the job to cancel. -- **Request Body:** (Empty) -- **Response Body (application/json):** - - Success (200 OK): - ```json - { - "status": "cancelled", - "job_id": "..." // String (UUID) - } - ``` - - Job Not Found (404 Not Found): - ```json - { - "status": "not_found", - "job_id": "..." // String (UUID) - } - ``` +See the `quantus-miner-api` crate for the canonical Rust definitions. + +### MinerMessage (Enum) + +```rust +pub enum MinerMessage { + NewJob(MiningRequest), + JobResult(MiningResult), +} +``` + +### MiningRequest + +| Field | Type | Description | +|-------|------|-------------| +| `job_id` | String | Unique identifier (UUID recommended) | +| `mining_hash` | String | Header hash (64 hex chars, no 0x prefix) | +| `distance_threshold` | String | Difficulty (U512 as decimal string) | + +Note: Nonce range is not specified - each miner independently selects a random starting point. + +### MiningResult + +| Field | Type | Description | +|-------|------|-------------| +| `status` | ApiResponseStatus | Result status (see below) | +| `job_id` | String | Job identifier | +| `nonce` | Option | Winning nonce (U512 hex, no 0x prefix) | +| `work` | Option | Winning nonce as bytes (128 hex chars) | +| `hash_count` | u64 | Number of nonces checked | +| `elapsed_time` | f64 | Time spent mining (seconds) | + +### ApiResponseStatus (Enum) + +| Value | Description | +|-------|-------------| +| `completed` | Valid nonce found | +| `failed` | Nonce range exhausted without finding solution | +| `cancelled` | Job was cancelled (new job received) | +| `running` | Job still in progress (not typically sent) | + +## Protocol Flow + +### Normal Mining Flow + +``` +Miner Node + │ │ + │──── QUIC Connect ─────────────────────────►│ + │◄─── Connection Established ────────────────│ + │ │ + │◄─── NewJob { job_id: "abc", ... } ─────────│ + │ │ + │ (picks random nonce, starts mining) │ + │ │ + │──── JobResult { job_id: "abc", ... } ─────►│ (found solution!) + │ │ + │ (node submits block, gets new work) │ + │ │ + │◄─── NewJob { job_id: "def", ... } ─────────│ + │ │ +``` + +### Job Cancellation (Implicit) + +When a new block arrives before the miner finds a solution, the node simply sends a new `NewJob`. The miner automatically cancels the previous job: + +``` +Miner Node + │ │ + │◄─── NewJob { job_id: "abc", ... } ─────────│ + │ │ + │ (mining "abc") │ + │ │ + │ (new block arrives at node!) │ + │ │ + │◄─── NewJob { job_id: "def", ... } ─────────│ + │ │ + │ (cancels "abc", starts "def") │ + │ │ + │──── JobResult { job_id: "def", ... } ─────►│ +``` + +### Miner Connect During Active Job + +When a miner connects while a job is active, it immediately receives the current job: + +``` +Miner (new) Node + │ │ (already mining job "abc") + │──── QUIC Connect ─────────────────────────►│ + │◄─── Connection Established ────────────────│ + │◄─── NewJob { job_id: "abc", ... } ─────────│ (current job sent immediately) + │ │ + │ (joins mining effort) │ +``` + +### Stale Result Handling + +If a result arrives for an old job, the node discards it: + +``` +Miner Node + │ │ + │◄─── NewJob { job_id: "abc", ... } ─────────│ + │ │ + │◄─── NewJob { job_id: "def", ... } ─────────│ (almost simultaneous) + │ │ + │──── JobResult { job_id: "abc", ... } ─────►│ (stale, node ignores) + │ │ + │──── JobResult { job_id: "def", ... } ─────►│ (current, node uses) +``` + +## Configuration + +### Node + +```bash +# Listen for external miner connections on port 9833 +quantus-node --miner-listen-port 9833 +``` + +### Miner + +```bash +# Connect to node +quantus-miner serve --node-addr 127.0.0.1:9833 +``` + +## TLS Configuration + +The node generates a self-signed TLS certificate at startup. The miner skips certificate verification by default (insecure mode). For production deployments, consider: + +1. **Certificate pinning**: Configure the miner to accept only specific certificate fingerprints +2. **Proper CA**: Use certificates signed by a trusted CA +3. **Network isolation**: Run node and miner on a private network + +## Error Handling + +### Connection Loss + +The miner automatically reconnects with exponential backoff: +- Initial delay: 1 second +- Maximum delay: 30 seconds + +The node continues operating with remaining connected miners. + +### Validation Errors + +If the miner receives an invalid `MiningRequest`, it sends a `JobResult` with status `failed`. ## Notes -- The external miner should iterate from `nonce_start` up to and including `nonce_end` when searching for a valid nonce. -- The miner should return the `nonce` and the calculated `work` hash when a solution is found. -- The node uses the returned `nonce` and `work` (along with the fetched `difficulty`) to construct the `QPoWSeal` and submit it. -- The external miner should not need to know anything about the runtime or the code; it only needs to perform the nonce search and return the results. \ No newline at end of file +- All hex values should be sent **without** the `0x` prefix +- The miner implements validation logic from `qpow_math::is_valid_nonce` +- The node uses the `work` field from `MiningResult` to construct `QPoWSeal` +- ALPN protocol identifier: `quantus-miner` +- Each miner independently generates a random nonce starting point using cryptographically secure randomness +- With a 512-bit nonce space, collision between miners is statistically impossible diff --git a/README.md b/README.md index 5deeea82..836dfc79 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ The binary will be available at `target/release/quantus-miner`. |----------|---------------------|-------------|---------| | `--cpu-workers ` | `MINER_CPU_WORKERS` | Number of CPU worker threads | Auto-detect | | `--gpu-devices ` | `MINER_GPU_DEVICES` | Number of GPU devices | 0 | -| `--port ` | `MINER_PORT` | HTTP API port | 9833 | +| `--port ` | `MINER_PORT` | QUIC server port | 9833 | | `--metrics-port ` | `MINER_METRICS_PORT` | Prometheus metrics port | Disabled | ## GPU Mining @@ -88,13 +88,15 @@ RUST_LOG=debug ./target/release/quantus-miner serve --cpu-workers 2 --gpu-device --metrics-port 9900 ``` -## API Endpoints +## Protocol -- `POST /mine`: Submit mining job -- `GET /result/{job_id}`: Get job status/result -- `POST /cancel/{job_id}`: Cancel job +The miner uses a QUIC-based protocol for communication with the node: -Full API specification: `api/openapi.yaml` +- **Transport**: QUIC with TLS 1.3 (self-signed certificates) +- **Port**: 9833 (default) +- **Messages**: `NewJob` (from node) and `JobResult` (from miner) + +For full protocol specification, see `EXTERNAL_MINER_PROTOCOL.md`. ## Docker diff --git a/api/openapi.yaml b/api/openapi.yaml deleted file mode 100644 index 516ff07a..00000000 --- a/api/openapi.yaml +++ /dev/null @@ -1,242 +0,0 @@ -openapi: 3.0.0 -info: - title: Quantus External Miner API - version: 0.1.0 - description: API for managing QPoW mining jobs for the Resonance Network external miner service. - -paths: - /mine: - post: - summary: Submit a new mining job - description: Accepts a mining job request and adds it to the queue if valid. - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/MiningRequest' - responses: - '200': - description: Job accepted successfully. - content: - application/json: - schema: - $ref: '#/components/schemas/MiningResponseAccepted' - '400': - description: Invalid request (e.g., missing fields, invalid format). - content: - application/json: - schema: - $ref: '#/components/schemas/MiningResponseError' - '409': - description: Conflict (e.g., duplicate job ID). - content: - application/json: - schema: - $ref: '#/components/schemas/MiningResponseError' - - /result/{job_id}: - get: - summary: Get mining job status and result - description: Retrieves the current status and potentially the found nonce for a given job ID. - parameters: - - name: job_id - in: path - required: true - description: The ID of the mining job to retrieve. - schema: - type: string - format: uuid - responses: - '200': - description: Job status retrieved successfully (could be running, completed, or failed). - content: - application/json: - schema: - $ref: '#/components/schemas/MiningResult' - '404': - description: Job ID not found. - content: - application/json: - schema: - $ref: '#/components/schemas/MiningResultNotFound' - - /cancel/{job_id}: - post: - summary: Cancel a mining job - description: Attempts to cancel an ongoing mining job. - parameters: - - name: job_id - in: path - required: true - description: The ID of the mining job to cancel. - schema: - type: string - format: uuid - responses: - '200': - description: Job cancelled successfully. - content: - application/json: - schema: - $ref: '#/components/schemas/MiningResponseCancelled' - '404': - description: Job ID not found. - content: - application/json: - schema: - $ref: '#/components/schemas/MiningResponseNotFound' - -components: - schemas: - ApiResponseStatus: - type: string - description: Status code indicating the result of an API call or the state of a mining job. - enum: - - accepted - - running - - completed - - failed - - cancelled - - not_found - - error - example: running - - MiningRequest: - type: object - required: - - job_id - - mining_hash - - difficulty - - nonce_start - - nonce_end - properties: - job_id: - type: string - format: uuid - description: Unique identifier for the mining job (UUID format recommended). - example: "123e4567-e89b-12d3-a456-426614174000" - mining_hash: - type: string - description: The header hash to mine on (hex encoded, 64 characters, no 0x). - example: "a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0" - pattern: '^[a-fA-F0-9]{64}$' - difficulty: - type: string - description: Target difficulty for the mining job (string representation of a u64). - example: "10000000000" - nonce_start: - type: string - description: Starting nonce value (hex encoded, 128 characters, no 0x). - example: "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - pattern: '^[a-fA-F0-9]{128}$' - nonce_end: - type: string - description: Ending nonce value (hex encoded, 128 characters, no 0x). - example: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" - pattern: '^[a-fA-F0-9]{128}$' - - MiningResponseBase: - type: object - required: - - status - - job_id - properties: - status: - $ref: '#/components/schemas/ApiResponseStatus' - job_id: - type: string - format: uuid - description: The ID of the job associated with the response. - example: "123e4567-e89b-12d3-a456-426614174000" - - MiningResponseAccepted: - allOf: - - $ref: '#/components/schemas/MiningResponseBase' - - type: object - properties: - status: - example: accepted - - MiningResponseCancelled: - allOf: - - $ref: '#/components/schemas/MiningResponseBase' - - type: object - properties: - status: - example: cancelled - - MiningResponseError: - allOf: - - $ref: '#/components/schemas/MiningResponseBase' - - type: object - properties: - status: - example: error - message: - type: string - description: Details about the error that occurred. - example: "Job already exists" - - MiningResponseNotFound: - allOf: - - $ref: '#/components/schemas/MiningResponseBase' - - type: object - properties: - status: - example: not_found - - MiningResult: - allOf: - - $ref: '#/components/schemas/MiningResponseBase' - - type: object - required: - - hash_count - - elapsed_time - properties: - nonce: - type: string - nullable: true - description: The current or winning nonce (U512 hex, no 0x). - example: "1a2b3c" - pattern: '^[a-fA-F0-9]+$' - work: - type: string - nullable: true - description: The winning nonce ([u8; 64] hex, 128 chars, no 0x). Only present if status is 'completed'. - example: "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a2b3c" - pattern: '^[a-fA-F0-9]{128}$' - hash_count: - type: integer - format: uint64 - description: Number of nonces checked so far for this job. - example: 123456 - elapsed_time: - type: number - format: double - description: Time elapsed (in seconds) since the job started. - example: 15.732 - - MiningResultNotFound: - allOf: - - $ref: '#/components/schemas/MiningResponseBase' - - type: object - properties: - status: - example: not_found - nonce: - type: string - nullable: true - example: null - work: - type: string - nullable: true - example: null - hash_count: - type: integer - format: uint64 - example: 0 - elapsed_time: - type: number - format: double - example: 0.0 \ No newline at end of file diff --git a/crates/engine-cpu/Cargo.toml b/crates/engine-cpu/Cargo.toml index 6b669719..3553e9e1 100644 --- a/crates/engine-cpu/Cargo.toml +++ b/crates/engine-cpu/Cargo.toml @@ -20,9 +20,6 @@ simd-poseidon2 = ["pow-core/simd-poseidon2"] [dependencies] pow-core = { path = "../pow-core" } primitive-types = { workspace = true } -log = { workspace = true } -thiserror = { workspace = true } -anyhow = { workspace = true } [dev-dependencies] hex = { workspace = true } diff --git a/crates/engine-cpu/src/lib.rs b/crates/engine-cpu/src/lib.rs index efb58589..4f79d427 100644 --- a/crates/engine-cpu/src/lib.rs +++ b/crates/engine-cpu/src/lib.rs @@ -1,23 +1,14 @@ #![deny(rust_2018_idioms)] #![forbid(unsafe_code)] -//! CPU mining engine scaffolding and trait definition. +//! CPU mining engine for Quantus. //! -//! This crate defines a minimal `MinerEngine` trait so the service layer can -//! orchestrate mining without knowing about specific math/device details, -//! plus a baseline CPU implementation that uses the reference path in `pow-core`. -//! -//! The baseline engine performs a straightforward linear scan across an inclusive -//! nonce range, computing distance per nonce using the context (header-derived) -//! constants. It is intended as a correctness reference and may be replaced at -//! runtime by faster engines (e.g., incremental CPU path, CUDA/OpenCL engines). - -use core::cmp::Ordering; +//! This crate defines the `MinerEngine` trait for mining orchestration, +//! plus a fast CPU implementation using optimized paths from `pow-core`. -use pow_core::{is_valid_nonce, JobContext}; +use pow_core::JobContext; use primitive_types::U512; -use std::sync::atomic::{AtomicBool, AtomicU64, Ordering as AtomicOrdering}; -use std::time::Duration; +use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering}; /// An inclusive nonce range to search. #[derive(Clone, Debug)] @@ -44,13 +35,6 @@ pub enum FoundOrigin { } /// Status from an engine search. -/// -/// For synchronous `search_range` calls, the final outcome will be one of: -/// - `Found` -/// - `Exhausted` -/// - `Cancelled` -/// -/// `Running` is included for potential future async/streaming engines. #[derive(Clone, Debug)] pub enum EngineStatus { Running { @@ -72,106 +56,22 @@ pub enum EngineStatus { /// Abstract mining engine interface. /// /// The service layer depends only on this trait to manage jobs. -/// Different engines (baseline CPU, optimized CPU, CUDA, OpenCL) can implement -/// this trait and be selected at runtime via configuration. +/// Different engines (CPU, CUDA, OpenCL) can implement this trait. pub trait MinerEngine: Send + Sync { - /// Human-readable engine name (for logs/metrics). + /// Human-readable engine name (for logs). fn name(&self) -> &'static str; /// Prepare a precomputed context for a job (header + difficulty). fn prepare_context(&self, header_hash: [u8; 32], difficulty: U512) -> JobContext; /// Search an inclusive nonce range with cancellation support. - /// - /// Implementations should: - /// - Respect `cancel` promptly to minimize wasted work after a solution is found. - /// - Return `Found` with a `Candidate` on success. - /// - Return `Exhausted` if the range is fully searched without a solution. - /// - Return `Cancelled` if `cancel` was observed during the search. fn search_range(&self, ctx: &JobContext, range: Range, cancel: &AtomicBool) -> EngineStatus; /// Enable downcasting to concrete engine types. fn as_any(&self) -> &dyn std::any::Any; } -/// Baseline CPU engine. -/// -/// This implementation uses the reference path from `pow-core` for distance -/// computation and scans the range linearly, one nonce at a time. -#[derive(Default)] -pub struct BaselineCpuEngine; - -impl BaselineCpuEngine { - pub fn new() -> Self { - Self - } -} - -impl MinerEngine for BaselineCpuEngine { - fn name(&self) -> &'static str { - "cpu-baseline" - } - - fn prepare_context(&self, header_hash: [u8; 32], difficulty: U512) -> JobContext { - JobContext::new(header_hash, difficulty) - } - - fn as_any(&self) -> &dyn std::any::Any { - self - } - - fn search_range(&self, ctx: &JobContext, range: Range, cancel: &AtomicBool) -> EngineStatus { - // Ensure start <= end (inclusive range). If not, treat as exhausted. - if range.start > range.end { - return EngineStatus::Exhausted { hash_count: 0 }; - } - - let mut current = range.start; - let mut hash_count: u64 = 0; - - loop { - // Cancellation check - if cancel.load(AtomicOrdering::Relaxed) { - return EngineStatus::Cancelled { hash_count }; - } - - // Compute hash for this nonce using Bitcoin-style double Poseidon2 - let (is_valid, hash) = - is_valid_nonce(ctx.header, current.to_big_endian(), ctx.difficulty); - hash_count = hash_count.saturating_add(1); - - // Check if it meets difficulty target - if is_valid { - let work = current.to_big_endian(); - let candidate = Candidate { - nonce: current, - work, - hash, - }; - return EngineStatus::Found { - candidate, - hash_count, - origin: FoundOrigin::Cpu, - }; - } - - // Advance or finish - match current.cmp(&range.end) { - Ordering::Less => { - current = current.saturating_add(U512::one()); - } - _ => { - // End of inclusive range reached - break EngineStatus::Exhausted { hash_count }; - } - } - } - } -} - -// Re-export commonly used items for convenience by consumers. - -// Fast CPU engine using incremental pow-core helpers (init_worker_y0 + step_mul) +/// Fast CPU engine using optimized pow-core helpers. #[derive(Default)] pub struct FastCpuEngine; @@ -197,7 +97,6 @@ impl MinerEngine for FastCpuEngine { fn search_range(&self, ctx: &JobContext, range: Range, cancel: &AtomicBool) -> EngineStatus { use pow_core::{hash_from_nonce, is_valid_hash, step_nonce}; - // Ensure start <= end (inclusive range). If not, treat as exhausted. if range.start > range.end { return EngineStatus::Exhausted { hash_count: 0 }; } @@ -206,12 +105,10 @@ impl MinerEngine for FastCpuEngine { let mut hash_count: u64 = 0; loop { - // Cancellation check if cancel.load(AtomicOrdering::Relaxed) { return EngineStatus::Cancelled { hash_count }; } - // Compute hash using Bitcoin-style double Poseidon2 let hash = hash_from_nonce(ctx, current); hash_count = hash_count.saturating_add(1); @@ -232,7 +129,6 @@ impl MinerEngine for FastCpuEngine { break; } - // Advance to next nonce current = step_nonce(current); } @@ -240,140 +136,8 @@ impl MinerEngine for FastCpuEngine { } } -#[derive(Default)] -pub struct ChainManipulatorEngine { - /// Base sleep per batch in nanoseconds; actual sleep increases linearly with job count. - pub base_delay_ns: u64, - /// Number of nonce attempts between sleeps. - pub step_batch: u64, - /// Optional cap for solved-block throttling (sleep index will not exceed this). - pub throttle_cap: Option, - /// Monotonically increasing solved-block counter used to scale throttling. - /// Public so the service can initialize from CLI (pick up where we left off). - pub job_index: AtomicU64, -} - -impl ChainManipulatorEngine { - pub fn new() -> Self { - // Start fast: first block has no throttle (job_index = 0 -> 0ns sleep), - // then 0.5ms per batch at block 1, 1.0ms at block 2, etc. - Self { - base_delay_ns: 500_000, // 0.5 ms - step_batch: 10_000, // sleep every 10k nonce checks - throttle_cap: None, // unlimited by default - job_index: AtomicU64::new(0), - } - } -} - -impl MinerEngine for ChainManipulatorEngine { - fn name(&self) -> &'static str { - "cpu-chain-manipulator" - } - - fn prepare_context(&self, header_hash: [u8; 32], difficulty: U512) -> JobContext { - // Per-block throttling: do NOT increment here. We increment on Found (i.e., when a block is solved). - let ctx = JobContext::new(header_hash, difficulty); - // Debug: log current throttle state at job start - log::debug!( - target: "miner", - "manipulator throttle start: solved_blocks={}, sleep_ns_per_batch={}, step_batch={}", - self.job_index.load(AtomicOrdering::Relaxed), - self.base_delay_ns.saturating_mul(self.job_index.load(AtomicOrdering::Relaxed)), - self.step_batch - ); - ctx - } - - fn as_any(&self) -> &dyn std::any::Any { - self - } - - fn search_range(&self, ctx: &JobContext, range: Range, cancel: &AtomicBool) -> EngineStatus { - use pow_core::{hash_from_nonce, is_valid_hash, step_nonce}; - - if range.start > range.end { - return EngineStatus::Exhausted { hash_count: 0 }; - } - - // Bitcoin-style hashing path - let mut current = range.start; - let mut hash_count: u64 = 0; - let mut batch_counter: u64 = 0; - - // Per-block throttling: derived from blocks solved so far (apply cap if configured). - let mut solved_blocks = self.job_index.load(AtomicOrdering::Relaxed); - if let Some(cap) = self.throttle_cap { - if solved_blocks > cap { - solved_blocks = cap; - } - } - let sleep_ns = self.base_delay_ns.saturating_mul(solved_blocks); - let do_sleep = sleep_ns > 0; - - loop { - if cancel.load(AtomicOrdering::Relaxed) { - return EngineStatus::Cancelled { hash_count }; - } - - // Compute hash using Bitcoin-style double Poseidon2 - let hash = hash_from_nonce(ctx, current); - hash_count = hash_count.saturating_add(1); - batch_counter = batch_counter.saturating_add(1); - - // Optional detailed debug of throttle progression within a job - #[allow(unused_variables)] - let _dbg_batch = batch_counter; - - if is_valid_hash(ctx, hash) { - let work = current.to_big_endian(); - // Increment solved-block counter so the NEXT block throttles more. - let _new_idx = self.job_index.fetch_add(1, AtomicOrdering::Relaxed) + 1; - { - let capped = if let Some(cap) = self.throttle_cap { - std::cmp::min(_new_idx, cap) - } else { - _new_idx - }; - log::debug!( - target: "miner", - "manipulator throttle increment: solved_blocks={} (next sleep_ns_per_batch={}, cap={:?})", - _new_idx, - self.base_delay_ns.saturating_mul(capped), - self.throttle_cap - ); - } - return EngineStatus::Found { - candidate: Candidate { - nonce: current, - work, - hash, - }, - hash_count, - origin: FoundOrigin::Cpu, - }; - } - - // Throttle after each batch to artificially slow down based on blocks solved so far. - if do_sleep && batch_counter >= self.step_batch { - std::thread::sleep(Duration::from_nanos(sleep_ns)); - batch_counter = 0; - } - - // Advance - if current < range.end { - current = step_nonce(current); - } else { - break EngineStatus::Exhausted { hash_count }; - } - } - } -} - -pub use { - BaselineCpuEngine as DefaultEngine, Candidate as EngineCandidate, - ChainManipulatorEngine as ChainEngine, FastCpuEngine as FastEngine, Range as EngineRange, -}; +// Re-exports for convenience +pub use {Candidate as EngineCandidate, Range as EngineRange}; #[cfg(test)] mod tests { @@ -381,33 +145,25 @@ mod tests { use primitive_types::U512; use std::sync::atomic::AtomicBool; - fn make_ctx() -> JobContext { - let header = [1u8; 32]; - let difficulty = U512::from(1u64); // easy difficulty for "found" parity test - JobContext::new(header, difficulty) - } - #[test] fn engine_returns_exhausted_when_no_solution_in_range() { - // Use a very hard difficulty to make solutions effectively impossible in a tiny range. let header = [2u8; 32]; - let difficulty = U512::MAX; + let difficulty = U512::MAX; // impossible difficulty let ctx = JobContext::new(header, difficulty); let range = Range { start: U512::from(1u64), - end: U512::from(1000u64), // small range; probability of accidental match is negligible + end: U512::from(1000u64), }; let cancel = AtomicBool::new(false); - let baseline = BaselineCpuEngine::new(); + let engine = FastCpuEngine::new(); - let status = baseline.search_range(&ctx, range.clone(), &cancel); + let status = engine.search_range(&ctx, range.clone(), &cancel); match status { EngineStatus::Exhausted { hash_count } => { - // Inclusive range length = end - start + 1 let expected = (range.end - range.start + U512::one()).as_u64(); - assert_eq!(hash_count, expected, "hash_count should equal range length"); + assert_eq!(hash_count, expected); } other => panic!("expected Exhausted, got {other:?}"), } @@ -415,19 +171,22 @@ mod tests { #[test] fn engine_respects_immediate_cancellation() { - let ctx = make_ctx(); + let header = [1u8; 32]; + let difficulty = U512::from(1u64); + let ctx = JobContext::new(header, difficulty); + let range = Range { start: U512::from(0u64), end: U512::from(1_000_000u64), }; - let cancel = AtomicBool::new(true); // cancelled before starting - let baseline = BaselineCpuEngine::new(); - let status = baseline.search_range(&ctx, range, &cancel); + let cancel = AtomicBool::new(true); // pre-cancelled + let engine = FastCpuEngine::new(); + + let status = engine.search_range(&ctx, range, &cancel); match status { EngineStatus::Cancelled { hash_count } => { - // Cancellation was pre-set; allow zero or near-zero work depending on timing. - assert_eq!(hash_count, 0, "expected no work when cancelled immediately"); + assert_eq!(hash_count, 0); } other => panic!("expected Cancelled, got {other:?}"), } diff --git a/crates/engine-gpu/Cargo.toml b/crates/engine-gpu/Cargo.toml index 531c4c84..47e7ba23 100644 --- a/crates/engine-gpu/Cargo.toml +++ b/crates/engine-gpu/Cargo.toml @@ -11,26 +11,21 @@ description = "GPU mining engine implementation for the Quantus External Miner" [features] # Default to the baseline/reference implementation; enable others as needed. -default = ["baseline", "metrics"] +default = ["baseline"] # Map engine features (pow-core dependency removed for now) baseline = [] simd-poseidon2 = [] -metrics = ["dep:metrics"] [dependencies] engine-cpu = { path = "../engine-cpu" } pow-core = { path = "../pow-core" } -metrics = { path = "../metrics", optional = true } primitive-types = { workspace = true } log = { workspace = true } -thiserror = { workspace = true } -anyhow = { workspace = true } qp-poseidon-core = { version = "1.0.2", features = ["p2"] } qp-poseidon-constants = { version = "1.0.2" } -qp-plonky2 = { version = "1.1.1" } qp-plonky2-field = { version = "1.1.1" } - +plonky2 = { package = "qp-plonky2", version = "1.1.3" } wgpu = { version = "27.0.1" } # GPU compute library futures = "0.3" # For async executor diff --git a/crates/engine-gpu/benches/gpu_engine_bench.rs b/crates/engine-gpu/benches/gpu_engine_bench.rs index 0e7eec97..0f50e6e4 100644 --- a/crates/engine-gpu/benches/gpu_engine_bench.rs +++ b/crates/engine-gpu/benches/gpu_engine_bench.rs @@ -5,11 +5,10 @@ use pow_core::JobContext; use primitive_types::U512; use rand::RngCore; use std::sync::atomic::AtomicBool; -use std::time::Duration; fn bench_cpu_vs_gpu_small(c: &mut Criterion) { let cpu_engine = FastCpuEngine::new(); - let gpu_engine = GpuEngine::new(Duration::from_millis(3000)); + let gpu_engine = GpuEngine::new(); let cancel_flag = AtomicBool::new(false); // Small range: 10K nonces - reasonable for benchmarking @@ -59,7 +58,7 @@ fn bench_cpu_vs_gpu_small(c: &mut Criterion) { fn bench_cpu_vs_gpu_medium(c: &mut Criterion) { let cpu_engine = FastCpuEngine::new(); - let gpu_engine = GpuEngine::new(Duration::from_millis(3000)); + let gpu_engine = GpuEngine::new(); let cancel_flag = AtomicBool::new(false); // Medium range: 100K nonces @@ -109,7 +108,7 @@ fn bench_cpu_vs_gpu_medium(c: &mut Criterion) { fn bench_cpu_vs_gpu_large(c: &mut Criterion) { let cpu_engine = FastCpuEngine::new(); - let gpu_engine = GpuEngine::new(Duration::from_millis(3000)); + let gpu_engine = GpuEngine::new(); let cancel_flag = AtomicBool::new(false); // Large range: 1M nonces - where GPU should really shine @@ -159,7 +158,7 @@ fn bench_cpu_vs_gpu_large(c: &mut Criterion) { fn bench_solution_finding(c: &mut Criterion) { let cpu_engine = FastCpuEngine::new(); - let gpu_engine = GpuEngine::new(Duration::from_millis(3000)); + let gpu_engine = GpuEngine::new(); let cancel_flag = AtomicBool::new(false); // Range where we expect to find solutions quickly @@ -209,7 +208,7 @@ fn bench_solution_finding(c: &mut Criterion) { fn bench_throughput_per_second(c: &mut Criterion) { let cpu_engine = FastCpuEngine::new(); - let gpu_engine = GpuEngine::new(Duration::from_millis(3000)); + let gpu_engine = GpuEngine::new(); let cancel_flag = AtomicBool::new(false); // Fixed time benchmark - see how many hashes we can do in 1 second @@ -258,7 +257,7 @@ fn bench_throughput_per_second(c: &mut Criterion) { } fn bench_gpu_batch_efficiency(c: &mut Criterion) { - let gpu_engine = GpuEngine::new(Duration::from_millis(3000)); + let gpu_engine = GpuEngine::new(); let cancel_flag = AtomicBool::new(false); let mut group = c.benchmark_group("gpu_batch_sizes"); diff --git a/crates/engine-gpu/examples/verify_nonce.rs b/crates/engine-gpu/examples/verify_nonce.rs index bf444ed5..28d823df 100644 --- a/crates/engine-gpu/examples/verify_nonce.rs +++ b/crates/engine-gpu/examples/verify_nonce.rs @@ -1,8 +1,7 @@ -use engine_cpu::{BaselineCpuEngine, EngineStatus, MinerEngine, Range}; +use engine_cpu::{EngineStatus, FastCpuEngine, MinerEngine, Range}; use engine_gpu::GpuEngine; use primitive_types::U512; use std::sync::atomic::AtomicBool; -use std::time::Duration; fn main() { // Initialize logging @@ -17,7 +16,7 @@ fn main() { // Use a fixed header and easy difficulty (1) so any nonce is valid let header = [1u8; 32]; let difficulty = U512::from(u64::MAX); // High difficulty - no solutions expected - let cpu_engine = BaselineCpuEngine::new(); + let cpu_engine = FastCpuEngine::new(); let ctx = cpu_engine.prepare_context(header, difficulty); log::info!("Context prepared. Difficulty: {}", difficulty); @@ -26,7 +25,7 @@ fn main() { // 3. Verify with GPU engine log::info!("Initializing GPU engine..."); - let gpu_engine = GpuEngine::new(Duration::from_millis(3000)); + let gpu_engine = GpuEngine::new(); // Search a small range around the valid nonce let gpu_range = Range { diff --git a/crates/engine-gpu/src/end_to_end_tests.rs b/crates/engine-gpu/src/end_to_end_tests.rs index 6526be19..233588d1 100644 --- a/crates/engine-gpu/src/end_to_end_tests.rs +++ b/crates/engine-gpu/src/end_to_end_tests.rs @@ -82,8 +82,8 @@ pub async fn test_end_to_end_mining( let zeros = vec![0u8; results_size]; queue.write_buffer(&results_buffer, 0, &zeros); - // Dispatch config buffer: [total_threads, nonces_per_thread, work_per_batch, threads_per_workgroup] - let dispatch_config_data: [u32; 4] = [256, 1, 1, 256]; + // Dispatch config buffer: [total_threads, nonces_per_thread, total_nonces, cancel_check_interval] + let dispatch_config_data: [u32; 4] = [256, 1, 256, 10000]; let dispatch_config_buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("Dispatch Config Buffer"), size: 16, @@ -96,6 +96,20 @@ pub async fn test_end_to_end_mining( bytemuck::cast_slice(&dispatch_config_data), ); + // Cancel flag buffer: 0 = running, 1 = cancel requested + let cancel_flag_data: [u32; 1] = [0]; + let cancel_flag_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Cancel Flag Buffer"), + size: 4, + usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + queue.write_buffer( + &cancel_flag_buffer, + 0, + bytemuck::cast_slice(&cancel_flag_data), + ); + // Load Shader let shader_source = include_str!("mining.wgsl"); let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { @@ -137,6 +151,10 @@ pub async fn test_end_to_end_mining( binding: 4, resource: dispatch_config_buffer.as_entire_binding(), }, + wgpu::BindGroupEntry { + binding: 5, + resource: cancel_flag_buffer.as_entire_binding(), + }, ], }); @@ -193,18 +211,27 @@ pub async fn test_end_to_end_mining( let found_nonce = U512::from_little_endian(&nonce_bytes); println!("GPU Nonce: {}", found_nonce); - assert_eq!(found_nonce, nonce_val, "Nonce mismatch!"); - - // Parse hash + // Parse hash from GPU result let mut hash_bytes = [0u8; 64]; for i in 0..16 { let bytes = result_u32s[17 + i].to_le_bytes(); hash_bytes[i * 4..(i + 1) * 4].copy_from_slice(&bytes); } - let found_hash = U512::from_little_endian(&hash_bytes); - println!("GPU Hash: {:x}", found_hash); + let gpu_hash = U512::from_little_endian(&hash_bytes); + println!("GPU Hash: {:x}", gpu_hash); + + // Verify: compute hash on CPU for the found nonce and compare + let cpu_verified_hash = hash_from_nonce(&ctx, found_nonce); + println!("CPU verified hash: {:x}", cpu_verified_hash); + + assert_eq!( + gpu_hash, cpu_verified_hash, + "GPU hash doesn't match CPU verification for the found nonce!" + ); + + // Verify hash is below target (should always pass with difficulty 1) + assert!(gpu_hash <= ctx.target, "Hash is not below target!"); - assert_eq!(found_hash, expected_hash, "Hash mismatch!"); println!("✅ End-to-End Test Passed!"); } else { println!("❌ GPU did not find solution (should have passed with MAX target)"); diff --git a/crates/engine-gpu/src/lib.rs b/crates/engine-gpu/src/lib.rs index 73b8df09..5b56f841 100644 --- a/crates/engine-gpu/src/lib.rs +++ b/crates/engine-gpu/src/lib.rs @@ -3,14 +3,16 @@ use engine_cpu::{Candidate, EngineStatus, FoundOrigin, MinerEngine, Range}; use futures::executor::block_on; -use pow_core::JobContext; +use pow_core::{format_hashrate, format_u512, JobContext}; use primitive_types::U512; use std::cell::RefCell; use std::sync::{ - atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}, + atomic::{AtomicBool, AtomicUsize, Ordering}, Arc, }; -use std::time::Duration; + +/// Default interval for checking cancel flag in shader (in nonces) +const DEFAULT_CANCEL_CHECK_INTERVAL: u32 = 10_000; /// Represents a single GPU device context. struct GpuContext { @@ -20,9 +22,6 @@ struct GpuContext { // Cached vendor configuration optimal_workgroups: u32, - - // Dynamic batch size tuning - batch_size: AtomicU64, } #[derive(Clone)] @@ -32,6 +31,7 @@ struct GpuResources { start_nonce_buffer: wgpu::Buffer, results_buffer: wgpu::Buffer, dispatch_config_buffer: wgpu::Buffer, + cancel_buffer: wgpu::Buffer, staging_buffer: wgpu::Buffer, bind_group: wgpu::BindGroup, } @@ -39,7 +39,7 @@ struct GpuResources { pub struct GpuEngine { contexts: Vec>, device_counter: AtomicUsize, - target_batch_duration: Duration, + cancel_check_interval: u32, } // Thread-local storage for consistent GPU device assignment per worker thread @@ -87,7 +87,7 @@ impl GpuContext { mapped_at_creation: false, }); - // Dispatch config: [total_threads, nonces_per_thread, workgroups, threads_per_workgroup] = 4 u32s + // Dispatch config: [total_threads, nonces_per_thread, total_nonces, cancel_check_interval] = 4 u32s let dispatch_config_buffer = self.device.create_buffer(&wgpu::BufferDescriptor { label: Some("Dispatch Config Buffer"), size: 16, @@ -95,6 +95,14 @@ impl GpuContext { mapped_at_creation: false, }); + // Cancel flag: single u32 (0 = running, 1 = cancel requested) + let cancel_buffer = self.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Cancel Buffer"), + size: 4, + usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + let staging_buffer = self.device.create_buffer(&wgpu::BufferDescriptor { label: Some("Staging Buffer"), size: results_size, @@ -126,6 +134,10 @@ impl GpuContext { binding: 4, resource: dispatch_config_buffer.as_entire_binding(), }, + wgpu::BindGroupEntry { + binding: 5, + resource: cancel_buffer.as_entire_binding(), + }, ], }); @@ -135,6 +147,7 @@ impl GpuContext { start_nonce_buffer, results_buffer, dispatch_config_buffer, + cancel_buffer, staging_buffer, bind_group, } @@ -143,22 +156,34 @@ impl GpuContext { impl Default for GpuEngine { fn default() -> Self { - // Default to 3 seconds if not specified via new() - Self::new(Duration::from_secs(3)) + Self::new() } } impl GpuEngine { - pub fn new(target_batch_duration: Duration) -> Self { - block_on(Self::init(target_batch_duration)).expect("Failed to initialize GPU engine") + pub fn new() -> Self { + block_on(Self::init(DEFAULT_CANCEL_CHECK_INTERVAL)) + .expect("Failed to initialize GPU engine") + } + + /// Create a new GPU engine with a custom cancel check interval. + pub fn with_cancel_interval(cancel_check_interval: u32) -> Self { + block_on(Self::init(cancel_check_interval)).expect("Failed to initialize GPU engine") } /// Try to initialize the GPU engine, returning an error if initialization fails. - pub fn try_new(target_batch_duration: Duration) -> Result> { - block_on(Self::init(target_batch_duration)) + pub fn try_new() -> Result> { + block_on(Self::init(DEFAULT_CANCEL_CHECK_INTERVAL)) + } + + /// Try to initialize the GPU engine with a custom cancel check interval. + pub fn try_with_cancel_interval( + cancel_check_interval: u32, + ) -> Result> { + block_on(Self::init(cancel_check_interval)) } - async fn init(target_batch_duration: Duration) -> Result> { + async fn init(cancel_check_interval: u32) -> Result> { log::info!("Initializing WGPU..."); let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { backends: wgpu::Backends::PRIMARY, @@ -241,52 +266,19 @@ impl GpuEngine { queue, pipeline, optimal_workgroups, - batch_size: AtomicU64::new(1_000_000), })); } - log::info!("GPU engine initialized with {} devices", contexts.len()); - - // Set engine backend info for metrics - #[cfg(feature = "metrics")] - { - metrics::set_gpu_device_count(contexts.len() as i64); - - for (i, adapter_info) in adapter_infos.iter().enumerate() { - let device_id = format!("gpu-{}", i); - let backend_str = format!("{:?}", adapter_info.backend); - let vendor_str = format!("{}", adapter_info.vendor); - let device_type_str = format!("{:?}", adapter_info.device_type); - let clean_name = adapter_info.name.replace(" ", "_").replace(",", ""); - - // Set general engine backend info - metrics::set_engine_backend(&device_id, &backend_str); - - // Set detailed GPU device info - metrics::set_gpu_device_info( - &device_id, - &clean_name, - &backend_str, - &vendor_str, - &device_type_str, - ); - - // Log GPU device info for monitoring - log::info!( - "📊 GPU Device {}: {} | Backend: {:?} | Vendor: {} | Device Type: {:?}", - i, - adapter_info.name, - adapter_info.backend, - adapter_info.vendor, - adapter_info.device_type - ); - } - } + log::info!( + "GPU engine initialized with {} devices (cancel check interval: {} nonces)", + contexts.len(), + cancel_check_interval + ); Ok(Self { contexts, device_counter: AtomicUsize::new(0), - target_batch_duration, + cancel_check_interval, }) } @@ -328,14 +320,17 @@ impl MinerEngine for GpuEngine { return EngineStatus::Exhausted { hash_count: 0 }; } + // Check for pre-cancellation + if cancel.load(Ordering::Relaxed) { + return EngineStatus::Cancelled { hash_count: 0 }; + } + // Use thread-local assignment for consistent worker-to-GPU mapping let device_index = ASSIGNED_GPU_DEVICE.with(|assigned| { let mut assigned_ref = assigned.borrow_mut(); if let Some(index) = *assigned_ref { - // This thread already has a GPU assigned index } else { - // First time this thread is calling search_range, assign a GPU device let index = if self.contexts.len() == 1 { 0 } else { @@ -352,14 +347,26 @@ impl MinerEngine for GpuEngine { }); let gpu_ctx = &self.contexts[device_index]; - let initial_batch_size = gpu_ctx.batch_size.load(Ordering::Relaxed); + + // Calculate range size (capped at u32::MAX for dispatch config) + let range_size_u512 = range + .end + .saturating_sub(range.start) + .saturating_add(U512::one()); + let range_size = if range_size_u512 > U512::from(u32::MAX) { + u32::MAX as u64 + } else { + range_size_u512.as_u64() + }; + log::info!( target: "gpu_engine", - "GPU {} search started: range {}..={} (inclusive), initial batch size: {}", + "GPU {} search started: range {}..{}, nonces: {}, cancel check interval: {}", device_index, - range.start, - range.end, - initial_batch_size + format_u512(range.start), + format_u512(range.end), + range_size, + self.cancel_check_interval ); // Ensure resources are initialized for this thread @@ -370,11 +377,10 @@ impl MinerEngine for GpuEngine { } }); - // Clone resources to use outside the closure (they are cheap handles) let resources = WORKER_RESOURCES .with(|resources_cell| resources_cell.borrow().as_ref().unwrap().clone()); - // Pre-convert header and target once (not per range) + // Pre-convert header and target let mut header_u32s = [0u32; 8]; for (i, item) in header_u32s.iter_mut().enumerate() { let chunk = &ctx.header[i * 4..(i + 1) * 4]; @@ -398,242 +404,222 @@ impl MinerEngine for GpuEngine { bytemuck::cast_slice(&target_u32s), ); - // --- Batch Processing Loop --- - let mut current_nonce = range.start; - let mut total_hashes_processed = 0u64; - - // Loop until range is covered or cancelled - while current_nonce <= range.end { - // Check for cancellation between batches to ensure responsiveness - if cancel.load(Ordering::Relaxed) { - log::info!( - target: "gpu_engine", - "GPU {} search cancelled: processed {} hashes", - device_index, - total_hashes_processed - ); - return EngineStatus::Cancelled { - hash_count: total_hashes_processed, - }; - } - - // Determine dynamic batch size for this iteration - let batch_size = gpu_ctx.batch_size.load(Ordering::Relaxed); - - log::debug!( - target: "gpu_engine", - "GPU {} starting batch: nonce {}..{}, batch size: {}", - device_index, - current_nonce, - current_nonce.saturating_add(U512::from(batch_size)).saturating_sub(U512::from(1u64)).min(range.end), - batch_size - ); + // Calculate dispatch configuration + let threads_per_workgroup = 256u32; + let limits = gpu_ctx.device.limits(); + let max_workgroups = limits.max_compute_workgroups_per_dimension; - // Calculate inclusive end for this batch - let batch_end = current_nonce - .saturating_add(U512::from(batch_size)) - .saturating_sub(U512::from(1u64)); + let hinted_workgroups = gpu_ctx.optimal_workgroups.max(1).min(max_workgroups); + let hinted_threads = hinted_workgroups as u64 * threads_per_workgroup as u64; - let actual_end = if batch_end > range.end { - range.end - } else { - batch_end - }; + let logical_threads = range_size.min(hinted_threads).max(1); + let num_workgroups = ((logical_threads as u32).div_ceil(threads_per_workgroup)).max(1); + let total_threads = (num_workgroups * threads_per_workgroup) as u64; + let nonces_per_thread = (range_size.div_ceil(total_threads)).max(1) as u32; - // Range size for this specific batch - let range_size_u512 = actual_end - .saturating_sub(current_nonce) - .saturating_add(U512::from(1u64)); - let range_size = range_size_u512.as_u64(); // batch_size is u64, so this is safe + log::debug!( + target: "gpu_engine", + "GPU {} dispatch config: {} workgroups × {} threads, {} nonces/thread", + device_index, + num_workgroups, + threads_per_workgroup, + nonces_per_thread + ); - if range_size == 0 { - break; - } + // Dispatch config: [total_threads, nonces_per_thread, total_nonces, cancel_check_interval] + let dispatch_config = [ + total_threads as u32, + nonces_per_thread, + range_size as u32, + self.cancel_check_interval, + ]; - // --- Dispatch Logic (same as before but using range_size) --- - let threads_per_workgroup = 256u32; // Must match shader - let limits = gpu_ctx.device.limits(); - let max_workgroups = limits.max_compute_workgroups_per_dimension; + // Write dispatch config + gpu_ctx.queue.write_buffer( + &resources.dispatch_config_buffer, + 0, + bytemuck::cast_slice(&dispatch_config), + ); - let hinted_workgroups = gpu_ctx.optimal_workgroups.max(1).min(max_workgroups); - let hinted_threads = hinted_workgroups as u64 * threads_per_workgroup as u64; + // Write start nonce + let start_nonce_bytes = range.start.to_little_endian(); + gpu_ctx + .queue + .write_buffer(&resources.start_nonce_buffer, 0, &start_nonce_bytes); + + // Reset results buffer + const RESULTS_SIZE: usize = (1 + 16 + 16) * 4; + const ZEROS: [u8; RESULTS_SIZE] = [0; RESULTS_SIZE]; + gpu_ctx + .queue + .write_buffer(&resources.results_buffer, 0, &ZEROS); + + // Reset cancel buffer to 0 (not cancelled) + gpu_ctx + .queue + .write_buffer(&resources.cancel_buffer, 0, &[0u8; 4]); + + let search_start = std::time::Instant::now(); + + // Create and submit command buffer + let mut encoder = gpu_ctx + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + { + let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { + label: None, + timestamp_writes: None, + }); + cpass.set_pipeline(&gpu_ctx.pipeline); + cpass.set_bind_group(0, &resources.bind_group, &[]); + cpass.dispatch_workgroups(num_workgroups, 1, 1); + } + encoder.copy_buffer_to_buffer( + &resources.results_buffer, + 0, + &resources.staging_buffer, + 0, + RESULTS_SIZE as u64, + ); - let mut logical_threads = range_size.min(hinted_threads); - if logical_threads == 0 { - logical_threads = 1; - } + gpu_ctx.queue.submit(Some(encoder.finish())); - let mut num_workgroups = (logical_threads as u32).div_ceil(threads_per_workgroup); - if num_workgroups == 0 { - num_workgroups = 1; + // Poll GPU with periodic cancel checks + // We use a shared flag to know when the mapping is complete + let buffer_slice = resources.staging_buffer.slice(..); + let mapped = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); + let mapped_clone = mapped.clone(); + buffer_slice.map_async(wgpu::MapMode::Read, move |result| { + if result.is_ok() { + mapped_clone.store(true, Ordering::Release); } - let total_threads = (num_workgroups * threads_per_workgroup) as u64; - let nonces_per_thread = range_size.div_ceil(total_threads).max(1) as u32; - let total_threads_u32 = total_threads as u32; - - #[cfg(feature = "metrics")] - { - let device_id = "gpu-0"; // Simplified; ideally propagate device ID to metrics - metrics::set_gpu_batch_size(device_id, range_size as f64); - metrics::set_gpu_workgroups(device_id, num_workgroups as f64); - } - - // Dispatch config: [total_threads, nonces_per_thread, total_nonces, threads_per_workgroup] - let total_nonces_u32 = range_size.min(u32::MAX as u64) as u32; - let dispatch_config = [ - total_threads_u32, - nonces_per_thread, - total_nonces_u32, - threads_per_workgroup, - ]; - gpu_ctx.queue.write_buffer( - &resources.dispatch_config_buffer, - 0, - bytemuck::cast_slice(&dispatch_config), - ); + }); - // Starting nonce for this batch - let start_nonce_bytes = current_nonce.to_little_endian(); - gpu_ctx - .queue - .write_buffer(&resources.start_nonce_buffer, 0, &start_nonce_bytes); - - // Reset results - const RESULTS_SIZE: usize = (1 + 16 + 16) * 4; - const ZEROS: [u8; RESULTS_SIZE] = [0; RESULTS_SIZE]; - gpu_ctx - .queue - .write_buffer(&resources.results_buffer, 0, &ZEROS); - - let batch_start_time = std::time::Instant::now(); - - // Encode and submit - let mut encoder = gpu_ctx - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); - { - let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor { - label: None, - timestamp_writes: None, - }); - cpass.set_pipeline(&gpu_ctx.pipeline); - cpass.set_bind_group(0, &resources.bind_group, &[]); - cpass.dispatch_workgroups(num_workgroups, 1, 1); + loop { + // Check if cancelled + if cancel.load(Ordering::Relaxed) { + // Write cancel flag to GPU buffer + gpu_ctx + .queue + .write_buffer(&resources.cancel_buffer, 0, &1u32.to_le_bytes()); + log::debug!(target: "gpu_engine", "GPU {} cancel flag propagated to GPU buffer", device_index); } - encoder.copy_buffer_to_buffer( - &resources.results_buffer, - 0, - &resources.staging_buffer, - 0, - RESULTS_SIZE as u64, - ); - - gpu_ctx.queue.submit(Some(encoder.finish())); - - let buffer_slice = resources.staging_buffer.slice(..); - buffer_slice.map_async(wgpu::MapMode::Read, |_| {}); + // Poll with short timeout let _ = gpu_ctx.device.poll(wgpu::PollType::Wait { submission_index: None, - timeout: None, + timeout: Some(std::time::Duration::from_millis(10)), }); - let data = buffer_slice.get_mapped_range(); - let result_u32s: &[u32] = bytemuck::cast_slice(&data); - - if result_u32s[0] != 0 { - // Solution found! - let nonce_u32s = &result_u32s[1..17]; - let hash_u32s = &result_u32s[17..33]; - let nonce = U512::from_little_endian(bytemuck::cast_slice(nonce_u32s)); - let hash = U512::from_little_endian(bytemuck::cast_slice(hash_u32s)); - let work = nonce.to_big_endian(); - - log::info!( - target: "gpu_engine", - "GPU {} found solution! Nonce: {}, Hash: {:x}", - device_index, - nonce, - hash - ); - - drop(data); - resources.staging_buffer.unmap(); - - log::info!( - target: "gpu_engine", - "GPU {} search ended (solution found): processed {} hashes", - device_index, - total_hashes_processed + range_size - ); - - // Even if found, return total hashes processed including this batch - return EngineStatus::Found { - candidate: Candidate { nonce, work, hash }, - hash_count: total_hashes_processed + range_size, - origin: FoundOrigin::GpuG1, - }; + // Check if buffer mapping is complete + if mapped.load(Ordering::Acquire) { + break; } + } + + let search_elapsed = search_start.elapsed(); + + // Read results + let data = buffer_slice.get_mapped_range(); + let result_u32s: &[u32] = bytemuck::cast_slice(&data); + + let was_cancelled = cancel.load(Ordering::Relaxed); + + // Calculate the actual number of nonces dispatched to the GPU + // This is total_threads * nonces_per_thread, capped at range_size + let dispatched_nonces = (total_threads * nonces_per_thread as u64).min(range_size); + + if result_u32s[0] != 0 { + // Solution found! + let nonce_u32s = &result_u32s[1..17]; + let hash_u32s = &result_u32s[17..33]; + let nonce = U512::from_little_endian(bytemuck::cast_slice(nonce_u32s)); + let hash = U512::from_little_endian(bytemuck::cast_slice(hash_u32s)); + let work = nonce.to_big_endian(); + + // Calculate hashes computed based on GPU parallel execution model. + // + // GPU threads process nonces in parallel, not sequentially: + // - Thread T processes nonces: start + T*nonces_per_thread + 0, +1, +2, ... + // - All threads run approximately in lockstep (SIMT execution) + // + // When thread T finds a solution at its iteration J: + // - logical_index = nonce - start = T * nonces_per_thread + J + // - winning_iteration = logical_index % nonces_per_thread = J + // - All threads have progressed to approximately iteration J + // - Total hashes ≈ total_threads * (J + 1) + // + // This gives a consistent hash rate regardless of which thread finds the solution. + let hashes_computed = if nonce >= range.start { + let logical_index = (nonce - range.start).as_u64(); + let winning_iteration = logical_index % (nonces_per_thread as u64); + // All threads processed approximately (winning_iteration + 1) nonces each + (total_threads * (winning_iteration + 1)).min(dispatched_nonces) + } else { + // Shouldn't happen, but fall back to dispatched count + dispatched_nonces + }; drop(data); resources.staging_buffer.unmap(); - // --- Auto-tuning Logic --- - let batch_elapsed = batch_start_time.elapsed(); - let batch_elapsed_secs = batch_elapsed.as_secs_f64(); - - // Update stats - total_hashes_processed += range_size; - - if batch_elapsed_secs > 0.0 { - let target_secs = self.target_batch_duration.as_secs_f64(); - - // Calculate ideal batch size: (current_size / elapsed) * target - let hashrate = range_size as f64 / batch_elapsed_secs; - let ideal_batch_size = (hashrate * target_secs) as u64; - - let old_size = gpu_ctx.batch_size.load(Ordering::Relaxed); - - // Smooth update: 50% old, 50% new to avoid oscillation - // Also clamp to avoid drastic changes (0.5x to 2.0x) - let min_clamp = old_size / 2; - let max_clamp = old_size * 2; - - // Ensure we don't go below a reasonable minimum (e.g. 100k) to avoid overhead dominance - let clamped_ideal = ideal_batch_size.clamp(min_clamp, max_clamp).max(100_000); - - // Simple moving average - let new_size = (old_size + clamped_ideal) / 2; - - if new_size != old_size { - gpu_ctx.batch_size.store(new_size, Ordering::Relaxed); - log::debug!( - target: "gpu_engine", - "GPU {} auto-tune: batch {} -> {} (elapsed: {:.3}s, target: {:.1}s, rate: {:.1} MH/s)", - device_index, - old_size, - new_size, - batch_elapsed_secs, - target_secs, - hashrate / 1_000_000.0 - ); - } - } + let hash_rate = hashes_computed as f64 / search_elapsed.as_secs_f64(); - // Move to next batch - current_nonce = actual_end.saturating_add(U512::from(1u64)); + log::debug!( + target: "gpu_engine", + "GPU {} found solution! Nonce: {}, Hash: {} ({} hashes in {:.2}s, {})", + device_index, + format_u512(nonce), + format_u512(hash), + hashes_computed, + search_elapsed.as_secs_f64(), + format_hashrate(hash_rate) + ); + + return EngineStatus::Found { + candidate: Candidate { nonce, work, hash }, + hash_count: hashes_computed, + origin: FoundOrigin::GpuG1, + }; } - // Finished entire range without solution + drop(data); + resources.staging_buffer.unmap(); + + if was_cancelled { + // For cancelled jobs, estimate hashes based on elapsed time and dispatched work. + // The shader checks cancel flag periodically, so we estimate based on how much + // of the dispatch likely completed. This is approximate but better than 0. + // We use the ratio of elapsed time to expected completion time. + // As a simple heuristic, if the GPU was running, it was doing work. + // We report dispatched_nonces as upper bound since the dispatch was submitted. + // In practice, cancellation happens quickly so this is often close to 0 useful hashes. + let estimated_hashes = dispatched_nonces; + log::info!( + target: "gpu_engine", + "GPU {} search cancelled after {:.2}s (~{} hashes dispatched)", + device_index, + search_elapsed.as_secs_f64(), + estimated_hashes + ); + return EngineStatus::Cancelled { + hash_count: estimated_hashes, + }; + } + + // Range exhausted without finding solution - all dispatched nonces were processed + let hash_rate = dispatched_nonces as f64 / search_elapsed.as_secs_f64(); log::info!( target: "gpu_engine", - "GPU {} search completed: processed {} hashes, final batch size: {}", + "GPU {} search exhausted: {} hashes in {:.2}s ({})", device_index, - total_hashes_processed, - gpu_ctx.batch_size.load(Ordering::Relaxed) + dispatched_nonces, + search_elapsed.as_secs_f64(), + format_hashrate(hash_rate) ); + EngineStatus::Exhausted { - hash_count: total_hashes_processed, + hash_count: dispatched_nonces, } } } diff --git a/crates/engine-gpu/src/mining.wgsl b/crates/engine-gpu/src/mining.wgsl index 5fbc8660..571b55d0 100644 --- a/crates/engine-gpu/src/mining.wgsl +++ b/crates/engine-gpu/src/mining.wgsl @@ -194,7 +194,8 @@ const MDS_MATRIX_DIAG_12: array, 12> = array, 12>( @group(0) @binding(1) var header: array; // 32 bytes @group(0) @binding(2) var start_nonce: array; // 64 bytes @group(0) @binding(3) var difficulty_target: array; // 64 bytes (U512 target) -@group(0) @binding(4) var dispatch_config: array; // [total_threads (logical), nonces_per_thread, total_nonces (logical nonces in this dispatch), threads_per_workgroup] +@group(0) @binding(4) var dispatch_config: array; // [total_threads, nonces_per_thread, total_nonces, cancel_check_interval] +@group(0) @binding(5) var cancel_flag: array; // 0 = running, 1 = cancel requested // Goldilocks field element represented as [limb0, limb1] // where the value is limb0 + limb1*2^32 @@ -805,16 +806,20 @@ fn is_below_target(hash: array, difficulty_tgt: array) -> bool // Main mining kernel @compute @workgroup_size(256) fn mining_main(@builtin(global_invocation_id) global_id: vec3) { - // If solution already found, exit early + // If solution already found or cancelled, exit early if (atomicLoad(&results[0]) != 0u) { return; } + if (cancel_flag[0] != 0u) { + return; + } let thread_id = global_id.x; // Read dispatch configuration from buffer let total_threads = dispatch_config[0]; // Total logical threads in this dispatch let nonces_per_thread = dispatch_config[1]; // Nonces processed by each thread let total_nonces = dispatch_config[2]; // Total logical nonces this dispatch should cover + let cancel_check_interval = dispatch_config[3]; // How often to check cancel flag // Guard against threads beyond configured total_threads if (thread_id >= total_threads) { @@ -831,6 +836,14 @@ fn mining_main(@builtin(global_invocation_id) global_id: vec3) { break; } + // Periodic checks for early exit + // Check cancel flag every cancel_check_interval iterations + if (cancel_check_interval > 0u && (j % cancel_check_interval) == 0u) { + if (cancel_flag[0] != 0u) { + return; + } + } + // Check if solution already found (early exit for entire dispatch) if (atomicLoad(&results[0]) != 0u) { return; diff --git a/crates/metrics/Cargo.toml b/crates/metrics/Cargo.toml index 98d0032f..c88a200c 100644 --- a/crates/metrics/Cargo.toml +++ b/crates/metrics/Cargo.toml @@ -6,11 +6,9 @@ publish = false description = "Metrics and Prometheus exporter utilities for the Quantus External Miner" [features] -# Provide a Registry by default; HTTP exporter is opt-in. -default = ["registry"] -registry = [] +default = [] # Enable a Warp-based HTTP exporter (used when --metrics-port is provided). -http-exporter = ["dep:serde", "dep:serde_json", "dep:tokio", "dep:warp"] +http-exporter = ["dep:tokio", "dep:warp"] [dependencies] anyhow = { workspace = true } @@ -21,5 +19,3 @@ prometheus = { version = "0.13", default-features = false } # Optional HTTP exporter stack (enabled via `http-exporter` feature) warp = { workspace = true, optional = true } tokio = { workspace = true, features = ["full"], optional = true } -serde = { workspace = true, features = ["derive"], optional = true } -serde_json = { workspace = true, optional = true } diff --git a/crates/metrics/src/lib.rs b/crates/metrics/src/lib.rs index 46148170..d138f54e 100644 --- a/crates/metrics/src/lib.rs +++ b/crates/metrics/src/lib.rs @@ -1,124 +1,81 @@ #![deny(rust_2018_idioms)] #![forbid(unsafe_code)] -//! Minimal metrics scaffolding for the Quantus External Miner. +//! Minimal metrics for the Quantus External Miner. //! -//! - Provides a global Prometheus registry and a handful of default metrics. -//! - Exposes helper functions to update metrics from the service layer. -//! - Optionally runs a Warp-based HTTP endpoint (/metrics) when the -//! `http-exporter` feature is enabled. This is gated at runtime by -//! the presence of a metrics port in the CLI (`--metrics-port`). +//! Exposes a small set of Prometheus metrics for monitoring mining performance: +//! - `miner_hash_rate`: Total hash rate (CPU + GPU combined) +//! - `miner_cpu_hash_rate`: CPU-only hash rate +//! - `miner_gpu_hash_rate`: GPU-only hash rate +//! - `miner_hashes_total`: Total hashes computed (all time) +//! - `miner_active_jobs`: Currently running jobs (0 or 1) +//! - `miner_workers`: Total worker count +//! - `miner_cpu_workers`: Number of CPU workers +//! - `miner_gpu_devices`: Number of GPU devices +//! - `miner_effective_cpus`: Detected CPU cores //! -//! Default metrics: -//! - miner_jobs_total{status} : number of jobs by terminal state -//! - miner_hashes_total : total nonces tested -//! - miner_hash_rate : current estimated hash rate (nonces/sec) -//! - miner_http_requests_total{code,endpoint} : HTTP request counts for miner API -//! -//! Notes: -//! - The HTTP exporter is spawned as a background task and does not block. -//! - If the `http-exporter` feature is disabled, `start_http_exporter` becomes -//! a no-op that returns immediately. -//! - When jobs/threads end, prefer removing gauge label children (series) rather than -//! writing a zero; this avoids scrape-timing artifacts and produces cleaner rollups. -//! - Service-level observability: expose an `active_jobs` gauge and a `mine_requests_total` -//! counter (labeled by result) to disambiguate "idle because no jobs" vs "actively mining". +//! Optionally runs a Warp-based HTTP endpoint (`/metrics`) when the +//! `http-exporter` feature is enabled. use once_cell::sync::Lazy; -use prometheus::{ - opts, Encoder, Gauge, GaugeVec, IntCounter, IntCounterVec, IntGauge, IntGaugeVec, Registry, - TextEncoder, -}; +use prometheus::{IntCounter, IntGauge, Registry}; +use std::sync::Mutex; +use std::time::Instant; #[cfg(feature = "http-exporter")] -use {anyhow::Result, std::net::SocketAddr, warp::Filter}; +use { + anyhow::Result, + prometheus::{Encoder, TextEncoder}, + std::net::SocketAddr, + warp::Filter, +}; #[cfg(not(feature = "http-exporter"))] use anyhow::Result; -use std::collections::{HashMap, HashSet}; -use std::sync::Mutex; -use std::thread; -use std::time::{Duration, Instant}; - -// ------------------------------------------------------------------------------------- -// Global Registry and Default Metrics -// ------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// Global Registry +// --------------------------------------------------------------------------- static REGISTRY: Lazy = Lazy::new(Registry::new); -// ------------------------------------------------------------------------------------- -// High-level service metrics -// ------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// Hash Rate Metrics +// --------------------------------------------------------------------------- -static ACTIVE_JOBS: Lazy = Lazy::new(|| { - let g = IntGauge::new( - "miner_active_jobs", - "Number of currently running mining jobs", - ) - .expect("create miner_active_jobs"); +static HASH_RATE: Lazy = Lazy::new(|| { + let g = IntGauge::new("miner_hash_rate", "Total hash rate in hashes per second") + .expect("create miner_hash_rate"); REGISTRY .register(Box::new(g.clone())) - .expect("register miner_active_jobs"); + .expect("register miner_hash_rate"); g }); -static ENGINE_BACKEND_INFO: Lazy = Lazy::new(|| { - let g = GaugeVec::new( - opts!( - "miner_engine_backend", - "Engine backend info (label-only gauge set to 1). Labels: engine, backend" - ), - &["engine", "backend"], - ) - .expect("create miner_engine_backend"); +static CPU_HASH_RATE: Lazy = Lazy::new(|| { + let g = IntGauge::new("miner_cpu_hash_rate", "CPU hash rate in hashes per second") + .expect("create miner_cpu_hash_rate"); REGISTRY .register(Box::new(g.clone())) - .expect("register miner_engine_backend"); + .expect("register miner_cpu_hash_rate"); g }); -static EFFECTIVE_CPUS: Lazy = Lazy::new(|| { - let g = IntGauge::new( - "miner_effective_cpus", - "Detected logical CPU capacity available to this process (cpuset-aware)", - ) - .expect("create miner_effective_cpus"); +static GPU_HASH_RATE: Lazy = Lazy::new(|| { + let g = IntGauge::new("miner_gpu_hash_rate", "GPU hash rate in hashes per second") + .expect("create miner_gpu_hash_rate"); REGISTRY .register(Box::new(g.clone())) - .expect("register miner_effective_cpus"); + .expect("register miner_gpu_hash_rate"); g }); -static MINE_REQUESTS_TOTAL: Lazy = Lazy::new(|| { - let c = IntCounterVec::new( - opts!( - "miner_mine_requests_total", - "Count of /mine requests by result" - ), - &["result"], // accepted, duplicate, invalid, error - ) - .expect("create miner_mine_requests_total"); - REGISTRY - .register(Box::new(c.clone())) - .expect("register miner_mine_requests_total"); - c -}); - -static JOBS_TOTAL: Lazy = Lazy::new(|| { - let c = IntCounterVec::new( - opts!("miner_jobs_total", "Number of jobs by status"), - &["status"], - ) - .expect("create miner_jobs_total"); - REGISTRY - .register(Box::new(c.clone())) - .expect("register miner_jobs_total"); - c -}); +// --------------------------------------------------------------------------- +// Counter Metrics +// --------------------------------------------------------------------------- static HASHES_TOTAL: Lazy = Lazy::new(|| { - let c = IntCounter::new("miner_hashes_total", "Total nonces tested") + let c = IntCounter::new("miner_hashes_total", "Total hashes computed") .expect("create miner_hashes_total"); REGISTRY .register(Box::new(c.clone())) @@ -126,832 +83,222 @@ static HASHES_TOTAL: Lazy = Lazy::new(|| { c }); -static HASH_RATE: Lazy = Lazy::new(|| { - let g = - Gauge::new("miner_hash_rate", "Estimated hash rate (nonces per second)").expect("create"); - REGISTRY - .register(Box::new(g.clone())) - .expect("register miner_hash_rate"); - g -}); - -type JobKey = (String, String); // (engine, job_id) -type ThreadKey = (String, String, String); // (engine, job_id, thread_id) - -#[derive(Debug)] -struct ThreadState { - start: Instant, - hashes: u64, -} - -#[derive(Debug)] -struct JobState { - start: Instant, - hashes: u64, - active_threads: usize, -} - -#[derive(Debug)] -struct MiningSession { - start: Option, - hashes: u64, - active_threads: usize, - threads: HashMap, - jobs: HashMap, -} - -impl MiningSession { - fn new() -> Self { - Self { - start: None, - hashes: 0, - active_threads: 0, - threads: HashMap::new(), - jobs: HashMap::new(), - } - } -} - -static SESSION: Lazy> = Lazy::new(|| Mutex::new(MiningSession::new())); - -static HTTP_REQUESTS_TOTAL: Lazy = Lazy::new(|| { - let c = IntCounterVec::new( - opts!( - "miner_http_requests_total", - "HTTP requests count by endpoint and status code" - ), - &["endpoint", "code"], - ) - .expect("create miner_http_requests_total"); - REGISTRY - .register(Box::new(c.clone())) - .expect("register miner_http_requests_total"); - c -}); - -/// Access the global Prometheus registry used by this crate. -// Engine-aware, per-job, and per-thread labeled metrics -pub fn set_effective_cpus(n: i64) { - EFFECTIVE_CPUS.set(n); -} - -static JOB_HASHES_TOTAL: Lazy = Lazy::new(|| { - let c = IntCounterVec::new( - opts!( - "miner_job_hashes_total", - "Total nonces tested per job and engine" - ), - &["engine", "job_id"], - ) - .expect("create miner_job_hashes_total"); - REGISTRY - .register(Box::new(c.clone())) - .expect("register miner_job_hashes_total"); - c -}); - -static JOB_HASH_RATE: Lazy = Lazy::new(|| { - let g = GaugeVec::new( - opts!( - "miner_job_hash_rate", - "Estimated hash rate (nonces per second) per job and engine" - ), - &["engine", "job_id"], - ) - .expect("create miner_job_hash_rate"); - REGISTRY - .register(Box::new(g.clone())) - .expect("register miner_job_hash_rate"); - g -}); +// --------------------------------------------------------------------------- +// Gauge Metrics +// --------------------------------------------------------------------------- -static JOB_ESTIMATED_RATE: Lazy = Lazy::new(|| { - let g = GaugeVec::new( - opts!( - "miner_job_estimated_rate", - "Estimated work rate (nonces per second) per engine and backend" - ), - &["engine", "backend"], +static ACTIVE_JOBS: Lazy = Lazy::new(|| { + let g = IntGauge::new( + "miner_active_jobs", + "Number of currently running mining jobs", ) - .expect("create miner_job_estimated_rate"); + .expect("create miner_active_jobs"); REGISTRY .register(Box::new(g.clone())) - .expect("register miner_job_estimated_rate"); + .expect("register miner_active_jobs"); g }); -static CANDIDATES_FOUND_TOTAL: Lazy = Lazy::new(|| { - let c = IntCounterVec::new( - opts!( - "miner_candidates_found_total", - "Total candidates found per job and engine" - ), - &["engine", "job_id"], - ) - .expect("create miner_candidates_found_total"); - REGISTRY - .register(Box::new(c.clone())) - .expect("register miner_candidates_found_total"); - c -}); - -static CANDIDATES_FALSE_POSITIVE_TOTAL: Lazy = Lazy::new(|| { - let c = IntCounterVec::new( - opts!( - "miner_candidates_false_positive_total", - "Total false-positive candidates rejected by host re-verification per engine" - ), - &["engine"], - ) - .expect("create miner_candidates_false_positive_total"); - REGISTRY - .register(Box::new(c.clone())) - .expect("register miner_candidates_false_positive_total"); - c -}); - -static SAMPLE_MISMATCH_TOTAL: Lazy = Lazy::new(|| { - let c = IntCounterVec::new( - opts!( - "miner_sample_mismatch_total", - "Total decision parity mismatches between engine and host per engine" - ), - &["engine"], - ) - .expect("create miner_sample_mismatch_total"); - REGISTRY - .register(Box::new(c.clone())) - .expect("register miner_sample_mismatch_total"); - c -}); - -static MISSED_WINNER_TOTAL: Lazy = Lazy::new(|| { - let c = IntCounter::new( - "miner_gpu_g2_missed_winner_total", - "Host detected a winner in G2 batch sample but device did not flag early-exit", - ) - .expect("create miner_gpu_g2_missed_winner_total"); - REGISTRY - .register(Box::new(c.clone())) - .expect("register miner_gpu_g2_missed_winner_total"); - c -}); - -static FOUND_BY_ORIGIN_TOTAL: Lazy = Lazy::new(|| { - let c = IntCounterVec::new( - opts!( - "miner_found_by_origin_total", - "Count of candidates found by origin per engine" - ), - &["engine", "origin"], - ) - .expect("create miner_found_by_origin_total"); - REGISTRY - .register(Box::new(c.clone())) - .expect("register miner_found_by_origin_total"); - c -}); - -static THREAD_HASHES_TOTAL: Lazy = Lazy::new(|| { - let c = IntCounterVec::new( - opts!( - "miner_thread_hashes_total", - "Total nonces tested per thread, job, and engine" - ), - &["engine", "job_id", "thread_id"], - ) - .expect("create miner_thread_hashes_total"); - REGISTRY - .register(Box::new(c.clone())) - .expect("register miner_thread_hashes_total"); - c -}); - -static THREAD_HASH_RATE: Lazy = Lazy::new(|| { - let g = GaugeVec::new( - opts!( - "miner_thread_hash_rate", - "Estimated hash rate (nonces per second) per thread, job, and engine" - ), - &["engine", "job_id", "thread_id"], - ) - .expect("create miner_thread_hash_rate"); +static WORKERS: Lazy = Lazy::new(|| { + let g = IntGauge::new("miner_workers", "Total number of worker threads") + .expect("create miner_workers"); REGISTRY .register(Box::new(g.clone())) - .expect("register miner_thread_hash_rate"); + .expect("register miner_workers"); g }); -static JOBS_BY_ENGINE_TOTAL: Lazy = Lazy::new(|| { - let c = IntCounterVec::new( - opts!( - "miner_jobs_by_engine_total", - "Number of jobs by engine and terminal status" - ), - &["engine", "status"], - ) - .expect("create miner_jobs_by_engine_total"); - REGISTRY - .register(Box::new(c.clone())) - .expect("register miner_jobs_by_engine_total"); - c -}); - -static JOB_STATUS_GAUGE: Lazy = Lazy::new(|| { - let g = IntGaugeVec::new( - opts!( - "miner_job_status", - "Job status gauge (set to 1 for current status)" - ), - &["engine", "job_id", "status"], - ) - .expect("create miner_job_status"); +static CPU_WORKERS: Lazy = Lazy::new(|| { + let g = IntGauge::new("miner_cpu_workers", "Number of CPU worker threads") + .expect("create miner_cpu_workers"); REGISTRY .register(Box::new(g.clone())) - .expect("register miner_job_status"); + .expect("register miner_cpu_workers"); g }); -static JOB_FOUND_ORIGIN: Lazy = Lazy::new(|| { - let g = GaugeVec::new( - opts!( - "miner_job_found_origin", - "Per-job found candidate origin (0=unknown, 1=cpu, 2=gpu-g1, 3=gpu-g2)" - ), - &["engine", "job_id", "origin"], - ) - .expect("create miner_job_found_origin"); +static GPU_DEVICES: Lazy = Lazy::new(|| { + let g = IntGauge::new("miner_gpu_devices", "Number of GPU devices") + .expect("create miner_gpu_devices"); REGISTRY .register(Box::new(g.clone())) - .expect("register miner_job_found_origin"); + .expect("register miner_gpu_devices"); g }); -// ------------------------------------------------------------------------------------- -// GPU-specific metrics -// ------------------------------------------------------------------------------------- - -static GPU_DEVICE_COUNT: Lazy = Lazy::new(|| { +static EFFECTIVE_CPUS: Lazy = Lazy::new(|| { let g = IntGauge::new( - "miner_gpu_devices_total", - "Number of GPU devices available for mining", - ) - .expect("create miner_gpu_devices_total"); - REGISTRY - .register(Box::new(g.clone())) - .expect("register miner_gpu_devices_total"); - g -}); - -static GPU_DEVICE_INFO: Lazy = Lazy::new(|| { - let g = GaugeVec::new( - opts!( - "miner_gpu_device_info", - "GPU device information (label-only gauge set to 1). Labels: device_id, name, backend, vendor, device_type" - ), - &["device_id", "name", "backend", "vendor", "device_type"], - ) - .expect("create miner_gpu_device_info"); - REGISTRY - .register(Box::new(g.clone())) - .expect("register miner_gpu_device_info"); - g -}); - -static GPU_BATCH_SIZE: Lazy = Lazy::new(|| { - let g = GaugeVec::new( - opts!( - "miner_gpu_batch_size", - "Current GPU batch size (hashes per batch) per device" - ), - &["device_id"], - ) - .expect("create miner_gpu_batch_size"); - REGISTRY - .register(Box::new(g.clone())) - .expect("register miner_gpu_batch_size"); - g -}); - -static GPU_WORKGROUPS: Lazy = Lazy::new(|| { - let g = GaugeVec::new( - opts!( - "miner_gpu_workgroups", - "Number of GPU workgroups dispatched per device" - ), - &["device_id"], + "miner_effective_cpus", + "Detected logical CPU cores available to this process", ) - .expect("create miner_gpu_workgroups"); + .expect("create miner_effective_cpus"); REGISTRY .register(Box::new(g.clone())) - .expect("register miner_gpu_workgroups"); + .expect("register miner_effective_cpus"); g }); -pub fn default_registry() -> &'static Registry { - ®ISTRY -} - -// ------------------------------------------------------------------------------------- -// Update Helpers (to be called from the service layer) -// ------------------------------------------------------------------------------------- - -/// Increment the total hashes counter by `n` (number of nonces tested). -// Labeled helpers for engine/job/thread metrics -// -// Service-level helpers -pub fn set_active_jobs(n: i64) { - ACTIVE_JOBS.set(n); -} - -pub fn set_engine_backend(engine: &str, backend: &str) { - ENGINE_BACKEND_INFO - .with_label_values(&[engine, backend]) - .set(1.0); -} - -pub fn inc_mine_requests(result: &str) { - MINE_REQUESTS_TOTAL.with_label_values(&[result]).inc(); -} - -pub fn inc_job_hashes(engine: &str, job_id: &str, n: u64) { - touch_job(engine, job_id); - JOB_HASHES_TOTAL - .with_label_values(&[engine, job_id]) - .inc_by(n); -} - -pub fn set_job_hash_rate(engine: &str, job_id: &str, rate: f64) { - touch_job(engine, job_id); - JOB_HASH_RATE.with_label_values(&[engine, job_id]).set(rate); -} - -pub fn set_job_estimated_rate_backend(engine: &str, backend: &str, rate: f64) { - JOB_ESTIMATED_RATE - .with_label_values(&[engine, backend]) - .set(rate); -} - -pub fn set_job_estimated_rate(engine: &str, _job_id: &str, rate: f64) { - // Compatibility shim: map per-job series to backend="unknown" to avoid cardinality growth - JOB_ESTIMATED_RATE - .with_label_values(&[engine, "unknown"]) - .set(rate); -} - -pub fn inc_candidates_found(engine: &str, job_id: &str) { - touch_job(engine, job_id); - CANDIDATES_FOUND_TOTAL - .with_label_values(&[engine, job_id]) - .inc(); -} - -pub fn inc_candidates_false_positive(engine: &str) { - CANDIDATES_FALSE_POSITIVE_TOTAL - .with_label_values(&[engine]) - .inc(); -} - -pub fn inc_sample_mismatch(engine: &str) { - SAMPLE_MISMATCH_TOTAL.with_label_values(&[engine]).inc(); -} - -pub fn inc_found_by_origin(engine: &str, origin: &str) { - FOUND_BY_ORIGIN_TOTAL - .with_label_values(&[engine, origin]) - .inc(); -} - -pub fn inc_gpu_g2_missed_winner() { - MISSED_WINNER_TOTAL.inc(); -} - -pub fn set_job_found_origin(engine: &str, job_id: &str, origin: &str) { - touch_job(engine, job_id); - JOB_FOUND_ORIGIN - .with_label_values(&[engine, job_id, origin]) - .set(1.0); -} - -pub fn job_estimated_rate_backend(engine: &str, backend: &str, rate: f64) { - JOB_ESTIMATED_RATE - .with_label_values(&[engine, backend]) - .set(rate); -} - -pub fn job_estimated_rate(engine: &str, _job_id: &str, rate: f64) { - // Compatibility shim: prefer new API using backend; fallback to backend="unknown" - JOB_ESTIMATED_RATE - .with_label_values(&[engine, "unknown"]) - .set(rate); -} - -pub fn inc_thread_hashes(engine: &str, job_id: &str, thread_id: &str, n: u64) { - THREAD_HASHES_TOTAL - .with_label_values(&[engine, job_id, thread_id]) - .inc_by(n); -} - -pub fn set_thread_hash_rate(engine: &str, job_id: &str, thread_id: &str, rate: f64) { - touch_thread(engine, job_id, thread_id); - THREAD_HASH_RATE - .with_label_values(&[engine, job_id, thread_id]) - .set(rate); -} - -pub fn inc_jobs_by_engine(engine: &str, status: &str) { - JOBS_BY_ENGINE_TOTAL - .with_label_values(&[engine, status]) - .inc(); -} - -pub fn set_job_status_gauge(engine: &str, job_id: &str, status: &str, value: i64) { - touch_job(engine, job_id); - JOB_STATUS_GAUGE - .with_label_values(&[engine, job_id, status]) - .set(value); -} - -pub fn inc_hashes(n: u64) { - HASHES_TOTAL.inc_by(n); -} - -fn ensure_session(session: &mut MiningSession, now: Instant) { - if session.start.is_none() { - session.start = Some(now); - session.hashes = 0; - } -} - -fn update_global_rate(session: &MiningSession, now: Instant) { - if let Some(start) = session.start { - let elapsed = now.saturating_duration_since(start).as_secs_f64(); - if elapsed > 0.0 && session.hashes > 0 { - HASH_RATE.set(session.hashes as f64 / elapsed); - } - } -} +// --------------------------------------------------------------------------- +// Hash Rate Tracking +// --------------------------------------------------------------------------- -fn finish_thread_locked(session: &mut MiningSession, job_key: &JobKey, thread_key: &ThreadKey) { - if session.threads.remove(thread_key).is_some() { - session.active_threads = session.active_threads.saturating_sub(1); - } - if let Some(job) = session.jobs.get_mut(job_key) { - if job.active_threads > 0 { - job.active_threads -= 1; +/// Tracks cumulative hashes to compute rolling hash rates. +/// +/// The hash rate is computed as total_hashes / elapsed_time since mining started. +/// Call `reset()` when a new mining session begins to get accurate rates. +struct HashRateTracker { + /// Cumulative CPU hashes since last reset + cpu_total: u64, + /// Cumulative GPU hashes since last reset + gpu_total: u64, + /// When tracking started - set via reset() when mining begins + start_time: Option, +} + +impl HashRateTracker { + fn new() -> Self { + Self { + cpu_total: 0, + gpu_total: 0, + start_time: None, } } - if session.active_threads == 0 { - session.start = None; - session.hashes = 0; - HASH_RATE.set(0.0); - } -} - -pub fn start_thread(engine: &str, job_id: &str, thread_id: usize) { - let now = Instant::now(); - let engine_key = engine.to_string(); - let job_key = job_id.to_string(); - let thread_key = (engine_key.clone(), job_key.clone(), thread_id.to_string()); - let job_key = (engine_key, job_key); - let mut session = SESSION.lock().unwrap(); - ensure_session(&mut session, now); - - if session.threads.contains_key(&thread_key) { - return; + /// Reset the tracker - call this when a new mining session starts. + fn reset(&mut self) { + self.cpu_total = 0; + self.gpu_total = 0; + self.start_time = Some(Instant::now()); } - session.active_threads = session.active_threads.saturating_add(1); - session.threads.insert( - thread_key, - ThreadState { - start: now, - hashes: 0, - }, - ); - - let job = session.jobs.entry(job_key).or_insert(JobState { - start: now, - hashes: 0, - active_threads: 0, - }); - if job.active_threads == 0 { - job.start = now; - job.hashes = 0; + fn record_cpu(&mut self, hashes: u64) { + self.cpu_total += hashes; + self.update_rates(); } - job.active_threads = job.active_threads.saturating_add(1); -} -pub fn record_thread_progress( - engine: &str, - job_id: &str, - thread_id: usize, - hashes: u64, - completed: bool, - update_job_metrics: bool, -) { - let now = Instant::now(); - let engine_key = engine.to_string(); - let job_key = job_id.to_string(); - let thread_id_str = thread_id.to_string(); - let thread_key = (engine_key.clone(), job_key.clone(), thread_id_str.clone()); - let job_key = (engine_key, job_key); - - let mut session = SESSION.lock().unwrap(); - ensure_session(&mut session, now); - - let thread_was_present = session.threads.contains_key(&thread_key); - if !thread_was_present { - session.active_threads = session.active_threads.saturating_add(1); - session.threads.insert( - thread_key.clone(), - ThreadState { - start: now, - hashes: 0, - }, - ); + fn record_gpu(&mut self, hashes: u64) { + self.gpu_total += hashes; + self.update_rates(); } - let (thread_start, thread_hashes_total) = { - let thread_state = session - .threads - .get_mut(&thread_key) - .expect("thread must exist"); - thread_state.hashes = thread_state.hashes.saturating_add(hashes); - (thread_state.start, thread_state.hashes) - }; - - let (job_start, job_hashes_total) = { - let job_state = session.jobs.entry(job_key.clone()).or_insert(JobState { - start: thread_start, - hashes: 0, - active_threads: 0, - }); - if job_state.active_threads == 0 { - job_state.start = thread_start; - job_state.hashes = 0; - } - if !thread_was_present { - job_state.active_threads = job_state.active_threads.saturating_add(1); + fn update_rates(&self) { + if let Some(start) = self.start_time { + let elapsed = start.elapsed().as_secs_f64(); + // Require at least 0.1 seconds of data for meaningful rate + if elapsed >= 0.1 { + let cpu_rate = (self.cpu_total as f64 / elapsed) as i64; + let gpu_rate = (self.gpu_total as f64 / elapsed) as i64; + CPU_HASH_RATE.set(cpu_rate); + GPU_HASH_RATE.set(gpu_rate); + HASH_RATE.set(cpu_rate + gpu_rate); + } } - job_state.hashes = job_state.hashes.saturating_add(hashes); - (job_state.start, job_state.hashes) - }; - - session.hashes = session.hashes.saturating_add(hashes); - - inc_hashes(hashes); - if update_job_metrics { - inc_job_hashes(engine, job_id, hashes); - inc_thread_hashes(engine, job_id, &thread_id_str, hashes); - } - - let thread_elapsed = now.saturating_duration_since(thread_start).as_secs_f64(); - if update_job_metrics && thread_elapsed > 0.0 && thread_hashes_total > 0 { - set_thread_hash_rate( - engine, - job_id, - &thread_id_str, - thread_hashes_total as f64 / thread_elapsed, - ); } +} - if update_job_metrics { - let job_elapsed = now.saturating_duration_since(job_start).as_secs_f64(); - if job_elapsed > 0.0 && job_hashes_total > 0 { - set_job_hash_rate(engine, job_id, job_hashes_total as f64 / job_elapsed); - } - } +static HASH_TRACKER: Lazy> = + Lazy::new(|| Mutex::new(HashRateTracker::new())); - update_global_rate(&session, now); +// --------------------------------------------------------------------------- +// Public API - Hash Recording +// --------------------------------------------------------------------------- - if completed { - finish_thread_locked(&mut session, &job_key, &thread_key); - update_global_rate(&session, now); +/// Reset the hash rate tracker - call this when mining starts. +/// This resets the cumulative hash counts and starts a fresh timing window. +pub fn reset_hash_tracker() { + if let Ok(mut tracker) = HASH_TRACKER.lock() { + tracker.reset(); } } -/// Get the current global estimated hash rate. -pub fn get_hash_rate() -> f64 { - HASH_RATE.get() -} - -/// Increment the jobs counter for a terminal status: completed | failed | cancelled. -pub fn inc_job_status(status: &str) { - JOBS_TOTAL.with_label_values(&[status]).inc(); +/// Record CPU hashes and update hash rate metrics. +pub fn record_cpu_hashes(n: u64) { + HASHES_TOTAL.inc_by(n); + if let Ok(mut tracker) = HASH_TRACKER.lock() { + tracker.record_cpu(n); + } } -/// Increment HTTP request counters for an endpoint with a status code. -pub fn inc_http_request(endpoint: &str, code: u16) { - HTTP_REQUESTS_TOTAL - .with_label_values(&[endpoint, &code.to_string()]) - .inc(); +/// Record GPU hashes and update hash rate metrics. +pub fn record_gpu_hashes(n: u64) { + HASHES_TOTAL.inc_by(n); + if let Ok(mut tracker) = HASH_TRACKER.lock() { + tracker.record_gpu(n); + } } -// ------------------------------------------------------------------------------------- -// Removal helpers for end-of-life series -// ------------------------------------------------------------------------------------- - -const METRICS_TTL_SECS: u64 = 300; -const JANITOR_INTERVAL_SECS: u64 = 60; - -static JOB_KEYS: Lazy>> = Lazy::new(|| Mutex::new(HashSet::new())); -static JOB_LAST: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); -static THREAD_KEYS: Lazy>> = Lazy::new(|| Mutex::new(HashSet::new())); -static THREAD_LAST: Lazy>> = - Lazy::new(|| Mutex::new(HashMap::new())); +// --------------------------------------------------------------------------- +// Public API - Hash Rates (for direct setting, kept for compatibility) +// --------------------------------------------------------------------------- -static JANITOR_INIT: Lazy<()> = Lazy::new(|| { - thread::spawn(|| loop { - thread::sleep(Duration::from_secs(JANITOR_INTERVAL_SECS)); - prune_stale(); - }); -}); - -fn prune_stale() { - let now = Instant::now(); - // Jobs - let mut to_remove_jobs: Vec = Vec::new(); - { - let last = JOB_LAST.lock().unwrap(); - for (k, ts) in last.iter() { - if now.duration_since(*ts).as_secs() > METRICS_TTL_SECS { - to_remove_jobs.push(k.clone()); - } - } - } - for (engine, job_id) in to_remove_jobs { - let _ = JOB_HASHES_TOTAL.remove_label_values(&[&engine, &job_id]); - let _ = JOB_HASH_RATE.remove_label_values(&[&engine, &job_id]); - let _ = CANDIDATES_FOUND_TOTAL.remove_label_values(&[&engine, &job_id]); - let _ = JOB_STATUS_GAUGE.remove_label_values(&[&engine, &job_id, "completed"]); - let _ = JOB_STATUS_GAUGE.remove_label_values(&[&engine, &job_id, "failed"]); - let _ = JOB_STATUS_GAUGE.remove_label_values(&[&engine, &job_id, "cancelled"]); - // Remove per-job origin gauge variants (known origins) - let _ = JOB_FOUND_ORIGIN.remove_label_values(&[&engine, &job_id, "cpu"]); - let _ = JOB_FOUND_ORIGIN.remove_label_values(&[&engine, &job_id, "gpu-g1"]); - let _ = JOB_FOUND_ORIGIN.remove_label_values(&[&engine, &job_id, "gpu-g2"]); - JOB_KEYS - .lock() - .unwrap() - .remove(&(engine.clone(), job_id.clone())); - JOB_LAST.lock().unwrap().remove(&(engine, job_id)); - } - // Threads - let mut to_remove_threads: Vec = Vec::new(); - { - let last = THREAD_LAST.lock().unwrap(); - for (k, ts) in last.iter() { - if now.duration_since(*ts).as_secs() > METRICS_TTL_SECS { - to_remove_threads.push(k.clone()); - } - } - } - for (engine, job_id, thread_id) in to_remove_threads { - let _ = THREAD_HASHES_TOTAL.remove_label_values(&[&engine, &job_id, &thread_id]); - let _ = THREAD_HASH_RATE.remove_label_values(&[&engine, &job_id, &thread_id]); - THREAD_KEYS - .lock() - .unwrap() - .remove(&(engine.clone(), job_id.clone(), thread_id.clone())); - THREAD_LAST - .lock() - .unwrap() - .remove(&(engine, job_id, thread_id)); - } +/// Set the total hash rate (CPU + GPU combined). +pub fn set_hash_rate(rate: i64) { + HASH_RATE.set(rate); } -fn touch_job(engine: &str, job_id: &str) { - *JANITOR_INIT; // ensure janitor starts - let key = (engine.to_string(), job_id.to_string()); - JOB_KEYS.lock().unwrap().insert(key.clone()); - JOB_LAST.lock().unwrap().insert(key, Instant::now()); +/// Set the CPU-only hash rate. +pub fn set_cpu_hash_rate(rate: i64) { + CPU_HASH_RATE.set(rate); } -fn touch_thread(engine: &str, job_id: &str, thread_id: &str) { - *JANITOR_INIT; // ensure janitor starts - let key = ( - engine.to_string(), - job_id.to_string(), - thread_id.to_string(), - ); - THREAD_KEYS.lock().unwrap().insert(key.clone()); - THREAD_LAST.lock().unwrap().insert(key, Instant::now()); +/// Set the GPU-only hash rate. +pub fn set_gpu_hash_rate(rate: i64) { + GPU_HASH_RATE.set(rate); } -/// Explicitly remove all job-scoped metrics for a job (call on job completion) -pub fn remove_job_metrics(engine: &str, job_id: &str) { - let _ = JOB_HASHES_TOTAL.remove_label_values(&[engine, job_id]); - let _ = JOB_HASH_RATE.remove_label_values(&[engine, job_id]); - let _ = CANDIDATES_FOUND_TOTAL.remove_label_values(&[engine, job_id]); - let _ = JOB_STATUS_GAUGE.remove_label_values(&[engine, job_id, "completed"]); - let _ = JOB_STATUS_GAUGE.remove_label_values(&[engine, job_id, "failed"]); - let _ = JOB_STATUS_GAUGE.remove_label_values(&[engine, job_id, "cancelled"]); - // Remove per-job origin gauge variants (known origins) - let _ = JOB_FOUND_ORIGIN.remove_label_values(&[engine, job_id, "cpu"]); - let _ = JOB_FOUND_ORIGIN.remove_label_values(&[engine, job_id, "gpu-g1"]); - let _ = JOB_FOUND_ORIGIN.remove_label_values(&[engine, job_id, "gpu-g2"]); - JOB_KEYS - .lock() - .unwrap() - .remove(&(engine.to_string(), job_id.to_string())); - JOB_LAST - .lock() - .unwrap() - .remove(&(engine.to_string(), job_id.to_string())); -} +// --------------------------------------------------------------------------- +// Public API - Counters (kept for compatibility) +// --------------------------------------------------------------------------- -/// Explicitly remove all thread-scoped metrics for a job -pub fn remove_thread_metrics_for_job(engine: &str, job_id: &str) { - // Collect matching thread keys - let keys: Vec = { - let set = THREAD_KEYS.lock().unwrap(); - set.iter() - .filter(|(e, j, _)| e == engine && j == job_id) - .cloned() - .collect() - }; - for (e, j, t) in keys { - let _ = THREAD_HASHES_TOTAL.remove_label_values(&[&e, &j, &t]); - let _ = THREAD_HASH_RATE.remove_label_values(&[&e, &j, &t]); - THREAD_KEYS - .lock() - .unwrap() - .remove(&(e.clone(), j.clone(), t.clone())); - THREAD_LAST - .lock() - .unwrap() - .remove(&(e.clone(), j.clone(), t.clone())); - } +/// Increment the total hashes counter (without updating rates). +/// Prefer `record_cpu_hashes` or `record_gpu_hashes` instead. +pub fn inc_hashes(n: u64) { + HASHES_TOTAL.inc_by(n); } -/// Remove the per-job hash rate series for a finished job. -pub fn remove_job_hash_rate(engine: &str, job_id: &str) { - let _ = JOB_HASH_RATE.remove_label_values(&[engine, job_id]); -} +// --------------------------------------------------------------------------- +// Public API - Gauges +// --------------------------------------------------------------------------- -/// Remove the per-thread hash rate series for a finished thread. -pub fn remove_thread_hash_rate(engine: &str, job_id: &str, thread_id: &str) { - let _ = THREAD_HASH_RATE.remove_label_values(&[engine, job_id, thread_id]); +/// Set the number of active jobs (0 or 1). +pub fn set_active_jobs(n: i64) { + ACTIVE_JOBS.set(n); } -// ------------------------------------------------------------------------------------- -// GPU-specific metric helpers -// ------------------------------------------------------------------------------------- - -/// Set the total number of GPU devices available for mining -pub fn set_gpu_device_count(count: i64) { - GPU_DEVICE_COUNT.set(count); +/// Set the total number of workers. +pub fn set_workers(n: i64) { + WORKERS.set(n); } -/// Set GPU device information (call once per device during initialization) -pub fn set_gpu_device_info( - device_id: &str, - name: &str, - backend: &str, - vendor: &str, - device_type: &str, -) { - GPU_DEVICE_INFO - .with_label_values(&[device_id, name, backend, vendor, device_type]) - .set(1.0); +/// Set the number of CPU workers. +pub fn set_cpu_workers(n: i64) { + CPU_WORKERS.set(n); } -/// Set GPU batch size for a specific device -pub fn set_gpu_batch_size(device_id: &str, batch_size: f64) { - GPU_BATCH_SIZE - .with_label_values(&[device_id]) - .set(batch_size); +/// Set the number of GPU devices. +pub fn set_gpu_devices(n: i64) { + GPU_DEVICES.set(n); } -/// Set GPU workgroup count for a specific device -pub fn set_gpu_workgroups(device_id: &str, workgroups: f64) { - GPU_WORKGROUPS - .with_label_values(&[device_id]) - .set(workgroups); +/// Set the detected CPU core count. +pub fn set_effective_cpus(n: i64) { + EFFECTIVE_CPUS.set(n); } -// ------------------------------------------------------------------------------------- -// HTTP Exporter (feature: http-exporter) -// ------------------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// HTTP Exporter +// --------------------------------------------------------------------------- -/// Start the Prometheus HTTP exporter on 0.0.0.0:`port`. +/// Start the Prometheus HTTP exporter on `0.0.0.0:port`. /// -/// Behavior: -/// - Spawns the exporter as a background task and returns immediately. -/// - Serves plaintext metrics at GET /metrics. -/// - If called multiple times, multiple servers may be created (call once). +/// Spawns the exporter as a background task and returns immediately. +/// Serves plaintext metrics at `GET /metrics`. #[cfg(feature = "http-exporter")] pub async fn start_http_exporter(port: u16) -> Result<()> { - // Encoder is created inside the handler to avoid capturing non-Clone state - // Use REGISTRY.gather() directly in the handler - - // GET /metrics -> plaintext Prometheus format let metrics_route = warp::path("metrics").and(warp::get()).map(|| { let encoder = TextEncoder::new(); let metric_families = REGISTRY.gather(); - let mut buffer = Vec::with_capacity(16 * 1024); + let mut buffer = Vec::with_capacity(4096); encoder .encode(&metric_families, &mut buffer) .unwrap_or_default(); @@ -969,7 +316,7 @@ pub async fn start_http_exporter(port: u16) -> Result<()> { Ok(()) } -// If the exporter feature is not enabled, expose a no-op to keep call sites simple. +/// No-op when HTTP exporter feature is disabled. #[cfg(not(feature = "http-exporter"))] pub async fn start_http_exporter(_port: u16) -> Result<()> { log::warn!( diff --git a/crates/miner-cli/Cargo.toml b/crates/miner-cli/Cargo.toml index 9334381a..f57af9c8 100644 --- a/crates/miner-cli/Cargo.toml +++ b/crates/miner-cli/Cargo.toml @@ -7,6 +7,7 @@ description = "CLI binary to run the Quantus External Miner service" [dependencies] miner-service = { path = "../miner-service" } +metrics = { path = "../metrics", features = ["http-exporter"] } clap = { workspace = true, features = ["derive", "env"] } tokio = { workspace = true, features = ["full"] } env_logger = { workspace = true } diff --git a/crates/miner-cli/src/main.rs b/crates/miner-cli/src/main.rs index 2f3905d4..a93612e1 100644 --- a/crates/miner-cli/src/main.rs +++ b/crates/miner-cli/src/main.rs @@ -9,104 +9,38 @@ use std::thread; use std::time::{Duration, Instant}; #[derive(Subcommand, Debug)] -#[allow(clippy::large_enum_variant)] enum Command { - /// Run the mining service (default behavior) + /// Run the mining service Serve { - /// Port number to listen on for the miner HTTP API - #[arg(short, long, env = "MINER_PORT", default_value_t = 9833)] - port: u16, + /// Address of the node to connect to (e.g., "127.0.0.1:9833") + #[arg(long, env = "MINER_NODE_ADDR")] + node_addr: std::net::SocketAddr, - /// Number of CPU worker threads to use for mining (None = auto-detect) + /// Number of CPU worker threads to use for mining (default: auto-detect) #[arg(long = "cpu-workers", env = "MINER_CPU_WORKERS")] cpu_workers: Option, - /// Number of GPU devices to use for mining (None = auto-detect) + /// Number of GPU devices to use for mining (default: auto-detect) #[arg(long = "gpu-devices", env = "MINER_GPU_DEVICES")] gpu_devices: Option, - /// Optional Prometheus metrics exporter port; if omitted, metrics are disabled - #[arg(long, env = "MINER_METRICS_PORT")] - metrics_port: Option, + /// GPU cancel check interval in nonces (default: 10000) + #[arg(long = "gpu-cancel-interval", env = "MINER_GPU_CANCEL_INTERVAL")] + gpu_cancel_interval: Option, - /// Enable verbose logging (shows debug info, progress details, etc.) - #[arg(short, long, env = "MINER_VERBOSE")] - verbose: bool, - - /// How often to report mining progress (in milliseconds). - /// Smaller values give more frequent updates but slightly reduce performance. - #[arg(long = "progress-interval-ms", env = "MINER_PROGRESS_INTERVAL_MS")] - progress_interval_ms: Option, - - /// Size of work chunks to process before reporting progress (number of hashes). - /// If omitted, uses engine-specific defaults (200K for CPU, 100M for GPU). - #[arg(long = "chunk-size", env = "MINER_CHUNK_SIZE")] - chunk_size: Option, - - /// For cpu-chain-manipulator: start throttle index at this many solved blocks - /// to "pick up where we left off" after restarts. - #[arg(long = "manip-solved-blocks", env = "MINER_MANIP_SOLVED_BLOCKS")] - manip_solved_blocks: Option, - - /// For cpu-chain-manipulator: base sleep per batch in nanoseconds (default 500_000 ns) - #[arg(long = "manip-base-delay-ns", env = "MINER_MANIP_BASE_DELAY_NS")] - manip_base_delay_ns: Option, - - /// For cpu-chain-manipulator: number of nonce attempts between sleeps (default 10_000) - #[arg(long = "manip-step-batch", env = "MINER_MANIP_STEP_BATCH")] - manip_step_batch: Option, - - /// For cpu-chain-manipulator: optional cap on solved-blocks throttle index - #[arg(long = "manip-throttle-cap", env = "MINER_MANIP_THROTTLE_CAP")] - manip_throttle_cap: Option, - - /// Target duration for GPU mining batches in milliseconds (default: 3000) - #[arg(long = "gpu-batch-duration-ms", env = "MINER_GPU_BATCH_DURATION_MS")] - gpu_batch_duration_ms: Option, - - /// Telemetry endpoints (repeat --telemetry-endpoint or comma-separated) - #[arg(long = "telemetry-endpoint", env = "MINER_TELEMETRY_ENDPOINTS", value_delimiter = ',', num_args = 0.., value_name = "URL")] - telemetry_endpoints: Option>, - - /// Enable or disable telemetry explicitly - #[arg(long = "telemetry-enabled", env = "MINER_TELEMETRY_ENABLED")] - telemetry_enabled: Option, - - /// Telemetry verbosity level (0..=4 typical) - #[arg(long = "telemetry-verbosity", env = "MINER_TELEMETRY_VERBOSITY")] - telemetry_verbosity: Option, - - /// Interval seconds for system.interval messages + /// Port for Prometheus metrics HTTP endpoint (default: 9900) #[arg( - long = "telemetry-interval-secs", - env = "MINER_TELEMETRY_INTERVAL_SECS" + long = "metrics-port", + env = "MINER_METRICS_PORT", + default_value_t = 9900 )] - telemetry_interval_secs: Option, - - /// Default association: chain name - #[arg(long = "telemetry-chain", env = "MINER_TELEMETRY_CHAIN")] - telemetry_chain: Option, - - /// Default association: genesis hash (hex) - #[arg(long = "telemetry-genesis", env = "MINER_TELEMETRY_GENESIS")] - telemetry_genesis: Option, - - /// Default association: node telemetry id - #[arg(long = "telemetry-node-id", env = "MINER_TELEMETRY_NODE_ID")] - telemetry_node_id: Option, - - /// Default association: node libp2p peer id - #[arg(long = "telemetry-node-peer-id", env = "MINER_TELEMETRY_NODE_PEER_ID")] - telemetry_node_peer_id: Option, + metrics_port: u16, - /// Default association: node name - #[arg(long = "telemetry-node-name", env = "MINER_TELEMETRY_NODE_NAME")] - telemetry_node_name: Option, - - /// Default association: node version - #[arg(long = "telemetry-node-version", env = "MINER_TELEMETRY_NODE_VERSION")] - telemetry_node_version: Option, + /// Enable verbose logging + #[arg(short, long, env = "MINER_VERBOSE")] + verbose: bool, }, + /// Run a quick benchmark of the mining engines Benchmark { /// Number of CPU workers to use for benchmark @@ -121,7 +55,7 @@ enum Command { #[arg(short, long, default_value_t = 10)] duration: u64, - /// Enable verbose logging during benchmark + /// Enable verbose logging #[arg(short, long, env = "MINER_VERBOSE")] verbose: bool, }, @@ -135,123 +69,65 @@ struct Args { command: Option, } -#[allow(clippy::enum_variant_names)] #[tokio::main] async fn main() { let args = Args::parse(); - match args.command.unwrap_or(Command::Serve { - port: 9833, - cpu_workers: None, - gpu_devices: None, - metrics_port: None, - verbose: false, - progress_interval_ms: None, - chunk_size: None, - manip_solved_blocks: None, - manip_base_delay_ns: None, - manip_step_batch: None, - manip_throttle_cap: None, - gpu_batch_duration_ms: None, - telemetry_endpoints: None, - telemetry_enabled: None, - telemetry_verbosity: None, - telemetry_interval_secs: None, - telemetry_chain: None, - telemetry_genesis: None, - telemetry_node_id: None, - telemetry_node_peer_id: None, - telemetry_node_name: None, - telemetry_node_version: None, - }) { + let Some(command) = args.command else { + eprintln!("Error: No command provided. Use 'serve --node-addr
' to start mining."); + eprintln!("Example: quantus-miner serve --node-addr 127.0.0.1:9833"); + std::process::exit(1); + }; + + match command { Command::Serve { - port, + node_addr, cpu_workers, gpu_devices, + gpu_cancel_interval, metrics_port, verbose, - progress_interval_ms, - chunk_size, - manip_solved_blocks, - manip_base_delay_ns, - manip_step_batch, - manip_throttle_cap, - gpu_batch_duration_ms, - telemetry_endpoints, - telemetry_enabled, - telemetry_verbosity, - telemetry_interval_secs, - telemetry_chain, - telemetry_genesis, - telemetry_node_id, - telemetry_node_peer_id, - telemetry_node_name, - telemetry_node_version, } => { - run_serve_command( - port, + init_logger(verbose); + + log::info!("Starting external miner service..."); + + // Start metrics HTTP server + if let Err(e) = metrics::start_http_exporter(metrics_port).await { + log::error!("Failed to start metrics exporter: {e:?}"); + std::process::exit(1); + } + log::info!( + "Metrics available at http://0.0.0.0:{}/metrics", + metrics_port + ); + + let config = ServiceConfig { + node_addr, cpu_workers, gpu_devices, - metrics_port, - verbose, - progress_interval_ms, - chunk_size, - manip_solved_blocks, - manip_base_delay_ns, - manip_step_batch, - manip_throttle_cap, - gpu_batch_duration_ms, - telemetry_endpoints, - telemetry_enabled, - telemetry_verbosity, - telemetry_interval_secs, - telemetry_chain, - telemetry_genesis, - telemetry_node_id, - telemetry_node_peer_id, - telemetry_node_name, - telemetry_node_version, - ) - .await; + gpu_cancel_interval, + }; + + if let Err(e) = run(config).await { + log::error!("Miner service terminated with error: {e:?}"); + std::process::exit(1); + } } + Command::Benchmark { cpu_workers, gpu_devices, duration, verbose, } => { - run_benchmark_command(cpu_workers, gpu_devices, duration, verbose).await; + init_logger(verbose); + run_benchmark(cpu_workers, gpu_devices, duration).await; } } } -#[allow(clippy::too_many_arguments)] -async fn run_serve_command( - port: u16, - cpu_workers: Option, - gpu_devices: Option, - metrics_port: Option, - verbose: bool, - progress_interval_ms: Option, - chunk_size: Option, - manip_solved_blocks: Option, - manip_base_delay_ns: Option, - manip_step_batch: Option, - manip_throttle_cap: Option, - gpu_batch_duration_ms: Option, - telemetry_endpoints: Option>, - telemetry_enabled: Option, - telemetry_verbosity: Option, - telemetry_interval_secs: Option, - telemetry_chain: Option, - telemetry_genesis: Option, - telemetry_node_id: Option, - telemetry_node_peer_id: Option, - telemetry_node_name: Option, - telemetry_node_version: Option, -) { - // Initialize logger early to capture startup messages. - // If RUST_LOG is not set, default to appropriate level based on verbose flag +fn init_logger(verbose: bool) { if std::env::var("RUST_LOG").is_err() { let log_level = if verbose { "debug,miner=debug,gpu_engine=debug,engine_cpu=debug" @@ -261,91 +137,17 @@ async fn run_serve_command( std::env::set_var("RUST_LOG", log_level); } env_logger::init(); - - // Telemetry CLI passthrough to env for miner-service bootstrap - if let Some(eps) = telemetry_endpoints.as_ref() { - if !eps.is_empty() { - std::env::set_var("MINER_TELEMETRY_ENDPOINTS", eps.join(",")); - } - } - if let Some(v) = telemetry_enabled { - std::env::set_var("MINER_TELEMETRY_ENABLED", if v { "1" } else { "0" }); - } - if let Some(v) = telemetry_verbosity { - std::env::set_var("MINER_TELEMETRY_VERBOSITY", v.to_string()); - } - if let Some(v) = telemetry_interval_secs { - std::env::set_var("MINER_TELEMETRY_INTERVAL_SECS", v.to_string()); - } - if let Some(v) = telemetry_chain.as_ref() { - std::env::set_var("MINER_TELEMETRY_CHAIN", v); - } - if let Some(v) = telemetry_genesis.as_ref() { - std::env::set_var("MINER_TELEMETRY_GENESIS", v); - } - if let Some(v) = telemetry_node_id.as_ref() { - std::env::set_var("MINER_TELEMETRY_NODE_ID", v); - } - if let Some(v) = telemetry_node_peer_id.as_ref() { - std::env::set_var("MINER_TELEMETRY_NODE_PEER_ID", v); - } - if let Some(v) = telemetry_node_name.as_ref() { - std::env::set_var("MINER_TELEMETRY_NODE_NAME", v); - } - if let Some(v) = telemetry_node_version.as_ref() { - std::env::set_var("MINER_TELEMETRY_NODE_VERSION", v); - } - - // Log effective configuration (concise; see ServiceConfig Display) - log::info!("Starting external miner service..."); - - let config = ServiceConfig { - port, - cpu_workers, - gpu_devices, - metrics_port, - progress_interval_ms, - chunk_size, - manip_solved_blocks, - manip_base_delay_ns, - manip_step_batch, - manip_throttle_cap, - gpu_batch_duration_ms, - }; - log::info!("Effective config: {config}"); - - if let Err(e) = run(config).await { - log::error!("Miner service terminated with error: {e:?}"); - std::process::exit(1); - } } -async fn run_benchmark_command( - cpu_workers: Option, - gpu_devices: Option, - duration: u64, - verbose: bool, -) { - // Initialize logger early to capture startup messages. - if std::env::var("RUST_LOG").is_err() { - let log_level = if verbose { - "debug,miner=debug,gpu_engine=debug,engine_cpu=debug" - } else { - "info,miner=info,gpu_engine=warn,engine_cpu=info" - }; - std::env::set_var("RUST_LOG", log_level); - } - env_logger::init(); - +async fn run_benchmark(cpu_workers: Option, gpu_devices: Option, duration: u64) { let effective_cpu_workers = cpu_workers.unwrap_or_else(num_cpus::get); - // Initialize GPU engine and determine effective GPU devices + // Initialize GPU engine let (gpu_engine, effective_gpu_devices) = match miner_service::resolve_gpu_configuration(gpu_devices, None) { Ok((engine, count)) => (engine, count), Err(e) => { eprintln!("❌ ERROR: {}", e); - eprintln!(" Please check your --gpu-devices setting or GPU hardware."); std::process::exit(1); } }; @@ -354,44 +156,35 @@ async fn run_benchmark_command( println!("🚀 Quantus Miner Benchmark"); println!("=========================="); - println!("Configuration:"); println!( - " CPU Workers: {} (Available Cores: {})", + "CPU Workers: {} (Available: {})", effective_cpu_workers, num_cpus::get() ); - println!(" GPU Devices: {}", effective_gpu_devices); + println!("GPU Devices: {}", effective_gpu_devices); println!("Duration: {} seconds", duration); - println!("Total Workers: {}", total_workers); - - // Create engines based on configuration - let cpu_engine = if effective_cpu_workers > 0 { - Some(Arc::new(engine_cpu::FastCpuEngine::new()) as Arc) - } else { - None - }; + println!(); if total_workers == 0 { eprintln!("Error: No workers specified"); std::process::exit(1); } - println!(); - // Run benchmark + // Create CPU engine + let cpu_engine: Option> = if effective_cpu_workers > 0 { + Some(Arc::new(engine_cpu::FastCpuEngine::new())) + } else { + None + }; + let cancel_flag = Arc::new(AtomicBool::new(false)); let benchmark_start = Instant::now(); - let benchmark_range = EngineRange { - start: U512::from(0u64), - end: U512::from(100_000_000u64), // 100M nonces - }; - + // Random header hash for benchmark let mut header = [0u8; 32]; rand::thread_rng().fill_bytes(&mut header); let difficulty = U512::MAX; // High difficulty - no solutions expected - // We need a context from *some* engine. They should be compatible (same job). - // Just pick one. let ref_engine = cpu_engine.as_ref().or(gpu_engine.as_ref()).unwrap(); let ctx = ref_engine.prepare_context(header, difficulty); @@ -399,81 +192,61 @@ async fn run_benchmark_command( // Spawn worker threads let mut handles = Vec::new(); - let total_hashes_arc = Arc::new(std::sync::Mutex::new(0u64)); + let total_hashes = Arc::new(std::sync::Mutex::new(0u64)); - // Chunk sizes let cpu_chunk = 10_000u64; - // GPU chunk size from 1 - 10M is good let gpu_chunk = 1_000_000u64; for worker_id in 0..total_workers { - // Determine type and engine - let (engine, nonces_per_worker) = if worker_id < effective_cpu_workers { - ( - cpu_engine.as_ref().expect("CPU engine missing").clone(), - cpu_chunk, - ) + let (engine, nonces_per_batch) = if worker_id < effective_cpu_workers { + (cpu_engine.as_ref().unwrap().clone(), cpu_chunk) } else { - ( - gpu_engine.as_ref().expect("GPU engine missing").clone(), - gpu_chunk, - ) + (gpu_engine.as_ref().unwrap().clone(), gpu_chunk) }; let ctx = ctx.clone(); - let cancel_flag = cancel_flag.clone(); - let total_hashes = total_hashes_arc.clone(); + let cancel = cancel_flag.clone(); + let hashes = total_hashes.clone(); + let start = benchmark_start; let handle = thread::spawn(move || { - // We'll just define a range based on worker ID and a large multiplier - // so they don't overlap easily, though it doesn't matter for pure hashrate. - // Use a large stride to separate workers. let stride = U512::from(1_000_000_000_000u64); - let worker_start = benchmark_range - .start - .saturating_add(U512::from(worker_id as u64).saturating_mul(stride)); - + let worker_start = U512::from(worker_id as u64).saturating_mul(stride); let worker_range = EngineRange { start: worker_start, end: worker_start - .saturating_add(U512::from(nonces_per_worker)) + .saturating_add(U512::from(nonces_per_batch)) .saturating_sub(U512::from(1u64)), }; - let mut worker_hashes = 0u64; - loop { - if cancel_flag.load(std::sync::atomic::Ordering::Relaxed) { + if cancel.load(std::sync::atomic::Ordering::Relaxed) { break; } - let result = engine.search_range(&ctx, worker_range.clone(), &cancel_flag); + let result = engine.search_range(&ctx, worker_range.clone(), &cancel); match result { engine_cpu::EngineStatus::Found { hash_count, .. } | engine_cpu::EngineStatus::Exhausted { hash_count } | engine_cpu::EngineStatus::Cancelled { hash_count } => { - worker_hashes += hash_count; - *total_hashes.lock().unwrap() += hash_count; + *hashes.lock().unwrap() += hash_count; } engine_cpu::EngineStatus::Running { .. } => {} } - // Check if we've exceeded the time limit - if benchmark_start.elapsed() >= Duration::from_secs(duration) { + if start.elapsed() >= Duration::from_secs(duration) { break; } } engine_gpu::GpuEngine::clear_worker_resources(); - - worker_hashes }); handles.push(handle); } - // Wait for duration or interrupt + // Progress updates let mut last_update = Instant::now(); loop { @@ -484,70 +257,47 @@ async fn run_benchmark_command( break; } - // Update progress every second if last_update.elapsed() >= Duration::from_secs(1) { - let current_hashes = *total_hashes_arc.lock().unwrap(); + let current = *total_hashes.lock().unwrap(); let elapsed = benchmark_start.elapsed().as_secs_f64(); - - if current_hashes > 0 { - let hash_rate = current_hashes as f64 / elapsed; - let hash_rate_str = if hash_rate >= 1_000_000.0 { - format!("{:.1}M", hash_rate / 1_000_000.0) - } else if hash_rate >= 1_000.0 { - format!("{:.1}K", hash_rate / 1_000.0) - } else { - format!("{:.0}", hash_rate) - }; - println!("⏱️ {:.1}s - {} H/s", elapsed, hash_rate_str); - } else if effective_gpu_devices > 0 { - println!("⏱️ {:.1}s - processing...", elapsed); - } else { - println!("⏱️ {:.1}s - starting...", elapsed); + if current > 0 { + let rate = current as f64 / elapsed; + println!("⏱️ {:.1}s - {} H/s", elapsed, format_hash_rate(rate)); } - last_update = Instant::now(); } } - // Wait for all threads to finish + // Wait for threads for handle in handles { let _ = handle.join(); } let total_elapsed = benchmark_start.elapsed(); - let final_hashes = *total_hashes_arc.lock().unwrap(); - let avg_hash_rate = final_hashes as f64 / total_elapsed.as_secs_f64(); + let final_hashes = *total_hashes.lock().unwrap(); + let avg_rate = final_hashes as f64 / total_elapsed.as_secs_f64(); println!(); println!("📊 Benchmark Results"); println!("==================="); - println!("Total time: {:.2} seconds", total_elapsed.as_secs_f64()); + println!("Total time: {:.2}s", total_elapsed.as_secs_f64()); println!("Total hashes: {}", final_hashes); - - let hash_rate_str = if avg_hash_rate >= 1_000_000.0 { - format!("{:.2}M H/s", avg_hash_rate / 1_000_000.0) - } else if avg_hash_rate >= 1_000.0 { - format!("{:.2}K H/s", avg_hash_rate / 1_000.0) - } else { - format!("{:.0} H/s", avg_hash_rate) - }; - - println!("Average hash rate: {}", hash_rate_str); + println!("Average rate: {} H/s", format_hash_rate(avg_rate)); if total_workers > 1 { - let per_worker_rate = avg_hash_rate / total_workers as f64; - let per_worker_str = if per_worker_rate >= 1_000_000.0 { - format!("{:.2}M H/s", per_worker_rate / 1_000_000.0) - } else if per_worker_rate >= 1_000.0 { - format!("{:.2}K H/s", per_worker_rate / 1_000.0) - } else { - format!("{:.0} H/s", per_worker_rate) - }; - println!( - "Per-worker rate: {} (across {} workers)", - per_worker_str, total_workers - ); + let per_worker = avg_rate / total_workers as f64; + println!("Per-worker: {} H/s", format_hash_rate(per_worker)); } - println!("✅ Benchmark completed successfully!"); + println!("✅ Benchmark completed!"); +} + +fn format_hash_rate(rate: f64) -> String { + if rate >= 1_000_000.0 { + format!("{:.2}M", rate / 1_000_000.0) + } else if rate >= 1_000.0 { + format!("{:.2}K", rate / 1_000.0) + } else { + format!("{:.0}", rate) + } } diff --git a/crates/miner-service/Cargo.toml b/crates/miner-service/Cargo.toml index 88e7c6a4..b0baca70 100644 --- a/crates/miner-service/Cargo.toml +++ b/crates/miner-service/Cargo.toml @@ -3,41 +3,32 @@ name = "miner-service" version.workspace = true edition.workspace = true publish = false -description = "Service layer: HTTP API, job orchestration, and engine abstraction for the Quantus External Miner" +description = "QUIC-based mining service for the Quantus External Miner" [features] -default = ["cpu", "metrics"] -# Enable CPU engine by default. +default = ["cpu"] cpu = ["engine-cpu"] -# Optional metrics/observability (Prometheus endpoint). -metrics = [ - "dep:metrics", - "metrics/http-exporter", -] [dependencies] # Workspace-shared deps -warp = { workspace = true } tokio = { workspace = true, features = ["full"] } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } hex = { workspace = true } primitive-types = { workspace = true } log = { workspace = true } crossbeam-channel = { workspace = true } num_cpus = { workspace = true } -thiserror = { workspace = true } anyhow = { workspace = true } -reqwest = { version = "0.11", features = ["json"] } + +# QUIC transport quinn = "0.10" rustls = { version = "0.21", default-features = false, features = ["dangerous_configuration", "quic"] } +getrandom = "0.2" -# Protocol types from the node repo (keep API compatibility) +# Protocol types from the node repo quantus-miner-api = { workspace = true } # Local crates pow-core = { path = "../pow-core" } engine-cpu = { path = "../engine-cpu", optional = true } engine-gpu = { path = "../engine-gpu" } -metrics = { path = "../metrics", optional = true } -miner-telemetry = { path = "../miner-telemetry" } +metrics = { path = "../metrics" } diff --git a/crates/miner-service/src/lib.rs b/crates/miner-service/src/lib.rs index ccf2ae5f..d81258d1 100644 --- a/crates/miner-service/src/lib.rs +++ b/crates/miner-service/src/lib.rs @@ -1,1472 +1,485 @@ +//! Mining service for Quantus external miners. +//! +//! This module provides the core mining functionality: +//! - Engine initialization (CPU and GPU) +//! - Persistent worker thread pool for efficient job processing +//! - QUIC-based communication with the node + #![deny(rust_2018_idioms)] #![forbid(unsafe_code)] +pub mod quic; + use crossbeam_channel::{bounded, Receiver, Sender}; use engine_cpu::{EngineCandidate, EngineRange, MinerEngine}; +use pow_core::format_u512; use primitive_types::U512; -use quantus_miner_api::*; -use std::collections::HashMap; -use std::fmt; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::Arc; use std::thread; -use std::time::Instant; -use tokio::sync::Mutex; -use warp::{Filter, Rejection, Reply}; /// Service runtime configuration provided by the CLI/binary. #[derive(Clone, Debug)] pub struct ServiceConfig { - /// Port for the HTTP miner API. - pub port: u16, + /// Address of the node to connect to (e.g., "127.0.0.1:9833"). + pub node_addr: std::net::SocketAddr, /// Number of CPU worker threads to use for mining (None = auto-detect) pub cpu_workers: Option, /// Number of GPU devices to use for mining (None = auto-detect) pub gpu_devices: Option, - /// Optional metrics port. When Some, metrics endpoint starts; when None, metrics are disabled. - pub metrics_port: Option, - /// How often to report mining progress (in milliseconds). If None, defaults to 10000ms. - pub progress_interval_ms: Option, - /// Size of work chunks to process before reporting progress (in number of hashes). If None, uses engine-specific defaults. - pub chunk_size: Option, - /// Optional starting value for the manipulator engine's solved-blocks throttle index. - pub manip_solved_blocks: Option, - /// Optional base sleep per batch in nanoseconds for manipulator engine (default 500_000ns). - pub manip_base_delay_ns: Option, - /// Optional number of nonce attempts between sleeps for manipulator engine (default 10_000). - pub manip_step_batch: Option, - /// Optional cap on solved-blocks throttle index for manipulator engine. - pub manip_throttle_cap: Option, - /// Optional target duration for GPU batches in milliseconds. - pub gpu_batch_duration_ms: Option, + /// GPU cancel check interval in nonces (None = use default of 10,000) + pub gpu_cancel_interval: Option, +} + +/// Engine type for tracking metrics per compute type. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EngineType { + Cpu, + Gpu, } impl Default for ServiceConfig { fn default() -> Self { Self { - port: 9833, + node_addr: "127.0.0.1:9833".parse().unwrap(), cpu_workers: None, gpu_devices: None, - metrics_port: None, - progress_interval_ms: None, - chunk_size: None, - manip_solved_blocks: None, - manip_base_delay_ns: None, - manip_step_batch: None, - manip_throttle_cap: None, - gpu_batch_duration_ms: None, - } - } -} - -impl fmt::Display for ServiceConfig { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "port={}, cpu_workers={:?}, gpu_devices={:?}, metrics_port={:?}, progress_interval_ms={:?}, chunk_size={:?}, manip_solved_blocks={:?}, manip_base_delay_ns={:?}, manip_step_batch={:?}, manip_throttle_cap={:?}, gpu_batch_duration_ms={:?}", - self.port, - self.cpu_workers, - self.gpu_devices, - self.metrics_port, - self.progress_interval_ms, - self.chunk_size, - self.manip_solved_blocks, - self.manip_base_delay_ns, - self.manip_step_batch, - self.manip_throttle_cap, - self.gpu_batch_duration_ms - ) - } -} - -/// The core service state: job registry, CPU/GPU engines, and thread configuration. -#[derive(Clone)] -pub struct MiningService { - pub jobs: Arc>>, - pub cpu_workers: usize, - pub gpu_devices: usize, - pub cpu_engine: Option>, - pub gpu_engine: Option>, - /// How often to report mining progress (in milliseconds). - pub progress_interval_ms: u64, - /// Work chunk size (number of hashes to process before progress update). - pub chunk_size: Option, - /// Gauge of currently running jobs (for metrics) - pub active_jobs_gauge: Arc>, -} - -impl MiningService { - fn new( - cpu_workers: usize, - gpu_devices: usize, - cpu_engine: Option>, - gpu_engine: Option>, - progress_interval_ms: u64, - chunk_size: Option, - ) -> Self { - Self { - jobs: Arc::new(Mutex::new(HashMap::new())), - cpu_workers, - gpu_devices, - cpu_engine, - gpu_engine, - progress_interval_ms, - chunk_size, - active_jobs_gauge: Arc::new(tokio::sync::Mutex::new(0)), - } - } - - pub async fn add_job(&self, job_id: String, mut job: MiningJob) -> Result<(), String> { - let mut jobs = self.jobs.lock().await; - if jobs.contains_key(&job_id) { - log::warn!("Attempted to add duplicate job ID: {job_id}"); - return Err("Job already exists".to_string()); - } - - log::info!("Adding mining job: {}", job_id,); - job.job_id = Some(job_id.clone()); - #[cfg(feature = "metrics")] - { - let engine_name = if self.cpu_workers > 0 && self.gpu_devices > 0 { - "hybrid" - } else if self.gpu_devices > 0 { - "gpu" - } else { - "cpu" - }; - metrics::set_job_status_gauge(engine_name, &job_id, "running", 1); - metrics::set_job_status_gauge(engine_name, &job_id, "completed", 0); - metrics::set_job_status_gauge(engine_name, &job_id, "failed", 0); - metrics::set_job_status_gauge(engine_name, &job_id, "cancelled", 0); - // increment active jobs - { - let mut g = self.active_jobs_gauge.lock().await; - *g += 1; - metrics::set_active_jobs(*g); - } - } - job.start_mining( - self.cpu_workers, - self.gpu_devices, - self.cpu_engine.clone(), - self.gpu_engine.clone(), - self.progress_interval_ms, - self.chunk_size, - ); - jobs.insert(job_id, job); - Ok(()) - } - - pub async fn get_job(&self, job_id: &str) -> Option { - let jobs = self.jobs.lock().await; - jobs.get(job_id).cloned() - } - - pub async fn mark_job_result_served(&self, job_id: &str) { - let mut jobs = self.jobs.lock().await; - if let Some(job) = jobs.get_mut(job_id) { - if !job.result_served { - job.result_served = true; - log::info!("Mining result served for job: {job_id}"); - #[cfg(feature = "metrics")] - { - // reuse existing counter to trace served events - metrics::inc_mine_requests("result_served"); - } - } + gpu_cancel_interval: None, } } - - pub async fn remove_job(&self, job_id: &str) -> Option { - let mut jobs = self.jobs.lock().await; - if let Some(mut job) = jobs.remove(job_id) { - log::info!("Removing mining job: {job_id}"); - job.cancel(); - Some(job) - } else { - None - } - } - - pub async fn cancel_job(&self, job_id: &str) -> bool { - let mut jobs = self.jobs.lock().await; - if let Some(job) = jobs.get_mut(job_id) { - job.cancel(); - true - } else { - false - } - } - - /// Periodically polls running jobs for results and advances their status. - pub async fn start_mining_loop(&self) { - let jobs = self.jobs.clone(); - log::info!("🔄 Starting job monitoring loop..."); - - tokio::spawn(async move { - let mut last_watchdog = std::time::Instant::now(); - let service_start = std::time::Instant::now(); - loop { - let mut jobs_guard = jobs.lock().await; - - jobs_guard.retain(|job_id, job| { - let was_running = job.status == JobStatus::Running; - // Always update from results to drain thread completion messages - // regardless of job status (until we decide to drop the job) - let now_not_running = job.update_from_results(); - - if was_running && now_not_running { - log::info!( - "Mining job {} finished with status {:?}, hashes: {}, time: {:?}", - job_id, - job.status, - job.total_hash_count, - job.start_time.elapsed() - ); - } - - // Retain running jobs, completed-but-not-yet-served jobs, or anything recent (<5m) - let retain = job.status == JobStatus::Running - || (job.status == JobStatus::Completed && !job.result_served) - || job.start_time.elapsed().as_secs() < 300; - if !retain { - log::info!("🧹 Cleaning up completed job {}", job_id); - } - retain - }); - - #[cfg(feature = "metrics")] - { - // Update active jobs gauge - let mut running_jobs = 0i64; - for (_job_id, job) in jobs_guard.iter() { - if job.status == JobStatus::Running { - running_jobs += 1; - } - } - metrics::set_active_jobs(running_jobs); - } - let do_watchdog = last_watchdog.elapsed().as_secs() >= 30; - let (total, running, completed, failed, cancelled) = if do_watchdog { - let mut running = 0usize; - let mut completed = 0usize; - let mut cancelled = 0usize; - let mut failed = 0usize; - let total = jobs_guard.len(); - for (_id, job) in jobs_guard.iter() { - match job.status { - JobStatus::Running => running += 1, - JobStatus::Completed => completed += 1, - JobStatus::Cancelled => cancelled += 1, - JobStatus::Failed => failed += 1, - } - } - (total, running, completed, failed, cancelled) - } else { - (0, 0, 0, 0, 0) - }; - drop(jobs_guard); - - if do_watchdog { - let uptime = service_start.elapsed(); - let uptime_str = if uptime.as_secs() < 60 { - format!("{}s", uptime.as_secs()) - } else if uptime.as_secs() < 3600 { - format!("{}m {}s", uptime.as_secs() / 60, uptime.as_secs() % 60) - } else { - format!( - "{}h {}m", - uptime.as_secs() / 3600, - (uptime.as_secs() % 3600) / 60 - ) - }; - - #[cfg(feature = "metrics")] - let total_hash_rate = metrics::get_hash_rate(); - #[cfg(not(feature = "metrics"))] - let total_hash_rate = 0.0; - - if total == 0 { - log::info!( - "📊 Mining service healthy - uptime: {} - waiting for jobs", - uptime_str - ); - } else if running == 0 { - log::info!( - "📊 Mining status - uptime: {} - no active jobs - {} completed, {} cancelled, {} failed", - uptime_str, completed, cancelled, failed - ); - } else { - let hash_rate_str = if total_hash_rate >= 1_000_000.0 { - format!("{:.1}M", total_hash_rate / 1_000_000.0) - } else if total_hash_rate >= 1_000.0 { - format!("{:.1}K", total_hash_rate / 1_000.0) - } else if total_hash_rate > 0.0 { - format!("{:.0}", total_hash_rate) - } else { - "starting...".to_string() - }; - - log::info!( - "📊 Mining status - uptime: {} - jobs: {} active, {} completed, {} cancelled, {} failed - hash rate: {} H/s", - uptime_str, running, completed, cancelled, failed, hash_rate_str - ); - } - last_watchdog = std::time::Instant::now(); - } - tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; - } - }); - } } -/// Mining job status enumeration for orchestration. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum JobStatus { - Running, - Completed, - Failed, - Cancelled, +/// Result from a single worker thread. +#[derive(Debug, Clone)] +pub struct WorkerResult { + pub thread_id: usize, + /// The type of engine (CPU or GPU) that produced this result. + pub engine_type: EngineType, + /// The winning candidate, if found. + pub candidate: Option, + /// Number of hashes computed by this worker. + pub hash_count: u64, + /// Whether this worker has finished its range. + pub completed: bool, } -/// Aggregated best result for a job. +/// A successful mining candidate. #[derive(Debug, Clone)] -pub struct MiningJobResult { +pub struct MiningCandidate { pub nonce: U512, pub work: [u8; 64], pub hash: U512, } -/// Mining job data structure stored in the service. -#[derive(Debug)] -pub struct MiningJob { - pub header_hash: [u8; 32], - pub difficulty: U512, - pub nonce_start: U512, - pub nonce_end: U512, - - pub status: JobStatus, - pub start_time: Instant, - pub total_hash_count: u64, - pub best_result: Option, - - pub engine_name: &'static str, - pub job_id: Option, - pub cancel_flag: Arc, - pub result_receiver: Option>, - pub thread_handles: Vec>, - pub thread_total_hashes: std::collections::HashMap, - completed_threads: usize, - pub result_served: bool, +/// Generate a random U512 nonce starting point. +fn generate_random_nonce() -> U512 { + let mut bytes = [0u8; 64]; + getrandom::getrandom(&mut bytes).expect("Failed to generate random bytes"); + U512::from_big_endian(&bytes) } -impl Clone for MiningJob { - fn clone(&self) -> Self { - MiningJob { - header_hash: self.header_hash, - difficulty: self.difficulty, - nonce_start: self.nonce_start, - nonce_end: self.nonce_end, - - status: self.status.clone(), - start_time: self.start_time, - total_hash_count: self.total_hash_count, - best_result: self.best_result.clone(), - engine_name: self.engine_name, - job_id: self.job_id.clone(), +// --------------------------------------------------------------------------- +// Persistent Worker Pool +// --------------------------------------------------------------------------- - cancel_flag: self.cancel_flag.clone(), - // Do not clone crossbeam receiver or thread handles; they are runtime artifacts. - result_receiver: None, - thread_handles: Vec::new(), - thread_total_hashes: self.thread_total_hashes.clone(), - completed_threads: self.completed_threads, - result_served: self.result_served, - } - } +/// A job to be executed by worker threads. +#[derive(Clone)] +pub struct MiningJob { + /// Job context with header hash and difficulty + pub ctx: pow_core::JobContext, + /// Epoch number to detect stale results after job transitions + pub epoch: u64, } -#[derive(Debug, Clone)] -pub struct ThreadResult { - thread_id: usize, - result: Option, - hash_count: u64, - origin: Option, - completed: bool, +/// Persistent worker thread pool that keeps threads alive between jobs. +/// +/// This avoids the overhead of spawning new threads and reinitializing +/// GPU resources for each mining job. +pub struct WorkerPool { + /// Senders for dispatching jobs to workers (one per worker) + job_senders: Vec>, + /// Receiver for collecting results from all workers + result_rx: Receiver, + /// Shared cancellation flag for all workers + cancel_flag: Arc, + /// Job epoch counter - incremented on each new job to detect stale results + job_epoch: Arc, + /// Thread handles (for cleanup) + _handles: Vec>, + /// Number of CPU workers + cpu_worker_count: usize, + /// Number of GPU workers + gpu_worker_count: usize, } -impl MiningJob { +impl WorkerPool { + /// Create a new persistent worker pool. pub fn new( - header_hash: [u8; 32], - difficulty: U512, - nonce_start: U512, - nonce_end: U512, - ) -> Self { - MiningJob { - header_hash, - difficulty, - nonce_start, - nonce_end, - status: JobStatus::Running, - start_time: Instant::now(), - total_hash_count: 0, - best_result: None, - engine_name: "unknown", - job_id: None, - cancel_flag: Arc::new(AtomicBool::new(false)), - result_receiver: None, - thread_handles: Vec::new(), - thread_total_hashes: std::collections::HashMap::new(), - completed_threads: 0, - result_served: false, - } - } - - pub fn start_mining( - &mut self, - cpu_workers: usize, - gpu_devices: usize, cpu_engine: Option>, gpu_engine: Option>, - progress_interval_ms: u64, - chunk_size: Option, - ) { + cpu_workers: usize, + gpu_devices: usize, + ) -> Self { let total_workers = cpu_workers + gpu_devices; - let chan_capacity = std::env::var("MINER_RESULT_CHANNEL_CAP") - .ok() - .and_then(|v| v.parse::().ok()) - .unwrap_or_else(|| total_workers.saturating_mul(64).max(256)); - let (sender, receiver) = bounded(chan_capacity); - self.result_receiver = Some(receiver); - - // Set engine name based on what's available - self.engine_name = if cpu_workers > 0 && gpu_devices > 0 { - "hybrid" - } else if gpu_devices > 0 { - "gpu" - } else { - "cpu" - }; + let (result_tx, result_rx) = bounded(total_workers * 64); + let cancel_flag = Arc::new(AtomicBool::new(false)); + let job_epoch = Arc::new(AtomicU64::new(0)); - // Partition nonce range between CPU and GPU workers - let total_range = self - .nonce_end - .saturating_sub(self.nonce_start) - .saturating_add(primitive_types::U512::from(1u64)); - let mut current_start = self.nonce_start; + let mut job_senders = Vec::with_capacity(total_workers); + let mut handles = Vec::with_capacity(total_workers); let mut thread_id = 0; log::info!( - "Starting mining with {} CPU + {} GPU workers, total range: {}", + "Creating persistent worker pool: {} CPU + {} GPU workers", cpu_workers, - gpu_devices, - total_range + gpu_devices ); - // Create CPU worker threads + // Spawn CPU workers if cpu_workers > 0 { - if let Some(cpu_engine) = cpu_engine { - let ctx = cpu_engine.prepare_context(self.header_hash, self.difficulty); - let cpu_partitions = compute_partitions( - current_start, - current_start - .saturating_add( - total_range.saturating_mul(primitive_types::U512::from(cpu_workers)) - / primitive_types::U512::from(total_workers), - ) - .saturating_sub(primitive_types::U512::from(1u64)), - cpu_workers, - ); - current_start = current_start.saturating_add( - total_range.saturating_mul(primitive_types::U512::from(cpu_workers)) - / primitive_types::U512::from(total_workers), - ); + if let Some(ref engine) = cpu_engine { + for _ in 0..cpu_workers { + let (job_tx, job_rx) = bounded::(1); + job_senders.push(job_tx); - for (start, end) in cpu_partitions.ranges.into_iter() { - let cancel_flag = self.cancel_flag.clone(); - let sender = sender.clone(); - let ctx = ctx.clone(); - let engine = cpu_engine.clone(); - let job_id = self.job_id.clone().unwrap_or_else(|| "unknown".to_string()); - - #[cfg(feature = "metrics")] - metrics::start_thread(self.engine_name, &job_id, thread_id); + let eng = engine.clone(); + let tx = result_tx.clone(); + let cancel = cancel_flag.clone(); + let epoch = job_epoch.clone(); + let tid = thread_id; let handle = thread::spawn(move || { - mine_range_with_engine_typed( - thread_id, - job_id, - engine.as_ref(), - "CPU", - ctx, - EngineRange { start, end }, - cancel_flag, - sender, - progress_interval_ms, - chunk_size, - ); + worker_loop(tid, EngineType::Cpu, eng, job_rx, tx, cancel, epoch); }); - self.thread_handles.push(handle); + handles.push(handle); thread_id += 1; } } } - // Create GPU worker threads + // Spawn GPU workers if gpu_devices > 0 { - if let Some(gpu_engine) = gpu_engine { - let ctx = gpu_engine.prepare_context(self.header_hash, self.difficulty); - let gpu_partitions = compute_partitions(current_start, self.nonce_end, gpu_devices); - - for (start, end) in gpu_partitions.ranges.into_iter() { - let cancel_flag = self.cancel_flag.clone(); - let sender = sender.clone(); - let ctx = ctx.clone(); - let engine = gpu_engine.clone(); - let job_id = self.job_id.clone().unwrap_or_else(|| "unknown".to_string()); + if let Some(ref engine) = gpu_engine { + for _ in 0..gpu_devices { + let (job_tx, job_rx) = bounded::(1); + job_senders.push(job_tx); - #[cfg(feature = "metrics")] - metrics::start_thread(self.engine_name, &job_id, thread_id); + let eng = engine.clone(); + let tx = result_tx.clone(); + let cancel = cancel_flag.clone(); + let epoch = job_epoch.clone(); + let tid = thread_id; let handle = thread::spawn(move || { - mine_range_with_engine_typed( - thread_id, - job_id, - engine.as_ref(), - "GPU", - ctx, - EngineRange { start, end }, - cancel_flag, - sender, - progress_interval_ms, - chunk_size, - ); + worker_loop(tid, EngineType::Gpu, eng, job_rx, tx, cancel, epoch); }); - self.thread_handles.push(handle); + handles.push(handle); thread_id += 1; } } } - } - - pub fn cancel(&mut self) { - log::info!( - "Cancelling mining job: {}", - self.job_id.as_ref().unwrap_or(&"unknown".to_string()) - ); - self.cancel_flag.store(true, Ordering::Relaxed); - self.status = JobStatus::Cancelled; - #[cfg(feature = "metrics")] - { - metrics::inc_job_status("cancelled"); - if let Some(job_id) = &self.job_id { - metrics::inc_jobs_by_engine(self.engine_name, "cancelled"); - metrics::set_job_status_gauge(self.engine_name, job_id, "running", 0); - metrics::set_job_status_gauge(self.engine_name, job_id, "completed", 0); - metrics::set_job_status_gauge(self.engine_name, job_id, "failed", 0); - metrics::set_job_status_gauge(self.engine_name, job_id, "cancelled", 1); - metrics::remove_job_metrics(self.engine_name, job_id); - metrics::remove_thread_metrics_for_job(self.engine_name, job_id); - // Remove all per-thread hash rate series on cancellation - for (tid, _) in self.thread_total_hashes.iter() { - metrics::remove_thread_hash_rate(self.engine_name, job_id, &tid.to_string()); - } - self.thread_total_hashes.clear(); - } - } - while let Some(handle) = self.thread_handles.pop() { - // Do not wait for threads to finish; detach them so they can exit gracefully - // when they detect the cancellation flag. This prevents blocking the control loop. - drop(handle); + Self { + job_senders, + result_rx, + cancel_flag, + job_epoch, + _handles: handles, + cpu_worker_count: cpu_workers, + gpu_worker_count: gpu_devices, } } - pub fn update_from_results(&mut self) -> bool { - let receiver = match &self.result_receiver { - Some(r) => r, - None => return false, + /// Start a new mining job. Cancels any currently running job first. + pub fn start_job(&self, header_hash: [u8; 32], difficulty: U512) { + // Increment epoch FIRST - this ensures any in-flight results from the old job + // will be detected as stale when workers check the epoch before sending results + let new_epoch = self.job_epoch.fetch_add(1, Ordering::SeqCst) + 1; + + // Cancel any running job + self.cancel_flag.store(true, Ordering::SeqCst); + + // Brief pause to let workers see the cancellation + std::thread::sleep(std::time::Duration::from_millis(1)); + + // Reset cancel flag for new job + self.cancel_flag.store(false, Ordering::SeqCst); + + // Create job context (shared across all workers) + let ctx = pow_core::JobContext::new(header_hash, difficulty); + let job = MiningJob { + ctx, + epoch: new_epoch, }; - while let Ok(thread_result) = receiver.try_recv() { - self.total_hash_count += thread_result.hash_count; - *self - .thread_total_hashes - .entry(thread_result.thread_id) - .or_default() += thread_result.hash_count; + // Dispatch job to all workers + for tx in &self.job_senders { + // Non-blocking send - if worker is still processing old job, it will + // see the cancel flag and exit soon + let _ = tx.try_send(job.clone()); + } - #[cfg(feature = "metrics")] - if let Some(job_id) = &self.job_id { - metrics::record_thread_progress( - self.engine_name, - job_id, - thread_result.thread_id, - thread_result.hash_count, - thread_result.completed, - self.status == JobStatus::Running, - ); - } + log::debug!( + "Job dispatched to {} workers (epoch {})", + self.job_senders.len(), + new_epoch + ); + } - if thread_result.completed { - self.completed_threads += 1; - let thread_total = *self - .thread_total_hashes - .get(&thread_result.thread_id) - .unwrap_or(&0); - let elapsed = self.start_time.elapsed().as_secs_f64(); + /// Cancel the current job. + pub fn cancel(&self) { + self.cancel_flag.store(true, Ordering::SeqCst); + } - if elapsed > 0.0 && thread_total > 0 { - let thread_rate = thread_total as f64 / elapsed; - log::info!( - "Thread {} finished - Rate: {:.2} H/s ({} hashes in {:.2}s)", - thread_result.thread_id, - thread_rate, - thread_total, - elapsed - ); - } - } + /// Get the result receiver for collecting worker results. + pub fn result_receiver(&self) -> &Receiver { + &self.result_rx + } - if let Some(result) = thread_result.result { - let is_better = self - .best_result - .as_ref() - .is_none_or(|current_best| result.hash < current_best.hash); + /// Get the shared cancel flag. + pub fn cancel_flag(&self) -> &Arc { + &self.cancel_flag + } - if is_better { - log::debug!(target: "miner", - "Found better result from thread {}: distance = {}, nonce = {}", - thread_result.thread_id, - result.hash, - result.nonce - ); - self.best_result = Some(result.clone()); - self.cancel_flag.store(true, Ordering::Relaxed); - // Result is now ready to be fetched via /result - log::info!(target: "miner", "Result ready: engine={}, nonce={}, distance={}", - self.engine_name, result.nonce, result.hash); - #[cfg(feature = "metrics")] - { - // reuse existing http metric bucket for visibility until dedicated counters exist - metrics::inc_mine_requests("result_ready"); - if let Some(job_id) = &self.job_id { - let origin_label = match thread_result.origin { - Some(engine_cpu::FoundOrigin::Cpu) => "cpu", - Some(engine_cpu::FoundOrigin::GpuG1) => "gpu-g1", - Some(engine_cpu::FoundOrigin::GpuG2) => "gpu-g2", - _ => "unknown", - }; - metrics::set_job_found_origin(self.engine_name, job_id, origin_label); - } - } - } - } - } + /// Total number of workers. + pub fn worker_count(&self) -> usize { + self.job_senders.len() + } - if self.status == JobStatus::Running { - if self.best_result.is_some() { - self.status = JobStatus::Completed; - #[cfg(feature = "metrics")] - { - metrics::inc_job_status("completed"); - if let Some(job_id) = &self.job_id { - metrics::inc_jobs_by_engine(self.engine_name, "completed"); - metrics::inc_candidates_found(self.engine_name, job_id); - // TODO(metrics): engine-level false-positive metric is emitted in engine implementations (e.g., gpu-cuda G2 host re-verification) - metrics::set_job_status_gauge(self.engine_name, job_id, "running", 0); - metrics::set_job_status_gauge(self.engine_name, job_id, "completed", 1); - metrics::set_job_status_gauge(self.engine_name, job_id, "failed", 0); - metrics::set_job_status_gauge(self.engine_name, job_id, "cancelled", 0); - metrics::remove_job_metrics(self.engine_name, job_id); - metrics::remove_thread_metrics_for_job(self.engine_name, job_id); - for (tid, _) in self.thread_total_hashes.iter() { - metrics::remove_thread_hash_rate( - self.engine_name, - job_id, - &tid.to_string(), - ); - } - } - } - } else if self.completed_threads >= self.thread_handles.len() - && !self.thread_handles.is_empty() - { - self.status = JobStatus::Failed; - #[cfg(feature = "metrics")] - { - metrics::inc_job_status("failed"); - if let Some(job_id) = &self.job_id { - metrics::inc_jobs_by_engine(self.engine_name, "failed"); - metrics::set_job_status_gauge(self.engine_name, job_id, "running", 0); - metrics::set_job_status_gauge(self.engine_name, job_id, "completed", 0); - metrics::set_job_status_gauge(self.engine_name, job_id, "failed", 1); - metrics::set_job_status_gauge(self.engine_name, job_id, "cancelled", 0); - for (tid, _) in self.thread_total_hashes.iter() { - metrics::remove_thread_hash_rate( - self.engine_name, - job_id, - &tid.to_string(), - ); - } - } - } - } - } + /// Number of CPU workers. + pub fn cpu_worker_count(&self) -> usize { + self.cpu_worker_count + } - self.status != JobStatus::Running + /// Number of GPU workers. + pub fn gpu_worker_count(&self) -> usize { + self.gpu_worker_count } } -#[allow(clippy::too_many_arguments)] // Reason: worker runner needs job_id, engine, context, range, cancel flag, sender, and chunking config -fn mine_range_with_engine_typed( +/// Main loop for a persistent worker thread. +fn worker_loop( thread_id: usize, - job_id: String, - engine: &dyn MinerEngine, - engine_type: &str, - ctx: pow_core::JobContext, - range: EngineRange, + engine_type: EngineType, + engine: Arc, + job_rx: Receiver, + result_tx: Sender, cancel_flag: Arc, - sender: Sender, - progress_interval_ms: u64, - chunk_size: Option, + current_epoch: Arc, ) { - if engine_type == "CPU" { - log::info!( - target: "miner-service", - "CPU thread {} search started: Job {} range {} to {} (inclusive)", - thread_id, - job_id, - range.start, - range.end - ); - } else { - log::debug!( - target: "miner-service", - "⛏️ Job {} {} thread {} mining range {} to {} (inclusive)", - job_id, - engine_type, - thread_id, - range.start, - range.end - ); - } + let type_str = match engine_type { + EngineType::Cpu => "CPU", + EngineType::Gpu => "GPU", + }; - // Chunk the range into subranges to emit periodic progress updates for metrics - let mut current_start = range.start; - let end = range.end; - // Derive a rough chunk size from target milliseconds and an estimate of hashes/sec. - let target_ms = progress_interval_ms; - let chunk_size_u64 = chunk_size.unwrap_or_else(|| { - // Use engine-specific defaults if not configured - if engine.name().contains("gpu") { - // GPU can handle much larger chunks efficiently. - // Increased to 4B to allow auto-tuned batches (e.g. 3s @ 1GH/s = 3B) to grow sufficiently. - 4_000_000_000 - } else if engine.name() == "hybrid" { - // Hybrid engines use GPU-sized chunks since they route to GPU workers - 4_000_000_000 - } else { - // CPU uses time-based chunks - let est_ops_per_sec = 100_000u64; // 100K ops/sec for CPU - ((est_ops_per_sec.saturating_mul(target_ms)) / 1000).max(5_000) - } - }); + log::info!("{} worker {} started (persistent)", type_str, thread_id); - // Log the effective chunk size to assist in debugging/tuning - if engine_type == "GPU" { - log::info!( - target: "miner-service", - "Job {} GPU thread {} configured with chunk size: {}", - job_id, - thread_id, - chunk_size_u64 - ); - } else { - log::debug!( - target: "miner-service", - "Job {} {} thread {} configured with chunk size: {}", - job_id, - engine_type, - thread_id, - chunk_size_u64 - ); - } + // Main job processing loop + loop { + // Wait for a job + let job = match job_rx.recv() { + Ok(job) => job, + Err(_) => { + // Channel closed, pool is shutting down + log::debug!("{} worker {} shutting down", type_str, thread_id); + break; + } + }; - let chunk_size = U512::from(chunk_size_u64); - let mut done = false; - let mut total_hashes_processed = 0u64; + // Capture the job's epoch for later validation + let job_epoch = job.epoch; - while current_start <= end && !cancel_flag.load(Ordering::Relaxed) { - let mut current_end = current_start - .saturating_add(chunk_size) - .saturating_sub(U512::from(1u64)); - if current_end > end { - current_end = end; + // Check if already cancelled before starting + if cancel_flag.load(Ordering::Relaxed) { + log::debug!("{} worker {} skipping cancelled job", type_str, thread_id); + continue; } - let sub_range = EngineRange { - start: current_start, - end: current_end, - }; + // Generate random starting nonce for this job + let start = generate_random_nonce(); + let end = U512::MAX; log::debug!( - "Job {} {} thread {} starting search on subrange {}..{} (inclusive)", - job_id, - engine_type, - thread_id, - sub_range.start, - sub_range.end - ); - let start_time = Instant::now(); - let status = engine.search_range(&ctx, sub_range.clone(), &cancel_flag); - let status_str = match status { - engine_cpu::EngineStatus::Found { .. } => "found", - engine_cpu::EngineStatus::Exhausted { .. } => "exhausted", - engine_cpu::EngineStatus::Cancelled { .. } => "cancelled", - engine_cpu::EngineStatus::Running { .. } => "running", - }; - log::info!( - target: "miner-service", - "{} thread {} finished search, status: {} job: {}", - engine_type, + "{} worker {} processing job (epoch {}): range {} to {}", + type_str, thread_id, - status_str, - job_id, + job_epoch, + format_u512(start), + format_u512(end) ); - match status { + // Execute the search + let range = EngineRange { start, end }; + let result = engine.search_range(&job.ctx, range, &cancel_flag); + + // Check if epoch changed during search - if so, this result is stale + let actual_epoch = current_epoch.load(Ordering::SeqCst); + if actual_epoch != job_epoch { + log::debug!( + "⏰ {} worker {} discarding stale result (job epoch {} != current epoch {})", + type_str, + thread_id, + job_epoch, + actual_epoch + ); + // Still send hash count for metrics, but without the candidate + let hash_count = match result { + engine_cpu::EngineStatus::Found { hash_count, .. } => hash_count, + engine_cpu::EngineStatus::Exhausted { hash_count } => hash_count, + engine_cpu::EngineStatus::Cancelled { hash_count } => hash_count, + engine_cpu::EngineStatus::Running { .. } => 0, + }; + let _ = result_tx.try_send(WorkerResult { + thread_id, + engine_type, + candidate: None, // Discard the stale candidate + hash_count, + completed: true, + }); + continue; + } + + // Process result + let (candidate, hash_count) = match result { engine_cpu::EngineStatus::Found { candidate: EngineCandidate { nonce, work, hash }, hash_count, - origin, + .. } => { - let _duration = start_time.elapsed(); - // Send final result with found candidate and the hashes covered in this subrange - let final_result = ThreadResult { - thread_id, - result: Some(MiningJobResult { nonce, work, hash }), - hash_count, - origin: Some(origin), - completed: true, - }; log::info!( - "🎉 Solution found! Job {} {} thread {} - Nonce: {}, Hash: {:x}", - job_id, - engine_type, + "🎉 {} worker {} found solution! Nonce: {}, Hash: {} (epoch {})", + type_str, thread_id, - nonce, - hash + format_u512(nonce), + format_u512(hash), + job_epoch ); - if sender.try_send(final_result).is_err() { - log::warn!( - "Job {} thread {} failed to send final result", - job_id, - thread_id - ); - } - done = true; - break; + (Some(MiningCandidate { nonce, work, hash }), hash_count) } engine_cpu::EngineStatus::Exhausted { hash_count } => { - let _duration = start_time.elapsed(); - total_hashes_processed += hash_count; - // Send intermediate progress update for this chunk - let update = ThreadResult { + log::debug!( + "{} worker {} exhausted range ({} hashes)", + type_str, thread_id, - result: None, - hash_count, - origin: None, - completed: false, - }; - if sender.try_send(update).is_err() { - log::warn!( - "Job {} thread {} failed to send progress update", - job_id, - thread_id - ); - break; - } else { - log::info!( - "⛏️ Job {} {} thread {} processed {} hashes (range: {}..{})", - job_id, - engine_type, - thread_id, - hash_count, - sub_range.start, - sub_range.end - ); - } + hash_count + ); + (None, hash_count) } engine_cpu::EngineStatus::Cancelled { hash_count } => { - let _duration = start_time.elapsed(); - total_hashes_processed += hash_count; - // Send last progress update and stop - let update = ThreadResult { + log::debug!( + "{} worker {} cancelled ({} hashes)", + type_str, thread_id, - result: None, - hash_count, - origin: None, - completed: true, - }; - if sender.try_send(update).is_err() { - log::warn!(target: "miner", "Job {job_id} thread {thread_id} failed to send cancel update"); - } - if engine_type == "CPU" { - log::info!( - target: "miner-service", - "CPU thread {} search cancelled: Job {} processed {} hashes (total: {})", - thread_id, - job_id, - hash_count, - total_hashes_processed - ); - } - done = true; - break; + hash_count + ); + (None, hash_count) } engine_cpu::EngineStatus::Running { .. } => { - // Not expected from a synchronous engine chunk call; ignore + // Should not happen for synchronous search + (None, 0) } - } - - if current_end == end { - break; - } - current_start = current_end.saturating_add(U512::from(1u64)); - } - - // Check if loop exited due to cancellation flag - if !done && cancel_flag.load(Ordering::Relaxed) { - if engine_type == "CPU" { - log::info!( - target: "miner-service", - "CPU thread {} search cancelled (flag set): Job {} processed {} hashes", - thread_id, - job_id, - total_hashes_processed - ); - } - done = true; - } + }; - if !done { - // Signal thread completion with no final candidate - let final_result = ThreadResult { + // Send result (non-blocking to avoid deadlock if receiver is full) + let _ = result_tx.try_send(WorkerResult { thread_id, - result: None, - hash_count: 0, - origin: None, + engine_type, + candidate, + hash_count, completed: true, - }; - if sender.try_send(final_result).is_err() { - log::warn!(target: "miner", "Job {job_id} thread {thread_id} failed to send completion status after chunked search"); - } - if engine_type == "CPU" { - log::info!( - target: "miner-service", - "CPU thread {} search completed: Job {} exhausted range, processed {} hashes", - thread_id, - job_id, - total_hashes_processed - ); - } + }); } - // Explicitly clear thread-local GPU resources to avoid TLS order panic - if engine_type == "GPU" { + // Clean up GPU resources on thread exit + if engine_type == EngineType::Gpu { engine_gpu::GpuEngine::clear_worker_resources(); } - log::debug!(target: "miner", "Job {job_id} thread {thread_id} completed."); + log::debug!("{} worker {} exited", type_str, thread_id); } -/// Validates incoming mining requests for structural correctness. -pub fn validate_mining_request(request: &MiningRequest) -> Result<(), String> { - if request.job_id.is_empty() { - return Err("job_id cannot be empty".to_string()); - } - - if request.mining_hash.len() != 64 { - return Err("mining_hash must be 64 hex characters".to_string()); - } - if hex::decode(&request.mining_hash).is_err() { - return Err("mining_hash must be valid hex".to_string()); - } - - if U512::from_dec_str(&request.distance_threshold).is_err() { - return Err("distance_threshold must be a valid decimal number".to_string()); - } - - if request.nonce_start.len() != 128 { - return Err("nonce_start must be 128 hex characters".to_string()); - } - if request.nonce_end.len() != 128 { - return Err("nonce_end must be 128 hex characters".to_string()); - } - - let nonce_start = U512::from_str_radix(&request.nonce_start, 16) - .map_err(|_| "nonce_start must be valid hex".to_string())?; - let nonce_end = U512::from_str_radix(&request.nonce_end, 16) - .map_err(|_| "nonce_end must be valid hex".to_string())?; - - if nonce_start > nonce_end { - return Err("nonce_start must be <= nonce_end".to_string()); - } - - Ok(()) -} - -/// HTTP handler for POST /mine -pub async fn handle_mine_request( - request: MiningRequest, - state: MiningService, -) -> Result { - log::debug!(target: "miner-servce", "Mine request: {request:?}"); - if let Err(e) = validate_mining_request(&request) { - log::warn!("Invalid mine request ({}): {}", request.job_id, e); - #[cfg(feature = "metrics")] - { - metrics::inc_mine_requests("invalid"); - } - return Ok(warp::reply::with_status( - warp::reply::json(&MiningResponse { - status: ApiResponseStatus::Error, - job_id: request.job_id, - message: Some(e), - }), - warp::http::StatusCode::BAD_REQUEST, - )); +/// Resolve GPU configuration and initialize the engine. +pub fn resolve_gpu_configuration( + requested_devices: Option, + cancel_interval: Option, +) -> anyhow::Result<(Option>, usize)> { + // Explicit 0 means no GPU + if requested_devices == Some(0) { + return Ok((None, 0)); } - let header_hash: [u8; 32] = hex::decode(&request.mining_hash) - .unwrap() - .try_into() - .expect("Validated hex string is 32 bytes"); - let difficulty = U512::from_dec_str(&request.distance_threshold).unwrap(); - let nonce_start = U512::from_str_radix(&request.nonce_start, 16).unwrap(); - let nonce_end = U512::from_str_radix(&request.nonce_end, 16).unwrap(); - - let job = MiningJob::new(header_hash, difficulty, nonce_start, nonce_end); - - match state.add_job(request.job_id.clone(), job).await { - Ok(_) => { - log::debug!(target: "miner-servce", "Accepted mine request for job ID: {}", request.job_id); - #[cfg(feature = "metrics")] - { - metrics::inc_mine_requests("accepted"); - } - Ok(warp::reply::with_status( - warp::reply::json(&MiningResponse { - status: ApiResponseStatus::Accepted, - job_id: request.job_id, - message: None, - }), - warp::http::StatusCode::OK, - )) - } + // Try to initialize GPU engine + let engine = match cancel_interval { + Some(interval) => engine_gpu::GpuEngine::try_with_cancel_interval(interval), + None => engine_gpu::GpuEngine::try_new(), + }; + let engine = match engine { + Ok(e) => e, Err(e) => { - log::error!("Failed to add job {}: {}", request.job_id, e); - #[cfg(feature = "metrics")] - { - let result = if e.contains("already") { - "duplicate" - } else { - "error" - }; - metrics::inc_mine_requests(result); + if requested_devices.is_some() { + anyhow::bail!("Failed to initialize GPU engine: {}", e); } - Ok(warp::reply::with_status( - warp::reply::json(&MiningResponse { - status: ApiResponseStatus::Error, - job_id: request.job_id, - message: Some(e), - }), - warp::http::StatusCode::CONFLICT, - )) - } - } -} - -/// HTTP handler for GET /result/{job_id} -pub async fn handle_result_request( - job_id: String, - state: MiningService, -) -> Result { - log::debug!(target: "miner-servce", "Result request: {job_id}"); - - let job = match state.get_job(&job_id).await { - Some(job) => job, - None => { - log::warn!("Result request for unknown job: {job_id}"); - return Ok(warp::reply::with_status( - warp::reply::json(&quantus_miner_api::MiningResult { - status: ApiResponseStatus::NotFound, - job_id, - nonce: None, - work: None, - hash_count: 0, - elapsed_time: 0.0, - }), - warp::http::StatusCode::NOT_FOUND, - )); + log::info!("No GPU available: {}", e); + return Ok((None, 0)); } }; - let elapsed = job.start_time.elapsed().as_secs_f64(); - match job.status { - JobStatus::Running => { - #[cfg(feature = "metrics")] - let current_rate = metrics::get_hash_rate(); - #[cfg(not(feature = "metrics"))] - let current_rate = 0.0; - log::debug!( - "🔍 Job {} still running - elapsed: {:.1}s - hash rate: {:.0} H/s", - job_id, - elapsed, - current_rate - ); - } - JobStatus::Completed if !job.result_served => { - log::info!( - "✅ Job {} completed - ready for pickup - elapsed: {:.1}s - {} hashes", - job_id, - elapsed, - job.total_hash_count + let available = engine.device_count(); + let count = match requested_devices { + Some(n) if n > available => { + anyhow::bail!( + "Requested {} GPU devices but only {} available", + n, + available ); } - JobStatus::Completed => { - log::debug!( - "📤 Job {} result already served - elapsed: {:.1}s", - job_id, - elapsed - ); + Some(n) => n, + None if available == 0 => { + log::info!("No GPU devices found"); + return Ok((None, 0)); } - _ => { - log::debug!( - "🔄 Job {} status: {:?} - elapsed: {:.1}s", - job_id, - job.status, - elapsed - ); + None => { + log::info!("Auto-detected {} GPU device(s)", available); + available } - } - let status = match job.status { - JobStatus::Running => ApiResponseStatus::Running, - JobStatus::Completed => ApiResponseStatus::Completed, - JobStatus::Failed => ApiResponseStatus::Failed, - JobStatus::Cancelled => ApiResponseStatus::Cancelled, }; - let (nonce_hex, work_hex) = match &job.best_result { - Some(result) => ( - Some(format!("{:x}", result.nonce)), - Some(hex::encode(result.work)), - ), - None => (None, None), - }; - - // Inline re-verify using the exact nonce bytes we will return - if let Some(result) = &job.best_result { - let nonce_be = result.nonce.to_big_endian(); - let (ok, hash_result) = pow_core::is_valid_nonce(job.header_hash, nonce_be, job.difficulty); - log::info!( - target: "miner", - "Serving result: job_id={}, engine={}, ok={}, hash={}, difficulty={}", - job_id, - job.engine_name, - ok, - hash_result, - job.difficulty - ); - #[cfg(feature = "metrics")] - { - metrics::inc_mine_requests("result_served"); - } - // Mark served to keep job until at least one fetch succeeds - state.mark_job_result_served(&job_id).await; - } - - let elapsed_time = job.start_time.elapsed().as_secs_f64(); - - Ok(warp::reply::with_status( - warp::reply::json(&quantus_miner_api::MiningResult { - status, - job_id, - nonce: nonce_hex, - work: work_hex, - hash_count: job.total_hash_count, - elapsed_time, - }), - warp::http::StatusCode::OK, - )) -} - -/// HTTP handler for POST /cancel/{job_id} -pub async fn handle_cancel_request( - job_id: String, - state: MiningService, -) -> Result { - log::info!(target: "miner-servce", "Cancel job: {job_id}"); - - if state.cancel_job(&job_id).await { - log::debug!(target: "miner", "Successfully cancelled job: {job_id}"); - Ok(warp::reply::with_status( - warp::reply::json(&MiningResponse { - status: ApiResponseStatus::Cancelled, - job_id, - message: None, - }), - warp::http::StatusCode::OK, - )) - } else { - log::warn!("Cancel request for unknown job: {job_id}"); - Ok(warp::reply::with_status( - warp::reply::json(&MiningResponse { - status: ApiResponseStatus::NotFound, - job_id, - message: Some("Job not found".to_string()), - }), - warp::http::StatusCode::NOT_FOUND, - )) - } -} - -/// Build the warp routes for the miner API using the provided service state. -pub fn build_routes( - state: MiningService, -) -> impl Filter + Clone { - let state_clone = state.clone(); - let state_filter = warp::any().map(move || state_clone.clone()); - - let mine_route = warp::post() - .and(warp::path("mine")) - .and(warp::body::json()) - .and(state_filter.clone()) - .and_then(handle_mine_request); - - let result_route = warp::get() - .and(warp::path("result")) - .and(warp::path::param()) - .and(state_filter.clone()) - .and_then(handle_result_request); - - let cancel_route = warp::post() - .and(warp::path("cancel")) - .and(warp::path::param()) - .and(state_filter.clone()) - .and_then(handle_cancel_request); - - mine_route.or(result_route).or(cancel_route) -} - -/// Helper structures and functions for safe range partitioning (placed before use) -#[derive(Debug, Clone)] -struct Partitions { - ranges: Vec<(U512, U512)>, -} - -/// Compute safe, inclusive partitions of [start, end] into `workers` slices. -/// Guarantees coverage without overflow and clamps to `end`. -fn compute_partitions(start: U512, end: U512, workers: usize) -> Partitions { - // total inclusive range - let total_range = end.saturating_sub(start).saturating_add(U512::from(1u64)); - - let workers = workers.max(1); - let divisor = U512::from(workers as u64).max(U512::from(1u64)); - let range_per = total_range / divisor; - let remainder = total_range % divisor; - - let mut ranges = Vec::with_capacity(workers); - for i in 0..workers { - let idx = U512::from(i as u64); - let s = start.saturating_add(range_per.saturating_mul(idx)); - let mut e = s.saturating_add(range_per).saturating_sub(U512::from(1u64)); - if i == workers - 1 { - e = e.saturating_add(remainder); - } - if e > end { - e = end; - } - ranges.push((s, e)); - } - - Partitions { ranges } -} - -pub fn resolve_gpu_configuration( - requested_devices: Option, - gpu_batch_duration_ms: Option, -) -> anyhow::Result<(Option>, usize)> { - // Default to 3000ms if not specified - let duration = std::time::Duration::from_millis(gpu_batch_duration_ms.unwrap_or(3000)); - - if let Some(req_count) = requested_devices { - if req_count == 0 { - return Ok((None, 0)); - } - // Explicit request > 0 - let engine = engine_gpu::GpuEngine::try_new(duration) - .map_err(|e| anyhow::anyhow!("Failed to initialize GPU engine: {}", e))?; - - let available = engine.device_count(); - if req_count > available { - return Err(anyhow::anyhow!( - "Requested {} GPU devices but only {} device(s) are available.", - req_count, - available - )); - } - Ok((Some(Arc::new(engine)), req_count)) - } else { - // Auto-detect - match engine_gpu::GpuEngine::try_new(duration) { - Ok(engine) => { - let available = engine.device_count(); - if available > 0 { - log::info!( - "Auto-detected {} GPU device(s). Using all available GPUs.", - available - ); - Ok((Some(Arc::new(engine)), available)) - } else { - log::info!("GPU auto-detection found 0 devices. Defaulting to CPU only."); - Ok((None, 0)) - } - } - Err(e) => { - log::info!("GPU auto-detection failed (no suitable GPU found): {}. Defaulting to CPU only.", e); - Ok((None, 0)) - } - } - } + Ok((Some(Arc::new(engine)), count)) } /// Start the miner service with the given configuration. -/// - Spawns the mining loop. -/// - Optionally exposes a metrics endpoint if `metrics_port` is provided and the `metrics` feature is enabled. -/// - Serves the HTTP API on `config.port`. pub async fn run(config: ServiceConfig) -> anyhow::Result<()> { - // Determine available logical CPUs and the cpuset mask (if any), preferring cgroup v2. - fn detect_effective_cpus_and_mask() -> (usize, Option) { - // Try cgroup v2 effective cpuset - if let Ok(mask) = std::fs::read_to_string("/sys/fs/cgroup/cpuset.cpus.effective") { - let trimmed = mask.trim(); - if let Some(count) = parse_cpuset_to_count(trimmed) { - return (count.max(1), Some(trimmed.to_string())); - } - } - // Fallback to legacy cgroup v1 path - if let Ok(mask) = std::fs::read_to_string("/sys/fs/cgroup/cpuset/cpuset.cpus") { - let trimmed = mask.trim(); - if let Some(count) = parse_cpuset_to_count(trimmed) { - return (count.max(1), Some(trimmed.to_string())); - } - } - // Fallback to all logical CPUs - (num_cpus::get().max(1), None) - } - - // Parse cpuset list/ranges like "0-3,6,8-11" into a count - fn parse_cpuset_to_count(s: &str) -> Option { - if s.is_empty() { - return None; - } - let mut count: usize = 0; - for part in s.split(',') { - let p = part.trim(); - if p.is_empty() { - continue; - } - if let Some((a, b)) = p.split_once('-') { - let start = a.trim().parse::().ok()?; - let end = b.trim().parse::().ok()?; - if end < start { - return None; - } - count += end - start + 1; - } else { - // single cpu id - let _ = p.parse::().ok()?; - count += 1; - } - } - Some(count) - } - // keep run(config) open; do not close here + // Detect effective CPU count + let effective_cpus = num_cpus::get().max(1); - // Detect effective CPU pool for this process (cpuset if available). - let (effective_cpus, cpuset_mask) = detect_effective_cpus_and_mask(); - if let Some(mask) = cpuset_mask.as_ref() { - log::debug!(target: "miner", "Detected cpuset mask: {mask}"); - } else { - log::debug!(target: "miner", "No cpuset mask detected; using full logical CPU count"); - } - #[cfg(feature = "metrics")] - { - // Expose effective CPUs as a gauge for dashboards/alerts. - metrics::set_effective_cpus(effective_cpus as i64); - } - - // Initialize GPU engine if requested or for auto-detection - let (gpu_engine, gpu_devices): (Option>, usize) = - match resolve_gpu_configuration(config.gpu_devices, config.gpu_batch_duration_ms) { - Ok((engine, count)) => (engine, count), - Err(e) => { - log::error!("❌ ERROR: {}", e); - if config.gpu_devices.is_some() { - log::error!(" Please check your --gpu-devices setting or GPU hardware."); - std::process::exit(1); - } else { - (None, 0) - } - } - }; + // Resolve GPU configuration + let (gpu_engine, gpu_devices) = + resolve_gpu_configuration(config.gpu_devices, config.gpu_cancel_interval)?; - // Calculate CPU workers + // Resolve CPU workers let cpu_workers = config.cpu_workers.unwrap_or_else(|| { - if gpu_devices == 0 { - // CPU-only mode: use default CPU allocation - let default_cpu_workers = effective_cpus - .saturating_sub(effective_cpus / 2) - .min(effective_cpus.saturating_sub(1)) - .max(1); - log::info!( - "No CPU workers specified. Defaulting to {} CPU workers (leaving ~{} for other processes).", - default_cpu_workers, - effective_cpus.saturating_sub(default_cpu_workers) - ); - default_cpu_workers - } else { - // Hybrid mode: default to half of effective CPUs for CPU - let default_cpu_workers = effective_cpus / 2; - log::info!( - "Hybrid mode: defaulting to {} CPU workers", - default_cpu_workers - ); - default_cpu_workers - } - }); - - let total_workers = cpu_workers + gpu_devices; - - let (cpu_workers, gpu_devices) = if total_workers == 0 { - log::warn!( - "No workers specified. Defaulting to CPU-only mode with {} workers.", + let default = (effective_cpus / 2).max(1); + log::info!( + "Auto-detected {} CPU workers (of {} available)", + default, effective_cpus ); - (effective_cpus, 0) - } else { - (cpu_workers, gpu_devices) - }; + default + }); - // Create engines based on worker configuration - let cpu_engine = if cpu_workers > 0 { - Some(Arc::new(engine_cpu::FastCpuEngine::new()) as Arc) + // Validate: must have at least one worker + if cpu_workers == 0 && gpu_devices == 0 { + anyhow::bail!("No workers configured. Specify --cpu-workers > 0 or --gpu-devices > 0."); + } + + // Create CPU engine + let cpu_engine: Option> = if cpu_workers > 0 { + Some(Arc::new(engine_cpu::FastCpuEngine::new())) } else { None }; - // Log the mining configuration + // Log configuration log::info!( "🚀 Mining configuration: {} CPU workers, {} GPU devices", cpu_workers, @@ -1476,645 +489,23 @@ pub async fn run(config: ServiceConfig) -> anyhow::Result<()> { if let Some(ref engine) = cpu_engine { log::info!("🖥️ CPU engine: {}", engine.name()); } - if let Some(ref engine) = gpu_engine { log::info!("🎮 GPU engine: {}", engine.name()); } - log::info!("⚙️ Service configuration: {config}"); - - let progress_interval_ms = config.progress_interval_ms.unwrap_or(10000); - let service = MiningService::new( - cpu_workers, - gpu_devices, - cpu_engine, - gpu_engine, - progress_interval_ms, - config.chunk_size, - ); - log::info!( - "⛏️ Mining service ready with {} total worker threads", - total_workers + "⛏️ Mining service ready with {} total workers", + cpu_workers + gpu_devices ); - if let Some(chunk_size) = config.chunk_size { - log::info!( - "📦 Custom chunk size: {} hashes per progress update", - chunk_size - ); - } - log::info!("⏰ Progress reporting interval: {}ms", progress_interval_ms); - - // Start mining loop - service.start_mining_loop().await; - - // Telemetry bootstrap from environment variables (optional) - let telemetry_handle_opt = { - let endpoints: Vec = std::env::var("MINER_TELEMETRY_ENDPOINTS") - .ok() - .map(|s| { - s.split(',') - .map(|p| p.trim().to_string()) - .filter(|p| !p.is_empty()) - .collect() - }) - .unwrap_or_default(); - - let enabled = std::env::var("MINER_TELEMETRY_ENABLED") - .ok() - .map(|v| v != "0" && !v.eq_ignore_ascii_case("false")) - .unwrap_or(!endpoints.is_empty()); - - let verbosity = std::env::var("MINER_TELEMETRY_VERBOSITY") - .ok() - .and_then(|v| v.parse::().ok()) - .unwrap_or(0); - - if enabled && !endpoints.is_empty() { - let interval_secs = std::env::var("MINER_TELEMETRY_INTERVAL_SECS") - .ok() - .and_then(|v| v.parse::().ok()); - let chain = std::env::var("MINER_TELEMETRY_CHAIN").ok(); - let genesis = std::env::var("MINER_TELEMETRY_GENESIS").ok(); - // Optional linked node info - let link = miner_telemetry::TelemetryNodeLink { - node_telemetry_id: std::env::var("MINER_TELEMETRY_NODE_ID").ok(), - node_peer_id: std::env::var("MINER_TELEMETRY_NODE_PEER_ID").ok(), - node_name: std::env::var("MINER_TELEMETRY_NODE_NAME").ok(), - node_version: std::env::var("MINER_TELEMETRY_NODE_VERSION").ok(), - chain: chain.clone(), - genesis_hash: genesis.clone(), - }; - let default_link = if link.node_telemetry_id.is_some() - || link.node_peer_id.is_some() - || link.node_name.is_some() - || link.node_version.is_some() - || link.chain.is_some() - || link.genesis_hash.is_some() - { - Some(link) - } else { - None - }; - - let cfg = miner_telemetry::TelemetryConfig { - enabled, - endpoints, - verbosity, - name: Some("quantus-miner".to_string()), - implementation: Some("quantus-miner".to_string()), - version: Some( - option_env!("MINER_VERSION") - .unwrap_or(env!("CARGO_PKG_VERSION")) - .to_string(), - ), - chain, - genesis_hash: genesis, - interval_secs, - default_link, - }; - Some(miner_telemetry::start(cfg)) - } else { - None - } - }; - - if let Some(telemetry) = telemetry_handle_opt { - log::info!("Telemetry enabled; session_id={}", telemetry.session_id()); - telemetry.emit_system_connected(None).await; - - let telemetry_handle = telemetry.clone(); - let svc = service.clone(); - let engine_name_str = if cpu_workers > 0 && gpu_devices > 0 { - "hybrid".to_string() - } else if gpu_devices > 0 { - "gpu".to_string() - } else { - "cpu".to_string() - }; - let interval_secs = std::env::var("MINER_TELEMETRY_INTERVAL_SECS") - .ok() - .and_then(|v| v.parse::().ok()) - .unwrap_or(15); - let start_instant = std::time::Instant::now(); - - tokio::spawn(async move { - loop { - let active_jobs = { - let jobs = svc.jobs.lock().await; - jobs.values() - .filter(|job| job.status == JobStatus::Running) - .count() as i64 - }; - - #[cfg(feature = "metrics")] - let total_rate = metrics::get_hash_rate(); - #[cfg(not(feature = "metrics"))] - let total_rate = 0.0; - - let uptime_ms = start_instant.elapsed().as_millis() as u64; - - let interval = miner_telemetry::SystemInterval { - uptime_ms, - engine: Some(engine_name_str.clone()), - workers: Some((svc.cpu_workers + svc.gpu_devices) as u32), - hash_rate: Some(total_rate), - active_jobs: Some(active_jobs), - linked_node_hint: None, - }; - - telemetry_handle.emit_system_interval(&interval, None).await; - - tokio::time::sleep(tokio::time::Duration::from_secs(interval_secs)).await; - } - }); - } else { - log::info!("Telemetry disabled (no endpoints configured)"); - } - - // Optionally start metrics exporter if enabled via CLI and feature flag. - if let Some(port) = config.metrics_port { - #[cfg(feature = "metrics")] - { - log::info!("Starting metrics endpoint on 0.0.0.0:{port}"); - metrics::start_http_exporter(port).await?; - } - #[cfg(not(feature = "metrics"))] - { - log::warn!( - "Metrics port provided ({port}), but 'metrics' feature is not enabled. Skipping." - ); - } - } else { - log::info!("Metrics disabled (no --metrics-port provided)"); - } - - // Build routes - let routes = build_routes(service); - - // Start server - let addr = ([0, 0, 0, 0], config.port); - let socket = std::net::SocketAddr::from(addr); - log::info!("🌐 HTTP API server starting on http://{}", socket); - log::info!("📡 Mining endpoints available:"); - log::info!(" POST http://{}/mine - Submit mining jobs", socket); - log::info!( - " GET http://{}/result/{{job_id}} - Check mining results", - socket - ); - if config.metrics_port.is_some() { - log::info!( - " GET http://{}:{}/metrics - Prometheus metrics", - socket.ip(), - config.metrics_port.unwrap() - ); - } - warp::serve(routes).run(socket).await; - - Ok(()) -} - -#[cfg(test)] -mod tests { - - use super::{JobStatus, MiningJob, MiningService}; - use engine_cpu::MinerEngine; - use primitive_types::U512; - use std::sync::Arc; - use tokio::time::{sleep, Duration}; - - #[tokio::test] - async fn test_mining_state_add_get_remove() { - let cpu_engine: Arc = Arc::new(engine_cpu::FastCpuEngine::new()); - let state = MiningService::new(2, 0, Some(cpu_engine), None, 2000, None); - - let job = MiningJob::new( - [1u8; 32], - U512::from(1000000u64), - U512::zero(), - U512::from(1000u64), - ); - assert!(state.add_job("test".to_string(), job).await.is_ok()); - assert!(state.get_job("test").await.is_some()); - assert!(state.remove_job("test").await.is_some()); - assert!(state.get_job("test").await.is_none()); - } - - #[tokio::test] - async fn test_job_lifecycle_fail() { - // Test that a job fails if no nonce is found (threshold too strict). - // To make this deterministic, avoid nonce=0, which some math paths treat as special. - let cpu_engine: Arc = Arc::new(engine_cpu::FastCpuEngine::new()); - let state = MiningService::new(1, 0, Some(cpu_engine), None, 2000, None); - state.start_mining_loop().await; - - // Impossible difficulty with a nonce range that excludes 0 - let header_hash = [1u8; 32]; - let difficulty = U512::MAX; - let nonce_start = U512::from(1); - let nonce_end = U512::from(100); - - let job = MiningJob::new(header_hash, difficulty, nonce_start, nonce_end); - state.add_job("fail_job".to_string(), job).await.unwrap(); - - let mut finished_job = None; - for _ in 0..50 { - // Poll for 5 seconds max (50 * 100ms) - let job_status = state.get_job("fail_job").await.unwrap(); - if job_status.status != JobStatus::Running { - finished_job = Some(job_status); - break; - } - sleep(Duration::from_millis(100)).await; - } - - let finished_job = finished_job.expect("Job did not finish in time"); - assert_eq!(finished_job.status, JobStatus::Failed); - } - - #[cfg(test)] - mod partition_tests { - use crate::compute_partitions; - use primitive_types::U512; - - fn u(x: u64) -> U512 { - U512::from(x) - } - - #[test] - fn partitions_cover_entire_range_even_workers() { - // Range [0, 99] split into 4 workers - let p = compute_partitions(u(0), u(99), 4); - // total_range field removed, verify by checking all ranges sum to 100 - let total: u64 = p.ranges.iter().map(|(s, e)| (e - s + 1).low_u64()).sum(); - assert_eq!(total, 100); - // Check coverage and ordering - let mut covered = 0u64; - let mut prev_end = u(0); - for (i, (s, e)) in p.ranges.iter().enumerate() { - if i == 0 { - assert_eq!(*s, u(0)); - } else { - assert_eq!(*s, prev_end.saturating_add(u(1))); - } - assert!(e >= s); - covered += (e.saturating_sub(*s).saturating_add(u(1))).as_u64(); - prev_end = *e; - } - assert_eq!(covered, 100); - assert_eq!(prev_end, u(99)); - } - - #[test] - fn partitions_cover_entire_range_odd_workers() { - // Range [0, 99] split into 3 workers - let p = compute_partitions(u(0), u(99), 3); - // total_range field removed, verify by checking all ranges sum to 100 - let total: u64 = p.ranges.iter().map(|(s, e)| (e - s + 1).low_u64()).sum(); - assert_eq!(total, 100); - // Ensure last range takes remainder and we end exactly at 99 - assert_eq!(p.ranges.len(), 3); - assert_eq!(p.ranges[0], (u(0), u(32))); - assert_eq!(p.ranges[1], (u(33), u(65))); - assert_eq!(p.ranges[2], (u(66), u(99))); - } - - #[test] - fn partitions_handles_start_eq_end() { - // Range [42, 42] split into any workers -> only last partition reaches the end - let p = compute_partitions(u(42), u(42), 5); - // total_range field removed, verify by checking range sums to 1 - let total: u64 = p - .ranges - .iter() - .map(|(s, e)| { - if e >= s { - (e - s + 1).low_u64() - } else { - 0 // Empty range - } - }) - .sum(); - assert_eq!(total, 1); - assert_eq!(p.ranges.len(), 5); - // All but the last partition may have end < start (empty), but no overflow, and last ends at 42 - assert_eq!(p.ranges[4].1, u(42)); - } - - #[test] - fn partitions_huge_values_no_overflow() { - // Use near-max U512 values to ensure no overflow - let start = U512::from(0); - let end = U512::MAX.saturating_sub(u(10_000)); // keep a finite total_range - let p = compute_partitions(start, end, 3); - // Check that each end <= original end - for (s, e) in p.ranges { - assert!(e >= s); - assert!(e <= end); - } - } - } - - #[tokio::test] - async fn test_job_lifecycle_success() { - // Test that a job completes successfully - let cpu_engine: Arc = Arc::new(engine_cpu::FastCpuEngine::new()); - let state = MiningService::new(2, 0, Some(cpu_engine), None, 2000, None); - state.start_mining_loop().await; - - // Easy difficulty - let header_hash = [1u8; 32]; - let difficulty = U512::from(1u64); // Easiest difficulty - let nonce_start = U512::from(0); - let nonce_end = U512::from(10000); - - let job = MiningJob::new(header_hash, difficulty, nonce_start, nonce_end); - state.add_job("success_job".to_string(), job).await.unwrap(); - - let mut finished_job = None; - for _ in 0..50 { - // Poll for 5 seconds max - let job_status = state.get_job("success_job").await.unwrap(); - if job_status.status != JobStatus::Running { - finished_job = Some(job_status); - break; - } - sleep(Duration::from_millis(100)).await; - } - - let finished_job = finished_job.expect("Job did not finish in time"); - assert_eq!(finished_job.status, JobStatus::Completed); - assert!(finished_job.best_result.is_some()); - } - - #[test] - fn validate_mining_request_rejects_bad_inputs() { - // Helper to build a baseline-valid request we can mutate per case - fn valid_req() -> quantus_miner_api::MiningRequest { - quantus_miner_api::MiningRequest { - job_id: "job-1".to_string(), - // 64 hex chars (32 bytes) - mining_hash: "11".repeat(32), - distance_threshold: "1".to_string(), - // 128 hex chars (64 bytes) - nonce_start: "00".repeat(64), - nonce_end: format!("{:0128x}", 1u8), - } - } - - // 1) Empty job_id - { - let mut r = valid_req(); - r.job_id = "".to_string(); - let e = super::validate_mining_request(&r).unwrap_err(); - assert!(e.contains("job_id cannot be empty")); - } - - // 2) Bad mining_hash length - { - let mut r = valid_req(); - r.mining_hash = "aa".repeat(31); // 62 chars - let e = super::validate_mining_request(&r).unwrap_err(); - assert!(e.contains("mining_hash must be 64 hex characters")); - } - - // 3) Bad mining_hash hex - { - let mut r = valid_req(); - r.mining_hash = "zz".repeat(32); - let e = super::validate_mining_request(&r).unwrap_err(); - assert!(e.contains("mining_hash must be valid hex")); - } - - // 4) Bad difficulty decimal - { - let mut r = valid_req(); - r.distance_threshold = "not-a-decimal".to_string(); - let e = super::validate_mining_request(&r).unwrap_err(); - assert!(e.contains("distance_threshold must be a valid decimal number")); - } - - // 5) Bad nonce_start length - { - let mut r = valid_req(); - r.nonce_start = "00".repeat(63); // 126 chars - let e = super::validate_mining_request(&r).unwrap_err(); - assert!(e.contains("nonce_start must be 128 hex characters")); - } - - // 6) Bad nonce_end length - { - let mut r = valid_req(); - r.nonce_end = "00".repeat(63); // 126 chars - let e = super::validate_mining_request(&r).unwrap_err(); - assert!(e.contains("nonce_end must be 128 hex characters")); - } - - // 7) Bad nonce_start hex - { - let mut r = valid_req(); - r.nonce_start = "0g".repeat(64); - let e = super::validate_mining_request(&r).unwrap_err(); - assert!(e.contains("nonce_start must be valid hex")); - } - - // 8) Bad nonce_end hex - { - let mut r = valid_req(); - r.nonce_end = "0g".repeat(64); - let e = super::validate_mining_request(&r).unwrap_err(); - assert!(e.contains("nonce_end must be valid hex")); - } - - // 9) nonce_start > nonce_end - { - let mut r = valid_req(); - // start = 2, end = 1 - r.nonce_start = format!("{:0128x}", 2u8); - r.nonce_end = format!("{:0128x}", 1u8); - let e = super::validate_mining_request(&r).unwrap_err(); - assert!(e.contains("nonce_start must be <= nonce_end")); - } - } - - #[tokio::test] - async fn http_endpoints_handle_basic_flows() { - use warp::test::request; - - let cpu_engine: Arc = Arc::new(engine_cpu::BaselineCpuEngine::new()); - let service = MiningService::new(2, 0, Some(cpu_engine), None, 1000, None); - service.start_mining_loop().await; - - // Build routes - let routes = super::build_routes(service.clone()); - - // 1) GET /result for unknown job -> 404 - let res = request() - .method("GET") - .path("/result/unknown") - .reply(&routes) - .await; - assert_eq!(res.status(), warp::http::StatusCode::NOT_FOUND); - - // 2) POST /cancel for unknown job -> 404 - let res = request() - .method("POST") - .path("/cancel/unknown") - .reply(&routes) - .await; - assert_eq!(res.status(), warp::http::StatusCode::NOT_FOUND); - - // 3) POST /mine valid -> 200 Accepted, duplicate -> 409 - let req = quantus_miner_api::MiningRequest { - job_id: "job-http-1".to_string(), - mining_hash: "11".repeat(32), // 64 hex chars - distance_threshold: "99999999999999".to_string(), // hard, likely to fail later; OK for accept flow - nonce_start: "00".repeat(64), // 128 hex chars - nonce_end: format!("{:0128x}", 1u8), - }; - - let res = request() - .method("POST") - .path("/mine") - .json(&req) - .reply(&routes) - .await; - assert_eq!(res.status(), warp::http::StatusCode::OK); - - let res_dup = request() - .method("POST") - .path("/mine") - .json(&req) - .reply(&routes) - .await; - assert_eq!(res_dup.status(), warp::http::StatusCode::CONFLICT); - } - - #[test] - fn chunked_mining_sends_progress_and_completion() { - use crossbeam_channel::bounded; - use std::sync::atomic::AtomicBool; - - // Baseline engine, hard difficulty to force Exhausted path for the sub-range - let engine = engine_cpu::FastCpuEngine::new(); - let header = [3u8; 32]; - let difficulty = U512::MAX; - let ctx = engine.prepare_context(header, difficulty); - - // Small range; chunking derives a large chunk size, so it will be a single chunk, - // which still exercises the Exhausted -> progress update and final completion paths. - let range = super::EngineRange { - start: U512::from(1u64), - end: U512::from(10_000u64), - }; - - let cancel = Arc::new(AtomicBool::new(false)); - let (tx, rx) = bounded::(8); - - super::mine_range_with_engine_typed( - 0, - "test-job".to_string(), - &engine, - "CPU", - ctx, - range, - cancel, - tx, - 10, // ms (min chunk size is 5k; fine for testing progress + completion) - None, // chunk_size - ); - - // Expect at least two messages: one progress (completed=false) and one final completion (completed=true) - let first = rx.recv().expect("expected first progress update"); - assert!( - !first.completed, - "first message should be a progress update" - ); - assert!( - first.hash_count > 0, - "progress update should report non-zero hash_count" - ); - - // Drain messages until we see the completion marker. - let mut final_msg = first; - while !final_msg.completed { - final_msg = rx.recv().expect("expected next message"); - } - assert!( - final_msg.completed, - "final message should indicate thread completion" - ); - assert_eq!( - final_msg.hash_count, 0, - "final completion message carries zero hash_count" - ); - } - - #[tokio::test] - async fn test_cancel_returns_immediately() { - use engine_cpu::{EngineRange, EngineStatus, MinerEngine}; - use pow_core::JobContext; - use std::any::Any; - use std::sync::atomic::{AtomicBool, Ordering}; - use std::sync::Arc; - use std::time::{Duration, Instant}; - - struct SlowEngine; - - impl MinerEngine for SlowEngine { - fn name(&self) -> &'static str { - "slow-engine" - } - fn prepare_context(&self, header_hash: [u8; 32], difficulty: U512) -> JobContext { - JobContext::new(header_hash, difficulty) - } - fn search_range( - &self, - _ctx: &JobContext, - _range: EngineRange, - cancel: &AtomicBool, - ) -> EngineStatus { - // Sleep to simulate a long-running batch on GPU - std::thread::sleep(Duration::from_secs(2)); - - if cancel.load(Ordering::Relaxed) { - EngineStatus::Cancelled { hash_count: 0 } - } else { - EngineStatus::Exhausted { hash_count: 0 } - } - } - fn as_any(&self) -> &dyn Any { - self - } - } - - let engine = Arc::new(SlowEngine); - let service = MiningService::new(1, 0, Some(engine), None, 10000, None); - - let job = MiningJob::new([0u8; 32], U512::MAX, U512::zero(), U512::from(1000u64)); - - service - .add_job("slow-job".to_string(), job) - .await - .expect("Failed to add job"); - - // Give thread a moment to start and enter sleep - tokio::time::sleep(Duration::from_millis(100)).await; - - let start = Instant::now(); - service.cancel_job("slow-job").await; - let elapsed = start.elapsed(); - - // Cancel should be near-instant (<< 2 seconds) - assert!( - elapsed < Duration::from_millis(500), - "Cancel took too long: {:?} (expected < 500ms)", - elapsed - ); - - // Verify job is actually cancelled - let job = service.get_job("slow-job").await.unwrap(); - assert_eq!(job.status, JobStatus::Cancelled); - } + // Connect to node and start mining + log::info!("🌐 Connecting to node at {}", config.node_addr); + quic::connect_and_mine( + config.node_addr, + cpu_engine, + gpu_engine, + cpu_workers, + gpu_devices, + ) + .await } diff --git a/crates/miner-service/src/quic.rs b/crates/miner-service/src/quic.rs new file mode 100644 index 00000000..00d1eefc --- /dev/null +++ b/crates/miner-service/src/quic.rs @@ -0,0 +1,309 @@ +//! QUIC client for connecting to blockchain nodes. +//! +//! This module provides a QUIC client that connects to a blockchain node +//! and handles bidirectional streaming for receiving mining jobs and +//! sending results. + +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use engine_cpu::MinerEngine; +use primitive_types::U512; +use quinn::{ClientConfig, Endpoint}; +use rustls::client::ServerCertVerified; + +use quantus_miner_api::{ + read_message, write_message, ApiResponseStatus, MinerMessage, MiningResult, +}; + +use crate::{EngineType, WorkerPool}; +use pow_core::format_hashrate; + +/// Connect to a node and start mining. +/// +/// This function connects to the node, receives mining jobs, and sends results. +/// It automatically reconnects if the connection is lost. +/// +/// Uses a persistent worker pool to avoid thread creation overhead between jobs. +pub async fn connect_and_mine( + node_addr: SocketAddr, + cpu_engine: Option>, + gpu_engine: Option>, + cpu_workers: usize, + gpu_devices: usize, +) -> anyhow::Result<()> { + // Create persistent worker pool once - it lives for the entire miner lifetime + let worker_pool = WorkerPool::new(cpu_engine, gpu_engine, cpu_workers, gpu_devices); + + let mut reconnect_delay = Duration::from_secs(1); + const MAX_RECONNECT_DELAY: Duration = Duration::from_secs(30); + + loop { + log::info!("⛏️ Connecting to node at {}...", node_addr); + + match establish_connection(node_addr).await { + Ok((connection, send, recv)) => { + log::info!("⛏️ Connected to node at {}", node_addr); + reconnect_delay = Duration::from_secs(1); + + if let Err(e) = handle_connection(connection, send, recv, &worker_pool).await { + log::info!("⛏️ Connection lost: {}", e); + // Cancel any running job when connection drops + worker_pool.cancel(); + } + } + Err(e) => { + log::warn!("⛏️ Failed to connect to node: {}", e); + } + } + + log::info!("⛏️ Reconnecting in {:?}...", reconnect_delay); + tokio::time::sleep(reconnect_delay).await; + reconnect_delay = (reconnect_delay * 2).min(MAX_RECONNECT_DELAY); + } +} + +/// Establish a QUIC connection to the node. +async fn establish_connection( + addr: SocketAddr, +) -> anyhow::Result<(quinn::Connection, quinn::SendStream, quinn::RecvStream)> { + let mut crypto = rustls::ClientConfig::builder() + .with_safe_defaults() + .with_custom_certificate_verifier(Arc::new(InsecureCertVerifier)) + .with_no_client_auth(); + + crypto.alpn_protocols = vec![b"quantus-miner".to_vec()]; + + let mut client_config = ClientConfig::new(Arc::new(crypto)); + + let mut transport_config = quinn::TransportConfig::default(); + transport_config.keep_alive_interval(Some(Duration::from_secs(5))); + transport_config.max_idle_timeout(Some(Duration::from_secs(15).try_into().unwrap())); + client_config.transport_config(Arc::new(transport_config)); + + let mut endpoint = Endpoint::client("0.0.0.0:0".parse().unwrap())?; + endpoint.set_default_client_config(client_config); + + let connection = endpoint.connect(addr, "localhost")?.await?; + log::info!("⛏️ QUIC connection established to {}", addr); + + log::info!("⛏️ Opening bidirectional stream to node..."); + let (mut send, recv) = connection.open_bi().await?; + + // Send Ready message to establish the stream + write_message(&mut send, &MinerMessage::Ready).await?; + log::info!("⛏️ Bidirectional stream established"); + + Ok((connection, send, recv)) +} + +/// Helper to send a message while monitoring connection health. +async fn send_message_checked( + connection: &quinn::Connection, + send: &mut quinn::SendStream, + msg: &MinerMessage, +) -> anyhow::Result<()> { + tokio::select! { + biased; + reason = connection.closed() => { + Err(anyhow::anyhow!("Connection closed: {}", reason)) + } + result = write_message(send, msg) => { + result.map_err(|e| anyhow::anyhow!("Failed to send message: {}", e)) + } + } +} + +/// Handle an established connection, receiving jobs and sending results. +async fn handle_connection( + connection: quinn::Connection, + mut send: quinn::SendStream, + mut recv: quinn::RecvStream, + worker_pool: &WorkerPool, +) -> anyhow::Result<()> { + use crossbeam_channel::RecvTimeoutError; + + // Set static metrics once per connection + metrics::set_effective_cpus(num_cpus::get() as i64); + metrics::set_workers(worker_pool.worker_count() as i64); + metrics::set_cpu_workers(worker_pool.cpu_worker_count() as i64); + metrics::set_gpu_devices(worker_pool.gpu_worker_count() as i64); + metrics::reset_hash_tracker(); + + // Current job state + let mut current_job_id: Option = None; + let mut job_start_time: Option = None; + let mut cpu_hashes: u64 = 0; + let mut gpu_hashes: u64 = 0; + let mut result_sent_for_current_job = false; + + log::info!("⛏️ Waiting for mining jobs from node..."); + + loop { + // Poll for worker results (non-blocking via spawn_blocking) + let poll_result = if current_job_id.is_some() && !result_sent_for_current_job { + let rx = worker_pool.result_receiver().clone(); + tokio::task::spawn_blocking(move || rx.recv_timeout(Duration::from_millis(10))) + .await + .unwrap_or(Err(RecvTimeoutError::Disconnected)) + } else { + Err(RecvTimeoutError::Timeout) + }; + + // Handle worker result if any + if let Ok(worker_result) = poll_result { + // Track hashes by engine type + match worker_result.engine_type { + EngineType::Cpu => { + cpu_hashes += worker_result.hash_count; + metrics::record_cpu_hashes(worker_result.hash_count); + } + EngineType::Gpu => { + gpu_hashes += worker_result.hash_count; + metrics::record_gpu_hashes(worker_result.hash_count); + } + } + + // Only send result for the FIRST solution found + if let Some(candidate) = worker_result.candidate { + if !result_sent_for_current_job { + if let Some(ref job_id) = current_job_id { + let total_hashes = cpu_hashes + gpu_hashes; + let elapsed = job_start_time + .map(|t| t.elapsed().as_secs_f64()) + .unwrap_or(0.0); + + log::info!( + "⛏️ Job {} completed: {} hashes in {:.2}s ({})", + job_id, + total_hashes, + elapsed, + format_hashrate(total_hashes as f64 / elapsed.max(0.001)) + ); + + // Mark as sent BEFORE sending to prevent duplicates + result_sent_for_current_job = true; + worker_pool.cancel(); + metrics::set_active_jobs(0); + + let result = MiningResult { + status: ApiResponseStatus::Completed, + job_id: job_id.clone(), + nonce: Some(format!("{:x}", candidate.nonce)), + work: Some(hex::encode(candidate.work)), + hash_count: total_hashes, + elapsed_time: elapsed, + }; + + let msg = MinerMessage::JobResult(result); + send_message_checked(&connection, &mut send, &msg).await?; + } + } + } + } + + // Check for incoming messages and connection health + tokio::select! { + biased; + + reason = connection.closed() => { + return Err(anyhow::anyhow!("Connection closed: {}", reason)); + } + + msg_result = read_message(&mut recv) => { + match msg_result { + Ok(MinerMessage::NewJob(request)) => { + log::info!( + "⛏️ Received job: id={}, hash={}...", + request.job_id, + &request.mining_hash[..8] + ); + + // Parse header hash + let header_hash: [u8; 32] = match hex::decode(&request.mining_hash) { + Ok(bytes) if bytes.len() == 32 => bytes.try_into().unwrap(), + _ => { + log::warn!("Invalid mining_hash in request"); + let result = MiningResult { + status: ApiResponseStatus::Failed, + job_id: request.job_id, + nonce: None, + work: None, + hash_count: 0, + elapsed_time: 0.0, + }; + let msg = MinerMessage::JobResult(result); + send_message_checked(&connection, &mut send, &msg).await?; + continue; + } + }; + + // Parse difficulty + let difficulty = match U512::from_dec_str(&request.distance_threshold) { + Ok(d) => d, + Err(_) => { + log::warn!("Invalid difficulty in request"); + let result = MiningResult { + status: ApiResponseStatus::Failed, + job_id: request.job_id, + nonce: None, + work: None, + hash_count: 0, + elapsed_time: 0.0, + }; + let msg = MinerMessage::JobResult(result); + send_message_checked(&connection, &mut send, &msg).await?; + continue; + } + }; + + // Reset state for new job + cpu_hashes = 0; + gpu_hashes = 0; + job_start_time = Some(Instant::now()); + current_job_id = Some(request.job_id.clone()); + result_sent_for_current_job = false; + + log::debug!("Starting job {}", request.job_id); + metrics::set_active_jobs(1); + + worker_pool.start_job(header_hash, difficulty); + } + Ok(MinerMessage::JobResult(_)) => { + log::warn!("Received unexpected JobResult from node"); + } + Ok(MinerMessage::Ready) => { + log::warn!("Received unexpected Ready from node"); + } + Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => { + return Err(anyhow::anyhow!("Node disconnected")); + } + Err(e) => { + return Err(anyhow::anyhow!("Read error: {}", e)); + } + } + } + + // Short sleep to yield when no messages + _ = tokio::time::sleep(Duration::from_millis(1)) => {} + } + } +} + +/// Certificate verifier that accepts any certificate (for self-signed certs). +struct InsecureCertVerifier; + +impl rustls::client::ServerCertVerifier for InsecureCertVerifier { + fn verify_server_cert( + &self, + _end_entity: &rustls::Certificate, + _intermediates: &[rustls::Certificate], + _server_name: &rustls::ServerName, + _scts: &mut dyn Iterator, + _ocsp_response: &[u8], + _now: std::time::SystemTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) + } +} diff --git a/crates/miner-telemetry/Cargo.toml b/crates/miner-telemetry/Cargo.toml index 64da47a0..617f0adc 100644 --- a/crates/miner-telemetry/Cargo.toml +++ b/crates/miner-telemetry/Cargo.toml @@ -21,5 +21,4 @@ serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } tokio = { workspace = true, features = ["full"] } tokio-tungstenite = { version = "0.23", features = ["rustls-tls-webpki-roots"] } -url = "2" uuid = { version = "1", features = ["fast-rng", "v4"] } diff --git a/crates/pow-core/Cargo.toml b/crates/pow-core/Cargo.toml index d7919037..5c462505 100644 --- a/crates/pow-core/Cargo.toml +++ b/crates/pow-core/Cargo.toml @@ -18,7 +18,6 @@ default = ["baseline", "std"] # std feature wires through to dependencies that have separate std/no_std builds. std = [ "primitive-types/std", - "qp-poseidon-core/std", ] # Baseline/reference implementation using BigUint-backed modular exponentiation. @@ -34,14 +33,8 @@ simd-poseidon2 = [] gpu-interop = [] [dependencies] -log = { workspace = true } primitive-types = { workspace = true } -num-bigint = { workspace = true } -num-traits = { workspace = true } -qp-poseidon-core = { workspace = true } qpow-math = { workspace = true } -thiserror = { workspace = true } -anyhow = { workspace = true } [dev-dependencies] hex = { workspace = true } diff --git a/crates/pow-core/src/lib.rs b/crates/pow-core/src/lib.rs index c47ff4c2..7f909fb8 100644 --- a/crates/pow-core/src/lib.rs +++ b/crates/pow-core/src/lib.rs @@ -2,6 +2,33 @@ use primitive_types::U512; pub use qpow_math::{get_nonce_hash, is_valid_nonce, mine_range}; +/// Format a U512 in a human-readable way (scientific notation for large numbers). +pub fn format_u512(n: U512) -> String { + if n.is_zero() { + return "0".to_string(); + } + let s = format!("{}", n); + let len = s.len(); + if len <= 12 { + s + } else { + format!("{}e{}", &s[..4], len - 1) + } +} + +/// Format a hash rate with appropriate units (H/s, KH/s, MH/s, GH/s). +pub fn format_hashrate(hashes_per_sec: f64) -> String { + if hashes_per_sec >= 1_000_000_000.0 { + format!("{:.2} GH/s", hashes_per_sec / 1_000_000_000.0) + } else if hashes_per_sec >= 1_000_000.0 { + format!("{:.2} MH/s", hashes_per_sec / 1_000_000.0) + } else if hashes_per_sec >= 1_000.0 { + format!("{:.2} KH/s", hashes_per_sec / 1_000.0) + } else { + format!("{:.2} H/s", hashes_per_sec) + } +} + /// Job context for Bitcoin-style PoW mining with double Poseidon2 hashing #[derive(Debug, Clone)] pub struct JobContext {