diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7024f89 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +/.idea/ +/.vscode/ + +/assets/ +/run/ +/src/bin/tls_data/ +/src/db/db/ +/src/wallet/wallet/ +/target/ +*.log diff --git a/Cargo.lock b/Cargo.lock index b80e39f..fa18bdf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,6 +93,19 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if 1.0.0", + "getrandom 0.2.11", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.2" @@ -106,6 +119,7 @@ dependencies = [ name = "aiblock_network" version = "1.1.2" dependencies = [ + "ahash", "async-std", "async-stream", "async-trait", @@ -114,8 +128,12 @@ dependencies = [ "chrono", "clap", "config", + "crevice", + "debug-ignore", "futures", "futures-util", + "gl", + "glfw", "hex", "keccak_prime", "merkle-log 0.0.3", @@ -141,6 +159,8 @@ dependencies = [ "trust-dns-resolver", "tw_chain", "url", + "vulkano", + "vulkano-shaders", "warp", ] @@ -186,6 +206,15 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +[[package]] +name = "ash" +version = "0.37.3+1.3.251" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e9c3835d686b0a6084ab4234fcd1b07dbf6e4767dce60874b12356a25ecd4a" +dependencies = [ + "libloading 0.7.4", +] + [[package]] name = "async-channel" version = "1.9.0" @@ -514,6 +543,26 @@ version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205" +[[package]] +name = "bytemuck" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee891b04274a59bd38b412188e24b849617b2e45a0fd8d057deb63e7403761b" +dependencies = [ + "proc-macro2 1.0.76", + "quote 1.0.35", + "syn 2.0.48", +] + [[package]] name = "byteorder" version = "1.5.0" @@ -633,7 +682,7 @@ checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" dependencies = [ "glob", "libc", - "libloading", + "libloading 0.8.1", ] [[package]] @@ -660,6 +709,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + [[package]] name = "concurrent-queue" version = "2.4.0" @@ -685,12 +743,33 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + [[package]] name = "cpufeatures" version = "0.2.12" @@ -706,6 +785,28 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" +[[package]] +name = "crevice" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30fe135406c91a9839100f4b69ff18dab4da1efbc18bd8e7493ca69a1809956c" +dependencies = [ + "bytemuck", + "crevice-derive", + "mint", +] + +[[package]] +name = "crevice-derive" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2320c07ceb3e491e2bd09ade90a91c29a42d9553f1bde60c945cb5c34958b26e" +dependencies = [ + "proc-macro2 1.0.76", + "quote 1.0.35", + "syn 1.0.109", +] + [[package]] name = "crossbeam-channel" version = "0.5.11" @@ -749,6 +850,15 @@ dependencies = [ "crossbeam-utils 0.8.19", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +dependencies = [ + "crossbeam-utils 0.8.19", +] + [[package]] name = "crossbeam-utils" version = "0.7.2" @@ -797,6 +907,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" +[[package]] +name = "debug-ignore" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe7ed1d93f4553003e20b629abe9085e1e81b1429520f897f8f8860bc6dfc21" + [[package]] name = "deranged" version = "0.3.11" @@ -1106,6 +1222,48 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "gl" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a94edab108827d67608095e269cf862e60d920f144a5026d3dbcfd8b877fb404" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glfw" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728542cdb04ce0f441f0b1e6e99b67e9772dfc3545406b870aba2f0db03aa9c9" +dependencies = [ + "bitflags 1.3.2", + "glfw-sys", + "objc2", + "raw-window-handle 0.6.2", + "winapi", +] + +[[package]] +name = "glfw-sys" +version = "5.0.0+3.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dfc32d45fb58ff38b112696907963a7d671e9cf742b16f882062169a053cf88" +dependencies = [ + "cmake", +] + [[package]] name = "glob" version = "0.3.1" @@ -1153,6 +1311,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "bytemuck", + "cfg-if 1.0.0", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.1.8" @@ -1427,6 +1596,12 @@ dependencies = [ "num-traits 0.2.17", ] +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + [[package]] name = "kv-log-macro" version = "1.0.7" @@ -1467,6 +1642,16 @@ version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if 1.0.0", + "winapi", +] + [[package]] name = "libloading" version = "0.8.1" @@ -1579,6 +1764,15 @@ dependencies = [ "libc", ] +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "match_cfg" version = "0.1.0" @@ -1696,6 +1890,12 @@ dependencies = [ "adler", ] +[[package]] +name = "mint" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" + [[package]] name = "mio" version = "0.8.10" @@ -1820,6 +2020,37 @@ dependencies = [ "libc", ] +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" + [[package]] name = "object" version = "0.32.2" @@ -2005,6 +2236,16 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + [[package]] name = "proc-macro2" version = "0.3.8" @@ -2201,6 +2442,18 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + [[package]] name = "rayon" version = "1.8.0" @@ -2323,6 +2576,15 @@ dependencies = [ "librocksdb-sys", ] +[[package]] +name = "roxmltree" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "921904a62e410e37e215c40381b7117f830d9d89ba60ab5236170541dd25646b" +dependencies = [ + "xmlparser", +] + [[package]] name = "rug" version = "1.22.0" @@ -2593,6 +2855,27 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "shaderc" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27e07913ada18607bb60d12431cbe3358d3bbebbe95948e1618851dc01e63b7b" +dependencies = [ + "libc", + "shaderc-sys", +] + +[[package]] +name = "shaderc-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73120d240fe22196300f39ca8547ca2d014960f27b19b47b21288b396272f7f7" +dependencies = [ + "cmake", + "libc", + "roxmltree", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -2708,6 +2991,17 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2 1.0.76", + "quote 1.0.35", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.48" @@ -2923,6 +3217,23 @@ dependencies = [ "serde 1.0.195", ] +[[package]] +name = "toml_datetime" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + [[package]] name = "tower-service" version = "0.3.2" @@ -3241,6 +3552,72 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "vk-parse" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81086c28be67a8759cd80cbb3c8f7b520e0874605fc5eb74d5a1c9c2d1878e79" +dependencies = [ + "xml-rs", +] + +[[package]] +name = "vulkano" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f4278f76307b3c388679234b397b4f90de29cdba53873c26b624ed82653d75" +dependencies = [ + "ahash", + "ash", + "bytemuck", + "core-graphics-types", + "crossbeam-queue", + "half", + "heck", + "indexmap", + "libloading 0.8.1", + "objc", + "once_cell", + "parking_lot", + "proc-macro2 1.0.76", + "quote 1.0.35", + "raw-window-handle 0.5.2", + "regex", + "serde 1.0.195", + "serde_json", + "smallvec", + "thread_local", + "vk-parse", + "vulkano-macros", +] + +[[package]] +name = "vulkano-macros" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52be622d364272fd77e298e7f68e8547ae66e7687cb86eb85335412cee7e3965" +dependencies = [ + "proc-macro-crate", + "proc-macro2 1.0.76", + "quote 1.0.35", + "syn 1.0.109", +] + +[[package]] +name = "vulkano-shaders" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1f63401297565d74afb96e9add12587d8e46235140cee325a8eb6ba4602f4ee" +dependencies = [ + "ahash", + "heck", + "proc-macro2 1.0.76", + "quote 1.0.35", + "shaderc", + "syn 2.0.48", + "vulkano", +] + [[package]] name = "waker-fn" version = "1.1.1" @@ -3637,6 +4014,15 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" @@ -3647,6 +4033,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "xml-rs" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791978798f0597cfc70478424c2b4fdc2b7a8024aaff78497ef00f24ef674193" + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + [[package]] name = "yaml-rust" version = "0.4.5" @@ -3656,6 +4054,26 @@ dependencies = [ "linked-hash-map 0.5.6", ] +[[package]] +name = "zerocopy" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +dependencies = [ + "proc-macro2 1.0.76", + "quote 1.0.35", + "syn 2.0.48", +] + [[package]] name = "zeroize" version = "1.7.0" diff --git a/Cargo.toml b/Cargo.toml index 7c72942..2655b87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ readme = "README.md" description = "The AIBlock Network" [dependencies] +ahash = "0.8.11" async-std = "1.7.0" async-trait = "0.1.58" async-stream = "0.3.2" @@ -15,8 +16,12 @@ bytes = { version = "1.0.1", features = ["serde"] } chrono = "0.4.10" clap = "2.33.0" config = { version = "0.10.1", features = ["toml"] } +crevice = "0.16.0" +debug-ignore = "1.0.5" futures = "0.3" futures-util = "0.3.15" +gl = "0.14.0" +glfw = "0.57.0" hex = "0.4.2" merkletree = "0.23.0" merkle-log = "0.0.3" @@ -41,9 +46,12 @@ tracing-subscriber = "0.2.3" tracing-futures = "0.2.3" warp = { version = "0.3.1", features = ["tls"] } url = "2.4.1" +vulkano = "0.34.0" +vulkano-shaders = "0.34.0" trust-dns-resolver = "0.23.2" rustls-pemfile = "2.0.0" [features] mock = [] config_override = [] +benchmark_miners = [] diff --git a/Dockerfile b/Dockerfile index 5385065..c1100b8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM --platform=$BUILDPLATFORM rust:1.76.0-slim-bullseye AS chef -RUN apt-get update && apt-get -y --no-install-recommends install git build-essential m4 llvm libclang-dev diffutils curl +RUN apt-get update && apt-get -y --no-install-recommends install git build-essential m4 llvm libclang-dev diffutils curl cmake libglfw3-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev python3 RUN cargo install cargo-chef WORKDIR /aiblock ENV CARGO_TARGET_DIR=/aiblock @@ -21,11 +21,11 @@ COPY . . RUN cargo build --release # Use distroless -FROM cgr.dev/chainguard/glibc-dynamic:latest - -# COPY --from=busybox:1.35.0-uclibc /bin/sh /bin/sh - -USER nonroot +#FROM cgr.dev/chainguard/glibc-dynamic:latest +# +FROM rust:1.79.0-slim-bullseye +RUN apt-get update && apt-get -y --no-install-recommends install libclang-dev libxinerama-dev +#USER nonroot # Set these in the environment to override [use once we have env vars available] ARG NODE_TYPE_ARG="mempool" @@ -39,15 +39,15 @@ ENV API_USE_TLS="0" ENV MEMPOOL_MINER_WHITELIST="/etc/mempool_miner_whitelist.json" ENV RUST_LOG=info,debug -# RUN echo "Node type is $NODE_TYPE" - # Copy node bin -COPY --from=builder /aiblock/release/node ./node +COPY --from=builder /aiblock/release/node /aiblock/aiblock +#COPY --from=builder /usr/lib/x86_64-linux-gnu/libX11.so.6 /usr/lib/x86_64-linux-gnu/libX11.so.6 +#RUN cp /aiblock/release/node /aiblock/aiblock # Default config for the node COPY .docker/conf/* /etc/. -ENTRYPOINT ["./node"] +ENTRYPOINT ["/aiblock/aiblock"] CMD [$NODE_TYPE] diff --git a/docker-compose.yml b/docker-compose.yml index 73fcdaf..ed6f580 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,7 @@ services: volumes: - /tmp/mempool:/src command: mempool + platform: linux/amd64 networks: aiblock: ipv4_address: 172.28.0.3 @@ -32,6 +33,8 @@ services: miner-node: <<: *node-default + environment: + RUST_LOG: trace depends_on: - mempool-node - storage-node diff --git a/src/api/tests.rs b/src/api/tests.rs index 549be35..21d68df 100644 --- a/src/api/tests.rs +++ b/src/api/tests.rs @@ -251,8 +251,10 @@ async fn get_db_with_block_no_mutex() -> SimpleDb { block.set_txs_merkle_root_and_hash().await; block.header = apply_mining_tx(block.header, nonce, "test".to_string()); - if !validate_pow_block(&block.header) { - block.header = generate_pow_for_block(block.header); + if validate_pow_block(&block.header).is_err() { + block.header.nonce_and_mining_tx_hash.0 = generate_pow_for_block(&block.header) + .expect("error occurred while mining block") + .expect("couldn't find a valid nonce"); let new_nonce = hex::encode(&block.header.nonce_and_mining_tx_hash.0); panic!( "get_db_with_block_no_mutex: Out of date nonce: {} -> new({})", diff --git a/src/asert.rs b/src/asert.rs index 1b0b8e7..f35d259 100644 --- a/src/asert.rs +++ b/src/asert.rs @@ -1,3 +1,4 @@ +use std::convert::TryInto; use { crate::constants::{ASERT_HALF_LIFE, ASERT_TARGET_HASHES_PER_BLOCK}, rug::{integer::ParseIntegerError, Integer}, @@ -117,7 +118,7 @@ fn map_asert_inputs( const TARGET_BLOCK_TIME_D: Duration = Duration::from_secs(ASERT_TARGET_HASHES_PER_BLOCK); const HALF_LIFE_D: Duration = Duration::from_secs(ASERT_HALF_LIFE); - let anchor_target = "0x1f00ffff".parse().unwrap(); + let anchor_target = "0xc800ffff".parse().unwrap(); let context = Asert::with_parameters(TARGET_BLOCK_TIME_D, HALF_LIFE_D) .with_anchor(anchor_block_height, anchor_target) @@ -278,6 +279,31 @@ impl Sub for Timestamp { } } +/// An error which can occur when working with `CompactTarget`. +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum CompactTargetError { + /// Indicates that an attempt was made to construct a `CompactTarget` from a byte slice with + /// an invalid length. + SliceLength { + /// The length of the slice + length: usize, + }, +} + +impl std::error::Error for CompactTargetError {} + +impl fmt::Display for CompactTargetError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::SliceLength { length } => write!( + f, + "Cannot construct CompactTarget from slice of length {}", + length + ), + } + } +} + /// A 32-bit approximation of a 256-bit number that represents the target /// a block hash must be lower than in order to meet PoW requirements. /// @@ -290,8 +316,9 @@ impl Sub for Timestamp { pub struct CompactTarget(u32); impl CompactTarget { - /// This is defined ... somewhere. - pub const MAX: CompactTarget = CompactTarget(u32::from_be_bytes([0x1d, 0x00, 0xff, 0xff])); + /// This is the easiest difficulty possible. It's roughly equivalent to requiring a single + /// leading zero byte. + pub const MAX: CompactTarget = CompactTarget(u32::from_be_bytes([0x22, 0x00, 0x00, 0x01])); pub fn expand(&self) -> Target { let byte_len = self.0 >> 24; @@ -308,6 +335,10 @@ impl CompactTarget { Target(target) } + pub fn expand_integer(&self) -> Integer { + self.expand().0 + } + pub fn into_array(self) -> [u8; 4] { self.0.to_be_bytes() } @@ -316,14 +347,14 @@ impl CompactTarget { Self(u32::from_be_bytes(array)) } - pub fn try_from_slice(slice: &[u8]) -> Option { - if slice.len() < 4 { - return None; - } - - let mut array = [0u8; 4]; - array.copy_from_slice(&slice[slice.len() - 4..]); - Some(Self::from_array(array)) + pub fn try_from_slice(slice: &[u8]) -> Result { + // This requires that the slice's length is exactly 4 + slice + .try_into() + .map(Self::from_array) + .map_err(|_| CompactTargetError::SliceLength { + length: slice.len(), + }) } } @@ -374,30 +405,56 @@ impl CompactTarget { } } +#[derive(Copy, Clone, Eq, PartialEq, Debug)] pub struct HeaderHash(sha3_256::Output); impl HeaderHash { - pub fn try_calculate(header: &BlockHeader) -> Option { - let serialized = bincode::serialize(header).ok()?; - let hashed = sha3_256::digest(&serialized); - Some(Self(hashed)) + pub fn calculate(header: &BlockHeader) -> Self { + let serialized = bincode::serialize(header).unwrap(); + Self(sha3_256::digest(&serialized)) } pub fn is_below_target(&self, target: &Target) -> bool { let h_int = Integer::from_digits(self.0.as_slice(), rug::integer::Order::MsfBe); + println!("h_int: {:?}", h_int); + println!("target: {:?}", target.0); + println!("h_int <= target.0: {:?}", h_int <= target.0); h_int <= target.0 } pub fn is_below_compact_target(&self, target: &CompactTarget) -> bool { let target = target.expand(); + println!("target: {:?}", target); self.is_below_target(&target) } + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + pub fn into_vec(self) -> Vec { self.0.to_vec() } } +impl From> for HeaderHash { + fn from(value: sha3_256::Output) -> Self { + Self(value) + } +} + +impl From for sha3_256::Output { + fn from(value: HeaderHash) -> Self { + value.0 + } +} + +impl AsRef<[u8]> for HeaderHash { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + /// The expanded integer from of a `CompactTarget` that represents the target /// a block hash must be lower than in order to meet PoW requirements. /// diff --git a/src/bin/node_settings_local_raft_1.toml b/src/bin/node_settings_local_raft_1.toml index 16b658c..2b1e779 100644 --- a/src/bin/node_settings_local_raft_1.toml +++ b/src/bin/node_settings_local_raft_1.toml @@ -14,7 +14,7 @@ jurisdiction = "US" backup_block_modulo = 4 peer_limit = 5 sub_peer_limit = 1 -mempool_mining_event_timeout= 3000 +mempool_mining_event_timeout= 30000 storage_block_timeout = 30000 #backup_restore = true enable_trigger_messages_pipeline_reset = true diff --git a/src/constants.rs b/src/constants.rs index 471abb3..43c8a61 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -71,6 +71,15 @@ pub const SANC_LIST_TEST: &str = "src/db/sanc_list_test.json"; /*------- LIMIT CONSTANTS -------*/ +/// Length of address PoW nonce +pub const ADDRESS_POW_NONCE_LEN: usize = 4; + +/// Maximum length of PoW nonce +pub const POW_NONCE_MAX_LEN: usize = 32; // The size of a SHA3-256 digest + +/// Length of random number generation for miner subselection +pub const POW_RNUM_SELECT: usize = 10; + /// Default limit on number of internal transactions for a miner node pub const INTERNAL_TX_LIMIT: usize = 999; @@ -109,7 +118,7 @@ pub const COINBASE_MATURITY: u64 = if cfg!(test) { 0 } else { 100 }; // note that this can be overriden through configuration, // which is handy for running locally or for low-difficulty test networks. /// Block height at which ASERT DAA is activated -pub const ACTIVATION_HEIGHT_ASERT: u64 = u64::MAX; +pub const ACTIVATION_HEIGHT_ASERT: u64 = 3; /// Number of desired hashes submitted per block interval by miners pub const ASERT_TARGET_HASHES_PER_BLOCK: u64 = 11; diff --git a/src/interfaces.rs b/src/interfaces.rs index a2533d5..96af7fa 100644 --- a/src/interfaces.rs +++ b/src/interfaces.rs @@ -220,29 +220,6 @@ pub struct ProofOfWork { pub nonce: Vec, } -/// PoW structure for blocks -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ProofOfWorkBlock { - pub nonce: Vec, - pub block: Block, -} - -impl Default for ProofOfWorkBlock { - fn default() -> Self { - Self::new() - } -} - -impl ProofOfWorkBlock { - ///Proof of work block constructor - pub fn new() -> Self { - ProofOfWorkBlock { - nonce: Vec::new(), - block: Block::new(), - } - } -} - /// Winning PoW structure #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct WinningPoWInfo { diff --git a/src/lib.rs b/src/lib.rs index 6797bf2..2edf6ae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,7 @@ pub mod key_creation; mod mempool; mod mempool_raft; mod miner; +mod miner_pow; mod pre_launch; mod raft; mod raft_store; diff --git a/src/mempool.rs b/src/mempool.rs index 1eec738..60bac84 100644 --- a/src/mempool.rs +++ b/src/mempool.rs @@ -2079,10 +2079,10 @@ impl MempoolNode { // Perform validation let coinbase_hash = construct_tx_hash(&coinbase); let block_to_check = apply_mining_tx(block_to_check, nonce, coinbase_hash); - if !validate_pow_block(&block_to_check) { + if let Err(err) = validate_pow_block(&block_to_check) { return Some(Response { success: false, - reason: "Invalid PoW for block".to_owned(), + reason: format!("Invalid PoW for block: {}", err), }); } diff --git a/src/miner.rs b/src/miner.rs index ee0e405..fc1ab51 100644 --- a/src/miner.rs +++ b/src/miner.rs @@ -42,6 +42,7 @@ use tw_chain::primitives::transaction::Transaction; use tw_chain::utils::transaction_utils::{ construct_tx_core, construct_tx_hash, update_input_signatures, }; +use warp::filters::trace; /// Key for last pow coinbase produced pub const LAST_COINBASE_KEY: &str = "LastCoinbaseKey"; @@ -1136,8 +1137,13 @@ impl MinerNode { .map(|c| c.block.b_num); if new_b_num <= current_b_num { if new_b_num == current_b_num { + info!("Received block with same b_num as current block"); + info!("Current block: {:?}", self.current_block.lock().await); + self.process_found_block_pow().await; } + + warn!("Received block with b_num less than current block"); return false; } @@ -1192,6 +1198,8 @@ impl MinerNode { /// Process the found PoW sending it to the related peer and logging errors pub async fn process_found_block_pow(&mut self) -> bool { + trace!("Current mining block task: {:?}", self.mining_block_task); + let BlockPoWInfo { peer, start_time, @@ -1536,7 +1544,15 @@ impl MinerNode { /// * `info` - Block Proof of work info fn generate_pow_for_block(mut info: BlockPoWInfo) -> task::JoinHandle { task::spawn_blocking(move || { - info.header = generate_pow_for_block(info.header); + info.header.nonce_and_mining_tx_hash.0 = generate_pow_for_block(&info.header) + .expect("error occurred while mining block") + // TODO: We should make BlockPoWInfo actually indicate if no PoW could be found + .expect("couldn't find a valid nonce"); + + trace!( + "Found possible POW for block: {:?}", + info.header.nonce_and_mining_tx_hash.0 + ); info }) } diff --git a/src/miner_pow/cpu.rs b/src/miner_pow/cpu.rs new file mode 100644 index 0000000..b94e079 --- /dev/null +++ b/src/miner_pow/cpu.rs @@ -0,0 +1,90 @@ +use crate::constants::MINING_DIFFICULTY; +use crate::miner_pow::{MineError, MinerStatistics, PoWDifficulty, Sha3_256PoWMiner}; +use tw_chain::crypto::sha3_256; + +/// A miner which runs on the CPU. +#[derive(Copy, Clone, Debug)] +pub struct CpuMiner(); // this is stateless + +impl CpuMiner { + /// Creates a new CPU miner instance. + pub fn new() -> Self { + Self() + } +} + +impl Sha3_256PoWMiner for CpuMiner { + fn is_hw_accelerated(&self) -> bool { + false + } + + fn min_nonce_count(&self) -> u32 { + 1 + } + + fn nonce_peel_amount(&self) -> u32 { + // TODO: We'd probably want to choose a better metric for this + 1 << 10 + } + + fn generate_pow_internal( + &mut self, + leading_bytes: &[u8], + trailing_bytes: &[u8], + difficulty: &PoWDifficulty, + first_nonce: u32, + nonce_count: u32, + statistics: &mut MinerStatistics, + ) -> Result, MineError> { + if nonce_count == 0 { + return Ok(None); + } + + let mut stats_updater = statistics.update_safe(); + + let difficulty_target = match difficulty { + // The target value is higher than the largest possible SHA3-256 hash. Therefore, + // every hash will meet the required difficulty threshold, so we can just return + // an arbitrary nonce. + PoWDifficulty::TargetHashAlwaysPass => return Ok(Some(first_nonce)), + + PoWDifficulty::LeadingZeroBytes { leading_zeroes } => { + assert_eq!(*leading_zeroes, MINING_DIFFICULTY); + None + } + PoWDifficulty::TargetHash { target_hash } => Some(target_hash), + }; + + let block_header_nonce_offset = leading_bytes.len(); + let mut block_header_bytes = [leading_bytes, &0u32.to_le_bytes(), trailing_bytes].concat(); + + for i in 0..nonce_count { + let nonce = first_nonce.wrapping_add(i); + + block_header_bytes[block_header_nonce_offset..block_header_nonce_offset + 4] + .copy_from_slice(&nonce.to_ne_bytes()); + + let hash = sha3_256::digest(&block_header_bytes); + + stats_updater.computed_hashes(1); + + match difficulty_target { + None => { + // There isn't a difficulty function, check if the first MINING_DIFFICULTY bytes + // of the hash are 0 + let hash_prefix = hash.first_chunk::().unwrap(); + if hash_prefix == &[0u8; MINING_DIFFICULTY] { + return Ok(Some(nonce)); + } + } + Some(target) => { + if hash.as_slice() <= target.as_slice() { + return Ok(Some(nonce)); + } + } + } + } + + Ok(None) + } +} diff --git a/src/miner_pow/mod.rs b/src/miner_pow/mod.rs new file mode 100644 index 0000000..e1ed30f --- /dev/null +++ b/src/miner_pow/mod.rs @@ -0,0 +1,876 @@ +pub mod cpu; +pub mod opengl; +pub mod vulkan; + +use crate::asert::{CompactTarget, CompactTargetError}; +use crate::constants::{ADDRESS_POW_NONCE_LEN, MINING_DIFFICULTY, POW_NONCE_MAX_LEN}; +use crate::interfaces::ProofOfWork; +use crate::miner_pow::cpu::CpuMiner; +use crate::utils::{all_byte_strings, split_range_into_blocks, UnitsPrefixed}; +use std::fmt; +use std::fmt::Debug; +use std::ops::RangeInclusive; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex, OnceLock}; +use std::time::{Duration, Instant}; +use tracing::{debug, info, warn}; +use tw_chain::primitives::block::BlockHeader; + +pub const SHA3_256_BYTES: usize = 32; +pub const BLOCK_HEADER_MAX_BYTES: usize = 1024; + +/// A difficulty requirement for a proof-of-work object. +#[derive(Clone, Eq, PartialEq, Debug)] +pub enum PoWDifficulty { + /// Indicates that the object's hash should start with a fixed number of leading zero bytes. + LeadingZeroBytes { + /// The number of zero bytes which the object's hash must start with. + leading_zeroes: usize, + }, + /// Indicates that the object's hash is irrelevant; any hash would meet the difficulty + /// requirements. + TargetHashAlwaysPass, + /// Indicates that the object's hash must be lexicographically less than or equal to the given + /// target hash threshold. + TargetHash { + /// The target hash threshold which the object's hash must be lexicographically less than + /// or equal to. + target_hash: [u8; SHA3_256_BYTES], + }, +} + +impl PoWDifficulty { + /// Checks if the given hash meets this difficulty requirement. + /// + /// ### Arguments + /// + /// * `hash` - the hash to check + pub fn check_hash(&self, hash: &[u8; SHA3_256_BYTES]) -> bool { + match self { + PoWDifficulty::TargetHashAlwaysPass => true, + PoWDifficulty::LeadingZeroBytes { leading_zeroes } => { + *leading_zeroes <= hash.len() && hash[..*leading_zeroes].iter().all(|b| *b == 0) + } + PoWDifficulty::TargetHash { target_hash } => hash <= target_hash, + } + } +} + +/// An error relating to an invalid nonce value. +#[derive(Clone, Eq, PartialEq, Debug)] +pub enum PoWError { + /// Indicates that an invalid nonce length was provided. + NonceLength { + /// The provided nonce length + nonce_length: usize, + /// The permitted nonce lengths + permitted_lengths: RangeInclusive, + }, +} + +impl std::error::Error for PoWError {} + +impl fmt::Display for PoWError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::NonceLength { + nonce_length, + permitted_lengths, + } => write!( + f, + "The provided nonce length {} is invalid (should be in range {}..={})", + nonce_length, + permitted_lengths.start(), + permitted_lengths.end() + ), + } + } +} + +fn find_nonce_location( + object: &T, + nonce_length: usize, +) -> Result<(Box<[u8]>, Box<[u8]>), PoWError> { + if !::permitted_nonce_lengths().contains(&nonce_length) { + return Err(PoWError::NonceLength { + nonce_length, + permitted_lengths: ::permitted_nonce_lengths(), + }); + } + + assert_ne!(nonce_length, 0, "nonce_length may not be 0!"); + + let mut object = object.clone(); + + let nonce_00 = vec![0x00u8; nonce_length]; + let nonce_ff = vec![0xFFu8; nonce_length]; + + object.set_nonce(nonce_00.clone()).unwrap(); + let serialized_00 = bincode::serialize(&object).unwrap(); + object.set_nonce(nonce_ff.clone()).unwrap(); + let serialized_ff = bincode::serialize(&object).unwrap(); + + assert_ne!( + serialized_00, serialized_ff, + "changing the nonce didn't affect the serialized object?!?" + ); + assert_eq!( + serialized_00.len(), + serialized_ff.len(), + "changing the nonce affected the object's serialized length?!?" + ); + + // find the index at which the two headers differ + let nonce_offset = (0..serialized_00.len()) + .find(|offset| serialized_00[*offset] != serialized_ff[*offset]) + .expect("the serialized objects are not equal, but are equal at every index?!?"); + + assert_eq!( + &serialized_00.as_slice()[nonce_offset..nonce_offset + 4], + nonce_00.as_slice(), + "serialized object with nonce 0x00000000 has different bytes at presumed nonce offset!" + ); + assert_eq!( + &serialized_ff.as_slice()[nonce_offset..nonce_offset + 4], + nonce_ff.as_slice(), + "serialized object with nonce 0xFFFFFFFF has different bytes at presumed nonce offset!" + ); + + let leading_bytes = serialized_00[..nonce_offset].into(); + let trailing_bytes = serialized_00[nonce_offset + nonce_length..].into(); + Ok((leading_bytes, trailing_bytes)) +} + +fn expand_compact_target_difficulty(compact_target: CompactTarget) -> Option<[u8; SHA3_256_BYTES]> { + let expanded_target = compact_target.expand_integer(); + let byte_digits: Vec = expanded_target.to_digits(rug::integer::Order::MsfBe); + if byte_digits.len() > SHA3_256_BYTES { + // The target value is higher than the largest possible SHA3-256 hash. + return None; + } + + // Pad the target hash with leading zeroes to make it exactly SHA3_256_BYTES bytes long. + let mut result = [0u8; SHA3_256_BYTES]; + result[SHA3_256_BYTES - byte_digits.len()..].copy_from_slice(&byte_digits); + + assert_eq!( + expanded_target, + rug::Integer::from_digits(&result, rug::integer::Order::MsfBe) + ); + + Some(result) +} + +/// An object which contains a nonce and can therefore be mined. +pub trait PoWObject: Clone { + /// Gets a range containing all nonce lengths permitted by this object. + fn permitted_nonce_lengths() -> RangeInclusive; + + fn check_nonce_length(nonce_length: usize) -> Result<(), PoWError> { + if Self::permitted_nonce_lengths().contains(&nonce_length) { + Ok(()) + } else { + Err(PoWError::NonceLength { + nonce_length, + permitted_lengths: Self::permitted_nonce_lengths(), + }) + } + } + + /// Sets this object's nonce to the given value. + /// + /// ### Arguments + /// + /// * `nonce` - the nonce which the cloned object should contain + fn set_nonce(&mut self, nonce: Vec) -> Result<(), PoWError>; + + /// Gets a reference to this object's nonce. + fn get_nonce(&self) -> &[u8]; + + /// Gets the difficulty requirements for mining this object. + fn pow_difficulty(&self) -> Result; + + /// Gets the leading and trailing bytes for this object. + /// + /// These bytes are concatenated with a nonce of the given length in the middle while mining. + /// + /// ### Arguments + /// + /// * `nonce_length` - The length (in bytes) of the nonce to be inserted between the leading + /// and trailing bytes + fn get_leading_and_trailing_bytes_for_mine( + &self, + nonce_length: usize, + ) -> Result<(Box<[u8]>, Box<[u8]>), PoWError>; +} + +impl PoWObject for BlockHeader { + fn permitted_nonce_lengths() -> RangeInclusive { + 1..=POW_NONCE_MAX_LEN + } + + fn set_nonce(&mut self, nonce: Vec) -> Result<(), PoWError> { + Self::check_nonce_length(nonce.len())?; + self.nonce_and_mining_tx_hash.0 = nonce; + Ok(()) + } + + fn get_nonce(&self) -> &[u8] { + &self.nonce_and_mining_tx_hash.0 + } + + fn pow_difficulty(&self) -> Result { + if self.difficulty.is_empty() { + // There is no difficulty function enabled + return Ok(PoWDifficulty::LeadingZeroBytes { + leading_zeroes: MINING_DIFFICULTY, + }); + } + + // Decode the difficulty bytes into a CompactTarget and then expand that into a target + // hash threshold. + let compact_target = CompactTarget::try_from_slice(&self.difficulty)?; + + match expand_compact_target_difficulty(compact_target) { + // The target value is higher than the largest possible SHA3-256 hash. + None => Ok(PoWDifficulty::TargetHashAlwaysPass), + Some(target_hash) => Ok(PoWDifficulty::TargetHash { target_hash }), + } + } + + fn get_leading_and_trailing_bytes_for_mine( + &self, + nonce_length: usize, + ) -> Result<(Box<[u8]>, Box<[u8]>), PoWError> { + find_nonce_location(self, nonce_length) + } +} + +impl PoWObject for ProofOfWork { + fn permitted_nonce_lengths() -> RangeInclusive { + ADDRESS_POW_NONCE_LEN..=ADDRESS_POW_NONCE_LEN + } + + fn set_nonce(&mut self, nonce: Vec) -> Result<(), PoWError> { + Self::check_nonce_length(nonce.len())?; + self.nonce = nonce; + Ok(()) + } + + fn get_nonce(&self) -> &[u8] { + &self.nonce + } + + fn pow_difficulty(&self) -> Result { + // see utils::validate_pow_for_address() + Ok(PoWDifficulty::LeadingZeroBytes { + leading_zeroes: MINING_DIFFICULTY, + }) + } + + fn get_leading_and_trailing_bytes_for_mine( + &self, + nonce_length: usize, + ) -> Result<(Box<[u8]>, Box<[u8]>), PoWError> { + find_nonce_location(self, nonce_length) + } +} + +/// A response from a mining operation which didn't encounter any errors. +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub enum MineResult { + /// Indicates that a valid Proof-of-Work was found for the block. + FoundNonce { + /// The found nonce which meets the block's difficulty requirements. + nonce: Vec, + }, + /// Indicates that despite testing all possible nonce values, the miner was unable to find a + /// nonce which could meet the block's difficulty requirements. + Exhausted, + /// Indicates that the miner terminated prematurely because it received an interrupt request. + TerminateRequested, + /// Indicates that the miner terminated prematurely because it reached the timeout duration + /// without finding a valid nonce. + TimeoutReached, +} + +/// Statistics indicating current mining performance. +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] +pub struct MinerStatistics { + pub total_computed_hashes: u128, + pub total_mining_duration: Duration, +} + +impl MinerStatistics { + /// Updates these statistics with the given data from a completed mining round. + /// + /// ### Arguments + /// + /// * `computed_hashes` - The number of hashes computed in this mining round + /// * `duration` - How long it took to compute the indicated number of hashes + pub fn update_immediate(&mut self, computed_hashes: u128, duration: Duration) { + (self.total_computed_hashes, self.total_mining_duration) = ( + self.total_computed_hashes + .checked_add(computed_hashes) + .unwrap(), + self.total_mining_duration.checked_add(duration).unwrap(), + ); + } + + /// Gets a handle to update these statistics safely, even in the event of an error. + pub fn update_safe(&mut self) -> MinerStatisticsUpdater { + MinerStatisticsUpdater { + statistics: self, + computed_hashes: 0, + start_time: Instant::now(), + } + } + + /// Gets a human-readable indication of the current mining hash rate. + pub fn hash_rate_units(&self) -> UnitsPrefixed { + UnitsPrefixed { + value: self.total_computed_hashes as f64, + unit_name: "H", + duration: Some(self.total_mining_duration), + } + } +} + +/// A handle for eventually and safely updating a `MinerStatistics`. +pub struct MinerStatisticsUpdater<'a> { + statistics: &'a mut MinerStatistics, + computed_hashes: u128, + start_time: Instant, +} + +impl<'a> MinerStatisticsUpdater<'a> { + pub fn computed_hashes(&mut self, count: u128) { + self.computed_hashes += count; + } +} + +impl<'a> Drop for MinerStatisticsUpdater<'a> { + fn drop(&mut self) { + self.statistics + .update_immediate(self.computed_hashes, self.start_time.elapsed()) + } +} + +impl fmt::Display for MinerStatistics { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Computed {} hashes in {:.3}s, speed: {:.3}", + self.total_computed_hashes, + self.total_mining_duration.as_secs_f64(), + self.hash_rate_units() + ) + } +} + +/// An error which is thrown by a miner. +#[derive(Debug)] +pub enum MineError { + GetDifficulty(CompactTargetError), + GetLeadingTrailingBytes(PoWError), + Wrapped(Box), +} + +impl MineError { + pub fn wrap(value: E) -> Self { + Self::Wrapped(Box::new(value)) + } +} + +impl std::error::Error for MineError {} + +impl fmt::Display for MineError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::GetDifficulty(cause) => { + write!(f, "Unable to determine PoW difficulty: {}", cause) + } + Self::GetLeadingTrailingBytes(cause) => write!( + f, + "Unable to get leading and trailing bytes for PoW object: {}", + cause + ), + Self::Wrapped(cause) => write!(f, "An error occurred while mining: {}", cause), + } + } +} + +pub trait Sha3_256PoWMiner: Debug { + /// Returns true if this miner is hardware-accelerated. + fn is_hw_accelerated(&self) -> bool; + + /// Returns the recommended minimum number of nonces which this implementation should compute + /// at a time. Calling `generate_pow_block_internal` with a `nonce_count` smaller than this + /// will likely cause the implementation's overhead to exceed any potential performance + /// advantages. + fn min_nonce_count(&self) -> u32; + + /// Returns the recommended number of nonces to test at a time. + /// + /// The number is implementation-defined, but should be a value which results in minimal + /// overhead while also preventing unnecessarily long wait times from a single mining + /// iteration. + fn nonce_peel_amount(&self) -> u32; + + /// Tries to generate a Proof-of-Work for the given bytes. + /// + /// This will try to generate a Proof-of-Work by inserting 4-byte (32-bit) nonces (in + /// little-endian encoding) between the given leading and trailing byte sequences and hashing + /// the result. It will return the first nonce which met the given difficulty requirements, or + /// `None` if none of the nonces could meet the difficulty requirements. + /// + /// ### Arguments + /// + /// * `leading_bytes` - The bytes to hash before the 4-byte nonce + /// * `trailing_bytes` - The bytes to after the 4-byte nonce + /// * `difficulty` - The difficulty requirements + /// * `first_nonce` - The first nonce to test + /// * `nonce_count` - The number of nonces to test + /// * `statistics` - A `MinerStatistics` instance to be updated with information + /// about the mining progress + fn generate_pow_internal( + &mut self, + leading_bytes: &[u8], + trailing_bytes: &[u8], + difficulty: &PoWDifficulty, + first_nonce: u32, + nonce_count: u32, + statistics: &mut MinerStatistics, + ) -> Result, MineError>; +} + +/// Tries to generate a Proof-of-Work for the given object. +/// +/// This will try to generate a Proof-of-Work by inserting nonces between the given leading and +/// trailing byte sequences and hashing the result. It will return the first nonce which met the +/// given difficulty requirements, or `None` if none of the nonces could meet the object's +/// difficulty requirements. +/// +/// ### Arguments +/// +/// * `object` - The object to be mined +/// * `statistics` - A `MinerStatistics` instance to be updated with information +/// about the mining progress +/// * `terminate_flag` - If set, this is a reference to an externally mutable boolean flag. +/// Setting the value to `true` will signal the miner to stop. +/// * `timeout_duration` - If set, this is the maximum duration which the miner will run for. +/// The miner will stop after approximately this duration, regardless +/// of whether a result was found. +pub fn generate_pow( + miner: &mut dyn Sha3_256PoWMiner, + object: &O, + statistics: &mut MinerStatistics, + terminate_flag: Option>, + timeout_duration: Option, +) -> Result { + let total_start_time = Instant::now(); + + let difficulty = object.pow_difficulty().map_err(MineError::GetDifficulty)?; + let peel_amount = miner.nonce_peel_amount(); + + // Try to generate nonces of every length + for nonce_length in ::permitted_nonce_lengths() { + if nonce_length < std::mem::size_of::() { + // SHA3_256PoWMiner only supports inserting exactly 4 bytes of nonce between objects, + // so we'll skip all nonce lengths lower than 4 + continue; + } + + // Precompute the leading and trailing bytes for the object assuming a `nonce_length`-byte + // nonce. + let (leading_bytes, trailing_bytes) = object + .get_leading_and_trailing_bytes_for_mine(nonce_length) + .map_err(MineError::GetLeadingTrailingBytes)?; + + // Since SHA3_256PoWMiner always inserts exactly 4 bytes of nonce, we'll need to provide + // an additional `nonce_length - 4` bytes so that the resulting nonce does end up being + // exactly `nonce_length` bytes long. + for nonce_supplement in all_byte_strings(nonce_length - std::mem::size_of::()) { + // In theory, it shouldn't really matter whether we put the nonce supplement before + // or after the 4-byte nonce inserted by the miner, but the miner implementation may + // prefer the leading/trailing bytes to be aligned on some boundary? That's a potential + // optimization for The Future(tm). + // For now, we'll always put the nonce supplement after the 4-byte nonce. + let leading_bytes = &leading_bytes; + let trailing_bytes = [nonce_supplement.as_ref(), trailing_bytes.as_ref()].concat(); + + // Have the miner try `peel_amount` nonces at a time. This value should be chosen + // appropriately so that mining doesn't take unnecessarily long, or cause other + // undesirable side effects like locking up the GPU for so long that the desktop + // environment crashes. + for (first_nonce, nonce_count) in split_range_into_blocks(0, u32::MAX, peel_amount) { + let result = miner.generate_pow_internal( + &leading_bytes, + &trailing_bytes, + &difficulty, + first_nonce, + nonce_count, + statistics, + )?; + + info!("Mining statistics: {}", statistics); + + if let Some(nonce_4byte) = result { + let full_nonce = + [&nonce_4byte.to_le_bytes(), nonce_supplement.as_ref()].concat(); + info!("Miner found nonce: {:?}", full_nonce); + return Ok(MineResult::FoundNonce { nonce: full_nonce }); + } + + // If a termination flag was provided and is set to true, stop mining now. + if let Some(terminate_flag) = &terminate_flag { + if terminate_flag.load(Ordering::Acquire) { + debug!("Miner terminating after being requested to terminate"); + return Ok(MineResult::TerminateRequested); + } + } + + // If a timeout duration was provided and has been reached, stop mining now. + if let Some(timeout_duration) = &timeout_duration { + let total_elapsed_time = total_start_time.elapsed(); + if total_elapsed_time >= *timeout_duration { + debug!( + "Miner terminating after reaching timeout (timeout={}s, elapsed time={}s)", + timeout_duration.as_secs_f64(), + total_elapsed_time.as_secs_f64()); + return Ok(MineResult::TimeoutReached); + } + } + } + } + } + + Ok(MineResult::Exhausted) +} + +static OPENGL_ERRORED: OnceLock<()> = OnceLock::new(); + +/// Creates a miner. +/// +/// ### Arguments +/// +/// * `difficulty` - An optional hint for the difficulty of the resource that will be mined, to +/// help choose an optimal miner implementation. +pub fn create_any_miner(difficulty: Option<&PoWDifficulty>) -> Arc> { + if let Some(difficulty) = difficulty { + match difficulty { + // If the difficulty is sufficiently low that the overhead of a GPU miner would make + // things slower, don't bother! + PoWDifficulty::TargetHashAlwaysPass + | PoWDifficulty::LeadingZeroBytes { + leading_zeroes: ..=1, + } => return Arc::new(Mutex::new(CpuMiner::new())), + _ => (), + } + } + + match vulkan::VulkanMiner::get() { + Ok(miner) => return miner.clone(), + Err(cause) => warn!("Failed to create Vulkan miner: {cause}"), + }; + + if OPENGL_ERRORED.get().is_none() { + // Previous attempts to create an OpenGL miner have succeeded, or we haven't tried yet + match opengl::OpenGlMiner::new() { + Ok(miner) => return Arc::new(Mutex::new(miner)), + Err(cause) => { + warn!("Failed to create OpenGL miner: {cause}"); + + // Remember that OpenGL miner creation failed, so we don't keep trying over and over + // on subsequent attempts. + OPENGL_ERRORED.get_or_init(|| ()); + } + }; + } + + Arc::new(Mutex::new(CpuMiner::new())) +} + +#[cfg(test)] +pub(super) mod test { + use super::*; + use crate::miner_pow::cpu::CpuMiner; + use crate::miner_pow::opengl::OpenGlMiner; + use crate::miner_pow::vulkan::VulkanMiner; + + #[derive(Copy, Clone, Debug)] + pub struct TestBlockMinerInternal { + pub name: &'static str, + pub difficulty: &'static [u8], + pub expected_nonce: u32, + pub max_nonce_count: u32, + pub requires_hw_accel: (bool, bool), + } + + impl TestBlockMinerInternal { + const NO_DIFFICULTY: Self = Self { + name: "NO_DIFFICULTY", + difficulty: &[], + expected_nonce: 455, + max_nonce_count: 1024, + requires_hw_accel: (false, false), + }; + const THRESHOLD_EASY: Self = Self { + name: "THRESHOLD_EASY", + difficulty: b"\x22\x00\x00\x01", + expected_nonce: 28, + max_nonce_count: 1024, + requires_hw_accel: (false, false), + }; + const THRESHOLD_HARD: Self = Self { + name: "THRESHOLD_HARD", + difficulty: b"\x20\x00\x00\x01", + expected_nonce: 4894069, + max_nonce_count: 4900000, + requires_hw_accel: (true, false), + }; + const THRESHOLD_VERY_HARD: Self = Self { + name: "THRESHOLD_VERY_HARD", + difficulty: b"\x1f\x00\x00\xFF", + expected_nonce: 14801080, + max_nonce_count: 15000000, + requires_hw_accel: (true, true), + }; + + pub const ALL_TEST: &'static [TestBlockMinerInternal] = &[ + Self::NO_DIFFICULTY, + Self::THRESHOLD_EASY, + Self::THRESHOLD_HARD, + Self::THRESHOLD_VERY_HARD, + ]; + + pub const ALL_BENCH: &'static [TestBlockMinerInternal] = &[ + Self::NO_DIFFICULTY, + Self::THRESHOLD_EASY, + Self::THRESHOLD_HARD, + Self::THRESHOLD_VERY_HARD, + ]; + + pub fn test_miner(&self, miner: &mut impl Sha3_256PoWMiner, is_bench: bool) { + if !miner.is_hw_accelerated() + && if is_bench { + self.requires_hw_accel.1 + } else { + self.requires_hw_accel.0 + } + { + println!("Skipping test case {} (too hard)", self.name); + return; + } + + let block_header = test_block_header(self.difficulty); + + let difficulty = block_header.pow_difficulty().expect(self.name); + let (leading_bytes, trailing_bytes) = block_header + .get_leading_and_trailing_bytes_for_mine(4) + .expect(self.name); + + let mut statistics = Default::default(); + assert_eq!( + miner + .generate_pow_internal( + &leading_bytes, + &trailing_bytes, + &difficulty, + 0, + self.max_nonce_count, + &mut statistics, + ) + .expect(self.name), + Some(self.expected_nonce), + "Test case {:?}", + self + ); + + println!("Test case {} statistics: {}", self.name, statistics); + } + } + + pub const TEST_MINING_DIFFICULTY: &'static [u8] = b"\x22\x00\x00\x01"; + + fn test_block_header(difficulty: &[u8]) -> BlockHeader { + BlockHeader { + version: 1337, + bits: 10973, + nonce_and_mining_tx_hash: (vec![], "abcde".to_string()), + b_num: 2398927, + timestamp: 29837637, + difficulty: difficulty.to_vec(), + seed_value: b"2983zuifsigezd".to_vec(), + previous_hash: Some("jeff".to_string()), + txs_merkle_root_and_hash: ("merkle_root".to_string(), "hash".to_string()), + } + } + + #[test] + fn test_big_integer_behaves_as_expected() { + use rug::Integer; + use std::cmp::Ordering::{self, *}; + type DigestArr = [u8; SHA3_256_BYTES]; + + fn to_int(digits: &[u8]) -> Integer { + let res = Integer::from_digits(digits, rug::integer::Order::MsfBe); + assert_eq!( + res, + Integer::from_digits(digits, rug::integer::Order::MsfLe) + ); + res + } + + fn test(hash: &[u8], target: &[u8], order: Ordering) { + let (hash, target) = (to_int(hash), to_int(target)); + assert_eq!( + PartialOrd::partial_cmp(&hash, &target), + Some(order), + "hash: {hash}, target: {target}" + ); + } + + test(b"\x00", b"\x00", Equal); + test(b"\x02", b"\x00", Greater); + test(b"\x02", b"\x80", Less); + + test(b"\x0201234567", b"\x8001234567", Less); + test(b"\x8001234567", b"\x8001234567", Equal); + test( + b"01234567890abcde01234567890abcde", + b"01234567890abcde01234567890abcde", + Equal, + ); + test( + b"01234567890abcde01234567890abcde", + b"91234567890abcde01234567890abcde", + Less, + ); + test( + b"01234567890abcde01234567890abcde", + b"01234567890abcde91234567890abcde", + Less, + ); + } + + #[test] + fn test_expand_compact_target_difficulty() { + fn test_hex(compact_target: u32, target_hash_hex: Option<&str>) { + let target_hash = expand_compact_target_difficulty(CompactTarget::from_array( + compact_target.to_be_bytes(), + )) + .map(|h| hex::encode_upper(&h)); + assert_eq!( + target_hash.as_ref().map(|s| s.as_str()), + target_hash_hex, + "compact_target=0x{:08x}", + compact_target + ) + } + + test_hex( + 0x00000000, + Some("0000000000000000000000000000000000000000000000000000000000000000"), + ); + test_hex( + 0x03000000, + Some("0000000000000000000000000000000000000000000000000000000000000000"), + ); + test_hex( + 0xFF000000, + Some("0000000000000000000000000000000000000000000000000000000000000000"), + ); + + test_hex( + 0x03000001, + Some("0000000000000000000000000000000000000000000000000000000000000001"), + ); + test_hex( + 0x037FFFFF, + Some("00000000000000000000000000000000000000000000000000000000007FFFFF"), + ); + test_hex( + 0x03FFFFFF, + Some("00000000000000000000000000000000000000000000000000000000007FFFFF"), + ); + test_hex( + 0x02FFFFFF, + Some("0000000000000000000000000000000000000000000000000000000000007FFF"), + ); + test_hex( + 0x01FFFFFF, + Some("000000000000000000000000000000000000000000000000000000000000007F"), + ); + test_hex( + 0x00FFFFFF, + Some("0000000000000000000000000000000000000000000000000000000000000000"), + ); + + test_hex( + 0x22000001, + Some("0100000000000000000000000000000000000000000000000000000000000000"), + ); + + test_hex(0xFFFFFFFF, None); + } + + #[test] + fn verify_cpu() { + let mut miner = CpuMiner::new(); + for case in TestBlockMinerInternal::ALL_TEST { + case.test_miner(&mut miner, false); + } + } + + #[test] + fn verify_opengl() { + let mut miner = OpenGlMiner::new().unwrap(); + for case in TestBlockMinerInternal::ALL_TEST { + case.test_miner(&mut miner, false); + } + } + + #[test] + fn verify_vulkan() { + let miner = VulkanMiner::get().unwrap(); + for case in TestBlockMinerInternal::ALL_TEST { + case.test_miner(&mut *miner.lock().unwrap(), false); + } + } +} + +// cargo bench --package aiblock_network --lib miner_pow::bench --features benchmark_miners -- --show-output --test +#[cfg(test)] +#[cfg(feature = "benchmark_miners")] +mod bench { + use super::test::*; + use super::*; + use crate::miner_pow::cpu::CpuMiner; + use crate::miner_pow::opengl::OpenGlMiner; + use crate::miner_pow::vulkan::VulkanMiner; + + #[test] + fn bench_cpu() { + let mut miner = CpuMiner::new(); + for case in TestBlockMinerInternal::ALL_BENCH { + case.test_miner(&mut miner, true); + } + } + + #[test] + fn bench_opengl() { + let mut miner = OpenGlMiner::new().unwrap(); + for case in TestBlockMinerInternal::ALL_BENCH { + case.test_miner(&mut miner, true); + } + } + + #[test] + fn bench_vulkan() { + let miner = VulkanMiner::get().unwrap(); + for case in TestBlockMinerInternal::ALL_BENCH { + case.test_miner(&mut *miner.lock().unwrap(), true); + } + } +} diff --git a/src/miner_pow/opengl.rs b/src/miner_pow/opengl.rs new file mode 100644 index 0000000..d65ccce --- /dev/null +++ b/src/miner_pow/opengl.rs @@ -0,0 +1,1125 @@ +use crate::miner_pow::opengl::gl_error::{ + AddContext, CompileShaderError, GlError, LinkProgramError, +}; +use crate::miner_pow::opengl::gl_wrapper::{ + Buffer, GetIntIndexedType, GetProgramIntType, GetStringType, ImmutableBuffer, + IndexedBufferTarget, MemoryBarrierBit, Program, Shader, ShaderType, UniformLocation, +}; +use crate::miner_pow::{MineError, MinerStatistics, PoWDifficulty, Sha3_256PoWMiner}; +use crate::utils::split_range_into_blocks; +use gl::types::*; +use glfw::{ + Context, Glfw, GlfwReceiver, OpenGlProfileHint, PWindow, WindowEvent, WindowHint, WindowMode, +}; +use std::convert::TryInto; +use std::ffi::CStr; +use std::sync::{Mutex, MutexGuard, OnceLock}; +use std::{error, fmt}; +use tracing::{debug, info, warn}; + +// libglfw3-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev + +#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +pub enum OpenGlMinerError { + LockFailed, + InitializeGlfwMsg(glfw::Error, String), + InitializeGlfw(glfw::InitError), + CreateGlfwWindow, + CompileShader(CompileShaderError), + LinkProgram(LinkProgramError), + GlError(GlError), +} + +impl fmt::Display for OpenGlMinerError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::LockFailed => f.write_str("couldn't acquire global lock for using GLFW context"), + Self::InitializeGlfwMsg(cause, msg) => write!(f, "Failed to initialize GLFW: {cause}: {msg}"), + Self::InitializeGlfw(cause) => write!(f, "Failed to initialize GLFW: {cause}"), + Self::CreateGlfwWindow => f.write_str("Failed to create GLFW window"), + Self::CompileShader(cause) => write!(f, "Failed to compile shader: {cause}"), + Self::LinkProgram(cause) => write!(f, "Failed to link shader program: {cause}"), + Self::GlError(cause) => write!( + f, + "An OpenGL error occurred while initializing the GPU miner: {cause}" + ), + } + } +} + +impl error::Error for OpenGlMinerError {} + +#[derive(Debug)] +struct GlfwContext { + mutex: Mutex, + + glfw_window: PWindow, + glfw_events: GlfwReceiver<(f64, WindowEvent)>, + glfw: Glfw, + glfw_mutex_guard: MutexGuard<'static, ()>, +} + +impl Drop for GlfwContext { + fn drop(&mut self) { + debug!( + "Thread {} is destroying the GLFW context", + std::thread::current().name().unwrap_or("") + ); + } +} + +#[derive(Debug)] +pub struct GlfwContextGuard<'glfw, Ctx: fmt::Debug> { + mutex_guard: MutexGuard<'glfw, Ctx>, +} + +impl<'glfw, Ctx: fmt::Debug> std::ops::Deref for GlfwContextGuard<'glfw, Ctx> { + type Target = Ctx; + + fn deref(&self) -> &Self::Target { + &*self.mutex_guard + } +} + +impl<'glfw, Ctx: fmt::Debug> std::ops::DerefMut for GlfwContextGuard<'glfw, Ctx> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut *self.mutex_guard + } +} + +impl<'glfw, Ctx: fmt::Debug> Drop for GlfwContextGuard<'glfw, Ctx> { + fn drop(&mut self) { + // un-bind the context before releasing the context's mutex + debug!( + "Thread {} is releasing the GLFW context", + std::thread::current().name().unwrap_or("") + ); + glfw::make_context_current(None); + } +} + +static GLFW_MUTEX: Mutex<()> = Mutex::new(()); +static mut GLFW_INIT_ERR: Option<(glfw::Error, String)> = None; + +impl GlfwContext { + fn new(f: impl FnOnce() -> Result) -> Result { + debug!( + "Thread {} is creating the GLFW context", + std::thread::current().name().unwrap_or("") + ); + + let glfw_mutex_guard = GLFW_MUTEX + .lock() + .map_err(|_| OpenGlMinerError::LockFailed)?; + + // This is hacky, but glfw::init demands a static callback function. + // We'll save the error in a static field and then test it afterward to see if the error + // callback actually fired. If so, return the descriptive error provided by the callback. + let mut glfw = { + unsafe { GLFW_INIT_ERR = None }; + let init = glfw::init(|err, msg| unsafe { GLFW_INIT_ERR = Some((err, msg)) }); + if let Some((err, msg)) = unsafe { GLFW_INIT_ERR.take() } { + return Err(OpenGlMinerError::InitializeGlfwMsg(err, msg)); + } + init.map_err(OpenGlMinerError::InitializeGlfw)? + }; + + glfw.window_hint(WindowHint::ContextVersion(4, 5)); + glfw.window_hint(WindowHint::OpenGlProfile(OpenGlProfileHint::Core)); + glfw.window_hint(WindowHint::Visible(false)); + let (mut window, events) = glfw + .create_window(256, 256, "AIBlock Miner", WindowMode::Windowed) + .ok_or(OpenGlMinerError::CreateGlfwWindow)?; + + gl::load_with(|name| window.get_proc_address(name) as *const _); + + let vendor = gl_wrapper::get_string(GetStringType::VENDOR).unwrap(); + let renderer = gl_wrapper::get_string(GetStringType::RENDERER).unwrap(); + let version = gl_wrapper::get_string(GetStringType::VERSION).unwrap(); + let glsl_version = gl_wrapper::get_string(GetStringType::SHADING_LANGUAGE_VERSION).unwrap(); + info!("Created OpenGL context:\n Vendor: {vendor}\n Renderer: {renderer}\n Version: {version}\n GLSL version: {glsl_version}"); + + let ctx = f()?; + + glfw::make_context_current(None); + + Ok(Self { + mutex: Mutex::new(ctx), + glfw_window: window, + glfw_events: events, + glfw, + glfw_mutex_guard, + }) + } + + fn make_current(&mut self) -> GlfwContextGuard { + let guard = self.mutex.lock().unwrap(); + debug!( + "Thread {} is acquiring the GLFW context", + std::thread::current().name().unwrap_or("") + ); + self.glfw_window.make_current(); + GlfwContextGuard { mutex_guard: guard } + } +} + +#[derive(Debug)] +pub struct OpenGlMiner { + program: Program, + program_first_nonce_uniform: UniformLocation, + program_header_length_uniform: UniformLocation, + program_header_nonce_offset_uniform: UniformLocation, + program_difficulty_function_uniform: UniformLocation, + program_leading_zeroes_mining_difficulty_uniform: UniformLocation, + program_compact_target_expanded_uniform: UniformLocation, + program_work_group_size: u32, + max_work_group_count: u32, + + glfw_context: GlfwContext<()>, +} + +static mut GLOBAL_MINER: OnceLock> = OnceLock::new(); + +impl OpenGlMiner { + pub fn new() -> Result { + let mut glfw_context = GlfwContext::new(|| Ok(()))?; + let guard = glfw_context.make_current(); + + debug!("compiling shader..."); + + // compile and link the compute shader + let shader = Shader::compile( + ShaderType::Compute, + &[include_str!("opengl_miner.glsl")], + //&[std::fs::read_to_string("src/miner_pow/opengl_miner.glsl").expect("failed to read source").as_str()] + ) + .map_err(OpenGlMinerError::CompileShader)?; + + let program = Program::link(&[&shader]).map_err(OpenGlMinerError::LinkProgram)?; + + // figure out the compute shader's work group size + let work_group_size_arr = program + .get_program_int::<3>(GetProgramIntType::ComputeWorkGroupSize) + .map_err(OpenGlMinerError::GlError)?; + assert_eq!( + &work_group_size_arr[1..], + &[1; 2], + "work group size should be 1 on y and z axes..." + ); + + let max_work_group_count = + gl_wrapper::get_int(GetIntIndexedType::MAX_COMPUTE_WORK_GROUP_COUNT, 0) + .map_err(OpenGlMinerError::GlError)? as u32; + + let program_first_nonce_uniform = program.uniform_location("u_firstNonce").unwrap(); + let program_header_length_uniform = + program.uniform_location("u_blockHeader_length").unwrap(); + let program_header_nonce_offset_uniform = program + .uniform_location("u_blockHeader_nonceOffset") + .unwrap(); + let program_difficulty_function_uniform = + program.uniform_location("u_difficultyFunction").unwrap(); + let program_leading_zeroes_mining_difficulty_uniform = program + .uniform_location("u_leadingZeroes_miningDifficulty") + .unwrap(); + let program_compact_target_expanded_uniform = program + .uniform_location("u_compactTarget_expanded") + .unwrap(); + + drop(guard); + + Ok(Self { + program_first_nonce_uniform, + program_header_length_uniform, + program_header_nonce_offset_uniform, + program_difficulty_function_uniform, + program_leading_zeroes_mining_difficulty_uniform, + program_compact_target_expanded_uniform, + program_work_group_size: work_group_size_arr[0].try_into().unwrap(), + max_work_group_count, + program, + + glfw_context, + }) + } +} + +impl Sha3_256PoWMiner for OpenGlMiner { + fn is_hw_accelerated(&self) -> bool { + true + } + + fn min_nonce_count(&self) -> u32 { + self.program_work_group_size + } + + fn nonce_peel_amount(&self) -> u32 { + // TODO: Auto-detect this based on device speed + 1u32 << 20 + } + + fn generate_pow_internal( + &mut self, + leading_bytes: &[u8], + trailing_bytes: &[u8], + difficulty: &PoWDifficulty, + first_nonce: u32, + nonce_count: u32, + statistics: &mut MinerStatistics, + ) -> Result, MineError> { + if nonce_count == 0 { + return Ok(None); + } + + let dispatch_count = nonce_count.div_ceil(self.program_work_group_size); + + if dispatch_count > self.max_work_group_count { + warn!( + "OpenGL miner dispatched with too high nonce_count {} (work group size={}, max \ + work group count={})! Splitting into multiple dispatches.", + nonce_count, self.program_work_group_size, self.max_work_group_count + ); + + for (first, count) in split_range_into_blocks( + first_nonce, + nonce_count, + self.max_work_group_count * self.program_work_group_size, + ) { + match self.generate_pow_internal( + leading_bytes, + trailing_bytes, + difficulty, + first, + count, + statistics, + )? { + Some(nonce) => return Ok(Some(nonce)), + None => (), + }; + } + return Ok(None); + } + + let mut stats_updater = statistics.update_safe(); + + let block_header_nonce_offset = leading_bytes.len().try_into().unwrap(); + let header_bytes = [leading_bytes, &0u32.to_le_bytes(), trailing_bytes].concat(); + let block_header_length = header_bytes.len().try_into().unwrap(); + + let _guard = self.glfw_context.make_current(); + + self.program + .set_uniform_1ui(self.program_first_nonce_uniform, first_nonce) + .map_err(MineError::wrap)?; + self.program + .set_uniform_1ui(self.program_header_length_uniform, block_header_length) + .map_err(MineError::wrap)?; + self.program + .set_uniform_1ui( + self.program_header_nonce_offset_uniform, + block_header_nonce_offset, + ) + .map_err(MineError::wrap)?; + + match difficulty { + // The target value is higher than the largest possible SHA3-256 hash. Therefore, + // every hash will meet the required difficulty threshold, so we can just return + // an arbitrary nonce. + PoWDifficulty::TargetHashAlwaysPass => return Ok(Some(first_nonce)), + + PoWDifficulty::LeadingZeroBytes { leading_zeroes } => { + // DIFFICULTY_FUNCTION_LEADING_ZEROES + self.program + .set_uniform_1ui(self.program_difficulty_function_uniform, 1) + .map_err(MineError::wrap)?; + self.program + .set_uniform_1ui( + self.program_leading_zeroes_mining_difficulty_uniform, + (*leading_zeroes).try_into().unwrap(), + ) + .map_err(MineError::wrap)?; + } + PoWDifficulty::TargetHash { target_hash } => { + // DIFFICULTY_FUNCTION_COMPACT_TARGET + self.program + .set_uniform_1ui(self.program_difficulty_function_uniform, 2) + .map_err(MineError::wrap)?; + self.program + .set_uniform_1uiv( + self.program_compact_target_expanded_uniform, + &target_hash.map(|b| b as u32), + ) + .map_err(MineError::wrap)?; + } + } + + let block_header_buffer_data = header_bytes + .iter() + .map(|b| (*b as u32).to_ne_bytes()) + .collect::>() + .concat(); + let block_header_buffer = ImmutableBuffer::new_initialized(&block_header_buffer_data, 0) + .add_context("BlockHeader buffer") + .map_err(MineError::wrap)?; + + const RESPONSE_BUFFER_CAPACITY: usize = 12; + let response_buffer_data = [0u32, u32::MAX, 0u32]; + let mut response_buffer_data: [u8; RESPONSE_BUFFER_CAPACITY] = + unsafe { std::mem::transmute(response_buffer_data.map(u32::to_ne_bytes)) }; + let response_buffer = ImmutableBuffer::new_initialized(&response_buffer_data, 0) + .add_context("Response buffer") + .map_err(MineError::wrap)?; + + block_header_buffer + .bind_base(IndexedBufferTarget::ShaderStorageBuffer, 0) + .unwrap(); + response_buffer + .bind_base(IndexedBufferTarget::ShaderStorageBuffer, 2) + .unwrap(); + + self.program.bind().unwrap(); + + gl_wrapper::dispatch_compute(dispatch_count, 1, 1).map_err(MineError::wrap)?; + gl_wrapper::memory_barrier(&[MemoryBarrierBit::BufferUpdate]).map_err(MineError::wrap)?; + + response_buffer + .download_sub_data(0, &mut response_buffer_data) + .unwrap(); + + // Now that the hashes have been computed, update the statistics + stats_updater + .computed_hashes((dispatch_count as u128) * (self.program_work_group_size as u128)); + + let response_status = u32::from_ne_bytes(*response_buffer_data[0..].first_chunk().unwrap()); + let response_success_nonce = + u32::from_ne_bytes(*response_buffer_data[4..].first_chunk().unwrap()); + let response_error_code = + u32::from_ne_bytes(*response_buffer_data[8..].first_chunk().unwrap()); + + match response_status { + // RESPONSE_STATUS_NONE + 0 => Ok(None), + // RESPONSE_STATUS_SUCCESS + 1 => Ok(Some(response_success_nonce)), + // RESPONSE_STATUS_ERROR_CODE + 2 => panic!( + "compute shader returned error code 0x{:04x}: {}", + response_error_code, + match response_error_code { + 1 => "INVALID_DIFFICULTY_FUNCTION", + _ => "(unknown)", + } + ), + _ => panic!( + "compute shader returned unknown response status 0x{:04x}", + response_status + ), + } + } +} + +pub mod gl_error { + use super::*; + use std::{error, fmt}; + + pub(super) trait ErrorContext: Sized { + fn into_string(self) -> String; + + fn into_boxed_str(self) -> Box { + self.into_string().into_boxed_str() + } + } + + impl<'a> ErrorContext for &'a str { + fn into_string(self) -> String { + self.into() + } + } + + impl<'a> ErrorContext for fmt::Arguments<'a> { + fn into_string(self) -> String { + fmt::format(self) + } + } + + impl String> ErrorContext for F { + fn into_string(self) -> String { + self() + } + } + + pub(super) trait AddContext { + fn add_context(self, message: impl ErrorContext) -> Self; + } + + impl AddContext for Result { + fn add_context(self, message: impl ErrorContext) -> Self { + self.map_err(|err| err.add_context(message)) + } + } + + /// An OpenGL error code + #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] + pub enum GlError { + InvalidEnum, + InvalidValue, + InvalidOperation, + InvalidFramebufferOperation, + OutOfMemory, + StackUnderflow, + StackOverflow, + Unknown(GLenum), + WrappedWithMessage { + cause: Box, + message: String, + }, + } + + impl fmt::Display for GlError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::InvalidEnum => f.write_str("GL_INVALID_ENUM: An unacceptable value is specified for an enumerated argument. The offending command is ignored and has no other side effect than to set the error flag."), + Self::InvalidValue => f.write_str("GL_INVALID_VALUE: A numeric argument is out of range. The offending command is ignored and has no other side effect than to set the error flag."), + Self::InvalidOperation => f.write_str("GL_INVALID_OPERATION: The specified operation is not allowed in the current state. The offending command is ignored and has no other side effect than to set the error flag."), + Self::InvalidFramebufferOperation => f.write_str("GL_INVALID_FRAMEBUFFER_OPERATION: The framebuffer object is not complete. The offending command is ignored and has no other side effect than to set the error flag."), + Self::OutOfMemory => f.write_str("GL_OUT_OF_MEMORY: There is not enough memory left to execute the command. The state of the GL is undefined, except for the state of the error flags, after this error is recorded."), + Self::StackUnderflow => f.write_str("GL_STACK_UNDERFLOW: An attempt has been made to perform an operation that would cause an internal stack to underflow."), + Self::StackOverflow => f.write_str("GL_STACK_OVERFLOW: An attempt has been made to perform an operation that would cause an internal stack to overflow."), + Self::Unknown(id) => write!(f, "Unknown OpenGL error: {:#04x}", id), + Self::WrappedWithMessage { cause, message } => write!(f, "{}: {}", message, cause), + } + } + } + + impl error::Error for GlError { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match self { + Self::WrappedWithMessage { cause, .. } => Some(&**cause), + _ => None, + } + } + } + + impl AddContext for GlError { + fn add_context(self, message: impl ErrorContext) -> Self { + Self::WrappedWithMessage { + cause: Box::new(self), + message: message.into_string(), + } + } + } + + impl GlError { + /// Checks the OpenGL context for errors + pub(super) fn check() -> Result<(), Self> { + match unsafe { gl::GetError() } { + gl::NO_ERROR => Ok(()), + gl::INVALID_ENUM => Err(Self::InvalidEnum), + gl::INVALID_VALUE => Err(Self::InvalidValue), + gl::INVALID_OPERATION => Err(Self::InvalidOperation), + gl::INVALID_FRAMEBUFFER_OPERATION => Err(Self::InvalidFramebufferOperation), + gl::OUT_OF_MEMORY => Err(Self::OutOfMemory), + gl::STACK_UNDERFLOW => Err(Self::StackUnderflow), + gl::STACK_OVERFLOW => Err(Self::StackOverflow), + id => Err(Self::Unknown(id)), + } + } + + /// Checks the OpenGL context for errors + pub(super) fn check_msg(message: impl ErrorContext) -> Result<(), Self> { + Self::check().add_context(message) + } + } + + /// An error which can occur while compiling an OpenGL shader + #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] + pub enum CompileShaderError { + OpenGlError { + shader_type: ShaderType, + cause: GlError, + }, + CompilationFailed { + shader_type: ShaderType, + info_log: Box, + }, + } + + impl fmt::Display for CompileShaderError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::OpenGlError { shader_type, cause } => write!( + f, + "OpenGL error occurred while compiling {shader_type:?}: {cause}" + ), + Self::CompilationFailed { + shader_type, + info_log: message, + } => write!(f, "Failed to compile {shader_type:?}: {message}"), + } + } + } + + impl error::Error for CompileShaderError { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match self { + Self::OpenGlError { cause, .. } => Some(&*cause), + _ => None, + } + } + } + + /// An error which can occur while linking an OpenGL program + #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] + pub enum LinkProgramError { + OpenGlError(GlError), + LinkFailed { info_log: Box }, + } + + impl fmt::Display for LinkProgramError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::OpenGlError(cause) => write!( + f, + "OpenGL error occurred while linking an OpenGL shader: {cause}" + ), + Self::LinkFailed { info_log } => { + write!(f, "Failed to link an OpenGL shader program: {info_log}") + } + } + } + } + + impl error::Error for LinkProgramError { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match self { + Self::OpenGlError(cause) => Some(&*cause), + _ => None, + } + } + } +} + +#[allow(non_camel_case_types)] +mod gl_wrapper { + use super::*; + use gl_error::*; + use std::convert::TryInto; + use std::ffi::CString; + use std::ptr::null; + + #[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] + #[repr(u32)] + pub enum GetStringType { + VENDOR = gl::VENDOR, + RENDERER = gl::RENDERER, + VERSION = gl::VERSION, + SHADING_LANGUAGE_VERSION = gl::SHADING_LANGUAGE_VERSION, + } + + /// Wrapper around `glGetString` + pub fn get_string(t: GetStringType) -> Result<&'static str, GlError> { + let cptr = unsafe { gl::GetString(t as GLenum) }; + GlError::check_msg(format_args!("glGetString(GL_{t:?})"))?; + let cstr = unsafe { CStr::from_ptr(cptr as *const std::ffi::c_char) }; + Ok(cstr.to_str().unwrap()) + } + + #[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] + #[repr(u32)] + pub enum GetIntIndexedType { + MAX_COMPUTE_WORK_GROUP_COUNT = gl::MAX_COMPUTE_WORK_GROUP_COUNT, + } + + /// Wrapper around `glGetIntegeri_v` + pub fn get_int(t: GetIntIndexedType, index: GLuint) -> Result { + let mut value = 0; + unsafe { gl::GetIntegeri_v(t as GLenum, index, &mut value) }; + GlError::check_msg(format_args!("glGetIntegeri_v(GL_{t:?}, {index})"))?; + Ok(value) + } + + #[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] + #[repr(u32)] + pub enum MemoryBarrierBit { + BufferUpdate = gl::BUFFER_UPDATE_BARRIER_BIT, + } + + /// Wrapper around `glMemoryBarrier` + pub fn memory_barrier(bits: &[MemoryBarrierBit]) -> Result<(), GlError> { + let mask = bits + .iter() + .map(|bit| *bit as GLenum) + .reduce(::bitor) + .unwrap_or(0); + unsafe { gl::MemoryBarrier(mask) }; + GlError::check_msg(format_args!( + "glMemoryBarrier({bits:?}) -> glMemoryBarrier({mask:#08x})" + )) + } + + /// Wrapper around `glDispatchCompute` + pub fn dispatch_compute( + num_groups_x: u32, + num_groups_y: u32, + num_groups_z: u32, + ) -> Result<(), GlError> { + unsafe { gl::DispatchCompute(num_groups_x, num_groups_y, num_groups_z) }; + GlError::check_msg(format_args!( + "glDispatchCompute({num_groups_x}, {num_groups_y}, {num_groups_z})" + )) + } + + /// An OpenGL shader object + #[derive(Debug)] + pub struct Shader { + pub id: GLuint, + } + + #[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] + #[repr(u32)] + pub enum ShaderType { + Compute = gl::COMPUTE_SHADER, + } + + impl Shader { + /// Compiles an OpenGL shader. + /// + /// ### Arguments + /// + /// * `shader_type` - the type of shader + /// * `source` - the GLSL source code to compile + pub fn compile( + shader_type: ShaderType, + source: &[&str], + ) -> Result { + let check_error = |message: &'static str| -> Result<(), CompileShaderError> { + GlError::check_msg(message).map_err(|err| CompileShaderError::OpenGlError { + shader_type, + cause: err, + }) + }; + + let shader = Self { + id: unsafe { gl::CreateShader(shader_type as GLenum) }, + }; + check_error("glCreateShader")?; + + let count = source.len().try_into().unwrap(); + let (pointers, lengths): (Vec<_>, Vec<_>) = source + .iter() + .map(|str| { + ( + str.as_ptr() as *const GLchar, + >::try_into(str.len()).unwrap(), + ) + }) + .unzip(); + unsafe { gl::ShaderSource(shader.id, count, pointers.as_ptr(), lengths.as_ptr()) }; + check_error("glShaderSource")?; + + unsafe { gl::CompileShader(shader.id) }; + check_error("glCompileShader")?; + + let mut compile_status = 0; + unsafe { gl::GetShaderiv(shader.id, gl::COMPILE_STATUS, &mut compile_status) }; + check_error("glGetShaderiv(GL_COMPILE_STATUS)")?; + + let info_log: Box = { + let mut info_log_length = 0; + unsafe { gl::GetShaderiv(shader.id, gl::INFO_LOG_LENGTH, &mut info_log_length) }; + check_error("glGetShaderiv(GL_INFO_LOG_LENGTH)")?; + + if info_log_length != 0 { + let mut info_log = vec![0i8; info_log_length as usize]; + let mut actual_info_log_length: GLsizei = 0; + unsafe { + gl::GetShaderInfoLog( + shader.id, + info_log_length.into(), + &mut actual_info_log_length, + info_log.as_mut_ptr(), + ) + }; + check_error("glGetShaderInfoLog")?; + + let info_log_u8s = info_log.into_iter().map(|c| c as u8).collect::>(); + String::from_utf8_lossy(&info_log_u8s).as_ref().into() + } else { + Default::default() + } + }; + + if compile_status as GLboolean == gl::FALSE { + return Err(CompileShaderError::CompilationFailed { + shader_type, + info_log, + }); + } else if !info_log.is_empty() { + warn!("Shader compile warnings: {info_log}"); + } + + Ok(shader) + } + } + + impl Drop for Shader { + fn drop(&mut self) { + unsafe { gl::DeleteShader(self.id) } + } + } + + #[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] + #[repr(u32)] + pub enum GetProgramIntType { + ComputeWorkGroupSize = gl::COMPUTE_WORK_GROUP_SIZE, + } + + impl GetProgramIntType { + const fn element_count(&self) -> usize { + match self { + Self::ComputeWorkGroupSize => 3, + } + } + } + + /// The location of an OpenGL program uniform + #[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] + pub struct UniformLocation { + location: GLint, + name: &'static str, + } + + /// An OpenGL program object + #[derive(Debug)] + pub struct Program { + pub id: GLuint, + } + + impl Program { + /// Links an OpenGL program. + /// + /// ### Arguments + /// + /// * `shaders` - the shaders to be linked + /// * `source` - the GLSL source code to compile + pub fn link(shaders: &[&Shader]) -> Result { + fn check_error(message: &'static str) -> Result<(), LinkProgramError> { + GlError::check_msg(message).map_err(LinkProgramError::OpenGlError) + } + + let program = Self { + id: unsafe { gl::CreateProgram() }, + }; + check_error("glCreateProgram")?; + + // Attach the shaders to the program + for shader in shaders { + unsafe { gl::AttachShader(program.id, shader.id) }; + check_error("glAttachShader")?; + } + + unsafe { gl::LinkProgram(program.id) }; + check_error("glLinkProgram")?; + + // Detach the shaders from the program again + for shader in shaders { + unsafe { gl::DetachShader(program.id, shader.id) }; + check_error("glDetachShader")?; + } + + let mut link_status = 0; + unsafe { gl::GetProgramiv(program.id, gl::LINK_STATUS, &mut link_status) }; + check_error("glGetProgramiv(GL_LINK_STATUS)")?; + + let info_log: Box = { + let mut info_log_length = 0; + unsafe { gl::GetProgramiv(program.id, gl::INFO_LOG_LENGTH, &mut info_log_length) }; + check_error("glGetProgramiv(GL_INFO_LOG_LENGTH)")?; + + if info_log_length != 0 { + let mut info_log = vec![0i8; info_log_length as usize]; + let mut actual_info_log_length: GLsizei = 0; + unsafe { + gl::GetProgramInfoLog( + program.id, + info_log_length.into(), + &mut actual_info_log_length, + info_log.as_mut_ptr(), + ) + }; + check_error("glGetProgramInfoLog")?; + + let info_log_u8s = info_log.into_iter().map(|c| c as u8).collect::>(); + String::from_utf8_lossy(&info_log_u8s).as_ref().into() + } else { + Default::default() + } + }; + + if link_status as GLboolean == gl::FALSE { + return Err(LinkProgramError::LinkFailed { info_log }); + } else if !info_log.is_empty() { + warn!("Program link warnings: {info_log}"); + } + + Ok(program) + } + + pub fn bind(&self) -> Result<(), GlError> { + unsafe { gl::UseProgram(self.id) }; + GlError::check_msg("glUseProgram") + } + + pub fn get_program_int( + &self, + t: GetProgramIntType, + ) -> Result<[i32; N], GlError> { + assert_eq!( + N, + t.element_count(), + "{:?} returns {} elements, not {}", + t, + t.element_count(), + N + ); + let mut res = [0 as GLint; N]; + unsafe { gl::GetProgramiv(self.id, t as GLenum, res.as_mut_ptr()) }; + GlError::check_msg(format_args!("glGetProgramiv({}, {t:?})", self.id))?; + Ok(res) + } + + pub fn uniform_location( + &self, + uniform_name: &'static str, + ) -> Result { + let uniform_name_cstr = CString::new(uniform_name).expect(uniform_name); + let location = unsafe { gl::GetUniformLocation(self.id, uniform_name_cstr.as_ptr()) }; + GlError::check_msg(format_args!( + "glGetUniformLocation({}, \"{uniform_name}\")", + self.id + ))?; + Ok(UniformLocation { + location, + name: uniform_name, + }) + } + + pub fn set_uniform_1i( + &mut self, + location: UniformLocation, + value: i32, + ) -> Result<(), GlError> { + unsafe { gl::ProgramUniform1i(self.id, location.location, value) }; + GlError::check_msg(format_args!( + "glProgramUniform1i({}, {location:?})", + self.id + )) + } + + pub fn set_uniform_1ui( + &mut self, + location: UniformLocation, + value: u32, + ) -> Result<(), GlError> { + unsafe { gl::ProgramUniform1ui(self.id, location.location, value) }; + GlError::check_msg(format_args!( + "glProgramUniform1ui({}, {location:?})", + self.id + )) + } + + pub fn set_uniform_1uiv( + &mut self, + location: UniformLocation, + value: &[u32], + ) -> Result<(), GlError> { + unsafe { + gl::ProgramUniform1uiv( + self.id, + location.location, + value.len().try_into().unwrap(), + value.as_ptr(), + ) + }; + GlError::check_msg(format_args!( + "glProgramUniform1uiv({}, {location:?})", + self.id + )) + } + } + + impl Drop for Program { + fn drop(&mut self) { + unsafe { gl::DeleteProgram(self.id) } + } + } + + #[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] + #[repr(u32)] + pub enum BufferTarget { + ArrayBuffer = gl::ARRAY_BUFFER, + ElementArrayBuffer = gl::ELEMENT_ARRAY_BUFFER, + CopyReadBuffer = gl::COPY_READ_BUFFER, + CopyWriteBuffer = gl::COPY_WRITE_BUFFER, + UniformBuffer = gl::UNIFORM_BUFFER, + ShaderStorageBuffer = gl::SHADER_STORAGE_BUFFER, + AtomicCounterBuffer = gl::ATOMIC_COUNTER_BUFFER, + } + + #[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] + #[repr(u32)] + pub enum IndexedBufferTarget { + UniformBuffer = gl::UNIFORM_BUFFER, + ShaderStorageBuffer = gl::SHADER_STORAGE_BUFFER, + AtomicCounterBuffer = gl::ATOMIC_COUNTER_BUFFER, + } + + /// Shared trait providing functions accessible to both kinds of OpenGL buffers. + pub trait Buffer: fmt::Debug { + fn invalidate(&mut self) -> Result<(), GlError>; + + fn upload_sub_data(&mut self, offset: usize, data: &[u8]) -> Result<(), GlError>; + + fn download_sub_data(&self, offset: usize, data: &mut [u8]) -> Result<(), GlError>; + + fn bind_base(&self, target: IndexedBufferTarget, index: GLuint) -> Result<(), GlError>; + } + + impl + AsMut + fmt::Debug> Buffer for T { + fn invalidate(&mut self) -> Result<(), GlError> { + self.as_mut().invalidate() + } + + fn upload_sub_data(&mut self, offset: usize, data: &[u8]) -> Result<(), GlError> { + self.as_mut().upload_sub_data(offset, data) + } + + fn download_sub_data(&self, offset: usize, data: &mut [u8]) -> Result<(), GlError> { + >::as_ref(self).download_sub_data(offset, data) + } + + fn bind_base(&self, target: IndexedBufferTarget, index: GLuint) -> Result<(), GlError> { + >::as_ref(self).bind_base(target, index) + } + } + + /// An OpenGL buffer + #[derive(Debug)] + struct BaseBuffer { + id: GLuint, + size: usize, + } + + impl BaseBuffer { + fn new(size: usize) -> Result { + let mut id: GLuint = 0; + unsafe { gl::CreateBuffers(1, &mut id) }; + GlError::check_msg("glCreateBuffers")?; + Ok(Self { id, size }) + } + } + + impl Buffer for BaseBuffer { + fn invalidate(&mut self) -> Result<(), GlError> { + unsafe { gl::InvalidateBufferData(self.id) }; + GlError::check_msg("glInvalidateBufferData") + } + + fn upload_sub_data(&mut self, offset: usize, data: &[u8]) -> Result<(), GlError> { + unsafe { + gl::NamedBufferSubData( + self.id, + offset.try_into().expect("offset"), + data.len().try_into().expect("data.len()"), + data.as_ptr() as *const _, + ) + }; + GlError::check_msg("glNamedBufferSubData") + } + + fn download_sub_data(&self, offset: usize, data: &mut [u8]) -> Result<(), GlError> { + unsafe { + gl::GetNamedBufferSubData( + self.id, + offset.try_into().expect("offset"), + data.len().try_into().expect("data.len()"), + data.as_mut_ptr() as *mut _, + ) + }; + GlError::check_msg("glGetNamedBufferSubData") + } + + fn bind_base(&self, target: IndexedBufferTarget, index: GLuint) -> Result<(), GlError> { + unsafe { gl::BindBufferBase(target as GLenum, index, self.id) }; + GlError::check_msg("glBindBufferBase") + } + } + + impl Drop for BaseBuffer { + fn drop(&mut self) { + unsafe { gl::DeleteBuffers(1, &self.id) }; + } + } + + /// An OpenGL immutable buffer + #[derive(Debug)] + pub struct ImmutableBuffer { + internal_buffer: BaseBuffer, + flags: GLbitfield, + } + + impl ImmutableBuffer { + fn new_internal(size: usize, flags: GLbitfield) -> Result { + let internal_buffer = + BaseBuffer::new(size).add_context(format_args!("size={}", size))?; + Ok(Self { + internal_buffer, + flags, + }) + } + + pub fn new_uninitialized(size: usize, flags: GLbitfield) -> Result { + let buffer = Self::new_internal(size, flags)?; + + unsafe { + gl::NamedBufferStorage( + buffer.internal_buffer.id, + size.try_into().unwrap(), + null(), + flags, + ) + }; + GlError::check_msg("glNamedBufferStorage(NULL)")?; + + Ok(buffer) + } + + pub fn new_initialized(data: &[u8], flags: GLbitfield) -> Result { + let buffer = Self::new_internal(data.len(), flags)?; + + unsafe { + gl::NamedBufferStorage( + buffer.internal_buffer.id, + data.len().try_into().unwrap(), + data.as_ptr() as *const _, + flags, + ) + }; + GlError::check_msg("glNamedBufferStorage(data.as_ptr())")?; + + Ok(buffer) + } + } + + impl AsRef for ImmutableBuffer { + fn as_ref(&self) -> &BaseBuffer { + &self.internal_buffer + } + } + + impl AsMut for ImmutableBuffer { + fn as_mut(&mut self) -> &mut BaseBuffer { + &mut self.internal_buffer + } + } +} diff --git a/src/miner_pow/opengl_miner.glsl b/src/miner_pow/opengl_miner.glsl new file mode 100644 index 0000000..2208b88 --- /dev/null +++ b/src/miner_pow/opengl_miner.glsl @@ -0,0 +1,265 @@ +#version 450 core + +//#extension GL_EXT_shader_explicit_arithmetic_types_int8 : enable +//#extension GL_EXT_shader_explicit_arithmetic_types_int32 : enable +#extension GL_ARB_gpu_shader_int64 : require + +#define int32_t int +#define uint32_t uint + +#define uint8_t uint32_t + +layout(local_size_x = 256) in; + +// +// SHA-3 implementation +// This is ported from https://github.com/brainhub/SHA3IUF +// + +/* 'Words' here refers to uint64_t */ +#define SHA3_KECCAK_SPONGE_WORDS uint(((1600)/8/*bits to byte*/)/8/*sizeof(uint64_t)*/) +#define SHA3_256_BYTES uint(32) + +#define KECCAK_ROUNDS uint(24) + +/* + * This flag is used to configure "pure" Keccak, as opposed to NIST SHA3. + */ +#define SHA3_USE_KECCAK 0 + +#define SHA3_ROTL64(x, y) (((x) << (y)) | ((x) >> (64/*(sizeof(uint64_t)*8)*/ - (y)))) + +struct sha3_context { + uint64_t saved; /* the portion of the input message that we + * didn't consume yet */ + uint64_t s[SHA3_KECCAK_SPONGE_WORDS]; /* Keccak's state */ + uint byteIndex; /* 0..7--the next byte after the set one + * (starts from 0; 0--none are buffered) */ + uint wordIndex; /* 0..24--the next word to integrate input + * (starts from 0) */ + uint capacityWords; /* the double size of the hash output in + * words (e.g. 16 for Keccak 512) */ +}; + +const uint keccakf_rotc[24] = { + 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14, 27, 41, 56, 8, 25, 43, 62, + 18, 39, 61, 20, 44 +}; + +const uint keccakf_piln[24] = { + 10, 7, 11, 17, 18, 3, 5, 16, 8, 21, 24, 4, 15, 23, 19, 13, 12, 2, 20, + 14, 22, 9, 6, 1 +}; + +const uint64_t keccakf_rndc[24] = { + 0x0000000000000001UL, 0x0000000000008082UL, 0x800000000000808AUL, 0x8000000080008000UL, + 0x000000000000808BUL, 0x0000000080000001UL, 0x8000000080008081UL, 0x8000000000008009UL, + 0x000000000000008AUL, 0x0000000000000088UL, 0x0000000080008009UL, 0x000000008000000AUL, + 0x000000008000808BUL, 0x800000000000008BUL, 0x8000000000008089UL, 0x8000000000008003UL, + 0x8000000000008002UL, 0x8000000000000080UL, 0x000000000000800AUL, 0x800000008000000AUL, + 0x8000000080008081UL, 0x8000000000008080UL, 0x0000000080000001UL, 0x8000000080008008UL +}; + +/* generally called after SHA3_KECCAK_SPONGE_WORDS-ctx->capacityWords words + * are XORed into the state s + */ +void keccakf(inout uint64_t s[SHA3_KECCAK_SPONGE_WORDS]) { + uint i, j, round; + uint64_t t, bc[5]; + + for(round = 0; round < KECCAK_ROUNDS; round++) { + + /* Theta */ + for(i = 0; i < 5; i++) + bc[i] = s[i] ^ s[i + 5] ^ s[i + 10] ^ s[i + 15] ^ s[i + 20]; + + for(i = 0; i < 5; i++) { + t = bc[(i + 4) % 5] ^ SHA3_ROTL64(bc[(i + 1) % 5], 1); + for(j = 0; j < 25; j += 5) + s[j + i] ^= t; + } + + /* Rho Pi */ + t = s[1]; + for(i = 0; i < 24; i++) { + j = keccakf_piln[i]; + bc[0] = s[j]; + s[j] = SHA3_ROTL64(t, keccakf_rotc[i]); + t = bc[0]; + } + + /* Chi */ + for(j = 0; j < 25; j += 5) { + for(i = 0; i < 5; i++) + bc[i] = s[j + i]; + for(i = 0; i < 5; i++) + s[j + i] ^= (~bc[(i + 1) % 5]) & bc[(i + 2) % 5]; + } + + /* Iota */ + s[0] ^= keccakf_rndc[round]; + } +} + +void sha3_Init_256(inout sha3_context ctx) { + uint bitSize = 256u; + + ctx.saved = 0ul; + for (uint i = 0; i < SHA3_KECCAK_SPONGE_WORDS; i++) ctx.s[i] = 0ul; + ctx.byteIndex = 0u; + ctx.wordIndex = 0u; + ctx.capacityWords = 2u * bitSize / (8u * 8u/*sizeof(uint64_t)*/); +} + +void sha3_Update(inout sha3_context ctx, uint8_t byteIn) { + ctx.saved |= ((uint64_t(byteIn) & uint64_t(0xFFu)) << ((ctx.byteIndex++) * 8u)); + if (ctx.byteIndex == 8u) { + ctx.s[ctx.wordIndex] ^= ctx.saved; + ctx.saved = 0u; + ctx.byteIndex = 0u; + if (++ctx.wordIndex == (SHA3_KECCAK_SPONGE_WORDS - ctx.capacityWords)) { + ctx.wordIndex = 0u; + keccakf(ctx.s); + } + } +} + +uint8_t[SHA3_256_BYTES] sha3_Finalize(inout sha3_context ctx) { + /* Append 2-bit suffix 01, per SHA-3 spec. Instead of 1 for padding we + * use 1<<2 below. The 0x02 below corresponds to the suffix 01. + * Overall, we feed 0, then 1, and finally 1 to start padding. Without + * M || 01, we would simply use 1 to start padding. */ + + uint64_t t; +#if SHA3_USE_KECCAK + /* Keccak version */ + t = uint64_t((uint64_t(1)) << (ctx.byteIndex * 8u)); +#else + /* SHA3 version */ + t = uint64_t((uint64_t(0x02 | (1 << 2))) << ((ctx.byteIndex) * 8u)); +#endif + + ctx.s[ctx.wordIndex] ^= ctx.saved ^ t; + + ctx.s[uint(SHA3_KECCAK_SPONGE_WORDS) - ctx.capacityWords - 1u] ^= 0x8000000000000000UL; + keccakf(ctx.s); + + uint8_t result[SHA3_256_BYTES]; + for (uint i = 0; i < SHA3_256_BYTES / 8u; i++) { + t = ctx.s[i]; + result[i * 8 + 0] = uint8_t((t) & uint64_t(0xFFu)); + result[i * 8 + 1] = uint8_t((t >> 8u) & uint64_t(0xFFu)); + result[i * 8 + 2] = uint8_t((t >> 16u) & uint64_t(0xFFu)); + result[i * 8 + 3] = uint8_t((t >> 24u) & uint64_t(0xFFu)); + result[i * 8 + 4] = uint8_t((t >> 32u) & uint64_t(0xFFu)); + result[i * 8 + 5] = uint8_t((t >> 40u) & uint64_t(0xFFu)); + result[i * 8 + 6] = uint8_t((t >> 48u) & uint64_t(0xFFu)); + result[i * 8 + 7] = uint8_t((t >> 56u) & uint64_t(0xFFu)); + } + return result; +} + +// +// Miner implementation +// + +uniform uint u_firstNonce; +uniform uint u_blockHeader_length; +uniform uint u_blockHeader_nonceOffset; + +uniform uint u_difficultyFunction; +const uint DIFFICULTY_FUNCTION_LEADING_ZEROES = 1; +const uint DIFFICULTY_FUNCTION_COMPACT_TARGET = 2; + +uniform uint u_leadingZeroes_miningDifficulty; + +uniform uint32_t u_compactTarget_expanded[SHA3_256_BYTES]; + +layout(std430, binding = 0) readonly restrict buffer BlockHeader { + uint32_t bytes[]; +} b_blockHeader; + +/*layout(std430, binding = 1) writeonly restrict buffer HashOutput { + uint32_t hashes[][SHA3_256_BYTES]; +} b_hashOutput;*/ + +layout(std430, binding = 2) volatile restrict buffer Response { + uint32_t status; // initialized to 0 + uint32_t success_nonce; // initialized to u32::MAX + uint32_t error_code; // initialized to 0 +} b_response; + +const uint RESPONSE_STATUS_NONE = 0; +const uint RESPONSE_STATUS_SUCCESS = 1; +const uint RESPONSE_STATUS_ERROR = 2; + +const uint ERROR_CODE_INVALID_DIFFICULTY_FUNCTION = 1; + +void respond_success(uint32_t nonce) { + // race to change the response status from NONE to SUCCESS + atomicCompSwap(b_response.status, RESPONSE_STATUS_NONE, RESPONSE_STATUS_SUCCESS); + // set the response nonce to the min() of the current response nonce and this thread's nonce. + // this ensures that we always return the first valid nonce to the CPU, and works because + // b_response.success_nonce should be initialized to the maximum uint32_t value. + atomicMin(b_response.success_nonce, nonce); +} + +void respond_error(uint error_code) { + // set the response status to ERROR, regardless of what the current value is + // (this will hide any future threads which try to respond with success) + atomicMax(b_response.status, RESPONSE_STATUS_ERROR); + b_response.error_code = error_code; +} + +void main() { + uint32_t nonce = u_firstNonce + gl_GlobalInvocationID.x; + + if (b_response.success_nonce < nonce) { + // If another thread has already successfully found a nonce lower than this thread, exit immediately. + return; + } + + // hash the block header with the nonce inserted in the correct location + uint header_length = u_blockHeader_length; + uint header_nonceOffset = u_blockHeader_nonceOffset; + sha3_context ctx; + sha3_Init_256(ctx); + + // hash the block header, inserting the nonce in the correct location + for (uint i = 0; i < header_nonceOffset; i++) sha3_Update(ctx, uint8_t(b_blockHeader.bytes[i] & 0xFFu)); + for (uint i = 0; i < 4; i++) sha3_Update(ctx, uint8_t((nonce >> (i * 8u)) & 0xFFu)); + for (uint i = header_nonceOffset + 4u; i < header_length; i++) sha3_Update(ctx, uint8_t(b_blockHeader.bytes[i] & 0xFFu)); + + uint8_t[SHA3_256_BYTES] hash = sha3_Finalize(ctx); + + //for (uint i = 0; i < SHA3_256_BYTES; i++) b_hashOutput.hashes[uint(nonce)][i] = uint32_t(hash[i]); + + switch (u_difficultyFunction) { + default: { + respond_error(ERROR_CODE_INVALID_DIFFICULTY_FUNCTION); + return; + } + case DIFFICULTY_FUNCTION_LEADING_ZEROES: { + // check that the first u_leadingZeroes_miningDifficulty bytes of the hash are 0 + for (uint i = 0; i < u_leadingZeroes_miningDifficulty; i++) + if (hash[i] != 0) + return; + + respond_success(nonce); + return; + } + case DIFFICULTY_FUNCTION_COMPACT_TARGET: { + // check that the hash is lexicographically less than or equal to the expanded target hash + for (uint i = 0; i < SHA3_256_BYTES; i++) { + if (uint32_t(hash[i]) > uint32_t(u_compactTarget_expanded[i])) { + return; + } else if (uint32_t(hash[i]) < uint32_t(u_compactTarget_expanded[i])) { + break; + } + } + + respond_success(nonce); + return; + } + } +} diff --git a/src/miner_pow/vulkan.rs b/src/miner_pow/vulkan.rs new file mode 100644 index 0000000..3df8a14 --- /dev/null +++ b/src/miner_pow/vulkan.rs @@ -0,0 +1,815 @@ +use crate::miner_pow::vulkan::sha3_256_shader::SHA3_KECCAK_SPONGE_WORDS; +use crate::miner_pow::{ + MineError, MinerStatistics, PoWDifficulty, Sha3_256PoWMiner, BLOCK_HEADER_MAX_BYTES, + SHA3_256_BYTES, +}; +use crate::utils::split_range_into_blocks; +use crevice::std140::AsStd140; +use crevice::std430::AsStd430; +use debug_ignore::DebugIgnore; +use std::collections::BTreeMap; +use std::convert::TryInto; +use std::fmt; +use std::sync::{Arc, Mutex, OnceLock}; +use tracing::warn; +use vulkano::buffer::{Buffer, BufferCreateInfo, BufferUsage, Subbuffer}; +use vulkano::command_buffer::allocator::{ + StandardCommandBufferAllocator, StandardCommandBufferAllocatorCreateInfo, +}; +use vulkano::command_buffer::{AutoCommandBufferBuilder, CommandBufferUsage}; +use vulkano::descriptor_set::allocator::StandardDescriptorSetAllocator; +use vulkano::descriptor_set::layout::{ + DescriptorSetLayout, DescriptorSetLayoutBinding, DescriptorSetLayoutCreateInfo, DescriptorType, +}; +use vulkano::descriptor_set::{PersistentDescriptorSet, WriteDescriptorSet}; +use vulkano::device::{Device, DeviceCreateInfo, Features, Queue, QueueCreateInfo, QueueFlags}; +use vulkano::instance::{Instance, InstanceCreateInfo}; +use vulkano::memory::allocator::{AllocationCreateInfo, MemoryTypeFilter, StandardMemoryAllocator}; +use vulkano::pipeline::compute::ComputePipelineCreateInfo; +use vulkano::pipeline::layout::{PipelineDescriptorSetLayoutCreateInfo, PipelineLayoutCreateInfo}; +use vulkano::pipeline::{ + ComputePipeline, Pipeline, PipelineBindPoint, PipelineLayout, PipelineShaderStageCreateInfo, +}; +use vulkano::shader::{ShaderModule, ShaderStages, SpecializationConstant}; +use vulkano::sync::GpuFuture; +use vulkano::*; + +// synced with vulkan_miner.glsl +const WORK_GROUP_SIZE: u32 = 256; + +#[derive(Clone, Debug)] +pub enum VulkanMinerError { + Load(String), + Instance(Validated), + EnumerateDevices(VulkanError), + NoDevices, + NoComputeQueues, + Device(Validated), +} + +impl std::error::Error for VulkanMinerError {} + +impl fmt::Display for VulkanMinerError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Load(cause) => write!(f, "Failed to load Vulkan library: {cause}"), + Self::Instance(cause) => write!(f, "Failed to create Vulkan instance: {cause}"), + Self::EnumerateDevices(cause) => { + write!(f, "Failed to enumerate Vulkan devices: {cause}") + } + Self::NoDevices => f.write_str("No Vulkan devices found!"), + Self::NoComputeQueues => f.write_str("No compute queue family found!"), + Self::Device(cause) => write!(f, "Failed to create Vulkan device: {cause}"), + } + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)] +struct VulkanMinerVariant { + trailing_bytes_count: u32, + digest_initial_byte_index: u32, + digest_initial_word_index: u32, + difficulty_function: u32, + leading_zeroes_mining_difficulty: u32, +} + +impl VulkanMinerVariant { + pub fn specialization_constants( + &self, + ) -> impl IntoIterator { + [ + ( + 0, + SpecializationConstant::U32(BLOCK_HEADER_MAX_BYTES as u32), + ), + (1, SpecializationConstant::U32(self.trailing_bytes_count)), + ( + 5, + SpecializationConstant::U32(self.digest_initial_byte_index), + ), + ( + 6, + SpecializationConstant::U32(self.digest_initial_word_index), + ), + (10, SpecializationConstant::U32(self.difficulty_function)), + ( + 11, + SpecializationConstant::U32(self.leading_zeroes_mining_difficulty), + ), + ] + } +} + +#[derive(Debug)] +pub struct VulkanMiner { + uniform_buffer: Subbuffer<::Output>, + response_buffer: Subbuffer<::Output>, + memory_allocator: Arc, + + base_compute_shader: Arc, + compute_pipelines: BTreeMap>, + work_group_size: u32, + + descriptor_set_layout_index: usize, + descriptor_set: DebugIgnore>, + + queue: Arc, + device: Arc, + + instance: Arc, +} + +static VULKAN_MINER: OnceLock>, VulkanMinerError>> = OnceLock::new(); + +impl VulkanMiner { + pub fn get() -> Result>, VulkanMinerError> { + VULKAN_MINER + .get_or_init(|| Self::new().map(|miner| Arc::new(Mutex::new(miner)))) + .clone() + //Self::new().map(|miner| Arc::new(Mutex::new(miner))) + } + + fn new() -> Result { + let library = + VulkanLibrary::new().map_err(|err| VulkanMinerError::Load(err.to_string()))?; + let instance = Instance::new(library, InstanceCreateInfo::default()) + .map_err(VulkanMinerError::Instance)?; + + let physical_device = instance + .enumerate_physical_devices() + .map_err(VulkanMinerError::EnumerateDevices)? + .filter(|d| { + let features = d.supported_features(); + features.shader_int8 && features.shader_int64 + }) + .next() + .ok_or_else(|| VulkanMinerError::NoDevices)?; + + let queue_family_index = physical_device + .queue_family_properties() + .iter() + .enumerate() + .position(|(_queue_family_index, queue_family_properties)| { + queue_family_properties + .queue_flags + .contains(QueueFlags::COMPUTE) + }) + .ok_or_else(|| VulkanMinerError::NoComputeQueues)? + as u32; + + let (device, mut queues) = Device::new( + physical_device, + DeviceCreateInfo { + // here we pass the desired queue family to use by index + queue_create_infos: vec![QueueCreateInfo { + queue_family_index, + ..Default::default() + }], + enabled_features: Features { + shader_int8: true, + shader_int64: true, + ..Default::default() + }, + ..Default::default() + }, + ) + .map_err(VulkanMinerError::Device)?; + let queue = queues.next().unwrap(); + + // create buffers + let memory_allocator = Arc::new(StandardMemoryAllocator::new_default(device.clone())); + + let uniform_buffer = Buffer::from_data( + memory_allocator.clone(), + BufferCreateInfo { + usage: BufferUsage::UNIFORM_BUFFER, + ..Default::default() + }, + AllocationCreateInfo { + memory_type_filter: MemoryTypeFilter::PREFER_DEVICE + | MemoryTypeFilter::HOST_SEQUENTIAL_WRITE, + ..Default::default() + }, + MineUniforms::default().as_std140(), + ) + .unwrap(); + + let response_buffer = Buffer::from_data( + memory_allocator.clone(), + BufferCreateInfo { + usage: BufferUsage::STORAGE_BUFFER, + ..Default::default() + }, + AllocationCreateInfo { + memory_type_filter: MemoryTypeFilter::PREFER_DEVICE + | MemoryTypeFilter::HOST_SEQUENTIAL_WRITE, + ..Default::default() + }, + MineResponse::default().as_std430(), + ) + .unwrap(); + + let shader = mine_shader::load(device.clone()).unwrap(); + + let descriptor_set_allocator = + StandardDescriptorSetAllocator::new(device.clone(), Default::default()); + let pipeline_layout = PipelineLayout::new( + device.clone(), + PipelineLayoutCreateInfo { + set_layouts: vec![DescriptorSetLayout::new( + device.clone(), + DescriptorSetLayoutCreateInfo { + bindings: BTreeMap::from([ + ( + 0, + DescriptorSetLayoutBinding { + stages: ShaderStages::COMPUTE, + ..DescriptorSetLayoutBinding::descriptor_type( + DescriptorType::UniformBuffer, + ) + }, + ), + ( + 1, + DescriptorSetLayoutBinding { + stages: ShaderStages::COMPUTE, + ..DescriptorSetLayoutBinding::descriptor_type( + DescriptorType::StorageBuffer, + ) + }, + ), + ]), + ..Default::default() + }, + ) + .unwrap()], + push_constant_ranges: vec![], + ..Default::default() + }, + ) + .unwrap(); + let descriptor_set_layouts = pipeline_layout.set_layouts(); + + let descriptor_set_layout_index = 0; + let descriptor_set_layout = descriptor_set_layouts + .get(descriptor_set_layout_index) + .unwrap(); + let descriptor_set = PersistentDescriptorSet::new( + &descriptor_set_allocator, + descriptor_set_layout.clone(), + [ + WriteDescriptorSet::buffer(0, uniform_buffer.clone()), + WriteDescriptorSet::buffer(1, response_buffer.clone()), + ], + [], + ) + .unwrap(); + + Ok(Self { + uniform_buffer, + response_buffer, + memory_allocator, + + base_compute_shader: shader, + compute_pipelines: Default::default(), + work_group_size: WORK_GROUP_SIZE, + + descriptor_set_layout_index, + descriptor_set: descriptor_set.into(), + + queue, + device, + + instance, + }) + } + + fn get_compute_pipeline(&mut self, variant: VulkanMinerVariant) -> Arc { + if let Some(compute_pipeline) = self.compute_pipelines.get(&variant) { + // This pipeline variant is already cached! + return compute_pipeline.clone(); + } + + let specialized_shader = self + .base_compute_shader + .specialize(variant.specialization_constants().into_iter().collect()) + .unwrap(); + let cs = specialized_shader.entry_point("main").unwrap(); + let stage = PipelineShaderStageCreateInfo::new(cs); + let layout = PipelineLayout::new( + self.device.clone(), + PipelineDescriptorSetLayoutCreateInfo::from_stages([&stage]) + .into_pipeline_layout_create_info(self.device.clone()) + .unwrap(), + ) + .unwrap(); + let compute_pipeline = ComputePipeline::new( + self.device.clone(), + None, + ComputePipelineCreateInfo::stage_layout(stage, layout), + ) + .unwrap(); + + self.compute_pipelines + .insert(variant, compute_pipeline.clone()); + compute_pipeline + } +} + +impl Sha3_256PoWMiner for VulkanMiner { + fn is_hw_accelerated(&self) -> bool { + true + } + + fn min_nonce_count(&self) -> u32 { + self.work_group_size + } + + fn nonce_peel_amount(&self) -> u32 { + // TODO: do runtime benchmarks to find an optimal value for this + 1 << 20 + } + + fn generate_pow_internal( + &mut self, + leading_bytes: &[u8], + trailing_bytes: &[u8], + difficulty: &PoWDifficulty, + first_nonce: u32, + nonce_count: u32, + statistics: &mut MinerStatistics, + ) -> Result, MineError> { + if nonce_count == 0 { + return Ok(None); + } + + let work_group_counts = [nonce_count.div_ceil(self.work_group_size), 1, 1]; + + let max_work_group_count = self + .device + .physical_device() + .properties() + .max_compute_work_group_count[0]; + if work_group_counts[0] > max_work_group_count { + warn!( + "Vulkan miner dispatched with too high nonce_count {} (work group size={}, max \ + work group count={})! Splitting into multiple dispatches.", + nonce_count, self.work_group_size, max_work_group_count + ); + + for (first, count) in split_range_into_blocks( + first_nonce, + nonce_count, + max_work_group_count * self.work_group_size, + ) { + match self.generate_pow_internal( + leading_bytes, + trailing_bytes, + difficulty, + first, + count, + statistics, + )? { + Some(nonce) => return Ok(Some(nonce)), + None => (), + }; + } + return Ok(None); + } + + let mut stats_updater = statistics.update_safe(); + + let (difficulty_function, leading_zeroes_mining_difficulty, compact_target_expanded) = + match difficulty { + // The target value is higher than the largest possible SHA3-256 hash. Therefore, + // every hash will meet the required difficulty threshold, so we can just return + // an arbitrary nonce. + PoWDifficulty::TargetHashAlwaysPass => return Ok(Some(first_nonce)), + + PoWDifficulty::LeadingZeroBytes { leading_zeroes } => ( + // DIFFICULTY_FUNCTION_LEADING_ZEROES + 1, + (*leading_zeroes).try_into().unwrap(), + Default::default(), + ), + PoWDifficulty::TargetHash { target_hash } => ( + // DIFFICULTY_FUNCTION_COMPACT_TARGET + 2, + Default::default(), + target_hash.map(|b| b as u32).into(), + ), + }; + + // compute the initial digest state + let mut digest = sha3_256_shader::Sha3_256Context::default(); + for byte in leading_bytes { + digest.update(*byte); + } + + // extend the block header with trailing zeroes + let mut block_header_bytes = [0u8; BLOCK_HEADER_MAX_BYTES]; + block_header_bytes[..trailing_bytes.len()].copy_from_slice(trailing_bytes); + + //update the uniforms + *self.uniform_buffer.write().unwrap() = MineUniforms { + u_firstNonce: first_nonce, + u_compactTarget_expanded: compact_target_expanded, + u_digest_initialState: digest.state.into(), + u_blockHeader_bytes: block_header_bytes.map(|b| b as u32).into(), + } + .as_std140(); + + //reset the response buffer + *self.response_buffer.write().unwrap() = MineResponse::default().as_std430(); + + let command_buffer_allocator = StandardCommandBufferAllocator::new( + self.device.clone(), + StandardCommandBufferAllocatorCreateInfo::default(), + ); + + let mut command_buffer_builder = AutoCommandBufferBuilder::primary( + &command_buffer_allocator, + self.queue.queue_family_index(), + CommandBufferUsage::OneTimeSubmit, + ) + .unwrap(); + + let compute_pipeline = self.get_compute_pipeline(VulkanMinerVariant { + trailing_bytes_count: trailing_bytes.len().try_into().unwrap(), + digest_initial_byte_index: digest.byte_index, + digest_initial_word_index: digest.word_index, + difficulty_function, + leading_zeroes_mining_difficulty, + }); + + command_buffer_builder + .bind_pipeline_compute(compute_pipeline.clone()) + .unwrap() + .bind_descriptor_sets( + PipelineBindPoint::Compute, + compute_pipeline.layout().clone(), + self.descriptor_set_layout_index as u32, + (*self.descriptor_set).clone(), + ) + .unwrap() + .dispatch(work_group_counts) + .unwrap(); + + let command_buffer = command_buffer_builder.build().unwrap(); + + let future = sync::now(self.device.clone()) + .then_execute(self.queue.clone(), command_buffer) + .unwrap() + .then_signal_fence_and_flush() + .unwrap(); + + future.wait(None).unwrap(); + + let response = MineResponse::from_std430(*self.response_buffer.read().unwrap()); + + // Now that the hashes have been computed, update the statistics + stats_updater + .computed_hashes((work_group_counts[0] as u128) * (self.work_group_size as u128)); + + match response.status { + // RESPONSE_STATUS_NONE + 0 => Ok(None), + // RESPONSE_STATUS_SUCCESS + 1 => Ok(Some(response.success_nonce)), + // RESPONSE_STATUS_ERROR_CODE + 2 => panic!( + "compute shader returned error code 0x{:04x}: {}", + response.error_code, + match response.error_code { + 1 => "INVALID_DIFFICULTY_FUNCTION", + _ => "(unknown)", + } + ), + _ => panic!( + "compute shader returned unknown response status 0x{:04x}", + response.status + ), + } + } +} + +#[allow(non_snake_case)] +#[derive(Clone, Copy, Debug, Default, AsStd140)] +struct MineUniforms { + u_firstNonce: u32, + u_compactTarget_expanded: crevice_utils::FixedArray, + u_digest_initialState: crevice_utils::FixedArray, + u_blockHeader_bytes: crevice_utils::FixedArray, +} + +#[allow(non_snake_case)] +#[derive(Clone, Copy, Debug, AsStd430)] +struct MineResponse { + status: u32, + success_nonce: u32, + error_code: u32, +} + +impl Default for MineResponse { + fn default() -> Self { + Self { + status: 0, + success_nonce: u32::MAX, + error_code: 0, + } + } +} + +mod mine_shader { + vulkano_shaders::shader! { + ty: "compute", + path: "src/miner_pow/vulkan_miner.glsl", + } +} + +mod crevice_utils { + use super::*; + + // what follows is a gross hack since crevice doesn't support fixed arrays + #[derive(Copy, Clone, Debug)] + pub struct FixedArray([E; N]); + + impl From<[E; N]> for FixedArray { + fn from(value: [E; N]) -> Self { + Self(value) + } + } + + impl Default for FixedArray { + fn default() -> Self { + Self([::default(); N]) + } + } + + impl AsStd140 for FixedArray { + type Output = Std140FixedArray; + + fn as_std140(&self) -> Self::Output { + Std140FixedArray::( + self.0 + .map(|val| ::to_padded(val).as_std140()), + ) + } + + fn from_std140(val: Self::Output) -> Self { + Self( + val.0.map(|padded| { + ::from_padded(<_ as AsStd140>::from_std140(padded)) + }), + ) + } + } + + #[derive(Clone, Copy)] + pub struct Std140FixedArray( + [<::Output as AsStd140>::Output; N], + ); + + unsafe impl crevice::internal::bytemuck::Zeroable + for Std140FixedArray + { + } + + unsafe impl crevice::internal::bytemuck::Pod + for Std140FixedArray + { + } + + unsafe impl crevice::std140::Std140 + for Std140FixedArray + { + const ALIGNMENT: usize = crevice::internal::max_arr([ + 16usize, + <<::Output as AsStd140>::Output as crevice::std140::Std140>::ALIGNMENT, + ]); + } + + impl fmt::Debug for Std140FixedArray { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt( + & as AsStd140>::from_std140(self.clone()), + f, + ) + } + } + + impl AsStd430 for FixedArray { + type Output = Std430FixedArray; + + fn as_std430(&self) -> Self::Output { + Std430FixedArray::( + self.0 + .map(|val| ::to_padded(val).as_std430()), + ) + } + + fn from_std430(val: Self::Output) -> Self { + Self( + val.0.map(|padded| { + ::from_padded(<_ as AsStd430>::from_std430(padded)) + }), + ) + } + } + + #[derive(Clone, Copy)] + pub struct Std430FixedArray( + [<::Output as AsStd430>::Output; N], + ); + + unsafe impl crevice::internal::bytemuck::Zeroable + for Std430FixedArray + { + } + + unsafe impl crevice::internal::bytemuck::Pod + for Std430FixedArray + { + } + + unsafe impl crevice::std430::Std430 + for Std430FixedArray + { + const ALIGNMENT: usize = crevice::internal::max_arr([ + 0usize, + <<::Output as AsStd430>::Output as crevice::std430::Std430>::ALIGNMENT, + ]); + } + + impl fmt::Debug for Std430FixedArray { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt( + & as AsStd430>::from_std430(self.clone()), + f, + ) + } + } + + pub trait AsPadded: Copy + Default + fmt::Debug { + type Output: Copy + AsStd140 + AsStd430; + + fn to_padded(self) -> Self::Output; + + fn from_padded(value: Self::Output) -> Self; + } + + #[derive(Clone, Copy, Debug, AsStd140, AsStd430)] + pub struct U32Padded { + value: u32, + } + + impl AsPadded for u32 { + type Output = U32Padded; + + fn to_padded(self) -> Self::Output { + U32Padded { value: self } + } + + fn from_padded(value: Self::Output) -> Self { + value.value + } + } + + #[derive(Clone, Copy, Debug, AsStd140, AsStd430)] + pub struct F64Padded { + value: f64, + } + + impl AsPadded for f64 { + type Output = F64Padded; + + fn to_padded(self) -> Self::Output { + F64Padded { value: self } + } + + fn from_padded(value: Self::Output) -> Self { + value.value + } + } + + impl AsPadded for u64 { + type Output = F64Padded; + + fn to_padded(self) -> Self::Output { + F64Padded { + value: f64::from_ne_bytes(self.to_ne_bytes()), + } + } + + fn from_padded(value: Self::Output) -> Self { + Self::from_ne_bytes(value.value.to_ne_bytes()) + } + } +} + +/// Rust port of the SHA3-256 shader. +/// +/// This enables us to precompute common parts of the digest state on the CPU. +mod sha3_256_shader { + pub(super) const SHA3_KECCAK_SPONGE_WORDS: usize = ((1600)/8/*bits to byte*/)/8/*sizeof(uint64_t)*/; // 25 + const SHA3_256_BYTES: usize = 256 / 8; // 32 + const SHA3_256_CAPACITY_WORDS: usize = 2 * 256 / (8 * 8); // 8 + const KECCAK_ROUNDS: usize = 24; + + const KECCAKF_ROTC: &'static [u32; 24] = &[ + 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14, 27, 41, 56, 8, 25, 43, 62, 18, 39, 61, 20, 44, + ]; + + const KECCAKF_PILN: &'static [usize; 24] = &[ + 10, 7, 11, 17, 18, 3, 5, 16, 8, 21, 24, 4, 15, 23, 19, 13, 12, 2, 20, 14, 22, 9, 6, 1, + ]; + + const KECCAKF_RNDC: &'static [u64; KECCAK_ROUNDS] = &[ + 0x0000000000000001u64, + 0x0000000000008082u64, + 0x800000000000808Au64, + 0x8000000080008000u64, + 0x000000000000808Bu64, + 0x0000000080000001u64, + 0x8000000080008081u64, + 0x8000000000008009u64, + 0x000000000000008Au64, + 0x0000000000000088u64, + 0x0000000080008009u64, + 0x000000008000000Au64, + 0x000000008000808Bu64, + 0x800000000000008Bu64, + 0x8000000000008089u64, + 0x8000000000008003u64, + 0x8000000000008002u64, + 0x8000000000000080u64, + 0x000000000000800Au64, + 0x800000008000000Au64, + 0x8000000080008081u64, + 0x8000000000008080u64, + 0x0000000080000001u64, + 0x8000000080008008u64, + ]; + + fn keccakf(s: &mut [u64; SHA3_KECCAK_SPONGE_WORDS]) { + let mut bc = [0u64; 5]; + + for round in 0..KECCAK_ROUNDS { + /* Theta */ + for i in 0..5 { + bc[i] = s[i] ^ s[i + 5] ^ s[i + 10] ^ s[i + 15] ^ s[i + 20]; + } + + for i in 0..5 { + let t = bc[(i + 4) % 5] ^ bc[(i + 1) % 5].rotate_left(1); + for j in 0..5 { + s[j * 5 + i] ^= t; + } + } + + /* Rho Pi */ + let mut t = s[1]; + for i in 0..24 { + let j = KECCAKF_PILN[i]; + bc[0] = s[j]; + s[j] = t.rotate_left(KECCAKF_ROTC[i]); + t = bc[0]; + } + + /* Chi */ + for j in 0..25 { + for i in 0..5 { + bc[i] = s[j + i]; + } + for i in 0..5 { + s[j + i] ^= (!bc[(i + 1) % 5]) & bc[(i + 2) % 5]; + } + } + + /* Iota */ + s[0] ^= KECCAKF_RNDC[round]; + } + } + + #[derive(Default)] + pub(super) struct Sha3_256Context { + pub state: [u64; SHA3_KECCAK_SPONGE_WORDS], + pub byte_index: u32, + pub word_index: u32, + } + + impl Sha3_256Context { + pub fn update(&mut self, byte: u8) { + self.state[self.word_index as usize] ^= (byte as u64) << (self.byte_index * 8); + self.byte_index += 1; + if self.byte_index == 8 { + self.byte_index = 0; + self.word_index += 1; + if self.word_index == (SHA3_KECCAK_SPONGE_WORDS - SHA3_256_CAPACITY_WORDS) as u32 { + self.word_index = 0; + keccakf(&mut self.state); + } + } + } + } +} diff --git a/src/miner_pow/vulkan_miner.glsl b/src/miner_pow/vulkan_miner.glsl new file mode 100644 index 0000000..3f26566 --- /dev/null +++ b/src/miner_pow/vulkan_miner.glsl @@ -0,0 +1,112 @@ +#version 460 core +#extension GL_ARB_compute_shader : require + +#extension GL_EXT_shader_explicit_arithmetic_types_int8 : require +#extension GL_EXT_shader_explicit_arithmetic_types_int32 : require +#extension GL_ARB_gpu_shader_int64 : require + +layout(local_size_x = 256) in; + +#include "vulkan_miner_sha3_256.glsl" + +// TODO: Properly initialize this value from the Rust code, arrays sized by specialization constants don't +// seem to be entirely portable! +layout(constant_id = 0) const uint BLOCK_HEADER_MAX_BYTES = 1024; +layout(constant_id = 1) const uint u_blockHeader_trailingBytesCount = 1; + +layout(constant_id = 5) const uint u_digest_initialByteIndex = 0; +layout(constant_id = 6) const uint u_digest_initialWordIndex = 0; + +layout(constant_id = 10) const uint u_difficultyFunction = 0; +layout(constant_id = 11) const uint u_leadingZeroes_miningDifficulty = 0; + +layout(std140, set = 0, binding = 0) uniform Uniforms { + uint u_firstNonce; + + uint u_compactTarget_expanded[SHA3_256_BYTES]; + uint64_t u_digest_initialState[SHA3_KECCAK_SPONGE_WORDS]; + uint u_blockHeader_trailingBytes[BLOCK_HEADER_MAX_BYTES]; +}; + +const uint DIFFICULTY_FUNCTION_LEADING_ZEROES = 1; +const uint DIFFICULTY_FUNCTION_COMPACT_TARGET = 2; + +layout(std430, set = 0, binding = 1) volatile restrict buffer Response { + uint status; // initialized to 0 + uint success_nonce; // initialized to u32::MAX + uint error_code; // initialized to 0 +} b_response; + +const uint RESPONSE_STATUS_NONE = 0; +const uint RESPONSE_STATUS_SUCCESS = 1; +const uint RESPONSE_STATUS_ERROR = 2; + +const uint ERROR_CODE_INVALID_DIFFICULTY_FUNCTION = 1; + +void respond_success(uint32_t nonce) { + // race to change the response status from NONE to SUCCESS + atomicCompSwap(b_response.status, RESPONSE_STATUS_NONE, RESPONSE_STATUS_SUCCESS); + // set the response nonce to the min() of the current response nonce and this thread's nonce. + // this ensures that we always return the first valid nonce to the CPU, and works because + // b_response.success_nonce should be initialized to the maximum uint32_t value. + atomicMin(b_response.success_nonce, nonce); +} + +void respond_error(uint error_code) { + // set the response status to ERROR, regardless of what the current value is + // (this will hide any future threads which try to respond with success) + atomicMax(b_response.status, RESPONSE_STATUS_ERROR); + b_response.error_code = error_code; +} + +void main() { + uint32_t nonce = u_firstNonce + gl_GlobalInvocationID.x; + + if (b_response.success_nonce < nonce) { + // If another thread has already successfully found a nonce lower than this thread, exit immediately. + return; + } + + // load the initial digest state (the precomputed state after having digested the leading bytes) + sha3_256_context ctx; + ctx.saved = 0; + ctx.s = u_digest_initialState; + ctx.byteIndex = u_digest_initialByteIndex; + ctx.wordIndex = u_digest_initialWordIndex; + + // hash the block header, inserting the nonce in the correct location + sha3_256_Update_int32le(ctx, nonce); + for (uint i = 0; i < u_blockHeader_trailingBytesCount; i++) + sha3_256_Update(ctx, uint8_t(u_blockHeader_trailingBytes[i] & 0xFFu)); + + uint8_t[SHA3_256_BYTES] hash = sha3_256_Finalize(ctx); + + switch (u_difficultyFunction) { + default: { + respond_error(ERROR_CODE_INVALID_DIFFICULTY_FUNCTION); + return; + } + case DIFFICULTY_FUNCTION_LEADING_ZEROES: { + // check that the first u_leadingZeroes_miningDifficulty bytes of the hash are 0 + for (uint i = 0; i < u_leadingZeroes_miningDifficulty; i++) + if (hash[i] != 0) + return; + + respond_success(nonce); + return; + } + case DIFFICULTY_FUNCTION_COMPACT_TARGET: { + // check that the hash is lexicographically less than or equal to the expanded target hash + for (uint i = 0; i < SHA3_256_BYTES; i++) { + if (uint32_t(hash[i]) > uint32_t(u_compactTarget_expanded[i])) { + return; + } else if (uint32_t(hash[i]) < uint32_t(u_compactTarget_expanded[i])) { + break; + } + } + + respond_success(nonce); + return; + } + } +} diff --git a/src/miner_pow/vulkan_miner_sha3_256.glsl b/src/miner_pow/vulkan_miner_sha3_256.glsl new file mode 100644 index 0000000..1157646 --- /dev/null +++ b/src/miner_pow/vulkan_miner_sha3_256.glsl @@ -0,0 +1,255 @@ +// +// SHA-3 implementation +// This is ported from https://github.com/brainhub/SHA3IUF +// + +/* 'Words' here refers to uint64_t */ +const uint SHA3_KECCAK_SPONGE_WORDS = ((1600)/8/*bits to byte*/)/8/*sizeof(uint64_t)*/; // 25 +const uint SHA3_256_BYTES = 256 / 8; // 32 +const uint SHA3_256_CAPACITY_WORDS = 2 * 256 / (8 * 8); // 8 + +const uint KECCAK_ROUNDS = 24; + +/* + * This flag is used to configure "pure" Keccak, as opposed to NIST SHA3. + */ +const bool SHA3_USE_KECCAK = false; + +#define SHA3_ROTL64(x, y) (((x) << (y)) | ((x) >> (64/*(sizeof(uint64_t)*8)*/ - (y)))) + +struct sha3_256_context { + uint64_t saved; /* the portion of the input message that we + * didn't consume yet */ + uint64_t s[SHA3_KECCAK_SPONGE_WORDS]; /* Keccak's state */ + uint byteIndex; /* 0..7--the next byte after the set one + * (starts from 0; 0--none are buffered) */ + uint wordIndex; /* 0..24--the next word to integrate input + * (starts from 0) */ +}; + +const uint keccakf_rotc[24] = { + 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14, 27, 41, 56, 8, 25, 43, 62, + 18, 39, 61, 20, 44 +}; + +const uint keccakf_piln[24] = { + 10, 7, 11, 17, 18, 3, 5, 16, 8, 21, 24, 4, 15, 23, 19, 13, 12, 2, 20, + 14, 22, 9, 6, 1 +}; + +const uint64_t keccakf_rndc[24] = { + 0x0000000000000001UL, 0x0000000000008082UL, 0x800000000000808AUL, 0x8000000080008000UL, + 0x000000000000808BUL, 0x0000000080000001UL, 0x8000000080008081UL, 0x8000000000008009UL, + 0x000000000000008AUL, 0x0000000000000088UL, 0x0000000080008009UL, 0x000000008000000AUL, + 0x000000008000808BUL, 0x800000000000008BUL, 0x8000000000008089UL, 0x8000000000008003UL, + 0x8000000000008002UL, 0x8000000000000080UL, 0x000000000000800AUL, 0x800000008000000AUL, + 0x8000000080008081UL, 0x8000000000008080UL, 0x0000000080000001UL, 0x8000000080008008UL +}; + +/* generally called after SHA3_KECCAK_SPONGE_WORDS-ctx->capacityWords words + * are XORed into the state s + */ +void keccakf(inout uint64_t s[SHA3_KECCAK_SPONGE_WORDS]) { + uint i, j, round; + uint64_t t, bc[5]; + + for(round = 0; round < KECCAK_ROUNDS; round++) { + + /* Theta */ + for(i = 0; i < 5; i++) + bc[i] = s[i] ^ s[i + 5] ^ s[i + 10] ^ s[i + 15] ^ s[i + 20]; + + for(i = 0; i < 5; i++) { + t = bc[(i + 4) % 5] ^ SHA3_ROTL64(bc[(i + 1) % 5], 1); + for(j = 0; j < 25; j += 5) + s[j + i] ^= t; + } + + /* Rho Pi */ + t = s[1]; + for(i = 0; i < 24; i++) { + j = keccakf_piln[i]; + bc[0] = s[j]; + s[j] = SHA3_ROTL64(t, keccakf_rotc[i]); + t = bc[0]; + } + + /* Chi */ + for(j = 0; j < 25; j += 5) { + for(i = 0; i < 5; i++) + bc[i] = s[j + i]; + for(i = 0; i < 5; i++) + s[j + i] ^= (~bc[(i + 1) % 5]) & bc[(i + 2) % 5]; + } + + /* Iota */ + s[0] ^= keccakf_rndc[round]; + } +} + +/** + * Initializes the SHA3-256 context to begin computing a new hash. + * + * @param ctx the SHA3-256 context + */ +void sha3_256_Init(inout sha3_256_context ctx) { + ctx.saved = 0ul; + for (uint i = 0; i < SHA3_KECCAK_SPONGE_WORDS; i++) ctx.s[i] = 0ul; + ctx.byteIndex = 0u; + ctx.wordIndex = 0u; +} + +/** + * Checks if the SHA3-256 context has buffered an entire block, consuming the block if it has. + * + * @param ctx the SHA3-256 context + */ +void sha3_256_tryFlushBlock(inout sha3_256_context ctx) { + if (ctx.wordIndex == (SHA3_KECCAK_SPONGE_WORDS - SHA3_256_CAPACITY_WORDS)) { + ctx.wordIndex = 0u; + keccakf(ctx.s); + } +} + +/** + * Checks if the SHA3-256 context has buffered an entire word, consuming the word if it has. + * + * @param ctx the SHA3-256 context + */ +void sha3_256_tryFlushWord(inout sha3_256_context ctx) { + if (ctx.byteIndex == 8u) { + ctx.s[ctx.wordIndex] ^= ctx.saved; + ctx.saved = 0u; + ctx.byteIndex = 0u; + ctx.wordIndex++; + sha3_256_tryFlushBlock(ctx); + } +} + +/** + * Updates the SHA3-256 digest with a single byte. + * + * @param ctx the SHA3-256 context + * @param byteIn the byte to update the digest with + */ +void sha3_256_Update(inout sha3_256_context ctx, uint8_t byteIn) { + ctx.saved |= ((uint64_t(byteIn) & uint64_t(0xFFu)) << (ctx.byteIndex * 8u)); + ctx.byteIndex++; + sha3_256_tryFlushWord(ctx); +} + +/** + * Updates the SHA3-256 digest with 4 bytes, stored in a 32-bit integer in little-endian order. + * + * @param ctx the SHA3-256 context + * @param bytesIn an integer containing the 4 bytes to update the digest with in little-endian order + */ +void sha3_256_Update_int32le(inout sha3_256_context ctx, uint32_t bytesIn) { + if (ctx.byteIndex <= 4u) { + //ctx.s[ctx.wordIndex] ^= (uint64_t(bytesIn) << (ctx.byteIndex * 8u)); + ctx.saved |= (uint64_t(bytesIn) << (ctx.byteIndex * 8u)); + ctx.byteIndex += 4u; + sha3_256_tryFlushWord(ctx); + } else { + u8vec4 unpacked = unpack8(bytesIn); + sha3_256_Update(ctx, unpacked.x); + sha3_256_Update(ctx, unpacked.y); + sha3_256_Update(ctx, unpacked.z); + sha3_256_Update(ctx, unpacked.w); + } +} + +/** + * Updates the SHA3-256 digest with 8 bytes, stored in a 64-bit integer in little-endian order. + * + * @param ctx the SHA3-256 context + * @param bytesIn an integer containing the 8 bytes to update the digest with in little-endian order + */ +void sha3_256_Update_int64le(inout sha3_256_context ctx, uint64_t bytesIn) { + if (ctx.byteIndex == 0u) { + // Digest the entire word in one go + ctx.s[ctx.wordIndex] ^= bytesIn; + ctx.wordIndex++; + sha3_256_tryFlushBlock(ctx); + } else { + // Split the word in two halves and digest them separately + uvec2 unpacked = unpackUint2x32(bytesIn); + sha3_256_Update_int32le(ctx, unpacked.x); + sha3_256_Update_int32le(ctx, unpacked.y); + } +} + +/** + * Updates the SHA3-256 digest with between 0 and 8 bytes, stored in a 64-bit integer in little-endian order. + * + * @param ctx the SHA3-256 context + * @param bytesIn an integer containing the 8 bytes to update the digest with in little-endian order + * @param count the number of bytes to update the digest with. Must be 0 <= count <= 8 + */ +void sha3_256_Update_int64le_partial(inout sha3_256_context ctx, uint64_t bytesIn, uint count) { + switch (count) { + case 0u: + // Do nothing + break; + case 4u: + // Digest the entire lower half of the word in one go + sha3_256_Update_int32le(ctx, unpackUint2x32(bytesIn).x); + break; + case 8u: + // Digest the entire word in one go + sha3_256_Update_int64le(ctx, bytesIn); + break; + default: + if (ctx.byteIndex <= 8u - count) { + // Digest all the relevant bits of the word in one go + ctx.saved |= ((uint64_t(bytesIn) & ((uint64_t(1) << (count * 8u)) - 1)) << (ctx.byteIndex * 8u)); + ctx.byteIndex += count; + sha3_256_tryFlushWord(ctx); + break; + } else { + // Slow case! + for (uint i = 0; i < count; i++) { + sha3_256_Update(ctx, uint8_t((bytesIn >> (i * 8u)) & uint64_t(0xFFu))); + } + break; + } + } +} + +/** + * Finishes digesting data into the SHA3-256 digest and returns the 32-byte hash. + * + * @param ctx the SHA3-256 context + * @return the 32-byte hash + */ +uint8_t[SHA3_256_BYTES] sha3_256_Finalize(inout sha3_256_context ctx) { + /* Append 2-bit suffix 01, per SHA-3 spec. Instead of 1 for padding we + * use 1<<2 below. The 0x02 below corresponds to the suffix 01. + * Overall, we feed 0, then 1, and finally 1 to start padding. Without + * M || 01, we would simply use 1 to start padding. */ + + uint64_t t = SHA3_USE_KECCAK + /* Keccak version */ + ? uint64_t((uint64_t(1)) << (ctx.byteIndex * 8u)) + /* SHA3 version */ + : uint64_t((uint64_t(0x02 | (1 << 2))) << ((ctx.byteIndex) * 8u)); + + ctx.s[ctx.wordIndex] ^= ctx.saved ^ t; + + ctx.s[SHA3_KECCAK_SPONGE_WORDS - SHA3_256_CAPACITY_WORDS - 1u] ^= 0x8000000000000000UL; + keccakf(ctx.s); + + uint8_t result[SHA3_256_BYTES]; + for (uint i = 0; i < SHA3_256_BYTES / 8u; i++) { + t = ctx.s[i]; + result[i * 8 + 0] = uint8_t((t) & uint64_t(0xFFu)); + result[i * 8 + 1] = uint8_t((t >> 8u) & uint64_t(0xFFu)); + result[i * 8 + 2] = uint8_t((t >> 16u) & uint64_t(0xFFu)); + result[i * 8 + 3] = uint8_t((t >> 24u) & uint64_t(0xFFu)); + result[i * 8 + 4] = uint8_t((t >> 32u) & uint64_t(0xFFu)); + result[i * 8 + 5] = uint8_t((t >> 40u) & uint64_t(0xFFu)); + result[i * 8 + 6] = uint8_t((t >> 48u) & uint64_t(0xFFu)); + result[i * 8 + 7] = uint8_t((t >> 56u) & uint64_t(0xFFu)); + } + return result; +} diff --git a/src/tests.rs b/src/tests.rs index e97a3bc..3d911eb 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -4992,7 +4992,9 @@ async fn construct_mining_common_info( let tx = construct_coinbase_tx(block_num, amount, addr); let hash = construct_tx_hash(&tx); block.header = apply_mining_tx(block.header, Vec::new(), hash.clone()); - block.header = generate_pow_for_block(block.header); + block.header.nonce_and_mining_tx_hash.0 = generate_pow_for_block(&block.header) + .expect("error occurred while mining block") + .expect("couldn't find a valid nonce"); block_txs.insert(hash, tx); CommonBlockInfo { diff --git a/src/utils.rs b/src/utils.rs index 6a8272c..06e4a6d 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,12 +1,14 @@ +use crate::asert::{CompactTarget, CompactTargetError, HeaderHash}; use crate::comms_handler::Node; use crate::configurations::{UnicornFixedInfo, UtxoSetSpec, WalletTxSpec}; use crate::constants::{ - BLOCK_PREPEND, COINBASE_MATURITY, D_DISPLAY_PLACES_U64, MINING_DIFFICULTY, NETWORK_VERSION, - REWARD_ISSUANCE_VAL, REWARD_SMOOTHING_VAL, + ADDRESS_POW_NONCE_LEN, BLOCK_PREPEND, COINBASE_MATURITY, D_DISPLAY_PLACES_U64, + MINING_DIFFICULTY, NETWORK_VERSION, POW_RNUM_SELECT, REWARD_ISSUANCE_VAL, REWARD_SMOOTHING_VAL, }; use crate::interfaces::{ BlockchainItem, BlockchainItemMeta, DruidDroplet, PowInfo, ProofOfWork, StoredSerializingBlock, }; +use crate::miner_pow::{MineError, PoWError, PoWObject}; use crate::wallet::WalletDb; use crate::Rs2JsMsg; use bincode::serialize; @@ -17,18 +19,20 @@ use rand::{self, Rng}; use serde::Deserialize; use std::collections::BTreeMap; use std::error::Error; -use std::fmt; +use std::fmt::Write; use std::fs::File; use std::future::Future; use std::io::Read; use std::net::{IpAddr, SocketAddr}; use std::sync::{Arc, Mutex}; use std::time::{Duration, UNIX_EPOCH}; +use std::{fmt, vec}; use tokio::runtime::Runtime; use tokio::sync::{mpsc, oneshot}; use tokio::task; use tokio::time::Instant; use tracing::{info, trace, warn}; +use tracing_subscriber::field::debug; use trust_dns_resolver::TokioAsyncResolver; use tw_chain::constants::TOTAL_TOKENS; use tw_chain::crypto::sha3_256; @@ -178,21 +182,35 @@ impl fmt::Debug for LocalEventChannel { #[derive(PartialEq, Eq)] pub struct StringError(pub String); -impl Error for StringError { - fn description(&self) -> &str { - &self.0 +impl StringError { + pub fn from(value: T) -> Self { + Self(value.to_string()) } } +impl Error for StringError {} + impl fmt::Display for StringError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.0) + f.write_str(&self.0) } } impl fmt::Debug for StringError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.0) + f.write_str(&self.0) + } +} + +impl From for StringError { + fn from(value: String) -> Self { + Self(value) + } +} + +impl From<&str> for StringError { + fn from(value: &str) -> Self { + Self(value.to_owned()) } } @@ -462,14 +480,36 @@ pub fn apply_mining_tx(mut header: BlockHeader, nonce: Vec, tx_hash: String) header } -/// Generates a random sequence of values for a nonce -pub fn generate_pow_nonce() -> Vec { - generate_random_num(16) -} - /// Generates a random num for use for proof of work pub fn generate_pow_random_num() -> Vec { - generate_random_num(10) + generate_random_num(POW_RNUM_SELECT) +} + +/// Increments the provided nonce +/// +/// ### Arguments +/// +/// * `nonce` - Nonce to increment +fn get_nonce_increment(nonce: &[u8]) -> Vec { + // Ensure the input vector has exactly 4 bytes + if nonce.len() != 4 { + return vec![]; + } + + // Convert the u8 slice to a fixed-size array + let mut array: [u8; 4] = [0; 4]; + array.copy_from_slice(&nonce); + + // Interpret the fixed-size array as i32 + let current_value = i32::from_le_bytes(array); // Assuming little-endian byte order + + // Increment the value + let incremented_value = current_value + 1; + + // Convert incremented value back to u8 vector + let incremented_bytes = incremented_value.to_le_bytes().to_vec(); // Convert i32 to little-endian u8 vector + + incremented_bytes } /// Generates a garbage random num for use in network testing @@ -566,11 +606,13 @@ pub fn generate_pow_for_address( task::spawn_blocking(move || { let mut pow = ProofOfWork { address, - nonce: generate_pow_nonce(), + // TODO: instead of hard-coding a separate constant for this, rewrite this to use + // the existing miner infrastructure + nonce: vec![0; ADDRESS_POW_NONCE_LEN], }; while !validate_pow_for_address(&pow, &rand_num.as_ref()) { - pow.nonce = generate_pow_nonce(); + pow.nonce = get_nonce_increment(&pow.nonce); } (pow, pow_info, peer) @@ -583,7 +625,7 @@ pub fn validate_pow_for_address(pow: &ProofOfWork, rand_num: &Option<&Vec>) pow_body.extend(rand_num.iter().flat_map(|r| r.iter()).copied()); pow_body.extend(&pow.nonce); - validate_pow_leading_zeroes(&pow_body).is_some() + validate_pow_leading_zeroes(&pow_body).is_ok() } /// Will attempt deserialization of a given byte array using bincode @@ -600,12 +642,42 @@ pub fn try_deserialize<'a, T: Deserialize<'a>>(data: &'a [u8]) -> Result BlockHeader { - header.nonce_and_mining_tx_hash.0 = generate_pow_nonce(); - while !validate_pow_block(&header) { - header.nonce_and_mining_tx_hash.0 = generate_pow_nonce(); +pub fn generate_pow_for_block(header: &BlockHeader) -> Result>, MineError> { + use crate::miner_pow::*; + + let mut statistics = Default::default(); + let terminate_flag = None; + let timeout_duration = None; + + let miner = create_any_miner(Some( + &header.pow_difficulty().map_err(MineError::GetDifficulty)?, + )); + let result = generate_pow( + &mut *miner.lock().unwrap(), + header, + &mut statistics, + terminate_flag, + timeout_duration, + )?; + + match result { + MineResult::FoundNonce { nonce } => { + // Verify that the found nonce is actually valid + let mut header_copy = header.clone(); + header_copy.nonce_and_mining_tx_hash.0 = nonce.clone(); + assert_eq!( + validate_pow_block(&header_copy), + Ok(()), + "generated PoW nonce {} isn't actually valid! block header: {:#?}", + hex::encode(&nonce), + header + ); + Ok(Some(header_copy.nonce_and_mining_tx_hash.0)) + } + MineResult::Exhausted => Ok(None), + MineResult::TerminateRequested => Ok(None), + MineResult::TimeoutReached => Ok(None), } - header } /// Verify block is valid & consistent: Can be fully verified from PoW hash. @@ -626,22 +698,67 @@ pub fn construct_valid_block_pow_hash(block: &Block) -> Result fmt::Result { + match self { + Self::InvalidNonce { cause } => write!(f, "Block header nonce is invalid: {}", cause), + Self::InvalidDifficulty { cause } => { + write!(f, "Block header difficulty is invalid: {}", cause) + } + Self::InvalidLeadingBytes { cause } => write!( + f, + "Block header does not meet the difficulty requirements: {}", + cause + ), + Self::DoesntMeetThreshold { + header_hash: hash, + target, + } => write!( + f, + "Block header does not meet the difficulty requirements: target={}, hash={:?}", + target, + hash.as_bytes() + ), + } + } +} + /// Validate Proof of Work for a block with a mining transaction /// /// ### Arguments /// /// * `header` - The header for PoW -pub fn validate_pow_block(header: &BlockHeader) -> bool { - validate_pow_block_hash(header).is_some() +pub fn validate_pow_block(header: &BlockHeader) -> Result<(), ValidatePoWBlockError> { + validate_pow_block_hash(header).map(|_| ()) } /// Validate Proof of Work for a block with a mining transaction returning the PoW hash @@ -649,7 +766,7 @@ pub fn validate_pow_block(header: &BlockHeader) -> bool { /// ### Arguments /// /// * `header` - The header for PoW -fn validate_pow_block_hash(header: &BlockHeader) -> Option> { +fn validate_pow_block_hash(header: &BlockHeader) -> Result { // [AM] even though we've got explicit activation height in configuration // and a hard-coded fallback elsewhere in the code, here // we're basically sniffing at the difficulty field in the @@ -663,35 +780,77 @@ fn validate_pow_block_hash(header: &BlockHeader) -> Option> { // arguments or be moved to something more stateful that can // access configuration. + // Ensure the nonce length is valid + BlockHeader::check_nonce_length(header.get_nonce().len()) + .map_err(|cause| ValidatePoWBlockError::InvalidNonce { cause })?; + if header.difficulty.is_empty() { let pow = serialize(header).unwrap(); validate_pow_leading_zeroes(&pow) + .map(HeaderHash::from) + .map_err(|cause| ValidatePoWBlockError::InvalidLeadingBytes { cause }) } else { - use crate::asert::{CompactTarget, HeaderHash}; + info!("We have difficulty"); - let target = CompactTarget::try_from_slice(&header.difficulty)?; - let header_hash = HeaderHash::try_calculate(header)?; + let target = CompactTarget::try_from_slice(&header.difficulty) + .map_err(|cause| ValidatePoWBlockError::InvalidDifficulty { cause })?; + let header_hash = HeaderHash::calculate(header); if header_hash.is_below_compact_target(&target) { - Some(header_hash.into_vec()) + Ok(header_hash) } else { - None + Err(ValidatePoWBlockError::DoesntMeetThreshold { + header_hash, + target, + }) } } } +/// An error which indicates that a Proof-of-Work action contained an invalid number of leading +/// zero bytes. +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub struct ValidatePoWLeadingZeroesError { + /// The computed hash + pub hash: sha3::digest::Output, + /// The mining difficulty + pub mining_difficulty: usize, +} + +impl Error for ValidatePoWLeadingZeroesError {} + +impl fmt::Display for ValidatePoWLeadingZeroesError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Hash {:?} has fewer than {} leading zero bytes", + self.hash.as_slice(), + self.mining_difficulty + ) + } +} + /// Check the hash of given data reach MINING_DIFFICULTY /// /// ### Arguments /// /// * `mining_difficulty` - usize mining difficulty /// * `pow` - &u8 proof of work -fn validate_pow_leading_zeroes_for_diff(mining_difficulty: usize, pow: &[u8]) -> Option> { - let pow_hash = sha3_256::digest(pow).to_vec(); - if pow_hash[0..mining_difficulty].iter().all(|v| *v == 0) { - Some(pow_hash) +fn validate_pow_leading_zeroes_for_diff( + mining_difficulty: usize, + pow: &[u8], +) -> Result, ValidatePoWLeadingZeroesError> { + let hash = sha3_256::digest(pow); + if hash.as_slice()[0..mining_difficulty] + .iter() + .all(|v| *v == 0) + { + Ok(hash) } else { - None + Err(ValidatePoWLeadingZeroesError { + hash, + mining_difficulty, + }) } } @@ -700,7 +859,9 @@ fn validate_pow_leading_zeroes_for_diff(mining_difficulty: usize, pow: &[u8]) -> /// ### Arguments /// /// * `pow` - &u8 proof of work -fn validate_pow_leading_zeroes(pow: &[u8]) -> Option> { +fn validate_pow_leading_zeroes( + pow: &[u8], +) -> Result, ValidatePoWLeadingZeroesError> { validate_pow_leading_zeroes_for_diff(MINING_DIFFICULTY, pow) } @@ -1342,6 +1503,133 @@ pub mod rug_integer { } } +#[derive(Clone, Copy, PartialEq, PartialOrd, Debug)] +pub struct UnitsPrefixed { + pub value: f64, + pub unit_name: &'static str, + pub duration: Option, +} + +impl fmt::Display for UnitsPrefixed { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let scales = &[ + (1000_000_000_000f64, "T"), + (1000_000_000f64, "G"), + (1000_000f64, "M"), + (1000f64, "k"), + ]; + + let value = match &self.duration { + None => self.value, + Some(duration) => self.value / duration.as_secs_f64(), + }; + + let (divisor, prefix) = scales + .iter() + .find(|(threshold, _)| value > *threshold) + .unwrap_or(&(1f64, "")); + + match &self.duration { + None => write!(f, "{} {}{}", value / divisor, prefix, self.unit_name), + Some(_) => write!(f, "{} {}{}/s", value / divisor, prefix, self.unit_name), + } + } +} + +/// Splits the given integer range into smaller segments with the given maximum size. +/// +/// ### Arguments +/// +/// * `start` - the first value +/// * `len` - the total length of the range +/// * `max_block_size` - the maximum size of each block +pub fn split_range_into_blocks( + start: u32, + len: u32, + max_block_size: u32, +) -> impl Iterator { + assert_ne!(max_block_size, 0); + + struct Itr { + pos: u32, + remaining: u32, + max_block_size: u32, + } + + impl Iterator for Itr { + type Item = (u32, u32); + + fn next(&mut self) -> Option { + if self.remaining == 0 { + return None; + } + + let block_size = u32::min(self.remaining, self.max_block_size); + let result = (self.pos, block_size); + self.pos += block_size; + self.remaining -= block_size; + Some(result) + } + } + + Itr { + pos: start, + remaining: len, + max_block_size, + } +} + +/// Returns all possible byte strings with the given length. +/// +/// Note that for a length of `0` this will return a single 0-length byte string. +/// +/// ### Arguments +/// +/// * `len` - the length of the byte strings to generate +pub fn all_byte_strings(len: usize) -> impl Iterator> { + struct Itr { + buf: Box<[u8]>, + done: bool, + } + + impl Iterator for Itr { + type Item = Box<[u8]>; + + fn next(&mut self) -> Option { + if self.done { + // Iterator has already reached the end + return None; + } + + // Save the current value to be returned later + let result = self.buf.clone(); + + // Try to increment the value + for byte in self.buf.as_mut() { + if let Some(next_value) = (*byte).checked_add(1) { + // The byte was incremented successfully + *byte = next_value; + return Some(result); + } else { + // The byte overflowed, set it to 0 and proceed to increment the next one + *byte = 0; + } + } + + // If we got this far, all the bytes were 0xFF, meaning that we've successfully + // iterated through the entire sequence. + self.done = true; + + Some(result) + } + } + + Itr { + buf: vec![0u8; len].into_boxed_slice(), + done: false, + } +} + /*---- TESTS ----*/ #[cfg(test)] @@ -1381,4 +1669,46 @@ mod util_tests { SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 12300) ); } + + #[test] + fn test_split_range_into_blocks() { + assert_eq!( + split_range_into_blocks(0, 16, 4).collect::>(), + vec![(0, 4), (4, 4), (8, 4), (12, 4)] + ); + + assert_eq!( + split_range_into_blocks(0, 15, 4).collect::>(), + vec![(0, 4), (4, 4), (8, 4), (12, 3)] + ); + + assert_eq!( + split_range_into_blocks(0, u32::MAX, 1 << 30).collect::>(), + vec![ + (0 << 30, 1 << 30), + (1 << 30, 1 << 30), + (2 << 30, 1 << 30), + (3 << 30, (1 << 30) - 1), + ] + ); + } + + #[test] + fn test_all_byte_strings() { + assert_eq!(all_byte_strings(0).collect::>(), vec![Box::from([])]); + + assert_eq!( + all_byte_strings(1).collect::>(), + (u8::MIN..=u8::MAX) + .map(|b| b.to_le_bytes().into()) + .collect::>() + ); + + assert_eq!( + all_byte_strings(2).collect::>(), + (u16::MIN..=u16::MAX) + .map(|b| b.to_le_bytes().into()) + .collect::>() + ); + } }