pax_global_header00006660000000000000000000000064151271015150014510gustar00rootroot0000000000000052 comment=5b061233a3d3323771b2be98e17f543e59346619 datum_gateway-0.4.1beta/000077500000000000000000000000001512710151500152015ustar00rootroot00000000000000datum_gateway-0.4.1beta/.github/000077500000000000000000000000001512710151500165415ustar00rootroot00000000000000datum_gateway-0.4.1beta/.github/workflows/000077500000000000000000000000001512710151500205765ustar00rootroot00000000000000datum_gateway-0.4.1beta/.github/workflows/build.yaml000066400000000000000000000221141512710151500225610ustar00rootroot00000000000000name: Build DATUM Gateway on: schedule: - cron: '0 0 1 * *' push: pull_request: env: GLOBAL_CFLAGS: "-Wall -Werror" ASAN_CFLAGS: "-fsanitize=address" ASAN_OPTIONS: "abort_on_error=1:print_stacktrace=1" UBSAN_CFLAGS: "-fsanitize=undefined -fno-sanitize-recover=all" UBSAN_OPTIONS: "print_stacktrace=1" jobs: build: strategy: matrix: os: - ubuntu:latest - ubuntu:22.04 - debian:latest - debian:stable - debian:oldstable - almalinux:latest - amazonlinux:latest - fedora:latest - oraclelinux:9 - alpine:latest - archlinux:latest - gentoo/stage3:musl-hardened - gentoo/stage3:hardened - freebsd config: - cmake_args: "-DENABLE_API=ON -DCMAKE_C_COMPILER=gcc" - cmake_args: "-DENABLE_API=ON -DCMAKE_C_COMPILER=clang" - cmake_args: "-DENABLE_API=ON -DCMAKE_C_COMPILER=gcc -DCMAKE_C_FLAGS=-DDATUM_API_FOR_UMBREL=ON" extra_cflags: "-DDATUM_API_FOR_UMBREL" - cmake_args: "-DENABLE_API=OFF" exclude: # Clang configured for C11 rejects our C23 usage - os: debian:oldstable config: cmake_args: "-DENABLE_API=ON -DCMAKE_C_COMPILER=clang" runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Restore package cache if: startsWith(matrix.os, 'gentoo/') uses: actions/cache/restore@v4 with: path: pkg-cache key: never-exists restore-keys: | ${{ matrix.os }}/pkg-cache/${{ matrix.config.cmake_args }}/ ${{ matrix.os }}/pkg-cache/ - name: Build inside Docker if: "!startsWith(matrix.os, 'freebsd')" id: docker-build run: | PKG_CACHE_DIR=/pkg-cache PACKAGES_CLANG='clang' PACKAGES_GCC='gcc' # ASan detect_leaks is not supported on FreeBSD, so add it to options here ASAN_OPTIONS="${ASAN_OPTIONS}:detect_leaks=1" case "${{ matrix.os }}" in debian:*|ubuntu:*) INSTALL_CMD="apt update && apt install -y" PACKAGES="git libc6-dev cmake libcurl4-openssl-dev libjansson-dev libsodium-dev pkgconf" PACKAGES_API="libmicrohttpd-dev" ;; almalinux:*|amazonlinux:*|fedora:*|oraclelinux:*) INSTALL_CMD="dnf install -y" [[ "${{ matrix.os }}" =~ ^almalinux: ]] && INSTALL_CMD="dnf install -y dnf-plugins-core && dnf config-manager --set-enabled crb && $INSTALL_CMD" [[ "${{ matrix.os }}" =~ ^oraclelinux: ]] && INSTALL_CMD="dnf install -y dnf-plugins-core && dnf config-manager --set-enabled ol9_codeready_builder && $INSTALL_CMD" [[ "${{ matrix.os }}" =~ ^(alma|oracle)linux: ]] && INSTALL_CMD="dnf install -y epel-release && $INSTALL_CMD" PACKAGES="git cmake libcurl-devel jansson-devel libsodium-devel pkgconf" PACKAGES_API="libmicrohttpd-devel" PACKAGES_GCC="$PACKAGES_GCC libasan libubsan" [[ "${{ matrix.config.cmake_args }}" =~ clang|gcc ]] || PACKAGES="$PACKAGES $PACKAGES_GCC" ;; alpine:*) INSTALL_CMD="apk add --no-cache" PACKAGES="git build-base cmake argp-standalone curl-dev jansson-dev libsodium-dev compiler-rt" PACKAGES_API="libmicrohttpd-dev" # musl only supports ASan with clang [[ "${{ matrix.config.cmake_args }}" =~ clang ]] || ASAN_CFLAGS= ;; archlinux:*) INSTALL_CMD="pacman -Syu --noconfirm" PACKAGES="git base-devel cmake curl jansson libsodium" PACKAGES_API="libmicrohttpd" ;; gentoo/*) PKG_CACHE_DIR='/var/cache/binpkgs' INIT_CMD=' if [ -e /var/cache/binpkgs/gentoo-repo.txz ]; then rm -rf /var/db/repos/gentoo tar -C /var/db/repos -xpf /var/cache/binpkgs/gentoo-repo.txz rm /var/cache/binpkgs/gentoo-repo.txz cache_cksum() { find /var/db/repos/gentoo /var/cache/binpkgs -type f -print0 | sort -z | xargs -0 sha256sum } cache_cksum > /tmp/initial-pkg-cache.cksum emerge --sync --quiet || true # can fail if cache is recent enough else emerge --sync --quiet fi ' INSTALL_CMD="emerge -1 sec-keys/openpgp-keys-gentoo-release && getuto && cat /etc/portage/make.conf && USE='-httpsrr -perl -extra -static-analyzer compiler-rt -openmp sanitize -adns -alt-svc -ftp -hsts -http2 -http3 -imap -pop3 -progress-meter -psl -quic -curl_quic_openssl -smtp -tftp -websockets' emerge -vuDtkg1 --noreplace --jobs=\$(nproc) --buildpkg" PACKAGES="dev-vcs/git dev-build/cmake net-misc/curl dev-libs/jansson dev-libs/libsodium virtual/pkgconfig app-portage/gentoolkit" [[ "${{ matrix.os }}" =~ musl ]] && PACKAGES="$PACKAGES sys-libs/argp-standalone" PACKAGES_API="net-libs/libmicrohttpd" PACKAGES_CLANG="llvm-core/clang" # musl only supports ASan with clang [[ "${{ matrix.os }}" =~ musl && ! "${{ matrix.config.cmake_args }}" =~ clang ]] && ASAN_CFLAGS= POSTINSTALL_CMD=' set +ex source /etc/profile set -ex ' CLEANUP_CMD=' [ -e /tmp/initial-pkg-cache.cksum ] && cache_cksum >/tmp/final-pkg-cache.cksum if ! diff -u /tmp/{initial,final}-pkg-cache.cksum; then ( cd /var/db/repos && tar --sort=name -cpJf /var/cache/binpkgs/gentoo-repo.txz gentoo ) touch /output/SAVE_CACHE eclean -t 2w packages --changed-deps fi ' ;; esac PACKAGES="$PACKAGES ${{ matrix.config.extra_deps }}" if [[ "${{ matrix.config.cmake_args }}" =~ ENABLE_API=ON ]]; then PACKAGES="$PACKAGES $PACKAGES_API" fi if [[ "${{ matrix.config.cmake_args }}" =~ CMAKE_C_COMPILER=gcc ]]; then PACKAGES="$PACKAGES $PACKAGES_GCC" elif [[ "${{ matrix.config.cmake_args }}" =~ CMAKE_C_COMPILER=clang ]]; then PACKAGES="$PACKAGES $PACKAGES_CLANG" fi CMD="set -ex ${INIT_CMD} ${INSTALL_CMD} ${PACKAGES} ${POSTINSTALL_CMD} git config --global --add safe.directory /workspace mkdir -p build cd build CFLAGS='${{ env.GLOBAL_CFLAGS }}' CFLAGS=\"\$CFLAGS\"' ${ASAN_CFLAGS} ${UBSAN_CFLAGS}' CFLAGS=\"\$CFLAGS\"' ${{ matrix.config.extra_cflags }}' cmake /workspace ${{ matrix.config.cmake_args }} -DCMAKE_C_FLAGS=\"\$CFLAGS\" make -j\$(nproc) export ASAN_OPTIONS='$ASAN_OPTIONS' UBSAN_OPTIONS='$UBSAN_OPTIONS' ./datum_gateway --help ./datum_gateway --test ${CLEANUP_CMD} " docker run \ -v ./pkg-cache:"${PKG_CACHE_DIR}" \ -v ./output:"/output" \ -v "${{ github.workspace }}:/workspace":ro \ "${{ matrix.os }}" \ /bin/sh -c "${CMD}" if [ -e output/SAVE_CACHE ]; then echo 'save_cache=true' >> "$GITHUB_OUTPUT" else echo 'save_cache=false' >> "$GITHUB_OUTPUT" fi - name: Save package cache if: steps.docker-build.outputs.save_cache == 'true' uses: actions/cache/save@v4 with: path: pkg-cache key: ${{ matrix.os }}/pkg-cache/${{ matrix.config.cmake_args }}/${{ github.sha }} - name: Build inside FreeBSD VM if: startsWith(matrix.os, 'freebsd') uses: vmactions/freebsd-vm@v1 with: prepare: | PACKAGES="git cmake pkgconf curl jansson libsodium libmicrohttpd argp-standalone libepoll-shim" if echo "${{ matrix.config.cmake_args }}" | grep -q "CMAKE_C_COMPILER=gcc"; then PACKAGES="$PACKAGES gcc" fi pkg install -y $PACKAGES run: | ASAN_CFLAGS='${{ env.ASAN_CFLAGS }}' UBSAN_CFLAGS='${{ env.UBSAN_CFLAGS }}' # GCC ASan doesn't work with FreeBSD case "${{ matrix.config.cmake_args }}" in *gcc*) ASAN_CFLAGS= ;; esac git config --global --add safe.directory ${{ github.workspace }} mkdir -p build cd build CFLAGS='${{ env.GLOBAL_CFLAGS }}' CFLAGS="$CFLAGS ${ASAN_CFLAGS} ${UBSAN_CFLAGS}" CFLAGS="$CFLAGS"' ${{ matrix.config.extra_cflags }}' echo "Using CFLAGS: $CFLAGS" cmake ${{ github.workspace }} ${{ matrix.config.cmake_args }} -DCMAKE_C_FLAGS="${CFLAGS}" make export ASAN_OPTIONS='${{ env.ASAN_OPTIONS }}' UBSAN_OPTIONS='${{ env.UBSAN_OPTIONS }}' ./datum_gateway --help ./datum_gateway --test datum_gateway-0.4.1beta/.gitignore000066400000000000000000000002641512710151500171730ustar00rootroot00000000000000CMakeCache.txt CMakeFiles Makefile cmake_install.cmake git_version.h web_resources.h datum_gateway *.conf *.json !doc/example_datum_gateway_config.json .DS_Store *.orig *.rej *~ datum_gateway-0.4.1beta/CMakeLists.txt000066400000000000000000000123221512710151500177410ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.13) project(DATUM VERSION 0.4.1 LANGUAGES C) # Enable C23 if supported, else fall back to C11 for compatibility if(CMAKE_VERSION VERSION_LESS "3.21") # Older CMake: C23 not recognized; use C11 set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) else() # CMake 3.21+: C23 is available set(CMAKE_C_STANDARD 23) set(CMAKE_C_STANDARD_REQUIRED OFF) endif() option(ENABLE_API "Build API support." ON) include(GNUInstallDirs) add_executable(datum_gateway src/datum_blocktemplates.c src/datum_coinbaser.c src/datum_conf.c src/datum_conf_tests.c src/datum_gateway.c src/datum_jsonrpc.c src/datum_logger.c src/datum_protocol.c src/datum_queue.c src/datum_sockets.c src/datum_stratum.c src/datum_stratum_dupes.c src/datum_stratum_tests.c src/datum_submitblock.c src/datum_utils.c src/datum_utils_tests.c src/thirdparty_base58.c src/thirdparty_segwit_addr.c ${CMAKE_CURRENT_BINARY_DIR}/web_resources.h ) install(TARGETS datum_gateway DESTINATION bin) set(WEB_RESOURCES www/auth_failed.html www/home.html www/clients_top.html www/coinbaser_top.html www/config.html www/config_errors.html www/config_restart.html www/threads_top.html www/foot.html www/assets/post.js www/assets/style.css www/assets/icons/datum_logo.svg www/assets/icons/favicon.ico ) find_package(PkgConfig REQUIRED) pkg_check_modules(CURL REQUIRED libcurl) pkg_check_modules(JANSSON REQUIRED jansson) if(ENABLE_API) pkg_check_modules(MICROHTTPD REQUIRED libmicrohttpd) endif() pkg_check_modules(SODIUM REQUIRED libsodium) find_package(Threads REQUIRED) include(CheckFunctionExists) include(CheckLibraryExists) include(CMakePushCheckState) cmake_push_check_state(RESET) string(APPEND CMAKE_REQUIRED_FLAGS -Wno-error) set(POW_LIBS "") check_library_exists(m pow "" LIBM) if(LIBM) list(APPEND POW_LIBS "m") endif() set(ARGP_LIBS "") check_function_exists(argp_parse HAVE_ARGP_PARSE) if(NOT HAVE_ARGP_PARSE) check_library_exists(argp argp_parse "" ARGP) if(NOT ARGP AND CMAKE_SYSTEM_NAME STREQUAL "FreeBSD") # Workaround bug where CMake doesn't check the standard install location on FreeBSD unset(ARGP CACHE) check_library_exists(argp argp_parse "/usr/local/lib" ARGP) endif() if(ARGP) list(APPEND ARGP_LIBS "argp") endif() endif() check_function_exists(epoll_wait HAVE_EPOLL_WAIT) if(HAVE_EPOLL_WAIT) set(EPOLL_SHIM_INCLUDE_DIRS "") set(EPOLL_SHIM_LIBRARIES "") else() pkg_check_modules(EPOLL_SHIM REQUIRED epoll-shim) endif() cmake_pop_check_state() add_custom_target(generate_git_version BYPRODUCTS ${PROJECT_BINARY_DIR}/git_version.h COMMAND ${CMAKE_COMMAND} -DBUILD_INFO_HEADER_PATH=${PROJECT_BINARY_DIR}/git_version.h -DSOURCE_DIR=${PROJECT_SOURCE_DIR} -P ${PROJECT_SOURCE_DIR}/cmake/script/GenerateBuildInfo.cmake DEPENDS cmake/script/GenerateBuildInfo.cmake COMMENT "Generating git_version.h" VERBATIM ) add_dependencies(datum_gateway generate_git_version) add_custom_command( OUTPUT web_resources.h COMMAND ${CMAKE_COMMAND} "-DINPUT_FILES=${WEB_RESOURCES}" -DOUTPUT_FILE=web_resources.h -DSOURCE_DIR=${PROJECT_SOURCE_DIR} -P ${PROJECT_SOURCE_DIR}/cmake/script/EmbedResources.cmake DEPENDS ${WEB_RESOURCES} cmake/script/EmbedResources.cmake VERBATIM ) target_include_directories(datum_gateway PRIVATE $ ${EPOLL_SHIM_INCLUDE_DIRS} ${CURL_INCLUDE_DIRS} ${JANSSON_INCLUDE_DIRS} ${SODIUM_INCLUDE_DIRS} ) target_link_directories(datum_gateway PUBLIC ${CURL_LIBRARY_DIRS} ${JANSSON_LIBRARY_DIRS} ${SODIUM_LIBRARY_DIRS} ) target_link_libraries(datum_gateway PUBLIC ${POW_LIBS} Threads::Threads ${ARGP_LIBS} ${EPOLL_SHIM_LIBRARIES} ${CURL_LIBRARIES} ${CURL_LDFLAGS} ${CURL_LDFLAGS_OTHER} ${JANSSON_LIBRARIES} ${JANSSON_LDFLAGS} ${JANSSON_LDFLAGS_OTHER} ${SODIUM_LIBRARIES} ${SODIUM_LDFLAGS} ${SODIUM_LDFLAGS_OTHER} ) target_compile_options(datum_gateway PUBLIC ${CURL_CFLAGS} ${CURL_CFLAGS_OTHER} ${JANSSON_CFLAGS} ${JANSSON_CFLAGS_OTHER} ${SODIUM_CFLAGS} ${SODIUM_CFLAGS_OTHER} ) if(ENABLE_API) target_sources(datum_gateway PRIVATE src/datum_api.c) target_include_directories(datum_gateway PRIVATE ${MICROHTTPD_INCLUDE_DIRS}) target_link_directories(datum_gateway PUBLIC ${MICROHTTPD_LIBRARY_DIRS}) target_link_libraries(datum_gateway PUBLIC ${MICROHTTPD_LIBRARIES} ${MICROHTTPD_LDFLAGS} ${MICROHTTPD_LDFLAGS_OTHER}) target_compile_options(datum_gateway PUBLIC -DENABLE_API ${MICROHTTPD_CFLAGS} ${MICROHTTPD_CFLAGS_OTHER} ) endif() install(FILES README.md DESTINATION ${CMAKE_INSTALL_DOCDIR}) install(FILES doc/DATUM_recommended_setup-network_diagram.svg DESTINATION ${CMAKE_INSTALL_DOCDIR}/doc) install(FILES doc/usernames.md DESTINATION ${CMAKE_INSTALL_DOCDIR}/doc) set(PREGEN_DOC ${CMAKE_SOURCE_DIR}/doc/example_datum_gateway_config.json) install(FILES ${PREGEN_DOC} DESTINATION ${CMAKE_INSTALL_DOCDIR}) if(NOT CMAKE_CROSSCOMPILING) set(GENERATED_DOC ${CMAKE_BINARY_DIR}/CMakeFiles/generated_example_datum_gateway_config.json) add_custom_command( TARGET datum_gateway POST_BUILD COMMAND ${CMAKE_COMMAND} -DDATUM_GATEWAY=$ -DGENERATED_DOC=${GENERATED_DOC} -DPREGEN_DOC=${PREGEN_DOC} -P ${PROJECT_SOURCE_DIR}/cmake/script/VerifyExample.cmake COMMENT "Verifying pre-generated documentation is up-to-date" VERBATIM ) endif() datum_gateway-0.4.1beta/Dockerfile000066400000000000000000000035551512710151500172030ustar00rootroot00000000000000FROM debian:bookworm-slim AS builder # Install dependencies for building RUN apt-get update && apt-get install -y \ build-essential \ cmake \ gcc \ pkg-config \ libjansson-dev \ libmicrohttpd-dev \ libsodium-dev \ libcurl4-openssl-dev \ git \ --no-install-recommends \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* WORKDIR /build # Copy entire repository for git information COPY . . # Build the application with optimization flags RUN cmake -DCMAKE_BUILD_TYPE=Release . && make -j$(nproc) # Use the same Debian version for runtime to ensure library compatibility FROM debian:bookworm-slim AS runtime # Install only runtime dependencies RUN apt-get update && apt-get install -y \ libjansson4 \ libmicrohttpd12 \ libsodium23 \ libcurl4 \ netcat-traditional \ --no-install-recommends \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* \ && rm -rf /usr/share/doc /usr/share/man \ && find /var/cache -type f -delete WORKDIR /app # Copy only the built binary and necessary files from builder COPY --from=builder /build/datum_gateway /app/ COPY --from=builder /build/www/ /app/www/ COPY --from=builder /build/doc/example_datum_gateway_config.json /app/config/config.json # Create a configuration directory if it doesn't exist RUN mkdir -p /app/config # Verify shared library dependencies RUN ldd /app/datum_gateway # Create a non-root user RUN useradd -r -s /bin/false datumuser && \ chown -R datumuser:datumuser /app # Change to non-root user USER datumuser # Set up healthcheck HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ CMD nc -zv localhost 23334 || exit 1 # Expose ports EXPOSE 23334/tcp 7152/tcp # Create a volume for configuration and data VOLUME ["/app/config"] # Set the entrypoint ENTRYPOINT ["/app/datum_gateway", "--config", "/app/config/config.json"] datum_gateway-0.4.1beta/LICENSE000066400000000000000000000025631512710151500162140ustar00rootroot00000000000000Copyright (c) 2024-2025 Bitcoin Ocean, LLC, Jason Hughes, and individual contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. This copyright license for the DATUM Gateway does not include any express or implied right or license to use the DATUM trademark and any trademarks, logos, service marks, or trade names of Mummolin, Inc., Bitcoin OCEAN, LLC, or any other contributors to OCEAN's open source projects. datum_gateway-0.4.1beta/README.md000066400000000000000000000352171512710151500164700ustar00rootroot00000000000000# DATUM Gateway **Decentralized Alternative Templates for Universal Mining** (c) 2024-2025 Bitcoin Ocean, LLC, Jason Hughes, and individual contributors The DATUM Gateway implements lightweight efficient client side decentralized block template creation for true solo mining. It reaches out to a local Bitcoin node for block templates, generates and distributes work for mining hardware, and submits solved blocks to the network directly. For miners wanting to pool rewards, it facilitates communication with a DATUM-supporting pool in addition to the above. The pool is responsible for coordinating the block reward split based on work done for the pool by the miner, but does not create work for the miner. The work provided by the gateway to mining hardware is generated only from the local node generating templates for the miner. The real miner is always whoever is running the Bitcoin node. With DATUM, that's not the pool. As the protocol is intended solely for mining of decentralized block templates, the DATUM protocol has no mechanisms for the pool providing the information needed to construct work or a block template. Currently the DATUM Gateway supports communication with mining hardware using the Stratum v1 protocol with version rolling extensions (aka "ASICBoost"). Communication with the Bitcoin node is via RPC and must support GBT ("getblocktemplate"). Finally, communication with the pool is via the DATUM protocol. **Using Bitcoin Knots is highly recommended**. This gives miners fine controls over how they wish to construct their block templates. Other node implementations that support GBT can also be used. This includes Bitcoin Core, but it is severely lacking in template control options. That is unfortunately a centralizing force which partly defeats the purpose of decentralizing block template creation in the first place. The DATUM Gateway only supports mining Bitcoin. Modifying the code to support non-Bitcoin is not straightforward, as many optimizations and design considerations are tightly tied to Bitcoin-specific restraints for efficiency. ## DATUM Protocol The DATUM Gateway's communication with the mining pool is via the DATUM Protocol. This is an encrypted communication link between the DATUM Gateway (client) and DATUM Prime (pool side). The protocol itself was made from the ground up as a custom protocol. Its specification is evolving, subject to change, and will be published elsewhere. The core concepts of the protocol: - Encrypt communications between the Gateway and pool - Obfuscate the communications somewhat so a MITM is unable to glean useful or accurate insight into the miner's operation via analysis of the still-ciphered communications. - Retrieve proper generation transaction payout splits from the pool for locally constructed templates - Submit work to the pool with sufficient data to efficiently validate and accept the work for proper rewards - Communicate minimal guardrails and requirements for a valid template to earn pooled rewards With the current version of the protocol, the pool does block validation after coordinating with the miner. This is strictly to ensure miners are not accidentally creating invalid blocks while DATUM is still undergoing testing. In a future version of the protocol, the pool will not be in charge of this function and will be almost completely blinded to the contents of the miner's block template. The protocol is not specific to a pooled reward system, as the Gateway coordinates the appropriate generation transaction with the pool. However, in the spirit of maximum decentralization, the pool should implement rewarding miners directly from generated payouts, such as with OCEAN's TIDES reward system. ![DATUM v0 2-beta recommended setup - network diagram](doc/DATUM_recommended_setup-network_diagram.svg) ## Requirements - 64-bit AMD or Intel system. Other systems may work, but at this time it is at your own risk. - Linux-based operating system. Other OSs will be supported in the future. - Bitcoin full node ([Bitcoin Knots](https://bitcoinknots.org/) recommended) fully synced with the Bitcoin network. - Fast storage recommended for the Bitcoin node. - Stable internet connection for both the Bitcoin node and Gateway's communication with the pool. - CPU powerful enough to run the Bitcoin node without validation delays. - Approximately 1GB/RAM, plus 1GB/RAM per 1000 Stratum clients, plus Bitcoin node RAM requirements. - Bitcoin mining hardware able to reach the system running the DATUM Gateway. This list is not extensive, but the main goal is the have a stable system for your Bitcoin node and the Gateway such that your node is processing new incoming blocks and getting templates to the Gateway as quickly as possible. While this may all work on relatively low end hardware, your mileage may vary. No modifications to the Bitcoin node source code is required for the Gateway, as it uses the standard GBT mechanism for template fetch. The following external libraries are required: - libcurl - libjansson - libmicrohttpd - libsodium ## Node Configuration Your Bitcoin node must be configured to construct blocks as you desire. Bitcoin Knots provides many options for configuring your node's policy and is highly recommended. At this time, you must also reserve some block space for the pool's generation transaction. The following options are currently recommended: blockmaxsize=3985000 blockmaxweight=3985000 Note: This reservation requirement will be removed for Bitcoin Knots users in a future version of the DATUM Gateway thanks to support for on-the-fly specification of these metrics by the client in Knots. To avoid mining stale work, you will need to ensure the DATUM Gateway receives new block notifications from your node. It is suggested you run the DATUM Gateway as the same user as your full node and utilize the following configuration line in your bitcoin.conf: blocknotify=killall -USR1 datum_gateway Ensure you have "killall" installed on your system (*psmisc* package on many OSs). If the node and Gateway are on different systems, you may need to utilize the "NOTIFY" endpoint on the Gateway's dashboard/API instead. Finally, the Gateway must have RPC access to your node, and you must add an RPC user to your configuration to facilitate this, as well as ensuring the service running the Gateway is whitelisted for RPC access (if not on the same machine). Some additional recommendations: maxmempool=1000 blockreconstructionextratxn=1000000 As a true miner, you'll most likely want as many valid transactions as possible in your mempool which meet your node's policies. ## Installation Install and fully sync your Bitcoin full node. Instructions for this are beyond the scope of this document. Configure your node to create block templates as you desire. Be sure to reserve some space for the generation transaction, otherwise your work will not be able to fit a reward split. See node configuration recommendations above. Install the required libraries and development packages for dependencies: cmake, pkgconf, libcurl, jansson, libsodium, and libmicrohttpd. You may also need psmisc for your node to send blocknotify signals to the DATUM Gateway. For Debian/Ubuntu: sudo apt install cmake pkgconf libcurl4-openssl-dev libjansson-dev libsodium-dev libmicrohttpd-dev psmisc For Fedora/Amazon Linux: sudo dnf install cmake pkgconf libcurl-devel jansson-devel libsodium-devel libmicrohttpd-devel psmisc For Alma Linux: sudo dnf install epel-release dnf-plugins-core sudo dnf config-manager --set-enabled crb sudo dnf install cmake pkgconf libcurl-devel jansson-devel libsodium-devel libmicrohttpd-devel psmisc For Oracle Linux: sudo dnf install epel-release dnf-plugins-core sudo dnf config-manager --set-enabled ol9_codeready_builder sudo dnf install cmake pkgconf libcurl-devel jansson-devel libsodium-devel libmicrohttpd-devel psmisc For Alpine (also needs a standalone argp library): sudo apk add build-base cmake pkgconf argp-standalone curl-dev jansson-dev libsodium-dev libmicrohttpd-dev psmisc For Arch: sudo pacman -Syu base-devel cmake pkgconf curl jansson libsodium libmicrohttpd psmisc For Clear Linux: sudo swupd bundle-add c-basic cmake pkgconf devpkg-curl devpkg-jansson devpkg-libsodium devpkg-libmicrohttpd psmisc For FreeBSD: sudo pkg install cmake pkgconf curl jansson libsodium libmicrohttpd argp-standalone libepoll-shim Compile DATUM by running: cmake . && make ## Usage Run the datum_gateway executable with the -? flag for detailed configuration information, descriptions, and required options. Then construct a configuration file (defaults to "datum_gateway_config.json" in the current working directory). Be sure to also set your coinbase tags. The primary tag setting is unused in pooled mining, however the secondary tag is intended to show on things like block explorers when you mine a block. There is an [example configuration file included in the doc/ directory](doc/example_datum_gateway_config.json) you may wish to use as a template. Note that the API/web admin password is also used for preventing CSRF attacks, so it is crucial you set it to something reasonably secure (or disable the API/web interface entirely). You should review the [documentation on usernames](doc/usernames.md) next. Once you have everything running, you can point miners at the Gateway. ## Docker The DATUM Gateway is also available as a Docker image. ### Building the Docker Image To build the DATUM Gateway Docker image: ```bash # From the root of the repository docker build -t datum_gateway . ``` ### Running the Container To run the DATUM Gateway container: ```bash # Run with default configuration docker run -p 23334:23334 -p 7152:7152 --name datum-gateway datum_gateway ``` The container expects a configuration file at `/app/config/config.json`. Mount a volume to this path to use your own configuration: ```bash docker run -v /path/to/your/config/directory:/app/config -p 23334:23334 -p 7152:7152 datum_gateway ``` You will need to disable the notify fallback in your configuration file if you are using Docker. And in bitcoin.conf, you will need to set the following: ```bash blocknotify=wget -q -O /dev/null http://datum-gateway:7152/NOTIFY ``` ### Connecting to a Bitcoin Node When running the DATUM Gateway in Docker, you need to configure it to connect to your Bitcoin node. The connection method depends on where your Bitcoin node is running: #### 1. Bitcoin Node Running in Docker (Same Network) If your Bitcoin node is also running in a Docker container on the same network, use the container name as the hostname: ```json { "rpc_host": "bitcoin-node", "rpc_port": 8332, "rpc_user": "your_rpc_user", "rpc_pass": "your_rpc_password" } ``` In your `bitcoin.conf`, set the blocknotify to use the DATUM Gateway container name: ``` blocknotify=wget -q -O /dev/null http://datum-gateway:7152/NOTIFY ``` #### 2. Bitcoin Node Running on Host System If your Bitcoin node is running directly on the host system or in a container that binds to host ports, you have two options: **Option A: Using host.docker.internal (recommended)** ```json { "rpc_host": "host.docker.internal", "rpc_port": 8332, "rpc_user": "your_rpc_user", "rpc_pass": "your_rpc_password" } ``` **Option B: Using host networking mode** Run the DATUM Gateway container with `--network host`: ```bash docker run --network host -v /path/to/config:/app/config datum_gateway ``` Then configure using localhost: ```json { "rpc_host": "localhost", "rpc_port": 8332, "rpc_user": "your_rpc_user", "rpc_pass": "your_rpc_password" } ``` For blocknotify in `bitcoin.conf` when using host networking: ``` blocknotify=wget -q -O /dev/null http://localhost:7152/NOTIFY ``` #### 3. Bitcoin Node on Remote System If your Bitcoin node is running on a different machine, use the hostname or IP address: ```json { "rpc_host": "192.168.1.100", "rpc_port": 8332, "rpc_user": "your_rpc_user", "rpc_pass": "your_rpc_password" } ``` In your remote Bitcoin node's `bitcoin.conf`: ``` blocknotify=wget -q -O /dev/null http://datum-gateway-host-ip:7152/NOTIFY ``` **Important Notes:** - Ensure your Bitcoin node's RPC is configured to accept connections from the DATUM Gateway - For remote connections, you may need to configure `rpcbind` and `rpcallowip` in your `bitcoin.conf` - Always use strong RPC credentials and consider network security when exposing RPC endpoints - Remember to disable the notify fallback in your DATUM Gateway configuration when using Docker ## Template/Share Requirements for Pooled Mining - Must be a valid block and conform to current Bitcoin consensus rules - Submitted work must be for the current latest block height, valid time, etc - Must include generation transaction outputs provided by the pool in the order provided - Must include the primary coinbase tag as provided by the pool - Must include the unique identifier provided by the pool - Work must include the work target and meet/exceed that target - Any additional requirements by pool documentation ## Notes/Known Issues/Limitations - By default, if the connection with the pool is lost and fails to reconnect, the Gateway will disconnect all stratum clients. This way miners can use their built-in failover and switch to non-DATUM mining, or an alternate/backup Gateway. - Accepted/rejected share counts on mining hardware may not perfectly match with the pool. The delta may vary depending on the Gateway's configuration. This is because shares are first accepted or rejected as valid for your local template based on your local node, and then again accepted or rejected based on the pool's requirements, latency to the pool (stale work), latency between your node and the network (stale work), etc. Stratum v1 has no mechanism to report back to the miner that previously accepted work is now rejected, and it doesn't make sense to wait for the pool before responding, either. **Most importantly**, please note that this is currently a public **BETA** release. While best efforts have been made to ensure this software is as stable and as useful as possible, you may still encounter issues. This software is likely to undergo rapid development and revisions up until a v1.0 stable release. Some of these revisions may include changes, such as protocol changes, that require upgrading to the latest version with short or even no notice in order to continue using the software with a DATUM pool. Be sure to watch for important updates! Be sure you have failover settings on your miners. As a best practice, when mining on a DATUM pool, set your miner's failover to use that pool's Stratum endpoint. ## License The DATUM Gateway (including the DATUM Protocol) is free open source software and released under the terms of the MIT license. See LICENSE. datum_gateway-0.4.1beta/cmake/000077500000000000000000000000001512710151500162615ustar00rootroot00000000000000datum_gateway-0.4.1beta/cmake/script/000077500000000000000000000000001512710151500175655ustar00rootroot00000000000000datum_gateway-0.4.1beta/cmake/script/EmbedResources.cmake000066400000000000000000000017001512710151500234740ustar00rootroot00000000000000set(OUTPUT_CONTENT "#include \n") foreach(INPUT_FILE ${INPUT_FILES}) file(READ ${SOURCE_DIR}/${INPUT_FILE} INPUT_DATA_HEX HEX) string(SHA256 INPUT_HASH "${INPUT_DATA_HEX}") string(SUBSTRING "${INPUT_HASH}" 0 12 INPUT_HASH) string(REGEX REPLACE "[^a-zA-Z0-9_]" "_" OUTPUT_VAR ${INPUT_FILE}) string(LENGTH ${INPUT_DATA_HEX} INPUT_DATA_LEN) math(EXPR INPUT_DATA_LEN "${INPUT_DATA_LEN} / 2") string(REGEX REPLACE "................" "\\0\n" INPUT_DATA_C_ARRAY "${INPUT_DATA_HEX}") string(REGEX REPLACE "[^\n][^\n]" "\\\\x\\0" INPUT_DATA_C_ARRAY "${INPUT_DATA_C_ARRAY}") string(REGEX REPLACE "\n" "\"\n\"" INPUT_DATA_C_ARRAY "${INPUT_DATA_C_ARRAY}") string(APPEND OUTPUT_CONTENT "\nstatic const char ${OUTPUT_VAR}[]=\n\"${INPUT_DATA_C_ARRAY}\";\nstatic const size_t ${OUTPUT_VAR}_sz=${INPUT_DATA_LEN};\nstatic const char ${OUTPUT_VAR}_etag[] = \"\\\"${INPUT_HASH}\\\"\";\n") endforeach() file(WRITE ${OUTPUT_FILE} "${OUTPUT_CONTENT}") datum_gateway-0.4.1beta/cmake/script/GenerateBuildInfo.cmake000066400000000000000000000107021512710151500241150ustar00rootroot00000000000000# DATUM Gateway # Decentralized Alternative Templates for Universal Mining # # This file is part of OCEAN's Bitcoin mining decentralization # project, DATUM. # # https://ocean.xyz # # --- # # Copyright (c) 2024 Bitcoin Ocean, LLC & Luke Dashjr # Copyright (c) 2023-present The Bitcoin Core developers # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. macro(fatal_error) message(FATAL_ERROR "\n" "Usage:\n" " cmake -D BUILD_INFO_HEADER_PATH= [-D SOURCE_DIR=] -P ${CMAKE_CURRENT_LIST_FILE}\n" "All specified paths must be absolute ones.\n" ) endmacro() if(DEFINED BUILD_INFO_HEADER_PATH AND IS_ABSOLUTE "${BUILD_INFO_HEADER_PATH}") if(EXISTS "${BUILD_INFO_HEADER_PATH}") file(STRINGS ${BUILD_INFO_HEADER_PATH} INFO) endif() else() fatal_error() endif() if(DEFINED SOURCE_DIR) if(IS_ABSOLUTE "${SOURCE_DIR}" AND IS_DIRECTORY "${SOURCE_DIR}") set(WORKING_DIR ${SOURCE_DIR}) else() fatal_error() endif() else() set(WORKING_DIR ${CMAKE_CURRENT_SOURCE_DIR}) endif() set(GIT_TAG) set(GIT_COMMIT) if(NOT "$ENV{BITCOIN_GENBUILD_NO_GIT}" STREQUAL "1") find_package(Git QUIET) if(Git_FOUND) execute_process( COMMAND ${GIT_EXECUTABLE} rev-parse --is-inside-work-tree WORKING_DIRECTORY ${WORKING_DIR} OUTPUT_VARIABLE IS_INSIDE_WORK_TREE OUTPUT_STRIP_TRAILING_WHITESPACE ) if(IS_INSIDE_WORK_TREE) # Clean 'dirty' status of touched files that haven't been modified. execute_process( COMMAND ${GIT_EXECUTABLE} diff WORKING_DIRECTORY ${WORKING_DIR} OUTPUT_QUIET ERROR_QUIET ) execute_process( COMMAND ${GIT_EXECUTABLE} describe --abbrev=0 WORKING_DIRECTORY ${WORKING_DIR} OUTPUT_VARIABLE MOST_RECENT_TAG OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET ) execute_process( COMMAND ${GIT_EXECUTABLE} rev-list -1 ${MOST_RECENT_TAG} WORKING_DIRECTORY ${WORKING_DIR} OUTPUT_VARIABLE MOST_RECENT_TAG_COMMIT OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET ) execute_process( COMMAND ${GIT_EXECUTABLE} rev-parse HEAD WORKING_DIRECTORY ${WORKING_DIR} OUTPUT_VARIABLE HEAD_COMMIT OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET ) execute_process( COMMAND ${GIT_EXECUTABLE} diff-index --quiet HEAD -- WORKING_DIRECTORY ${WORKING_DIR} RESULT_VARIABLE IS_DIRTY ) if(HEAD_COMMIT STREQUAL MOST_RECENT_TAG_COMMIT AND NOT IS_DIRTY) # If latest commit is tagged and not dirty, then use the tag name. set(GIT_TAG ${MOST_RECENT_TAG}) else() # Otherwise, generate suffix from git, i.e. string like "0e0a5173fae3-dirty". execute_process( COMMAND ${GIT_EXECUTABLE} rev-parse --short=12 HEAD WORKING_DIRECTORY ${WORKING_DIR} OUTPUT_VARIABLE GIT_COMMIT OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET ) if(IS_DIRTY) string(APPEND GIT_COMMIT "-dirty") endif() endif() endif() endif() endif() if(HEAD_COMMIT) if(IS_DIRTY) string(APPEND HEAD_COMMIT "+") endif() else() set(HEAD_COMMIT UNKNOWN_GIT_HASH) endif() set(NEWINFO "#define GIT_COMMIT_HASH \"${HEAD_COMMIT}\"") if(GIT_TAG) string(APPEND NEWINFO "\n#define BUILD_GIT_TAG \"${GIT_TAG}\"") endif() # Only update the header if necessary. if(NOT "${INFO}" STREQUAL "${NEWINFO}") file(WRITE ${BUILD_INFO_HEADER_PATH} "${NEWINFO}\n") endif() datum_gateway-0.4.1beta/cmake/script/VerifyExample.cmake000066400000000000000000000010411512710151500233430ustar00rootroot00000000000000execute_process( COMMAND ${DATUM_GATEWAY} --example-conf OUTPUT_VARIABLE CURRENT_EXAMPLE RESULT_VARIABLE _result ) if (_result) message(FATAL_ERROR "${DATUM_GATEWAY} exited with code ${_result}") endif() file(WRITE ${GENERATED_DOC} ${CURRENT_EXAMPLE}) file(READ ${PREGEN_DOC} PREGEN_EXAMPLE) # string(STRIP ${PREGEN_EXAMPLE} PREGEN_EXAMPLE) # string(STRIP ${CURRENT_EXAMPLE} CURRENT_EXAMPLE) if(NOT "${CURRENT_EXAMPLE}" STREQUAL "${PREGEN_EXAMPLE}") message(FATAL_ERROR "${PREGEN_DOC} is outdated. Update it with ${GENERATED_DOC}") endif() datum_gateway-0.4.1beta/debian/000077500000000000000000000000001512710151500164235ustar00rootroot00000000000000datum_gateway-0.4.1beta/debian/changelog000066400000000000000000000002221512710151500202710ustar00rootroot00000000000000datum-gateway (0.2~beta-1) UNRELEASED; urgency=low * Initial release. -- Luke Dashjr Fri, 18 Oct 2024 03:06:41 +0000 datum_gateway-0.4.1beta/debian/compat000066400000000000000000000000021512710151500176210ustar00rootroot000000000000007 datum_gateway-0.4.1beta/debian/control000066400000000000000000000013061512710151500200260ustar00rootroot00000000000000Source: datum-gateway Maintainer: Luke Dashjr Section: net Priority: optional Standards-Version: 4.5.0 Homepage: https://ocean.xyz Vcs-Git: https://github.com/OCEAN-xyz/datum_gateway.git Vcs-Browser: https://github.com/OCEAN-xyz/datum_gateway Build-Depends: debhelper, cmake, pkg-config, libjansson-dev, libmicrohttpd-dev, libsodium-dev, libcurl-dev Package: datum-gateway Architecture: amd64 arm64 Depends: ${shlibs:Depends}, ${misc:Depends} Description: Decentralized Alternative Templates for Universal Mining The DATUM Gateway is a server providing solo mining capabilities for Bitcoin miners, including both non-pooled and pooled solo mining using the new DATUM protocol. datum_gateway-0.4.1beta/debian/copyright000066400000000000000000000025371512710151500203650ustar00rootroot00000000000000Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: datum-gateway Upstream-Contact: Luke Dashjr Source: https://github.com/OCEAN-xyz/datum_gateway Files: * Copyright: 2024-2025 Bitcoin Ocean, LLC, Jason Hughes, and individual contributors License: Expat License: Expat Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: . The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. . THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. datum_gateway-0.4.1beta/debian/rules000077500000000000000000000002311512710151500174770ustar00rootroot00000000000000#!/usr/bin/make -f export DH_VERBOSE = 1 %: dh $@ override_dh_auto_configure: dh_auto_configure -- \ -DCMAKE_INSTALL_DOCDIR=share/doc/datum-gateway datum_gateway-0.4.1beta/debian/source/000077500000000000000000000000001512710151500177235ustar00rootroot00000000000000datum_gateway-0.4.1beta/debian/source/format000066400000000000000000000000151512710151500211320ustar00rootroot000000000000003.0 (native) datum_gateway-0.4.1beta/doc/000077500000000000000000000000001512710151500157465ustar00rootroot00000000000000datum_gateway-0.4.1beta/doc/DATUM_recommended_setup-network_diagram.svg000066400000000000000000004411201512710151500263000ustar00rootroot00000000000000 Host machine Databases Bitcoin data TCP (default port: 8333) Bitcoin Knots JSON-RPC (8332) Internet DATUM/TCP (default port: 28915) ASIC miners Mining pool DATUM Prime Stratum/TCP (default port: 23334) DATUM recommended setup - network diagram DATUM Gateway datum_gateway-0.4.1beta/doc/example_datum_gateway_config.json000066400000000000000000000013251512710151500245350ustar00rootroot00000000000000{ "bitcoind": { "rpcuser": "datum", "rpcpassword": "something only you know", "rpcurl": "http://localhost:8332", "notify_fallback": true }, "stratum": { "listen_port": 23334 }, "mining": { "pool_address": "put your own Bitcoin invoice address here", "coinbase_tag_primary": "DATUM Gateway", "coinbase_tag_secondary": "DATUM User" }, "api": { "admin_password": "", "listen_port": 7152, "modify_conf": false }, "logger": { "log_to_console": true, "log_to_file": false, "log_file": "/var/log/datum.log", "log_rotate_daily": true, "log_level_console": 2, "log_level_file": 1 }, "datum": { "pool_pass_workers": true, "pool_pass_full_users": true, "pooled_mining_only": true } } datum_gateway-0.4.1beta/doc/usernames.md000066400000000000000000000134051512710151500202750ustar00rootroot00000000000000## General DATUM Gateway is designed with the assumption that pool usernames are generally Bitcoin addresses. While it is *possible* to specify non-addresses in your miner, and pass those through to the pool, the default username in the Gateway itself (the `mining`.`pool_address` configuration option) must always be a valid Bitcoin address, and the Gateway will not fully start until it is set to one. The rest of this document deals with Stratum usernames specifically, and how they are interpreted. **Always test your full mining stack configuration.** Misconfiguration of either the DATUM Gateway or your miners *can* result in lost work that is impossible to recover! ## Limitations DATUM Gateway has a limit of 191 characters for Stratum usernames, including all special features specified by them. Your miner likely has a lower limit. For example, Avalons truncate usernames at 63 characters; Whatsminer has a buffer overflow (which may damage your miner) if you exceed 127; and so on. Some miners replace special characters (anything except alphanumeric, underscores, periods, and tildes) with hex codes (for example, `%` becomes `%25`), which can contribute toward reaching these limits and/or potentially confuse anything looking for them. Note that Stratum usernames are *only* used for pooled mining. When in non-pooled mode, they have no effect whatsoever, and only `mining`.`pool_address` is used to create blocks. ## Bitcoin address requirements (non-pooled mode) This version of DATUM Gateway supports Base58 (aka Legacy), Bech32 (aka Segwit), and Bech32m (aka Taproot) addresses, for Bitcoin and Bitcoin testnet only. It will not detect if you are using an address for the wrong network. ## Worker names Immediately following the Bitcoin address, you may append a period (`.`) and an arbitrary worker name. For compatibility, pools might also support an underscore (`_`) separator, but the DATUM Gateway codebase itself does not, and the period must be used to make use of Gateway features. If the Stratum username *begins* with a period, it is interpreted as a worker name only, and appended to the Gateway's default username (`mining`.`pool_address`) before being sent to the pool. ## Passing usernames to the pool There are three different ways to pass usernames to your pool. By default, the Stratum username is always passed in full, as-is. You can make this explicit by setting `datum`.`pool_pass_full_users` to `true` in the config file, or "Send Miner Usernames To Pool: Override Bitcoin Address" in the web configurator. If you change `datum`.`pool_pass_full_users` to `false`, you can then set `datum`.`pool_pass_workers` instead (or "Send Miner Usernames To Pool: Send as worker names" in the web configurator). With this setting, the entire Stratum username will be appended after the default username (`mining`.`pool_address`) as a worker. Finally, if you set both options to `false`, the Stratum username will be ignored entirely. Instead, only the configured default username (`mining`.`pool_address`) will be used, without any worker names. ## Username modifiers (advanced) Some miners are in revenue sharing arrangements, and may wish to distribute a portion of their shares to different addresses. While ideally this should be implemented in miner firmware, many miners today do not support it, so the DATUM Gateway provides it as an optional feature. This is accomplished using "username modifiers". In the `stratum` section of your configuration, add `username_modifiers` as a JSON object. Example: "username_modifiers": { "modifier name 1": { "bitcoin address A": 0.2, "": 0.8 }, "modifier name 2": { "bitcoin address B": 0.5, "": 0.5 }, "modifier name 3": { "bitcoin address C": 0.01, "bitcoin address D": 0.99 } } This example defines three username modifiers, each named "modifier name 1" and so on, with differnent proportions. The first redirects approximately 20% (`0.2`) of shares to "bitcoin address A", and 80% (`0.8`) to the address specified in the Stratum username (which is specified as simply `""`). Similarly, the second defines a 50/50 split. The third redirects 1% to "bitcoin address C", and 99% to "bitcoin address D"; note that the Stratum username's address does not receive *any* shares in that scenario. To make use of a modifier, you must append a tilde (`~`) and the modifier name to your Stratum username. For example, you might use "bitcoin address E.workername~modifier name 2". This would send 20% of shares to the pool as username "bitcoin address A.workername" and 80% as "bitcoin address E.workername". Regardless of what Bitcoin address is being used by a modifier, the worker name (if any) specified by the Stratum username is copied over as-is. Be aware that this feature reassigns share submissions based on the proof-of-work hash. If you specify 80%/20%, shares beginning with 0000-cccc will be directed toward the first address, and shares beginning with cccd-ffff will be sent as the second. Since the hash is random, this may not be an exact split (though it should approach it over the long term). Modifiers should always add up to 100%, and behaviour when they do not is undefined. *Currently*, if you assign *less* than a full 100%, any shares which fall outside of the defined ranges will be submitted as the default username in `mining`.`pool_address` *without* the workername copied; if you assign *more* than 100%, that portion above will not have any shares submitted (the order of addresses may or may not be random). Do not rely on these behaviours. Always specify the full 100% range explicitly. NOTE: This feature is handled when shares are received by the Gateway's Stratum server, and will therefore only work if you have `datum`.`pool_pass_full_users` enabled. datum_gateway-0.4.1beta/src/000077500000000000000000000000001512710151500157705ustar00rootroot00000000000000datum_gateway-0.4.1beta/src/datum_api.c000066400000000000000000002220711512710151500201030ustar00rootroot00000000000000/* * * DATUM Gateway * Decentralized Alternative Templates for Universal Mining * * This file is part of OCEAN's Bitcoin mining decentralization * project, DATUM. * * https://ocean.xyz * * --- * * Copyright (c) 2024-2025 Bitcoin Ocean, LLC & Jason Hughes * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ // This is quick and dirty for now. Will be improved over time. #include #include #include #include #include #include #include #include #include #include #include #include "datum_api.h" #include "datum_blocktemplates.h" #include "datum_conf.h" #include "datum_gateway.h" #include "datum_jsonrpc.h" #include "datum_utils.h" #include "datum_stratum.h" #include "datum_sockets.h" #include "datum_protocol.h" #include "web_resources.h" const char * const homepage_html_end = ""; #define DATUM_API_HOMEPAGE_MAX_SIZE 128000 const char *cbnames[] = { "Blank", "Tiny", "Default", "Respect", "Yuge", "Antmain2" }; typedef struct MHD_Response *(*create_response_func_t)(); static struct MHD_Response *datum_api_create_empty_mhd_response() { return MHD_create_response_from_buffer(0, "", MHD_RESPMEM_PERSISTENT); } static void html_leading_zeros(char * const buffer, const size_t buffer_size, const char * const numstr) { int zeros = 0; while (numstr[zeros] == '0') { ++zeros; } if (zeros) { snprintf(buffer, buffer_size, "%.*s%s", zeros, numstr, &numstr[zeros]); } } void datum_api_var_DATUM_SHARES_ACCEPTED(char *buffer, size_t buffer_size, const T_DATUM_API_DASH_VARS *vardata) { snprintf(buffer, buffer_size, "%llu (%llu diff)", (unsigned long long)datum_accepted_share_count, (unsigned long long)datum_accepted_share_diff); } void datum_api_var_DATUM_SHARES_REJECTED(char *buffer, size_t buffer_size, const T_DATUM_API_DASH_VARS *vardata) { snprintf(buffer, buffer_size, "%llu (%llu diff)", (unsigned long long)datum_rejected_share_count, (unsigned long long)datum_rejected_share_diff); } void datum_api_var_DATUM_CONNECTION_STATUS(char *buffer, size_t buffer_size, const T_DATUM_API_DASH_VARS *vardata) { const char *colour = "lime"; const char *s, *s2 = ""; const char * const bt_err = datum_blocktemplates_error; if (bt_err) { colour = "red"; s = "ERROR: "; s2 = bt_err; } else if (!vardata->sjob) { colour = "silver"; s = "Initialising..."; } else if (datum_protocol_is_active()) { s = "Connected and Ready"; } else if (datum_config.datum_pooled_mining_only && datum_config.datum_pool_host[0]) { colour = "red"; s = "Not Ready"; } else { if (datum_config.datum_pool_host[0]) { colour = "yellow"; } s = "Non-Pooled Mode"; } snprintf(buffer, buffer_size, " %s%s", colour, s, s2); } void datum_api_var_DATUM_POOL_HOST(char *buffer, size_t buffer_size, const T_DATUM_API_DASH_VARS *vardata) { if (datum_config.datum_pool_host[0]) { snprintf(buffer, buffer_size, "%s:%u", datum_config.datum_pool_host, (unsigned)datum_config.datum_pool_port); } else { snprintf(buffer, buffer_size, "N/A"); } } void datum_api_var_DATUM_POOL_TAG(char *buffer, size_t buffer_size, const T_DATUM_API_DASH_VARS *vardata) { size_t i; buffer[0] = '"'; i = strncpy_html_escape(&buffer[1], datum_protocol_is_active()?datum_config.override_mining_coinbase_tag_primary:datum_config.mining_coinbase_tag_primary, buffer_size-3); buffer[i+1] = '"'; buffer[i+2] = 0; } void datum_api_var_DATUM_MINER_TAG(char *buffer, size_t buffer_size, const T_DATUM_API_DASH_VARS *vardata) { size_t i; buffer[0] = '"'; i = strncpy_html_escape(&buffer[1], datum_config.mining_coinbase_tag_secondary, buffer_size-3); buffer[i+1] = '"'; buffer[i+2] = 0; } void datum_api_var_DATUM_POOL_DIFF(char *buffer, size_t buffer_size, const T_DATUM_API_DASH_VARS *vardata) { snprintf(buffer, buffer_size, "%llu", (unsigned long long)datum_config.override_vardiff_min); } void datum_api_var_DATUM_POOL_PUBKEY(char *buffer, size_t buffer_size, const T_DATUM_API_DASH_VARS *vardata) { snprintf(buffer, buffer_size, "%s", datum_config.datum_pool_pubkey); } void datum_api_var_STRATUM_ACTIVE_THREADS(char *buffer, size_t buffer_size, const T_DATUM_API_DASH_VARS *vardata) { snprintf(buffer, buffer_size, "%d", vardata->STRATUM_ACTIVE_THREADS); } void datum_api_var_STRATUM_TOTAL_CONNECTIONS(char *buffer, size_t buffer_size, const T_DATUM_API_DASH_VARS *vardata) { snprintf(buffer, buffer_size, "%d", vardata->STRATUM_TOTAL_CONNECTIONS); } void datum_api_var_STRATUM_TOTAL_SUBSCRIPTIONS(char *buffer, size_t buffer_size, const T_DATUM_API_DASH_VARS *vardata) { snprintf(buffer, buffer_size, "%d", vardata->STRATUM_TOTAL_SUBSCRIPTIONS); } void datum_api_var_STRATUM_HASHRATE_ESTIMATE(char *buffer, size_t buffer_size, const T_DATUM_API_DASH_VARS *vardata) { snprintf(buffer, buffer_size, "%.2f Th/sec", vardata->STRATUM_HASHRATE_ESTIMATE); } void datum_api_var_DATUM_PROCESS_UPTIME(char *buffer, size_t buffer_size, const T_DATUM_API_DASH_VARS *vardata) { uint64_t uptime_seconds = get_process_uptime_seconds(); uint64_t days = uptime_seconds / (24 * 3600); unsigned int hours = (uptime_seconds % (24 * 3600)) / 3600; unsigned int minutes = (uptime_seconds % 3600) / 60; unsigned int seconds = uptime_seconds % 60; if (days > 0) { snprintf(buffer, buffer_size, "%"PRIu64" days, %u hours, %u minutes, %u seconds", days, hours, minutes, seconds); } else if (hours > 0) { snprintf(buffer, buffer_size, "%u hours, %u minutes, %u seconds", hours, minutes, seconds); } else if (minutes > 0) { snprintf(buffer, buffer_size, "%u minutes, %u seconds", minutes, seconds); } else { snprintf(buffer, buffer_size, "%u seconds", seconds); } } void datum_api_var_STRATUM_JOB_INFO(char *buffer, size_t buffer_size, const T_DATUM_API_DASH_VARS *vardata) { if (!vardata->sjob) return; snprintf(buffer, buffer_size, "%s (%d) @ %.3f", vardata->sjob->job_id, vardata->sjob->global_index, (double)vardata->sjob->tsms / 1000.0); } void datum_api_var_STRATUM_JOB_BLOCK_HEIGHT(char *buffer, size_t buffer_size, const T_DATUM_API_DASH_VARS *vardata) { snprintf(buffer, buffer_size, "%llu", (unsigned long long)vardata->sjob->block_template->height); } void datum_api_var_STRATUM_JOB_BLOCK_VALUE(char *buffer, size_t buffer_size, const T_DATUM_API_DASH_VARS *vardata) { snprintf(buffer, buffer_size, "%.8f BTC", (double)vardata->sjob->block_template->coinbasevalue / (double)100000000.0); } void datum_api_var_STRATUM_JOB_TARGET(char *buffer, size_t buffer_size, const T_DATUM_API_DASH_VARS *vardata) { html_leading_zeros(buffer, buffer_size, vardata->sjob->block_template->block_target_hex); } void datum_api_var_STRATUM_JOB_PREVBLOCK(char *buffer, size_t buffer_size, const T_DATUM_API_DASH_VARS *vardata) { html_leading_zeros(buffer, buffer_size, vardata->sjob->block_template->previousblockhash); } void datum_api_var_STRATUM_JOB_WITNESS(char *buffer, size_t buffer_size, const T_DATUM_API_DASH_VARS *vardata) { snprintf(buffer, buffer_size, "%s", vardata->sjob->block_template->default_witness_commitment); } void datum_api_var_STRATUM_JOB_DIFF(char *buffer, size_t buffer_size, const T_DATUM_API_DASH_VARS *vardata) { snprintf(buffer, buffer_size, "%.3Lf", calc_network_difficulty(vardata->sjob->nbits)); } void datum_api_var_STRATUM_JOB_VERSION(char *buffer, size_t buffer_size, const T_DATUM_API_DASH_VARS *vardata) { snprintf(buffer, buffer_size, "%s (%u)", vardata->sjob->version, (unsigned)vardata->sjob->version_uint); } void datum_api_var_STRATUM_JOB_BITS(char *buffer, size_t buffer_size, const T_DATUM_API_DASH_VARS *vardata) { snprintf(buffer, buffer_size, "%s", vardata->sjob->nbits); } void datum_api_var_STRATUM_JOB_TIMEINFO(char *buffer, size_t buffer_size, const T_DATUM_API_DASH_VARS *vardata) { snprintf(buffer, buffer_size, "Current: %llu / Min: %llu", (unsigned long long)vardata->sjob->block_template->curtime, (unsigned long long)vardata->sjob->block_template->mintime); } void datum_api_var_STRATUM_JOB_LIMITINFO(char *buffer, size_t buffer_size, const T_DATUM_API_DASH_VARS *vardata) { snprintf(buffer, buffer_size, "Size: %lu, Weight: %lu, SigOps: %lu", (unsigned long)vardata->sjob->block_template->sizelimit, (unsigned long)vardata->sjob->block_template->weightlimit, (unsigned long)vardata->sjob->block_template->sigoplimit); } void datum_api_var_STRATUM_JOB_SIZE(char *buffer, size_t buffer_size, const T_DATUM_API_DASH_VARS *vardata) { snprintf(buffer, buffer_size, "%lu", (unsigned long)vardata->sjob->block_template->txn_total_size); } void datum_api_var_STRATUM_JOB_WEIGHT(char *buffer, size_t buffer_size, const T_DATUM_API_DASH_VARS *vardata) { snprintf(buffer, buffer_size, "%lu", (unsigned long)vardata->sjob->block_template->txn_total_weight); } void datum_api_var_STRATUM_JOB_SIGOPS(char *buffer, size_t buffer_size, const T_DATUM_API_DASH_VARS *vardata) { snprintf(buffer, buffer_size, "%lu", (unsigned long)vardata->sjob->block_template->txn_total_sigops); } void datum_api_var_STRATUM_JOB_TXNCOUNT(char *buffer, size_t buffer_size, const T_DATUM_API_DASH_VARS *vardata) { snprintf(buffer, buffer_size, "%u", (unsigned)vardata->sjob->block_template->txn_count); } DATUM_API_VarEntry var_entries[] = { {"DATUM_SHARES_ACCEPTED", datum_api_var_DATUM_SHARES_ACCEPTED}, {"DATUM_SHARES_REJECTED", datum_api_var_DATUM_SHARES_REJECTED}, {"DATUM_CONNECTION_STATUS", datum_api_var_DATUM_CONNECTION_STATUS}, {"DATUM_POOL_HOST", datum_api_var_DATUM_POOL_HOST}, {"DATUM_POOL_TAG", datum_api_var_DATUM_POOL_TAG}, {"DATUM_MINER_TAG", datum_api_var_DATUM_MINER_TAG}, {"DATUM_POOL_DIFF", datum_api_var_DATUM_POOL_DIFF}, {"DATUM_POOL_PUBKEY", datum_api_var_DATUM_POOL_PUBKEY}, {"DATUM_PROCESS_UPTIME", datum_api_var_DATUM_PROCESS_UPTIME}, {"STRATUM_ACTIVE_THREADS", datum_api_var_STRATUM_ACTIVE_THREADS}, {"STRATUM_TOTAL_CONNECTIONS", datum_api_var_STRATUM_TOTAL_CONNECTIONS}, {"STRATUM_TOTAL_SUBSCRIPTIONS", datum_api_var_STRATUM_TOTAL_SUBSCRIPTIONS}, {"STRATUM_HASHRATE_ESTIMATE", datum_api_var_STRATUM_HASHRATE_ESTIMATE}, {"STRATUM_JOB_INFO", datum_api_var_STRATUM_JOB_INFO}, {"STRATUM_JOB_BLOCK_HEIGHT", datum_api_var_STRATUM_JOB_BLOCK_HEIGHT}, {"STRATUM_JOB_BLOCK_VALUE", datum_api_var_STRATUM_JOB_BLOCK_VALUE}, {"STRATUM_JOB_PREVBLOCK", datum_api_var_STRATUM_JOB_PREVBLOCK}, {"STRATUM_JOB_TARGET", datum_api_var_STRATUM_JOB_TARGET}, {"STRATUM_JOB_WITNESS", datum_api_var_STRATUM_JOB_WITNESS}, {"STRATUM_JOB_DIFF", datum_api_var_STRATUM_JOB_DIFF}, {"STRATUM_JOB_VERSION", datum_api_var_STRATUM_JOB_VERSION}, {"STRATUM_JOB_BITS", datum_api_var_STRATUM_JOB_BITS}, {"STRATUM_JOB_TIMEINFO", datum_api_var_STRATUM_JOB_TIMEINFO}, {"STRATUM_JOB_LIMITINFO", datum_api_var_STRATUM_JOB_LIMITINFO}, {"STRATUM_JOB_SIZE", datum_api_var_STRATUM_JOB_SIZE}, {"STRATUM_JOB_WEIGHT", datum_api_var_STRATUM_JOB_WEIGHT}, {"STRATUM_JOB_SIGOPS", datum_api_var_STRATUM_JOB_SIGOPS}, {"STRATUM_JOB_TXNCOUNT", datum_api_var_STRATUM_JOB_TXNCOUNT}, {NULL, NULL} // Mark the end of the array }; DATUM_API_VarFunc datum_api_find_var_func(const char * const var_start, const size_t var_name_len) { for (int i = 0; var_entries[i].var_name != NULL; i++) { if (strncmp(var_entries[i].var_name, var_start, var_name_len) == 0 && !var_entries[i].var_name[var_name_len]) { return var_entries[i].func; } } return NULL; // Variable not found } size_t datum_api_fill_var(const char * const var_start, const size_t var_name_len, char * const replacement, const size_t replacement_max_len, const T_DATUM_API_DASH_VARS * const vardata) { DATUM_API_VarFunc func = datum_api_find_var_func(var_start, var_name_len); if (!func) { DLOG_ERROR("%s: Unknown variable '%.*s'", __func__, (int)var_name_len, var_start); return 0; } // Skip running STRATUM_JOB functions if there's no sjob if (var_start[8] == 'J' && !vardata->sjob) { // Leave blank for now return 0; } assert(replacement_max_len > 0); replacement[0] = 0; func(replacement, replacement_max_len, vardata); return strlen(replacement); } size_t datum_api_fill_vars(const char *input, char *output, size_t max_output_size, const DATUM_API_VarFillFunc var_fill_func, const T_DATUM_API_DASH_VARS *vardata) { const char* p = input; size_t output_len = 0; size_t var_name_len = 0; const char *var_start; const char *var_end; while (*p && output_len < max_output_size - 1) { if (strncmp(p, "${", 2) == 0) { p += 2; // Skip "${" var_start = p; var_end = strchr(p, '}'); if (!var_end) { DLOG_ERROR("%s: Missing closing } for variable", __func__); break; } var_name_len = var_end - var_start; char * const replacement = &output[output_len]; size_t replacement_max_len = max_output_size - output_len; if (replacement_max_len > 256) replacement_max_len = 256; const size_t replacement_len = var_fill_func(var_start, var_name_len, replacement, replacement_max_len, vardata); output_len += replacement_len; output[output_len] = 0; p = var_end + 1; // Move past '}' } else { output[output_len++] = *p++; output[output_len] = 0; } } output[output_len] = 0; return output_len; } size_t strncpy_html_escape(char *dest, const char *src, size_t n) { size_t i = 0; while (*src && i < n) { switch (*src) { case '&': if (i + 5 <= n) { // & dest[i++] = '&'; dest[i++] = 'a'; dest[i++] = 'm'; dest[i++] = 'p'; dest[i++] = ';'; } else { return i; // Stop if there's not enough space } break; case '<': if (i + 4 <= n) { // < dest[i++] = '&'; dest[i++] = 'l'; dest[i++] = 't'; dest[i++] = ';'; } else { return i; // Stop if there's not enough space } break; case '>': if (i + 4 <= n) { // > dest[i++] = '&'; dest[i++] = 'g'; dest[i++] = 't'; dest[i++] = ';'; } else { return i; // Stop if there's not enough space } break; case '"': if (i + 6 <= n) { // " dest[i++] = '&'; dest[i++] = 'q'; dest[i++] = 'u'; dest[i++] = 'o'; dest[i++] = 't'; dest[i++] = ';'; } else { return i; // Stop if there's not enough space } break; default: dest[i++] = *src; break; } src++; } // Null-terminate the destination string if there's space if (i < n) { dest[i] = '\0'; } return i; } static void http_resp_prevent_caching(struct MHD_Response * const response) { MHD_add_response_header(response, "Cache-Control", "no-cache, no-store, must-revalidate"); MHD_add_response_header(response, "Pragma", "no-cache"); MHD_add_response_header(response, "Expires", "0"); } static enum MHD_Result datum_api_formdata_to_json_cb(void * const cls, const enum MHD_ValueKind kind, const char * const key, const char * const filename, const char * const content_type, const char * const transfer_encoding, const char * const data, const uint64_t off, const size_t size) { if (!key) return MHD_YES; if (off) return MHD_YES; assert(cls); json_t * const j = cls; json_object_set_new(j, key, json_stringn(data, size)); return MHD_YES; } bool datum_api_formdata_to_json(struct MHD_Connection * const connection, char * const post, const int len, json_t * const j) { struct MHD_PostProcessor * const pp = MHD_create_post_processor(connection, 32768, datum_api_formdata_to_json_cb, j); if (!pp) { return false; } if (MHD_YES != MHD_post_process(pp, post, len)) { MHD_destroy_post_processor(pp); return false; } MHD_destroy_post_processor(pp); return true; } int datum_api_submit_uncached_response(struct MHD_Connection * const connection, const unsigned int status_code, struct MHD_Response * const response) { http_resp_prevent_caching(response); int ret = MHD_queue_response(connection, status_code, response); MHD_destroy_response(response); return ret; } int datum_api_do_error(struct MHD_Connection * const connection, const unsigned int status_code) { struct MHD_Response *response = datum_api_create_empty_mhd_response(); return datum_api_submit_uncached_response(connection, status_code, response); } bool datum_api_check_admin_password_only(struct MHD_Connection * const connection, const char * const password, const create_response_func_t auth_failure_response_creator) { if (datum_secure_strequals(datum_config.api_admin_password, datum_config.api_admin_password_len, password) && datum_config.api_admin_password_len) { return true; } DLOG_DEBUG("Wrong password in request"); datum_api_submit_uncached_response(connection, MHD_HTTP_FORBIDDEN, auth_failure_response_creator()); return false; } static enum MHD_DigestAuthAlgorithm datum_api_pick_digest_algo(struct MHD_Connection * const connection) { const char * const ua = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, "User-Agent"); if (datum_config.api_allow_insecure_auth) { if (strstr(ua, "AppleWebKit/") && !(strstr(ua, "Chrome/") || strstr(ua, "Brave/") || strstr(ua, "Edge/"))) { return MHD_DIGEST_ALG_MD5; } } return MHD_DIGEST_ALG_SHA256; } bool datum_api_check_admin_password_httponly(struct MHD_Connection * const connection, const create_response_func_t auth_failure_response_creator) { int ret; static bool safari_warned = false; char * const username = MHD_digest_auth_get_username(connection); const enum MHD_DigestAuthAlgorithm algo = datum_api_pick_digest_algo(connection); const char * const realm = "DATUM Gateway"; if (username) { ret = MHD_digest_auth_check2(connection, realm, username, datum_config.api_admin_password, 300, algo); free(username); } else { ret = MHD_NO; } if (algo == MHD_DIGEST_ALG_MD5 && (ret == MHD_NO || !safari_warned)) { DLOG_WARN("Detected login request from Apple Safari. For some reason, this browser only supports obsolete and insecure MD5 digest authentication. Login at your own risk!"); safari_warned = true; } if (ret != MHD_YES) { const bool nonce_is_stale = (ret == MHD_INVALID_NONCE); if (username && !nonce_is_stale) { DLOG_DEBUG("Wrong password in HTTP authentication"); } struct MHD_Response * const response = auth_failure_response_creator(); ret = MHD_queue_auth_fail_response2(connection, realm, datum_config.api_csrf_token, response, nonce_is_stale ? MHD_YES : MHD_NO, algo); MHD_destroy_response(response); return false; } return true; } bool datum_api_check_admin_password(struct MHD_Connection * const connection, const json_t * const j, const create_response_func_t auth_failure_response_creator) { const json_t * const j_password = json_object_get(j, "password"); if (json_is_string(j_password)) { return datum_api_check_admin_password_only(connection, json_string_value(j_password), auth_failure_response_creator); } // Only accept HTTP authentication if there's an anti-CSRF token const json_t * const j_csrf = json_object_get(j, "csrf"); if (!json_is_string(j_csrf)) { DLOG_DEBUG("Missing CSRF token in request"); datum_api_submit_uncached_response(connection, MHD_HTTP_FORBIDDEN, auth_failure_response_creator()); return false; } if (!datum_secure_strequals(datum_config.api_csrf_token, sizeof(datum_config.api_csrf_token)-1, json_string_value(j_csrf))) { DLOG_DEBUG("Wrong CSRF token in request"); datum_api_submit_uncached_response(connection, MHD_HTTP_FORBIDDEN, auth_failure_response_creator()); return false; } return datum_api_check_admin_password_httponly(connection, auth_failure_response_creator); } static struct MHD_Response *datum_api_create_response_authfail(const char * const head, const size_t head_sz) { const size_t max_sz = head_sz + www_auth_failed_html_sz + www_foot_html_sz + 1; size_t sz = 0; char * const output = malloc(max_sz); if (!output) { return datum_api_create_empty_mhd_response(); } memcpy(&output[sz], head, head_sz); sz += head_sz; memcpy(&output[sz], www_auth_failed_html, www_auth_failed_html_sz); sz += www_auth_failed_html_sz; memcpy(&output[sz], www_foot_html, www_foot_html_sz); sz += www_foot_html_sz; struct MHD_Response * const response = MHD_create_response_from_buffer(sz, output, MHD_RESPMEM_MUST_FREE); MHD_add_response_header(response, "Content-Type", "text/html"); return response; } static struct MHD_Response *datum_api_create_response_authfail_clients() { return datum_api_create_response_authfail(www_clients_top_html, www_clients_top_html_sz); } size_t datum_api_fill_authfail_error(const char * const var_start, const size_t var_name_len, char * const replacement, const size_t replacement_max_len, const T_DATUM_API_DASH_VARS * const vardata) { assert(replacement_max_len >= www_auth_failed_html_sz); memcpy(replacement, www_auth_failed_html, www_auth_failed_html_sz); return www_auth_failed_html_sz; } static struct MHD_Response *datum_api_create_response_authfail_config() { const size_t max_sz = www_config_errors_html_sz + www_auth_failed_html_sz; char * const output = malloc(max_sz); if (!output) { return datum_api_create_empty_mhd_response(); } const size_t sz = datum_api_fill_vars(www_config_errors_html, output, max_sz, datum_api_fill_authfail_error, NULL); struct MHD_Response * const response = MHD_create_response_from_buffer(sz, output, MHD_RESPMEM_MUST_FREE); MHD_add_response_header(response, "Content-Type", "text/html"); return response; } static struct MHD_Response *datum_api_create_response_authfail_threads() { return datum_api_create_response_authfail(www_threads_top_html, www_threads_top_html_sz); } static int datum_api_asset(struct MHD_Connection * const connection, const char * const mimetype, const char * const data, const size_t datasz, const char * const etag) { const char * const if_none_match_header = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, "If-None-Match"); if (if_none_match_header && 0 == strcmp(if_none_match_header, etag)) { struct MHD_Response *response = datum_api_create_empty_mhd_response(); MHD_add_response_header(response, "Etag", etag); int ret = MHD_queue_response(connection, MHD_HTTP_NOT_MODIFIED, response); MHD_destroy_response(response); return ret; } struct MHD_Response * const response = MHD_create_response_from_buffer(datasz, (void*)data, MHD_RESPMEM_PERSISTENT); MHD_add_response_header(response, "Content-Type", mimetype); MHD_add_response_header(response, "Etag", etag); const int ret = MHD_queue_response (connection, MHD_HTTP_OK, response); MHD_destroy_response (response); return ret; } void datum_api_cmd_empty_thread(int tid) { if (global_stratum_app && (tid >= 0) && (tid < global_stratum_app->max_threads)) { DLOG_WARN("API Request to empty stratum thread %d!", tid); global_stratum_app->datum_threads[tid].empty_request = true; } } void datum_api_cmd_kill_client(int tid, int cid) { if (global_stratum_app && (tid >= 0) && (tid < global_stratum_app->max_threads)) { if ((cid >= 0) && (cid < global_stratum_app->max_clients_thread)) { DLOG_WARN("API Request to disconnect stratum client %d/%d!", tid, cid); global_stratum_app->datum_threads[tid].client_data[cid].kill_request = true; global_stratum_app->datum_threads[tid].has_client_kill_request = true; } } } void datum_api_cmd_kill_client2(const char * const data, const size_t size, const char ** const redirect_p) { const char * const end = &data[size]; const char *underscore_pos = memchr(data, '_', size); if (!underscore_pos) return; const size_t tid_size = underscore_pos - data; const int tid = datum_atoi_strict(data, tid_size); const char *p = &underscore_pos[1]; underscore_pos = memchr(p, '_', end - p); if (!underscore_pos) underscore_pos = end; const int cid = datum_atoi_strict(p, underscore_pos - p); // Valid input; unconditionally redirect back to clients dashboard *redirect_p = "/clients"; if (tid < 0 || tid >= global_stratum_app->max_threads || cid < 0 || cid >= global_stratum_app->max_clients_thread) { return; } if (underscore_pos != end) { // Check it's the same client intended p = &underscore_pos[1]; underscore_pos = memchr(p, '_', end - p); if (!underscore_pos) underscore_pos = end; const uint64_t connect_tsms = datum_atoi_strict_u64(p, underscore_pos - p); const T_DATUM_MINER_DATA * const m = global_stratum_app->datum_threads[tid].client_data[cid].app_client_data; if (connect_tsms != m->connect_tsms) { DLOG_WARN("API Request to disconnect FORMER stratum client %d/%d (ignored; connect tsms req=%lu vs cur=%lu)", tid, cid, (unsigned long)connect_tsms, (unsigned long)m->connect_tsms); return; } p = &underscore_pos[1]; const uint64_t unique_id = datum_atoi_strict_u64(p, end - p); if (unique_id != m->unique_id) { DLOG_WARN("API Request to disconnect FORMER stratum client %d/%d (ignored; unique id req=%lu vs cur=%lu)", tid, cid, (unsigned long)unique_id, (unsigned long)m->unique_id); return; } } datum_api_cmd_kill_client(tid, cid); } int datum_api_cmd(struct MHD_Connection *connection, char *post, int len) { struct MHD_Response *response; char output[1024]; int sz = 0; json_t *root, *cmd, *param; json_error_t error; const char *cstr; int tid,cid; if ((len) && (post)) { DLOG_DEBUG("POST DATA: %s", post); if (post[0] == '{') { // attempt to parse JSON command root = json_loadb(post, len, 0, &error); if (root) { if (json_is_object(root) && (cmd = json_object_get(root, "cmd"))) { if (!datum_api_check_admin_password(connection, root, datum_api_create_empty_mhd_response)) { json_decref(root); return MHD_YES; } if (json_is_string(cmd)) { cstr = json_string_value(cmd); DLOG_DEBUG("JSON CMD: %s",cstr); switch(cstr[0]) { case 'e': { if (!strcmp(cstr,"empty_thread")) { param = json_object_get(root, "tid"); if (json_is_integer(param)) { datum_api_cmd_empty_thread(json_integer_value(param)); } break; } break; } case 'k': { if (!strcmp(cstr,"kill_client")) { param = json_object_get(root, "tid"); if (json_is_integer(param)) { tid = json_integer_value(param); param = json_object_get(root, "cid"); if (json_is_integer(param)) { cid = json_integer_value(param); datum_api_cmd_kill_client(tid,cid); } } break; } break; } default: break; } } } json_decref(root); } } else { root = json_object(); if (!datum_api_formdata_to_json(connection, post, len, root)) { json_decref(root); return datum_api_do_error(connection, MHD_HTTP_INTERNAL_SERVER_ERROR); } param = json_object_get(root, "empty_thread"); if (!datum_api_check_admin_password(connection, root, param ? datum_api_create_response_authfail_threads : datum_api_create_response_authfail_clients)) { json_decref(root); return MHD_YES; } const char *redirect = "/"; // param set for "empty_thread" above if (param) { tid = datum_atoi_strict(json_string_value(param), json_string_length(param)); if (tid != -1) { datum_api_cmd_empty_thread(tid); redirect = "/threads"; } } param = json_object_get(root, "kill_client"); if (param) { const char * const data = json_string_value(param); const size_t size = json_string_length(param); datum_api_cmd_kill_client2(data, size, &redirect); } response = datum_api_create_empty_mhd_response(); MHD_add_response_header(response, "Location", redirect); return datum_api_submit_uncached_response(connection, MHD_HTTP_FOUND, response); } } sprintf(output, "{}"); response = MHD_create_response_from_buffer (sz, (void *) output, MHD_RESPMEM_MUST_COPY); MHD_add_response_header(response, "Content-Type", "application/json"); return datum_api_submit_uncached_response(connection, MHD_HTTP_OK, response); } int datum_api_coinbaser(struct MHD_Connection *connection) { struct MHD_Response *response; T_DATUM_STRATUM_JOB *sjob; int j, i, max_sz = 0, sz = 0; char tempaddr[256]; uint64_t tv = 0; char *output = NULL; pthread_rwlock_rdlock(&stratum_global_job_ptr_lock); j = global_latest_stratum_job_index; sjob = (j >= 0 && j < MAX_STRATUM_JOBS) ? global_cur_stratum_jobs[j] : NULL; pthread_rwlock_unlock(&stratum_global_job_ptr_lock); max_sz = www_coinbaser_top_html_sz + www_foot_html_sz + (sjob ? (sjob->available_coinbase_outputs_count * 512) : 0) + 2048; // approximate max size of each row output = calloc(max_sz+16,1); if (!output) { return MHD_NO; } sz = snprintf(output, max_sz-1-sz, "%s", www_coinbaser_top_html); sz += snprintf(&output[sz], max_sz-1-sz, ""); if (sjob) { for(i=0;iavailable_coinbase_outputs_count;i++) { output_script_2_addr(sjob->available_coinbase_outputs[i].output_script, sjob->available_coinbase_outputs[i].output_script_len, tempaddr); sz += snprintf(&output[sz], max_sz-1-sz, "", (double)sjob->available_coinbase_outputs[i].value_sats / (double)100000000.0, tempaddr); tv += sjob->available_coinbase_outputs[i].value_sats; } if (tv < sjob->coinbase_value) { output_script_2_addr(sjob->pool_addr_script, sjob->pool_addr_script_len, tempaddr); sz += snprintf(&output[sz], max_sz-1-sz, "", (double)(sjob->coinbase_value - tv) / (double)100000000.0, tempaddr); } } sz += snprintf(&output[sz], max_sz-1-sz, "
Value Address
%.8f BTC%s
%.8f BTC%s
"); sz += snprintf(&output[sz], max_sz-1-sz, "%s", www_foot_html); response = MHD_create_response_from_buffer (sz, (void *) output, MHD_RESPMEM_MUST_FREE); MHD_add_response_header(response, "Content-Type", "text/html"); return datum_api_submit_uncached_response(connection, MHD_HTTP_OK, response); } int datum_api_thread_dashboard(struct MHD_Connection *connection) { struct MHD_Response *response; int sz=0, max_sz = 0, j, ii; char *output = NULL; T_DATUM_MINER_DATA *m = NULL; uint64_t tsms; double hr; unsigned char astat; double thr = 0.0; int subs,conns; const int max_threads = global_stratum_app ? global_stratum_app->max_threads : 0; max_sz = www_threads_top_html_sz + www_foot_html_sz + (max_threads * 512) + 2048; // approximate max size of each row output = calloc(max_sz+16,1); if (!output) { return MHD_NO; } const bool have_admin = datum_config.api_admin_password_len; tsms = current_time_millis(); sz = snprintf(output, max_sz-1-sz, "%s", www_threads_top_html); sz += snprintf(&output[sz], max_sz-1-sz, "
", datum_config.api_csrf_token); for (j = 0; j < max_threads; ++j) { thr = 0.0; subs = 0; conns = 0; for(ii=0;iimax_clients_thread;ii++) { if (global_stratum_app->datum_threads[j].client_data[ii].fd > 0) { conns++; m = (T_DATUM_MINER_DATA *)global_stratum_app->datum_threads[j].client_data[ii].app_client_data; if (m->subscribed) { subs++; astat = m->stats.active_index?0:1; // inverted hr = 0.0; if ((m->stats.last_swap_ms > 0) && (m->stats.diff_accepted[astat] > 0)) { hr = ((double)m->stats.diff_accepted[astat] / (double)((double)m->stats.last_swap_ms/1000.0)) * 0.004294967296; // Th/sec based on shares/sec } if (((double)(tsms - m->stats.last_swap_tsms)/1000.0) < 180.0) { thr += hr; } } } } if (conns) { sz += snprintf(&output[sz], max_sz-1-sz, ""); } } sz += snprintf(&output[sz], max_sz-1-sz, "
TID Connection Count Sub Count Approx. Hashrate Command
%d %d %d %.2f Th/s
"); if (have_admin) { sz += snprintf(&output[sz], max_sz-1-sz, ""); } sz += snprintf(&output[sz], max_sz-1-sz, "%s", www_foot_html); response = MHD_create_response_from_buffer (sz, (void *) output, MHD_RESPMEM_MUST_FREE); MHD_add_response_header(response, "Content-Type", "text/html"); return datum_api_submit_uncached_response(connection, MHD_HTTP_OK, response); } int datum_api_client_dashboard(struct MHD_Connection *connection) { struct MHD_Response *response; int connected_clients = 0; int i, sz = 0, max_sz = 0, j, ii; char *output = NULL; T_DATUM_MINER_DATA *m = NULL; uint64_t tsms; double hr; unsigned char astat; double thr = 0.0; const int max_threads = global_stratum_app ? global_stratum_app->max_threads : 0; for (i = 0; i < max_threads; ++i) { connected_clients+=global_stratum_app->datum_threads[i].connected_clients; } max_sz = www_clients_top_html_sz + www_foot_html_sz + (connected_clients * 1024) + 2048; // approximate max size of each row output = calloc(max_sz+16,1); if (!output) { return MHD_NO; } tsms = current_time_millis(); sz = snprintf(output, max_sz-1-sz, "%s", www_clients_top_html); if (!datum_config.api_admin_password_len) { sz += snprintf(&output[sz], max_sz-1-sz, "This page requires admin access (add \"admin_password\" to \"api\" section of config file)"); sz += snprintf(&output[sz], max_sz-1-sz, "%s", www_foot_html); response = MHD_create_response_from_buffer(sz, output, MHD_RESPMEM_MUST_FREE); MHD_add_response_header(response, "Content-Type", "text/html"); return datum_api_submit_uncached_response(connection, MHD_HTTP_OK, response); } if (!datum_api_check_admin_password_httponly(connection, datum_api_create_response_authfail_clients)) { return MHD_YES; } sz += snprintf(&output[sz], max_sz-1-sz, "
", datum_config.api_csrf_token); for (j = 0; j < max_threads; ++j) { for(ii=0;iimax_clients_thread;ii++) { if (global_stratum_app->datum_threads[j].client_data[ii].fd > 0) { m = (T_DATUM_MINER_DATA *)global_stratum_app->datum_threads[j].client_data[ii].app_client_data; sz += snprintf(&output[sz], max_sz-1-sz, "", j,ii); sz += snprintf(&output[sz], max_sz-1-sz, "", global_stratum_app->datum_threads[j].client_data[ii].rem_host); sz += snprintf(&output[sz], max_sz-1-sz, ""); if (m->subscribed) { sz += snprintf(&output[sz], max_sz-1-sz, "", m->sid, (double)(tsms - m->subscribe_tsms)/1000.0); if (m->stats.last_share_tsms) { sz += snprintf(&output[sz], max_sz-1-sz, "", (double)(tsms - m->stats.last_share_tsms)/1000.0); } else { sz += snprintf(&output[sz], max_sz-1-sz, ""); } sz += snprintf(&output[sz], max_sz-1-sz, "", m->current_diff); sz += snprintf(&output[sz], max_sz-1-sz, "", m->share_diff_accepted, m->share_count_accepted); hr = 0.0; if (m->share_diff_accepted > 0) { hr = ((double)m->share_diff_rejected / (double)(m->share_diff_accepted + m->share_diff_rejected))*100.0; } sz += snprintf(&output[sz], max_sz-1-sz, "", m->share_diff_rejected, m->share_count_rejected, hr); astat = m->stats.active_index?0:1; // inverted hr = 0.0; if ((m->stats.last_swap_ms > 0) && (m->stats.diff_accepted[astat] > 0)) { hr = ((double)m->stats.diff_accepted[astat] / (double)((double)m->stats.last_swap_ms/1000.0)) * 0.004294967296; // Th/sec based on shares/sec } if (((double)(tsms - m->stats.last_swap_tsms)/1000.0) < 180.0) { thr += hr; } if (m->share_diff_accepted > 0) { sz += snprintf(&output[sz], max_sz-1-sz, "", hr, (double)(tsms - m->stats.last_swap_tsms)/1000.0); } else { sz += snprintf(&output[sz], max_sz-1-sz, ""); } if (m->coinbase_selection < (sizeof(cbnames) / sizeof(cbnames[0]))) { sz += snprintf(&output[sz], max_sz-1-sz, "", cbnames[m->coinbase_selection]); } else { sz += snprintf(&output[sz], max_sz-1-sz, ""); } sz += snprintf(&output[sz], max_sz-1-sz, ""); } else { sz += snprintf(&output[sz], max_sz-1-sz, ""); } sz += snprintf(&output[sz], max_sz-1-sz, "", j, ii, (unsigned long)m->connect_tsms, (unsigned long)m->unique_id, j, ii, (unsigned long)m->connect_tsms, (unsigned long)m->unique_id); } } } sz += snprintf(&output[sz], max_sz-1-sz, "
TID/CID RemHost Auth Username Subbed Last Accepted VDiff DiffA (A) DiffR (R) Hashrate (age) Coinbase UserAgent Command
%d/%d%s"); sz += strncpy_html_escape(&output[sz], m->last_auth_username, max_sz-1-sz); sz += snprintf(&output[sz], max_sz-1-sz, " %4.4x %.1fs%.1fsN/A%"PRIu64"%"PRIu64" (%"PRIu64")%"PRIu64" (%"PRIu64") %.2f%%%.2f Th/s (%.1fs)N/A%sUnknown"); sz += strncpy_html_escape(&output[sz], m->useragent, max_sz-1-sz); sz += snprintf(&output[sz], max_sz-1-sz, "Not Subscribed

Total active hashrate estimate: %.2f Th/s

%s", www_foot_html); // return the home page with some data and such response = MHD_create_response_from_buffer (sz, (void *) output, MHD_RESPMEM_MUST_FREE); MHD_add_response_header(response, "Content-Type", "text/html"); return datum_api_submit_uncached_response(connection, MHD_HTTP_OK, response); } size_t datum_api_fill_config_var(const char *var_start, const size_t var_name_len, char * const replacement, const size_t replacement_max_len, const T_DATUM_API_DASH_VARS * const vardata) { const char *colon_pos = memchr(var_start, ':', var_name_len); const char *var_start_2 = colon_pos ? &colon_pos[1] : var_start; const char * const var_end = &var_start[var_name_len]; const size_t var_name_len_2 = var_end - var_start_2; const char * const underscore_pos = memchr(var_start_2, '_', var_name_len_2); int val; if (var_name_len_2 == 3 && 0 == strncmp(var_start_2, "*ro", 3)) { val = !(datum_config.api_modify_conf && datum_config.api_admin_password_len); if (!colon_pos) { var_start = "readonly:"; colon_pos = &var_start[8]; } } else if (var_name_len_2 == 24 && 0 == strncmp(var_start_2, "*datum_pool_pass_workers", 24)) { val = datum_config.datum_pool_pass_workers && !datum_config.datum_pool_pass_full_users; } else if (var_name_len_2 == 16 && 0 == strncmp(var_start_2, "*datum_pool_host", 16)) { const char *s = NULL; if (datum_config.datum_pool_host[0]) { s = datum_config.datum_pool_host; } else if (datum_config.config_json) { const json_t * const config = datum_config.config_json; json_t *j = json_object_get(config, "datum"); if (j) j = json_is_object(j) ? json_object_get(j, "pool_host(old)") : NULL; if (j && json_is_string(j) && json_string_length(j) <= 1023) { s = json_string_value(j); } } if (!s) { const T_DATUM_CONFIG_ITEM * const cfginfo = datum_config_get_option_info("datum", 5, "pool_host", 9); s = cfginfo->default_string[0]; } size_t copy_sz = strlen(s); if (copy_sz >= replacement_max_len) copy_sz = replacement_max_len - 1; memcpy(replacement, s, copy_sz); return copy_sz; } else if (var_name_len_2 == 27 && 0 == strncmp(var_start_2, "*username_behaviour_private", 27)) { val = !(datum_config.datum_pool_pass_workers || datum_config.datum_pool_pass_full_users); } else if (var_name_len_2 == 22 && 0 == strncmp(var_start_2, "*reward_sharing_prefer", 22)) { val = (!datum_config.datum_pooled_mining_only) && datum_config.datum_pool_host[0]; } else if (var_name_len_2 == 21 && 0 == strncmp(var_start_2, "*reward_sharing_never", 21)) { val = (!datum_config.datum_pooled_mining_only) && !datum_config.datum_pool_host[0]; } else if (var_name_len_2 == 34 && 0 == strncmp(var_start_2, "*mining_coinbase_tag_secondary_max", 34)) { val = 88 - strlen(datum_config.mining_coinbase_tag_primary); if (val > 60) val = 60; } else if (var_name_len_2 == 11 && 0 == strncmp(var_start_2, "*CSRF_TOKEN", 11)) { size_t copy_sz = strlen(datum_config.api_csrf_token); if (copy_sz >= replacement_max_len) copy_sz = replacement_max_len - 1; memcpy(replacement, datum_config.api_csrf_token, copy_sz); return copy_sz; } else if (underscore_pos) { const T_DATUM_CONFIG_ITEM * const item = datum_config_get_option_info(var_start_2, underscore_pos - var_start_2, &underscore_pos[1], var_end - &underscore_pos[1]); if (item) { switch (item->var_type) { case DATUM_CONF_INT: { val = *((int *)item->ptr); break; } case DATUM_CONF_BOOL: { val = *((bool *)item->ptr); if ((!colon_pos) && replacement_max_len > 5) { const size_t len = val ? 4 : 5; memcpy(replacement, val ? "true" : "false", len); return len; } break; } case DATUM_CONF_STRING: { const char * const s = (char *)item->ptr; if (colon_pos) { DLOG_ERROR("%s: '%.*s' modifier not implemented for %s", __func__, (int)(colon_pos - var_start), var_start, "DATUM_CONF_STRING"); break; } size_t copy_sz = strlen(s); if (copy_sz >= replacement_max_len) copy_sz = replacement_max_len - 1; memcpy(replacement, s, copy_sz); return copy_sz; } case DATUM_CONF_STRING_ARRAY: { DLOG_ERROR("%s: %s not implemented", __func__, "DATUM_CONF_STRING_ARRAY"); break; } case DATUM_CONF_USERNAME_MODS: { DLOG_ERROR("%s: %s not implemented", __func__, "DATUM_CONF_USERNAME_MODS"); break; } } } else { DLOG_ERROR("%s: '%.*s' not implemented", __func__, (int)(var_end - var_start_2), var_start_2); return 0; } } else { DLOG_ERROR("%s: '%.*s' not implemented", __func__, (int)(var_end - var_start_2), var_start_2); return 0; } assert(replacement_max_len > 0); if (colon_pos) { if (0 == strncmp(var_start, "readonly:", 9) || 0 == strncmp(var_start, "selected:", 9) || 0 == strncmp(var_start, "checked:", 8) || 0 == strncmp(var_start, "disabled:", 9)) { size_t attr_len; if (val) { attr_len = colon_pos - var_start; if (attr_len + 2 > replacement_max_len) attr_len = replacement_max_len - 2; replacement[0] = ' '; memcpy(&replacement[1], var_start, attr_len); ++attr_len; } else { attr_len = 0; } return attr_len; } else if (0 == strncmp(var_start, "msg:", 4)) { if (val) { static const char * const msg = "
Config file disallows editing (set \"admin_password\" and \"modify_conf\" in \"api\" section of config file)"; const size_t len = strlen(msg); memcpy(replacement, msg, len); return len; } else { return 0; } } else { DLOG_ERROR("%s: '%.*s' modifier not implemented", __func__, (int)(colon_pos - var_start), var_start); return 0; } } return snprintf(replacement, replacement_max_len, "%d", val); } int datum_api_config_dashboard(struct MHD_Connection *connection) { struct MHD_Response *response; size_t sz = 0, max_sz = 0; char *output = NULL; max_sz = www_config_html_sz * 2; output = malloc(max_sz); if (!output) { return MHD_NO; } sz += datum_api_fill_vars(www_config_html, output, max_sz, datum_api_fill_config_var, NULL); response = MHD_create_response_from_buffer(sz, output, MHD_RESPMEM_MUST_FREE); MHD_add_response_header(response, "Content-Type", "text/html"); return datum_api_submit_uncached_response(connection, MHD_HTTP_OK, response); } #ifndef JSON_PRESERVE_ORDER #define JSON_PRESERVE_ORDER 0 #endif // Only modifies config_json; writing is done later void datum_api_json_modify_new(const char * const category, const char * const key, json_t * const val) { json_t * const config = datum_config.config_json; assert(config); json_t *j = json_object_get(config, category); if (!j) { j = json_object(); json_object_set_new(config, category, j); } json_object_set_new(j, key, val); } bool datum_api_json_write() { json_t * const config = datum_config.config_json; assert(config); assert(datum_gateway_config_filename); char buf[0x100]; int rv = snprintf(buf, sizeof(buf) - 4, "%s", datum_gateway_config_filename); assert(rv > 0); strcpy(&buf[rv], ".new"); if (json_dump_file(config, buf, JSON_PRESERVE_ORDER | JSON_INDENT(4))) { DLOG_ERROR("Failed to write new config to %s", buf); return false; } if (rename(buf, datum_gateway_config_filename)) { DLOG_ERROR("Failed to rename new config %s to %s", buf, datum_gateway_config_filename); return false; } DLOG_INFO("Wrote new config to %s", datum_gateway_config_filename); return true; } struct datum_api_config_set_status { json_t *errors; bool modified_config; bool need_restart; }; // This does several steps: // - If the value is unchanged, return true without doing anything // - Validate the value without changing anything // - Change the runtime dataum_config (and ensure it goes live) // - Modify the config file // If anything fails (including validation), errors is appended and false is returned bool datum_api_config_set(const char * const key, const char * const val, struct datum_api_config_set_status * const status) { json_t * const errors = status->errors; if (0 == strcmp(key, "mining_pool_address")) { if (0 == strcmp(val, datum_config.mining_pool_address)) return true; unsigned char dummy[64]; if (!addr_2_output_script(val, &dummy[0], 64)) { json_array_append_new(errors, json_string_nocheck("Invalid Bitcoin Address")); return false; } strcpy(datum_config.mining_pool_address, val); datum_api_json_modify_new("mining", "pool_address", json_string(val)); } else if (0 == strcmp(key, "username_behaviour")) { if (0 == strcmp(val, "datum_pool_pass_full_users")) { if (datum_config.datum_pool_pass_full_users) return true; datum_config.datum_pool_pass_full_users = true; // datum_pool_pass_workers doesn't matter with datum_pool_pass_full_users enabled } else if (0 == strcmp(val, "datum_pool_pass_workers")) { if (datum_config.datum_pool_pass_workers && !datum_config.datum_pool_pass_full_users) return true; datum_config.datum_pool_pass_full_users = false; datum_config.datum_pool_pass_workers = true; } else if (0 == strcmp(val, "private")) { if (!(datum_config.datum_pool_pass_workers || datum_config.datum_pool_pass_full_users)) return true; datum_config.datum_pool_pass_full_users = false; datum_config.datum_pool_pass_workers = false; } else { json_array_append_new(errors, json_string_nocheck("Invalid option for \"Send Miner Usernames To Pool\"")); return false; } datum_api_json_modify_new("datum", "pool_pass_full_users", json_boolean(datum_config.datum_pool_pass_full_users)); if (!datum_config.datum_pool_pass_full_users) { datum_api_json_modify_new("datum", "pool_pass_workers", json_boolean(datum_config.datum_pool_pass_workers)); } } else if (0 == strcmp(key, "mining_coinbase_tag_secondary")) { if (0 == strcmp(val, datum_config.mining_coinbase_tag_secondary)) return true; size_t len_limit = 88 - strlen(datum_config.mining_coinbase_tag_primary); if (len_limit > 60) len_limit = 60; if (strlen(val) > len_limit) { json_array_append_new(errors, json_string_nocheck("Coinbase Tag is too long")); return false; } strcpy(datum_config.mining_coinbase_tag_secondary, val); datum_api_json_modify_new("mining", "coinbase_tag_secondary", json_string(val)); } else if (0 == strcmp(key, "mining_coinbase_unique_id")) { const int val_int = datum_atoi_strict(val, strlen(val)); if (val_int == datum_config.coinbase_unique_id) return true; if (val_int > 65535 || val_int < 0) { json_array_append_new(errors, json_string_nocheck("Unique Gateway ID must be between 0 and 65535")); return false; } datum_config.coinbase_unique_id = val_int; datum_api_json_modify_new("mining", "coinbase_unique_id", json_integer(val_int)); } else if (0 == strcmp(key, "reward_sharing")) { json_t * const config = datum_config.config_json; assert(config); bool want_datum_pool_host = false; if (0 == strcmp(val, "require")) { if (datum_config.datum_pool_host[0] && datum_config.datum_pooled_mining_only) return true; datum_config.datum_pooled_mining_only = true; want_datum_pool_host = true; } else if (0 == strcmp(val, "prefer")) { if (datum_config.datum_pool_host[0] && !datum_config.datum_pooled_mining_only) return true; datum_config.datum_pooled_mining_only = false; want_datum_pool_host = true; } else if (0 == strcmp(val, "never")) { if (!(datum_config.datum_pool_host[0] || datum_config.datum_pooled_mining_only)) return true; datum_config.datum_pooled_mining_only = false; datum_config.datum_pool_host[0] = '\0'; // Only copy the old value if it's in the config file json_t *j = json_object_get(config, "datum"); if (j) j = json_is_object(j) ? json_object_get(j, "pool_host") : NULL; if (j) { datum_api_json_modify_new("datum", "pool_host(old)", json_incref(j)); } datum_api_json_modify_new("datum", "pool_host", json_string_nocheck("")); } else { json_array_append_new(errors, json_string_nocheck("Invalid option for \"Collaborative reward sharing\"")); return false; } if (want_datum_pool_host && !datum_config.datum_pool_host[0]) { json_t *j = json_object_get(config, "datum"); if (j) j = json_is_object(j) ? json_object_get(j, "pool_host(old)") : NULL; if (j && json_is_string(j) && json_string_length(j) <= 1023) { strcpy(datum_config.datum_pool_host, json_string_value(j)); json_object_del(j, "pool_host(old)"); datum_api_json_modify_new("datum", "pool_host", json_string(datum_config.datum_pool_host)); } else { const T_DATUM_CONFIG_ITEM * const cfginfo = datum_config_get_option_info("datum", 5, "pool_host", 9); strcpy(datum_config.datum_pool_host, cfginfo->default_string[0]); // Avoiding using null here because older versions handled it poorly j = json_object_get(config, "datum"); if (j) json_object_del(j, "pool_host"); } } datum_api_json_modify_new("datum", "pooled_mining_only", json_boolean(datum_config.datum_pooled_mining_only)); // TODO: apply change without restarting status->need_restart = true; } else if (0 == strcmp(key, "datum_pool_host")) { if (0 == strcmp(val, datum_config.datum_pool_host)) return true; if (strlen(val) > 1023) { json_array_append_new(errors, json_string_nocheck("Pool Host is too long")); return false; } if (datum_config.datum_pool_host[0]) { strcpy(datum_config.datum_pool_host, val); datum_api_json_modify_new("datum", "pool_host", json_string(val)); // TODO: apply change without restarting // TODO: switch pools smoother (keep old connection alive for share submissions until those jobs expire) status->need_restart = true; } else { json_t * const config = datum_config.config_json; assert(config); json_t *j = json_object_get(config, "datum"); if (j) j = json_is_object(j) ? json_object_get(j, "pool_host(old)") : NULL; const T_DATUM_CONFIG_ITEM * const cfginfo = datum_config_get_option_info("datum", 5, "pool_host", 9); // Avoid setting the default host in the config file, unless something else was already there if (0 != strcmp(val, cfginfo->default_string[0]) || json_is_string(j)) { datum_api_json_modify_new("datum", "pool_host(old)", json_string(val)); } } } else if (0 == strcmp(key, "datum_pool_port")) { const int val_int = datum_atoi_strict(val, strlen(val)); if (val_int == datum_config.datum_pool_port) return true; if (val_int > 65535 || val_int < 1) { json_array_append_new(errors, json_string_nocheck("Pool Port must be between 1 and 65535")); return false; } datum_config.datum_pool_port = val_int; datum_api_json_modify_new("datum", "pool_port", json_integer(val_int)); // TODO: apply change without restarting // TODO: switch pools smoother (keep old connection alive for share submissions until those jobs expire) status->need_restart = true; } else if (0 == strcmp(key, "datum_pool_pubkey")) { if (0 == strcmp(val, datum_config.datum_pool_pubkey)) return true; if (strlen(val) > 1023) { json_array_append_new(errors, json_string_nocheck("Pool Pubkey is too long")); return false; } strcpy(datum_config.datum_pool_pubkey, val); datum_api_json_modify_new("datum", "pool_pubkey", json_string(val)); // TODO: apply change without restarting // TODO: switch pools smoother (keep old connection alive for share submissions until those jobs expire) status->need_restart = true; } else if (0 == strcmp(key, "stratum_fingerprint_miners")) { bool val_bool; if (!datum_str_to_bool_strict(val, &val_bool)) { json_array_append_new(errors, json_string_nocheck("\"Fingerprint and workaround known miner bugs\" must be 0 or 1")); return false; } if (val_bool == datum_config.stratum_v1_fingerprint_miners) return true; datum_config.stratum_v1_fingerprint_miners = val_bool; datum_api_json_modify_new("stratum", "fingerprint_miners", json_boolean(val_bool)); // TODO: apply change to connected miners? } else if (0 == strcmp(key, "datum_always_pay_self")) { bool val_bool; if (!datum_str_to_bool_strict(val, &val_bool)) { json_array_append_new(errors, json_string_nocheck("\"Always pay self\" must be 0 or 1")); return false; } if (val_bool == datum_config.datum_always_pay_self) return true; datum_config.datum_always_pay_self = val_bool; datum_api_json_modify_new("datum", "always_pay_self", json_boolean(val_bool)); } else if (0 == strcmp(key, "bitcoind_work_update_seconds")) { const int val_int = datum_atoi_strict(val, strlen(val)); if (val_int == datum_config.bitcoind_work_update_seconds) return true; if (val_int > 120 || val_int < 5) { json_array_append_new(errors, json_string_nocheck("bitcoind work update interval must be between 5 and 120")); return false; } datum_config.bitcoind_work_update_seconds = val_int; datum_api_json_modify_new("bitcoind", "work_update_seconds", json_integer(val_int)); if (datum_config.bitcoind_work_update_seconds >= datum_config.datum_protocol_global_timeout - 5) { datum_config.datum_protocol_global_timeout = val_int + 5; datum_api_json_modify_new("datum", "protocol_global_timeout", json_integer(val_int + 5)); } // TODO: apply change without restarting (and don't interfere with existing jobs) status->need_restart = true; } else if (0 == strcmp(key, "bitcoind_rpcurl")) { if (0 == strcmp(val, datum_config.bitcoind_rpcurl)) return true; if (strlen(val) > 128) { json_array_append_new(errors, json_string_nocheck("bitcoind RPC URL is too long")); return false; } strcpy(datum_config.bitcoind_rpcurl, val); datum_api_json_modify_new("bitcoind", "rpcurl", json_string(val)); } else if (0 == strcmp(key, "bitcoind_rpcuser")) { if (0 == strcmp(val, datum_config.bitcoind_rpcuser)) return true; if (strlen(val) > 128) { json_array_append_new(errors, json_string_nocheck("bitcoind RPC user is too long")); return false; } strcpy(datum_config.bitcoind_rpcuser, val); datum_api_json_modify_new("bitcoind", "rpcuser", json_string(val)); update_rpc_auth(&datum_config); } else if (0 == strcmp(key, "bitcoind_rpcpassword")) { if (0 == strcmp(val, datum_config.bitcoind_rpcpassword)) return true; if (!val[0]) return true; // no password change if (strlen(val) > 128) { json_array_append_new(errors, json_string_nocheck("bitcoind RPC password is too long")); return false; } strcpy(datum_config.bitcoind_rpcpassword, val); datum_api_json_modify_new("bitcoind", "rpcpassword", json_string(val)); update_rpc_auth(&datum_config); } else { char err[0x100]; snprintf(err, sizeof(err), "Unknown setting '%s'", key); json_array_append_new(errors, json_string_nocheck(err)); DLOG_ERROR("%s: '%s' not implemented", __func__, key); return false; } status->modified_config = true; return true; } static const char datum_api_config_errors_fmt[] = "
%s
"; size_t datum_api_fill_config_errors(const char *var_start, const size_t var_name_len, char * const replacement, const size_t replacement_max_len, const T_DATUM_API_DASH_VARS * const vardata) { const json_t * const errors = (void*)vardata; size_t index, sz = 0; json_t *j_it; json_array_foreach(errors, index, j_it) { sz += snprintf(&replacement[sz], replacement_max_len, datum_api_config_errors_fmt, json_string_value(j_it)); } return sz; } void *datum_restart_thread(void *ptr) { // Give logger some time usleep(500000); // Wait for the response to actually get delivered // FIXME: css/svg/etc might fail (we don't support caching them yet) struct MHD_Daemon * const mhd = ptr; MHD_quiesce_daemon(mhd); while (MHD_get_daemon_info(mhd, MHD_DAEMON_INFO_CURRENT_CONNECTIONS)->num_connections > 0) { usleep(100); } MHD_stop_daemon(mhd); datum_reexec(); abort(); // impossible to get here } int datum_api_config_post(struct MHD_Connection * const connection, char * const post, const int len) { struct MHD_Response *response; int ret; const char *key; json_t *j_it; if (!datum_config.api_modify_conf) { return datum_api_do_error(connection, MHD_HTTP_FORBIDDEN); } json_t * const j = json_object(); if (!datum_api_formdata_to_json(connection, post, len, j)) { json_decref(j); return datum_api_do_error(connection, MHD_HTTP_INTERNAL_SERVER_ERROR); } if (!datum_api_check_admin_password(connection, j, datum_api_create_response_authfail_config)) { json_decref(j); return MHD_YES; } json_object_del(j, "csrf"); json_object_del(j, "password"); { // Unchecked checkboxes are simply omitted, so a hidden field is used to convey them const json_t * const j_checkboxes = json_object_get(j, "checkboxes"); const char * const checkboxes = json_string_value(j_checkboxes); const size_t checkboxes_len = json_string_length(j_checkboxes); const char *p = checkboxes; char buf[0x100]; while (p[0] != '\0') { const char *p2 = strchr(p, ' '); if (!p2) p2 = &checkboxes[checkboxes_len]; const size_t i_len = p2 - p; if (i_len < sizeof(buf)) { memcpy(buf, p, i_len); buf[i_len] = '\0'; json_t * const j_cb = json_object_get(j, buf); if ((!j_cb) || json_is_null(j_cb)) { json_object_set_new_nocheck(j, buf, json_string_nocheck("0")); } } p = *p2 ? &p2[1] : p2; } json_object_del(j, "checkboxes"); } json_t * const errors = json_array(); struct datum_api_config_set_status status = { .errors = errors, }; json_object_foreach(j, key, j_it) { datum_api_config_set(key, json_string_value(j_it), &status); } json_decref(j); if (status.modified_config) { if (!datum_api_json_write()) { if (status.need_restart) { json_array_append_new(errors, json_string_nocheck("Error writing new config file (changes will be lost)")); } else { json_array_append_new(errors, json_string_nocheck("Error writing new config file (changes will be lost at restart)")); } } } if (json_array_size(errors) > 0) { if (status.need_restart) { json_array_insert_new(errors, 0, json_string_nocheck("NOTE: Other changes require a gateway restart. Please wait a few seconds before trying again.")); } size_t index, max_sz; max_sz = www_config_errors_html_sz; json_array_foreach(errors, index, j_it) { max_sz += json_string_length(j_it) + sizeof(datum_api_config_errors_fmt); } char * const output = malloc(max_sz); if (!output) { return MHD_NO; } const size_t sz = datum_api_fill_vars(www_config_errors_html, output, max_sz, datum_api_fill_config_errors, (void*)errors); response = MHD_create_response_from_buffer(sz, output, MHD_RESPMEM_MUST_FREE); MHD_add_response_header(response, "Content-Type", "text/html"); } else if (status.need_restart) { response = MHD_create_response_from_buffer(www_config_restart_html_sz, (void*)www_config_restart_html, MHD_RESPMEM_PERSISTENT); MHD_add_response_header(response, "Content-Type", "text/html"); } else { response = datum_api_create_empty_mhd_response(); MHD_add_response_header(response, "Location", "/config"); } json_decref(errors); ret = datum_api_submit_uncached_response(connection, MHD_HTTP_FOUND, response); if (status.need_restart) { DLOG_INFO("Config change requires restarting gateway, proceeding"); struct MHD_Daemon * const mhd = MHD_get_connection_info(connection, MHD_CONNECTION_INFO_DAEMON)->daemon; pthread_t pthread_datum_restart_thread; pthread_create(&pthread_datum_restart_thread, NULL, datum_restart_thread, mhd); } return ret; } void datum_api_dash_stats(T_DATUM_API_DASH_VARS *dashdata) { int j, k = 0, kk = 0, ii; T_DATUM_MINER_DATA *m; unsigned char astat; double thr = 0.0; double hr; uint64_t tsms; pthread_rwlock_rdlock(&stratum_global_job_ptr_lock); j = global_latest_stratum_job_index; dashdata->sjob = (j >= 0 && j < MAX_STRATUM_JOBS) ? global_cur_stratum_jobs[j] : NULL; pthread_rwlock_unlock(&stratum_global_job_ptr_lock); tsms = current_time_millis(); if (global_stratum_app) { k = 0; kk = 0; for(j=0;jmax_threads;j++) { k+=global_stratum_app->datum_threads[j].connected_clients; for(ii=0;iimax_clients_thread;ii++) { if (global_stratum_app->datum_threads[j].client_data[ii].fd > 0) { m = (T_DATUM_MINER_DATA *)global_stratum_app->datum_threads[j].client_data[ii].app_client_data; if (m->subscribed) { kk++; astat = m->stats.active_index?0:1; // inverted hr = 0.0; if ((m->stats.last_swap_ms > 0) && (m->stats.diff_accepted[astat] > 0)) { hr = ((double)m->stats.diff_accepted[astat] / (double)((double)m->stats.last_swap_ms/1000.0)) * 0.004294967296; // Th/sec based on shares/sec } if (((double)(tsms - m->stats.last_swap_tsms)/1000.0) < 180.0) { thr += hr; } } } } } dashdata->STRATUM_ACTIVE_THREADS = global_stratum_app->datum_active_threads; dashdata->STRATUM_TOTAL_CONNECTIONS = k; dashdata->STRATUM_TOTAL_SUBSCRIPTIONS = kk; dashdata->STRATUM_HASHRATE_ESTIMATE = thr; } else { dashdata->STRATUM_ACTIVE_THREADS = 0; dashdata->STRATUM_TOTAL_CONNECTIONS = 0; dashdata->STRATUM_TOTAL_SUBSCRIPTIONS = 0; dashdata->STRATUM_HASHRATE_ESTIMATE = 0.0; } } int datum_api_homepage(struct MHD_Connection *connection) { struct MHD_Response *response; char output[DATUM_API_HOMEPAGE_MAX_SIZE]; T_DATUM_API_DASH_VARS vardata; memset(&vardata, 0, sizeof(T_DATUM_API_DASH_VARS)); datum_api_dash_stats(&vardata); output[0] = 0; datum_api_fill_vars(www_home_html, output, DATUM_API_HOMEPAGE_MAX_SIZE, datum_api_fill_var, &vardata); // return the home page with some data and such response = MHD_create_response_from_buffer (strlen(output), (void *) output, MHD_RESPMEM_MUST_COPY); MHD_add_response_header(response, "Content-Type", "text/html"); return datum_api_submit_uncached_response(connection, MHD_HTTP_OK, response); } int datum_api_OK(struct MHD_Connection *connection) { struct MHD_Response *response; const char *ok_response = "OK"; response = MHD_create_response_from_buffer(strlen(ok_response), (void *)ok_response, MHD_RESPMEM_PERSISTENT); MHD_add_response_header(response, "Content-Type", "text/html"); return datum_api_submit_uncached_response(connection, MHD_HTTP_OK, response); } #ifdef DATUM_API_FOR_UMBREL int datum_api_umbrel_widget(struct MHD_Connection * const connection) { char json_response[512]; T_DATUM_API_DASH_VARS umbreldata; const char *hash_unit; int json_response_len; datum_api_dash_stats(&umbreldata); hash_unit = dynamic_hash_unit(&umbreldata.STRATUM_HASHRATE_ESTIMATE); json_response_len = snprintf(json_response, sizeof(json_response), "{" "\"type\": \"three-stats\"," "\"refresh\": \"30s\"," "\"link\": \"\"," "\"items\": [" "{\"title\": \"Connections\", \"text\": \"%d\", \"subtext\": \"Worker\"}," "{\"title\": \"Hashrate\", \"text\": \"%.2f\", \"subtext\": \"%s\"}" "]" "}", umbreldata.STRATUM_TOTAL_CONNECTIONS, umbreldata.STRATUM_HASHRATE_ESTIMATE, hash_unit); if (json_response_len >= sizeof(json_response)) json_response_len = sizeof(json_response) - 1; struct MHD_Response *response = MHD_create_response_from_buffer(json_response_len, (void *)json_response, MHD_RESPMEM_MUST_COPY); MHD_add_response_header(response, "Content-Type", "application/json"); return datum_api_submit_uncached_response(connection, MHD_HTTP_OK, response); } #endif int datum_api_testnet_fastforward(struct MHD_Connection * const connection) { const char *time_str; time_str = MHD_lookup_connection_value(connection, MHD_GET_ARGUMENT_KIND, "password"); if (!datum_api_check_admin_password_only(connection, time_str, datum_api_create_empty_mhd_response)) { return MHD_YES; } // Get the time parameter from the URL query time_str = MHD_lookup_connection_value(connection, MHD_GET_ARGUMENT_KIND, "ts"); uint32_t t = -1000; if (time_str != NULL) { // Convert the time parameter to uint32_t t = (int)strtoul(time_str, NULL, 10); } datum_blocktemplates_notifynew("T", t); return datum_api_OK(connection); } struct ConnectionInfo { char *data; size_t data_size; }; static void datum_api_request_completed(void *cls, struct MHD_Connection *connection, void **con_cls, enum MHD_RequestTerminationCode toe) { struct ConnectionInfo *con_info = *con_cls; if (con_info != NULL) { if (con_info->data != NULL) free(con_info->data); free(con_info); } *con_cls = NULL; } enum MHD_Result datum_api_answer(void *cls, struct MHD_Connection *connection, const char *url, const char *method, const char *version, const char *upload_data, size_t *upload_data_size, void **con_cls) { char *user; char *pass; enum MHD_Result ret; struct MHD_Response *response; struct ConnectionInfo *con_info = *con_cls; int int_method = 0; int uds = 0; if (strcmp(method, "GET") == 0) { int_method = 1; } if (strcmp(method, "POST") == 0) { int_method = 2; } if (!int_method) { const char *error_response = "

Method not allowed.

"; response = MHD_create_response_from_buffer(strlen(error_response), (void *)error_response, MHD_RESPMEM_PERSISTENT); MHD_add_response_header(response, "Content-Type", "text/html"); ret = MHD_queue_response(connection, MHD_HTTP_METHOD_NOT_ALLOWED, response); MHD_destroy_response(response); return ret; } if (int_method == 2) { if (!con_info) { // Allocate memory for connection info con_info = malloc(sizeof(struct ConnectionInfo)); if (!con_info) { return MHD_NO; } con_info->data = calloc(16384,1); con_info->data_size = 0; if (!con_info->data) { free(con_info); return MHD_NO; } *con_cls = (void *)con_info; return MHD_YES; } if (*upload_data_size) { // Accumulate data // max 1 MB? seems reasonable if (con_info->data_size + *upload_data_size > (1024*1024)) return MHD_NO; con_info->data = realloc(con_info->data, con_info->data_size + *upload_data_size + 1); if (!con_info->data) { return MHD_NO; } memcpy(&(con_info->data[con_info->data_size]), upload_data, *upload_data_size); con_info->data_size += *upload_data_size; con_info->data[con_info->data_size] = '\0'; *upload_data_size = 0; return MHD_YES; } else if (!con_info->data_size) { const char *error_response = "

Invalid request.

"; response = MHD_create_response_from_buffer(strlen(error_response), (void *)error_response, MHD_RESPMEM_PERSISTENT); MHD_add_response_header(response, "Content-Type", "text/html"); ret = MHD_queue_response(connection, MHD_HTTP_BAD_REQUEST, response); MHD_destroy_response(response); return ret; } uds = *upload_data_size; } const union MHD_ConnectionInfo *conn_info = MHD_get_connection_info(connection, MHD_CONNECTION_INFO_CLIENT_ADDRESS); char *client_ip = inet_ntoa(((struct sockaddr_in*)conn_info->client_addr)->sin_addr); DLOG_DEBUG("REQUEST: %s, %s, %s, %d", client_ip, method, url, uds); pass = NULL; user = MHD_basic_auth_get_username_password (connection, &pass); ///////////////////////// // TODO: Implement API key or auth or something similar if (user) MHD_free(user); if (pass) MHD_free(pass); if (int_method == 1 && url[0] == '/' && url[1] == 0) { // homepage return datum_api_homepage(connection); } switch (url[1]) { case 'N': { if (!strcmp(url, "/NOTIFY")) { // TODO: Implement faster notifies with hash+height datum_blocktemplates_notifynew(NULL, 0); return datum_api_OK(connection); } break; } case 'a': { if (!strcmp(url, "/assets/icons/datum_logo.svg")) { return datum_api_asset(connection, "image/svg+xml", www_assets_icons_datum_logo_svg, www_assets_icons_datum_logo_svg_sz, www_assets_icons_datum_logo_svg_etag); } else if (!strcmp(url, "/assets/icons/favicon.ico")) { return datum_api_asset(connection, "image/x-icon", www_assets_icons_favicon_ico, www_assets_icons_favicon_ico_sz, www_assets_icons_favicon_ico_etag); } else if (!strcmp(url, "/assets/style.css")) { return datum_api_asset(connection, "text/css", www_assets_style_css, www_assets_style_css_sz, www_assets_style_css_etag); } break; } case 'c': { if (!strcmp(url, "/clients")) { return datum_api_client_dashboard(connection); } if (!strcmp(url, "/coinbaser")) { return datum_api_coinbaser(connection); } if (!strcmp(url, "/config")) { if (int_method == 2 && con_info) { return datum_api_config_post(connection, con_info->data, con_info->data_size); } else { return datum_api_config_dashboard(connection); } } if ((int_method==2) && (!strcmp(url, "/cmd"))) { if (con_info) { return datum_api_cmd(connection, con_info->data, con_info->data_size); } else { return MHD_NO; } } break; } case 'f': { if (!strcmp(url, "/favicon.ico")) { return datum_api_asset(connection, "image/x-icon", www_assets_icons_favicon_ico, www_assets_icons_favicon_ico_sz, www_assets_icons_favicon_ico_etag); } break; } case 't': { if (!strcmp(url, "/threads")) { return datum_api_thread_dashboard(connection); } if (!strcmp(url, "/testnet_fastforward")) { return datum_api_testnet_fastforward(connection); } break; } #ifdef DATUM_API_FOR_UMBREL case 'u': { if (!strcmp(url, "/umbrel-api")) { return datum_api_umbrel_widget(connection); } break; } #endif default: break; } const char *error_response = "

Not found

"; response = MHD_create_response_from_buffer(strlen(error_response), (void *)error_response, MHD_RESPMEM_PERSISTENT); MHD_add_response_header(response, "Content-Type", "text/html"); ret = MHD_queue_response (connection, MHD_HTTP_NOT_FOUND, response); MHD_destroy_response (response); return ret; } static struct MHD_Daemon *datum_api_try_start(unsigned int flags, const int sock) { flags |= MHD_USE_AUTO; // event loop API flags |= MHD_USE_INTERNAL_POLLING_THREAD; return MHD_start_daemon( flags, datum_config.api_listen_port, NULL, NULL, // accept policy filter &datum_api_answer, NULL, // default URI handler MHD_OPTION_LISTEN_SOCKET, sock, MHD_OPTION_CONNECTION_LIMIT, 128, MHD_OPTION_NOTIFY_COMPLETED, datum_api_request_completed, NULL, MHD_OPTION_LISTENING_ADDRESS_REUSE, (unsigned int)1, MHD_OPTION_END); } void *datum_api_thread(void *ptr) { struct MHD_Daemon *daemon; if (!datum_config.api_listen_port) { DLOG_INFO("No API port configured. API disabled."); return NULL; } int listen_socks[1]; size_t listen_socks_len = 1; if (!datum_sockets_setup_listening_sockets("API", datum_config.api_listen_addr, datum_config.api_listen_port, listen_socks, &listen_socks_len)) { return NULL; } daemon = datum_api_try_start(0, listen_socks[0]); if (!daemon) { DLOG_FATAL("Unable to start daemon for API"); panic_from_thread(__LINE__); return NULL; } DLOG_INFO("API listening on address %s port %d", datum_config.api_listen_addr[0] ? datum_config.api_listen_addr : "(any)", datum_config.api_listen_port); while(1) { sleep(3); } } int datum_api_init(void) { pthread_t pthread_datum_api_thread; if (!datum_config.api_listen_port) { DLOG_INFO("INFO: No API port configured. API disabled."); return 0; } pthread_create(&pthread_datum_api_thread, NULL, datum_api_thread, NULL); return 0; } datum_gateway-0.4.1beta/src/datum_api.h000066400000000000000000000040371512710151500201100ustar00rootroot00000000000000/* * * DATUM Gateway * Decentralized Alternative Templates for Universal Mining * * This file is part of OCEAN's Bitcoin mining decentralization * project, DATUM. * * https://ocean.xyz * * --- * * Copyright (c) 2024 Bitcoin Ocean, LLC & Jason Hughes * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ #ifndef _DATUM_API_H_ #define _DATUM_API_H_ #include "datum_stratum.h" typedef struct { int STRATUM_ACTIVE_THREADS; int STRATUM_TOTAL_CONNECTIONS; int STRATUM_TOTAL_SUBSCRIPTIONS; double STRATUM_HASHRATE_ESTIMATE; T_DATUM_STRATUM_JOB *sjob; } T_DATUM_API_DASH_VARS; typedef void (*DATUM_API_VarFunc)(char *buffer, size_t buffer_size, const T_DATUM_API_DASH_VARS *vardata); typedef size_t (*DATUM_API_VarFillFunc)(const char *var_start, size_t var_name_len, char *buffer, size_t buffer_size, const T_DATUM_API_DASH_VARS *vardata); typedef struct { const char *var_name; DATUM_API_VarFunc func; } DATUM_API_VarEntry; int datum_api_init(void); size_t strncpy_html_escape(char *dest, const char *src, size_t n); #endif datum_gateway-0.4.1beta/src/datum_blocktemplates.c000066400000000000000000000465471512710151500223570ustar00rootroot00000000000000/* * * DATUM Gateway * Decentralized Alternative Templates for Universal Mining * * This file is part of OCEAN's Bitcoin mining decentralization * project, DATUM. * * https://ocean.xyz * * --- * * Copyright (c) 2024-2025 Bitcoin Ocean, LLC & Jason Hughes * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "datum_gateway.h" #include "datum_jsonrpc.h" #include "datum_utils.h" #include "datum_blocktemplates.h" #include "datum_conf.h" #include "datum_stratum.h" volatile sig_atomic_t new_notify = 0; atomic_int new_notify_threadsafe = 0; atomic_int notify_othercause = 0; static pthread_mutex_t new_notify_lock = PTHREAD_MUTEX_INITIALIZER; volatile char new_notify_blockhash[256] = { 0 }; volatile int new_notify_height = 0; void datum_blocktemplates_notifynew_sighandler() { new_notify = 1; } void datum_blocktemplates_notifynew(const char * const prevhash, const int height) { if (prevhash && *prevhash) pthread_mutex_lock(&new_notify_lock); new_notify_threadsafe = 1; if (prevhash) { if (prevhash[0] > 0) { strncpy((char *)new_notify_blockhash, prevhash, 66); if (height > new_notify_height) { new_notify_height = height; } pthread_mutex_unlock(&new_notify_lock); } } } void datum_blocktemplates_notify_othercause() { notify_othercause = 1; } T_DATUM_TEMPLATE_DATA *template_data = NULL; int next_template_index = 0; const char *datum_blocktemplates_error = NULL; int datum_template_init(void) { char *temp = NULL, *ptr = NULL; int i,j; template_data = (T_DATUM_TEMPLATE_DATA *)calloc(sizeof(T_DATUM_TEMPLATE_DATA),MAX_TEMPLATES_IN_MEMORY+1); if (!template_data) { DLOG_FATAL("Could not allocate RAM for in-memory template data. :( (1)"); return -1; } // TODO: Be smarter about dependent RAM data and size // we're storing both binary and ascii hex versions of all txns for both processing and submitblock speedups j = (sizeof(T_DATUM_TEMPLATE_TXN)*16384) + (MAX_BLOCK_SIZE_BYTES*3) + 2000000; temp = calloc(j, MAX_TEMPLATES_IN_MEMORY); if (!temp) { DLOG_FATAL("ERROR: Could not allocate RAM for in-memory template data. :( (2)"); return -2; } ptr = temp; for(i=0;icoinbasevalue = 0; p->txn_count = 0; p->txn_total_size = 0; p->txn_data_offset = 0; p->txn_total_weight = 0; p->txn_total_sigops = 0; p->txns = p->local_data; } T_DATUM_TEMPLATE_DATA *get_next_template_ptr(void) { T_DATUM_TEMPLATE_DATA *p; if (!template_data) return NULL; p = &template_data[next_template_index]; datum_template_clear(p); next_template_index++; if (next_template_index >= MAX_TEMPLATES_IN_MEMORY) { next_template_index = 0; } return p; } T_DATUM_TEMPLATE_DATA *datum_gbt_parser(json_t *gbt) { T_DATUM_TEMPLATE_DATA *tdata; const char *s; int i,j; json_t *tx_array; tdata = get_next_template_ptr(); if (!tdata) { DLOG_ERROR("Could not get a template pointer."); return NULL; } tdata->height = json_integer_value(json_object_get(gbt, "height")); if (!tdata->height) { DLOG_ERROR("Missing data from GBT JSON (height)"); return NULL; } tdata->coinbasevalue = json_integer_value(json_object_get(gbt, "coinbasevalue")); if (!tdata->coinbasevalue) { DLOG_ERROR("Missing data from GBT JSON (coinbasevalue)"); return NULL; } tdata->mintime = json_integer_value(json_object_get(gbt, "mintime")); if (!tdata->mintime) { DLOG_ERROR("Missing data from GBT JSON (mintime)"); return NULL; } tdata->sigoplimit = json_integer_value(json_object_get(gbt, "sigoplimit")); if (!tdata->sigoplimit) { DLOG_ERROR("Missing data from GBT JSON (sigoplimit)"); return NULL; } tdata->curtime = json_integer_value(json_object_get(gbt, "curtime")); if (!tdata->curtime) { DLOG_ERROR("Missing data from GBT JSON (curtime)"); return NULL; } tdata->sizelimit = json_integer_value(json_object_get(gbt, "sizelimit")); if (!tdata->sizelimit) { DLOG_ERROR("Missing data from GBT JSON (sizelimit)"); return NULL; } tdata->weightlimit = json_integer_value(json_object_get(gbt, "weightlimit")); if (!tdata->weightlimit) { DLOG_ERROR("Missing data from GBT JSON (weightlimit)"); return NULL; } tdata->version = json_integer_value(json_object_get(gbt, "version")); if (!tdata->version) { DLOG_ERROR("Missing data from GBT JSON (version)"); return NULL; } s = json_string_value(json_object_get(gbt, "bits")); if (!s) { DLOG_ERROR("Missing data from GBT JSON (bits)"); return NULL; } if (strlen(s) != 8) { DLOG_ERROR("Wrong bits length from GBT JSON"); return NULL; } strcpy(tdata->bits, s); s = json_string_value(json_object_get(gbt, "previousblockhash")); if (!s) { DLOG_ERROR("Missing data from GBT JSON (previousblockhash)"); return NULL; } strncpy(tdata->previousblockhash, s, 71); s = json_string_value(json_object_get(gbt, "target")); if (!s) { DLOG_ERROR("Missing data from GBT JSON (target)"); return NULL; } strncpy(tdata->block_target_hex, s, 71); s = json_string_value(json_object_get(gbt, "default_witness_commitment")); if (!s) { DLOG_ERROR("Missing data from GBT JSON (default_witness_commitment)"); return NULL; } strncpy(tdata->default_witness_commitment, s, 95); // "20000000", "192e17d5", "66256be5" // version, bits, time // 192e17d5 // gbt format matches stratum for bits // stash useful binary versions of prevblockhash and nbits for(i=0;i<64;i+=2) { tdata->previousblockhash_bin[31-(i>>1)] = hex2bin_uchar(&tdata->previousblockhash[i]); } for(i=0;i<4;i++) { tdata->bits_bin[3-i] = hex2bin_uchar(&tdata->bits[i<<1]); } tdata->bits_uint = upk_u32le(tdata->bits_bin, 0); nbits_to_target(tdata->bits_uint, tdata->block_target); // store binary default witness commitment j = strlen(tdata->default_witness_commitment); for(i=0;idefault_witness_commitment_bin[(i>>1)] = hex2bin_uchar(&tdata->default_witness_commitment[i]); } // Get the txns tx_array = json_object_get(gbt, "transactions"); if (!json_is_array(tx_array)) { DLOG_ERROR("Missing data from GBT JSON (transactions)"); return NULL; } tdata->txn_count = json_array_size(tx_array); tdata->txn_data_offset = sizeof(T_DATUM_TEMPLATE_TXN)*tdata->txn_count; if (tdata->txn_count > 0) { if (tdata->txn_count > 16383) { DLOG_WARN("DATUM Gateway does not support blocks with more than 16383 transactions! %d txns in template. Truncating template to 16383 transactions.", (int)tdata->txn_count); tdata->txn_count = 16383; } for(i=0;itxn_count;i++) { json_t *tx = json_array_get(tx_array, i); if (!tx) { DLOG_ERROR("transaction %d not found!", i); return NULL; } if (!json_is_object(tx)) { DLOG_ERROR("transaction %d is not an object!", i); return NULL; } // index (1 based, like GBT depends) tdata->txns[i].index_raw = i+1; // txid s = json_string_value(json_object_get(tx, "txid")); if (!s) { DLOG_ERROR("Missing data from GBT JSON transactions[%d] (txid)",i); return NULL; } strcpy(tdata->txns[i].txid_hex, s); hex_to_bin_le(tdata->txns[i].txid_hex, tdata->txns[i].txid_bin); // hash s = json_string_value(json_object_get(tx, "hash")); if (!s) { DLOG_ERROR("Missing data from GBT JSON transactions[%d] (hash)",i); return NULL; } strcpy(tdata->txns[i].hash_hex, s); hex_to_bin_le(tdata->txns[i].hash_hex, tdata->txns[i].hash_bin); // fee tdata->txns[i].fee_sats = json_integer_value(json_object_get(tx, "fee")); // sigops tdata->txns[i].sigops = json_integer_value(json_object_get(tx, "sigops")); // weight tdata->txns[i].weight = json_integer_value(json_object_get(tx, "weight")); // data s = json_string_value(json_object_get(tx, "data")); if (!s) { DLOG_ERROR("Missing data from GBT JSON transactions[%d] (data)",i); return NULL; } // size tdata->txns[i].size = strlen(s)>>1; // raw txn data tdata->txns[i].txn_data_binary = &((uint8_t *)tdata->local_data)[tdata->txn_data_offset]; tdata->txn_data_offset += tdata->txns[i].size+1; tdata->txns[i].txn_data_hex = &((char *)tdata->local_data)[tdata->txn_data_offset]; tdata->txn_data_offset += (tdata->txns[i].size*2)+2; if (tdata->txn_data_offset >= tdata->local_data_size) { DLOG_ERROR("Exceeded template local size with txn data!"); return NULL; } strcpy(tdata->txns[i].txn_data_hex, s); hex_to_bin(s, tdata->txns[i].txn_data_binary); // tallies tdata->txn_total_weight+=tdata->txns[i].weight; tdata->txn_total_size+=tdata->txns[i].size; tdata->txn_total_sigops+=tdata->txns[i].sigops; } } return tdata; } void *datum_gateway_fallback_notifier(void *args) { CURL *tcurl = NULL; char req[512]; char p1[72]; p1[0] = 0; json_t *gbbh, *res_val; const char *s; tcurl = curl_easy_init(); if (!tcurl) { DLOG_FATAL("Could not initialize cURL"); panic_from_thread(__LINE__); } DLOG_DEBUG("Fallback notifier thread ready."); while(1) { snprintf(req, sizeof(req), "{\"jsonrpc\":\"1.0\",\"id\":\"%"PRIu64"\",\"method\":\"getbestblockhash\",\"params\":[]}", current_time_millis()); gbbh = bitcoind_json_rpc_call(tcurl, &datum_config, req); if (gbbh) { res_val = json_object_get(gbbh, "result"); if (!res_val) { DLOG_ERROR("ERROR: Could not decode getbestblockhash result!"); } else { s = json_string_value(res_val); if (s) { if (strlen(s) == 64) { if (p1[0] == 0) { strncpy(p1,s,70); } else { if (strcmp(s, p1) != 0) { // new block?!?!?! datum_blocktemplates_notifynew(s,0); strncpy(p1,s,70); } } } } } json_decref(gbbh); gbbh = NULL; } sleep(1); } } void *datum_gateway_template_thread(void *args) { CURL *tcurl = NULL; json_t *gbt = NULL, *res_val; uint64_t i = 0; char gbt_req[1024]; int j; T_DATUM_TEMPLATE_DATA *t; bool was_notified = false; int wnc = 0; uint64_t last_block_change = 0; pthread_t pthread_datum_gateway_fallback_notifier; tcurl = curl_easy_init(); if (!tcurl) { DLOG_FATAL("Could not initialize cURL"); panic_from_thread(__LINE__); } if (datum_template_init() < 1) { DLOG_FATAL("Couldn't setup template processor."); panic_from_thread(__LINE__); } { unsigned char dummy[64]; if (!addr_2_output_script(datum_config.mining_pool_address, &dummy[0], 64)) { if (datum_config.api_modify_conf) { DLOG_ERROR("Could not generate output script for pool addr! Perhaps invalid? Configure via API/dashboard."); } else { DLOG_FATAL("Could not generate output script for pool addr! Perhaps invalid? This is bad."); panic_from_thread(__LINE__); } } while (!addr_2_output_script(datum_config.mining_pool_address, &dummy[0], 64)) { usleep(50000); } } if (datum_config.bitcoind_notify_fallback) { // start getbestblockhash poller thread as a backup for notifications DLOG_DEBUG("Starting fallback block notifier"); pthread_create(&pthread_datum_gateway_fallback_notifier, NULL, datum_gateway_fallback_notifier, NULL); } DLOG_DEBUG("Template fetcher thread ready."); char p1[72]; p1[0] = 0; while(1) { i++; // fetch latest template snprintf(gbt_req, sizeof(gbt_req), "{\"method\":\"getblocktemplate\",\"params\":[{\"rules\":[\"segwit\"]}],\"id\":%"PRIu64"}",(uint64_t)((uint64_t)time(NULL)<<(uint64_t)8)|(uint64_t)(i&255)); gbt = bitcoind_json_rpc_call(tcurl, &datum_config, gbt_req); if (!gbt) { datum_blocktemplates_error = "Could not fetch new template!"; DLOG_ERROR("Could not fetch new template from %s!", datum_config.bitcoind_rpcurl); sleep(1); continue; } else { res_val = json_object_get(gbt, "result"); if (!res_val) { datum_blocktemplates_error = "Could not decode GBT result!"; DLOG_ERROR("%s", datum_blocktemplates_error); } else { DLOG_DEBUG("DEBUG: calling datum_gbt_parser (new=%d)", was_notified?1:0); t = datum_gbt_parser(res_val); if (t) { datum_blocktemplates_error = NULL; DLOG_DEBUG("height: %lu / value: %"PRIu64, (unsigned long)t->height, t->coinbasevalue); DLOG_DEBUG("--- prevhash: %s", t->previousblockhash); DLOG_DEBUG("--- txn_count: %u / sigops: %u / weight: %u / size: %u", t->txn_count, t->txn_total_sigops, t->txn_total_weight, t->txn_total_size); // If the previous block hash changed, or work is no longer valid, we should push clean work const bool new_block = strcmp(t->previousblockhash, p1); if (new_block || notify_othercause) { notify_othercause = 0; update_stratum_job(t,true,JOB_STATE_EMPTY_PLUS); if (new_block) { last_block_change = current_time_millis(); strcpy(p1, t->previousblockhash); was_notified = false; DLOG_INFO("NEW NETWORK BLOCK: %s (%lu)", t->previousblockhash, (unsigned long)t->height); } else { DLOG_DEBUG("Urgent work update triggered"); } // sleep for a milisecond // this will let other threads churn for a moment. we wont get all the empty jobs blasted out in a milisecond anyway usleep(1000); // wait for the empties to complete DLOG_DEBUG("Waiting on empty work send completion..."); for(j=0;j<4000;j++) { if (stratum_latest_empty_check_ready_for_full()) break; usleep(1001); } DLOG_DEBUG("Empty sends done!"); // use this template to setup for a coinbaser wait job while the empty + full w/blank jobs are blasted // then this job will get blasted when its ready. i = datum_stratum_v1_global_subscriber_count(); DLOG_INFO("Updating priority stratum job for block %lu: %.8f BTC, %lu txns, %lu bytes (Sent to %llu stratum client%s)", (unsigned long)t->height, (double)t->coinbasevalue / (double)100000000.0, (unsigned long)t->txn_count, (unsigned long)t->txn_total_size, (unsigned long long)i, (i!=1)?"s":""); update_stratum_job(t,false,JOB_STATE_FULL_PRIORITY_WAIT_COINBASER); } else { if (was_notified) { // we got a notification of a new block, but there doesn't seem to actually be a new block. // we should quickly check again instead of actually updating the stratum job pthread_mutex_lock(&new_notify_lock); if ((new_notify_blockhash[0] > 0) && (!strcmp(t->previousblockhash,(char *)new_notify_blockhash))) { // we got notified for work we already knew about if (new_notify_height <= 0) { was_notified = false; wnc = 0; } else { if (new_notify_height == t->height) { was_notified = false; wnc = 0; } } } if (!was_notified) { DLOG_DEBUG("Multi notified for block we knew details about. (%s)", new_notify_blockhash); } else { DLOG_DEBUG("Notified, however new = %s, t->previousblockhash = %s, t->height = %lu, new_notify_height = %d", new_notify_blockhash, t->previousblockhash, (unsigned long)t->height, new_notify_height); // Sometimes we call GBT before we get the signal from a blocknotify. It's a bit of a race condition. // Instead of freaking out, we'll just ignore things when we get a signal that results in the same block if it was // within 2.5s of a previous block change. // absolute worst case scenario here is that there's a reverse race condition of some kind where we get our notify early and GBT is still // returning the old block data... then we'd be one work change delay behind things. // that shouldn't be possible, though, if the notify comes from the same bitcoind that we're getting our templates from if ((current_time_millis()-2500) < last_block_change) { DLOG_DEBUG("This is probably a duplicate signal, since we just changed blocks less than 2.5s ago"); was_notified = false; } if (((t->height < 800000) || (t->height > 2980000)) && (new_notify_blockhash[0] == 'T')) { // some hardcoded guardrails that should last for quite some time for testnet3 and testnet4 DLOG_DEBUG("DEBUG: TESTNET FAST FORWARD HACK!!!"); // set diff 1 strcpy(t->bits, "1d00ffff"); for(j=0;j<4;j++) { t->bits_bin[3-j] = hex2bin_uchar(&t->bits[j<<1]); } t->bits_uint = upk_u32le(t->bits_bin, 0); nbits_to_target(t->bits_uint, t->block_target); // ff 20 min if (new_notify_height > t->curtime) { t->curtime = new_notify_height; new_notify_height = -1; } else { t->curtime += 1200; } DLOG_DEBUG("t->curtime = %llu", (unsigned long long)t->curtime); update_stratum_job(t,true,JOB_STATE_FULL_PRIORITY_WAIT_COINBASER); new_notify_blockhash[0] = 0; was_notified = false; } } pthread_mutex_unlock(&new_notify_lock); } else { i = datum_stratum_v1_global_subscriber_count(); DLOG_INFO("Updating standard stratum job for block %lu: %.8f BTC, %lu txns, %lu bytes (Sent to %llu stratum client%s)", (unsigned long)t->height, (double)t->coinbasevalue / (double)100000000.0, (unsigned long)t->txn_count, (unsigned long)t->txn_total_size, (unsigned long long)i, (i!=1)?"s":""); update_stratum_job(t,false,JOB_STATE_FULL_NORMAL_WAIT_COINBASER); } } } } json_decref(gbt); } gbt = NULL; if ((!was_notified) || (new_notify || new_notify_threadsafe)) { for(i=0;i<(((uint64_t)datum_config.bitcoind_work_update_seconds*(uint64_t)1000000)/(uint64_t)2500);i++) { usleep(2500); if (new_notify || new_notify_threadsafe) { new_notify = 0; new_notify_threadsafe = 0; was_notified = 1; wnc = 0; DLOG_INFO("NEW NETWORK BLOCK NOTIFICATION RECEIVED"); break; } } } else { usleep(250000); wnc++; if (wnc > 16) { // 4 seconds // something is weird. DLOG_WARN("We received a new block notification, however after 16 attempts we did not see a new block."); was_notified = false; wnc = 0; } } } // this thread is never intended to exit unless the application dies // TODO: Clean things up } datum_gateway-0.4.1beta/src/datum_blocktemplates.h000066400000000000000000000137341512710151500223540ustar00rootroot00000000000000/* * * DATUM Gateway * Decentralized Alternative Templates for Universal Mining * * This file is part of OCEAN's Bitcoin mining decentralization * project, DATUM. * * https://ocean.xyz * * --- * * Copyright (c) 2024 Bitcoin Ocean, LLC & Jason Hughes * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ #ifndef _DATUM_BLOCKTEMPLATE_H_ #define _DATUM_BLOCKTEMPLATE_H_ #ifndef uint64_t #include #endif #ifndef json_t #include #endif #ifndef bool #include #endif #include "datum_gateway.h" #define MAX_TEMPLATES_IN_MEMORY 32 // 32*30 seconds = 16 minutes of work remembered // consensus rules: // --- max sigops = 80000 // --- max size = 4000000 // --- max weight = 4000000 #define MAX_BLOCK_SIZE_BYTES 4000000 // Assumption notes // max possible transactions = 16394-ish .. close enough to say 16384, since we're just not going to be idiots // max non-segwit data = 1000000 // Maximum possible inputs spent in a block = ~24400 // 80 byte block header // 1 byte txcount // coinbase txn ~150 bytes // --- one giant transaction // 4 bytes version // 3 byte input count (up to 65535) // INPUT: 36 byte outpoint, 1 byte scriptsiglen (0), no scriptsig (segwit), 4 byte sequence // // 1 byte output count // OUTPUT: 8 byte value, 1 byte scriptlen, 22 bytes script // 4 bytes lock time // txn wo/inputs = 4+3+1+8+1+22+4 = 43 bytes // block with coinbase + txn wo/inputs = 80+1+150+43 = 274 bytes // remaining bytes (1000000-274) / input size (41) = ~24383 // Maximum possible transactions in a block = ~16400 // 80 byte block header // 1 byte txcount // coinbase txn ~150 bytes // --- one giant transaction // 4 bytes version // 1 byte input count (1) // INPUT: 36 byte outpoint, 1 byte scriptsiglen (0), no scriptsig (segwit/OP_TRUE), 4 byte sequence // 1 byte output count // OUTPUT: 8 byte value, 1 byte scriptlen, OP_TRUE (1 byte) // 4 bytes lock time // txn size = 4+1+36+1+4+1+8+1+1+4 = 61 bytes // remaining bytes (1000000-80-1-150) / 61 = ~16390 // maximum possible UTXOs in a block = ~100000 // 80 byte block header // 1 byte txcount // coinbase txn ~150 bytes // --- one giant transaction // 4 bytes version // 1 byte input count (1) // INPUT: 36 byte outpoint, 1 byte scriptsiglen (0), no scriptsig (segwit/OP_TRUE), 4 byte sequence // 3 byte output count // OUTPUT: 8 byte value, 1 byte scriptlen, OP_TRUE (1 byte) // 4 bytes lock time // txn size wo/outputs = 4+1+36+1+4+2+4 = 52 bytes // remaining bytes (1000000-80-1-150-52) / 10 = ~99972 // txn struct = ~264 bytes typedef struct T_DATUM_TEMPLATE_TXN { // *1-based* index of this transaction in the original GBT call // max txns in a block is about 16,400 (see notes above) uint16_t index_raw; // transaction ID in ASCII hex + calc'd NBO, as provided by GBT char txid_hex[72]; // big endian hex uint8_t txid_bin[32]; // little endian binary // "hash" (segwit) ID in ASCII hex + calc'd NBO, as provided by GBT char hash_hex[72]; // big endian hex uint8_t hash_bin[32]; // little endian binary // size of the transaction in bytes uint32_t size; // "weight" of the transaction // vBytes = weight>>2 uint32_t weight; // transaction fee paid, in sats uint64_t fee_sats; // signature operations uint32_t sigops; // binary of the raw transaction data, as provided by GBT // --- this should point to data allocated as a chunk in the base template uint8_t *txn_data_binary; char *txn_data_hex; // Info on dependancies returned by GBT // Who do I depend on? // if depends_on_count > 0, then this txn relies on some other txn in the GBT // it's possible those txns have other txns they depend on as well //uint16_t depends_on_count; //uint16_t *depends_on_list; } T_DATUM_TEMPLATE_TXN; typedef struct { uint16_t local_index; // tie to stratum work uint64_t coinbasevalue; // uint64_t mintime; // uint64_t curtime; // uint64_t sizelimit; // uint64_t weightlimit; // uint32_t height; // uint32_t version; // uint32_t sigoplimit; // char bits[9]; // char dummy[7]; // unused, possibly for alignment uint8_t bits_bin[4]; // uint32_t bits_uint; // char previousblockhash[72]; // uint8_t previousblockhash_bin[32]; // char default_witness_commitment[96]; // uint8_t default_witness_commitment_bin[48]; // char block_target_hex[72]; // uint8_t block_target[32]; // calculated from bits uint32_t txn_count; uint32_t txn_total_weight; uint32_t txn_total_size; uint32_t txn_total_sigops; T_DATUM_TEMPLATE_TXN *txns; uint32_t txn_data_offset; // Pointer to allocated data for this particular template copy void *local_data; uint32_t local_data_size; } T_DATUM_TEMPLATE_DATA; extern const char *datum_blocktemplates_error; int datum_template_init(void); T_DATUM_TEMPLATE_DATA *datum_gbt_parser(json_t *gbt); void *datum_gateway_template_thread(void *args); void datum_blocktemplates_notifynew_sighandler(); void datum_blocktemplates_notifynew(const char *prevhash, int height); void datum_blocktemplates_notify_othercause(); #endif datum_gateway-0.4.1beta/src/datum_coinbaser.c000066400000000000000000001055711512710151500213040ustar00rootroot00000000000000/* * * DATUM Gateway * Decentralized Alternative Templates for Universal Mining * * This file is part of OCEAN's Bitcoin mining decentralization * project, DATUM. * * https://ocean.xyz * * --- * * Copyright (c) 2024-2025 Bitcoin Ocean, LLC & Jason Hughes * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ // TODO: Empty work speedup work and an actual empty template will cause duplicate work. // It's kind of unlikely that a sane miner would be purposefully providing empty templates, but // this is a low priority bug to address nonetheless. #include #include #include #include #include #include #include #include #include "datum_conf.h" #include "datum_utils.h" #include "datum_stratum.h" #include "datum_jsonrpc.h" #include "datum_protocol.h" #include "datum_coinbaser.h" CURL *coinbaser_curl = NULL; const char *cbstart_hex = "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff"; // 82 len hex, 41 bytes #define MAX_COINBASE_TAG_SPACE 86 // leaves space for BIP34 height, extranonces, datum prime tag, etc. int generate_coinbase_input(int height, char *cb, int *target_pot_index) { int cb_input_sz = 0; int tag_len[2] = { 0, 0 }; int k, m, i; int excess; bool datum_active = false; // let's figure out our coinbase tags w/BIP34 height i = append_UNum_hex(height, &cb[0]); cb_input_sz += i>>1; datum_active = datum_protocol_is_active(); // Handle coinbase tagging // The first push after the height should be: // PUSHBYTES X, Primary tag, 0x0F, Secondary tag, 0x0F, Tertiary tag, 0x00 // We should then push a unique entropy tag (push + 2 bytes = 3 bytes) if (!datum_active) { tag_len[0] = strlen(datum_config.mining_coinbase_tag_primary); } else { tag_len[0] = strlen(datum_config.override_mining_coinbase_tag_primary); } tag_len[1] = strlen(datum_config.mining_coinbase_tag_secondary); k = tag_len[0] + tag_len[1] + 2; if (!tag_len[1]) { k--; if (!tag_len[0]) { k--; } } if (k > MAX_COINBASE_TAG_SPACE) { // something still needs truncating excess = k - MAX_COINBASE_TAG_SPACE; if (tag_len[1] > excess) { // truncating tag1 is enough to cover us tag_len[1] -= excess; k = MAX_COINBASE_TAG_SPACE; } else { // not enough, so need to remove this tag entirely if (tag_len[1]) { tag_len[1] = 0; k-=tag_len[1]+1; } } } if (k > MAX_COINBASE_TAG_SPACE) { // one tag should never exceed 64 bytes, so we're going to panic here. DLOG_FATAL("Could not fit coinbase primary tag alone somehow. This is probably a bug. Panicking. :("); panic_from_thread(__LINE__); sleep(1000000); } if (k > 0) { // ok, we have one or more coinbase tags with a total len of k if (k <= 75) { // OP_PUSHBYTES (1 byte, 1 to 75) uchar_to_hex(&cb[i], (unsigned char)k); i+=2; cb_input_sz++; } else { // OP_PUSHBYTES (2 byte, 76 to 94) uchar_to_hex(&cb[i], 0x4C); i+=2; cb_input_sz++; uchar_to_hex(&cb[i], (unsigned char)k); i+=2; cb_input_sz++; } if (tag_len[0]) { if (datum_active) { for(m=0;m>8)&0xFF)); i+=2; cb_input_sz++; } else { uchar_to_hex(&cb[i], 0x07); i+=2; cb_input_sz++; if (target_pot_index != NULL) *target_pot_index = cb_input_sz; uchar_to_hex(&cb[i], 0xFF); i+=2; cb_input_sz++; // placeholder for PoT target uchar_to_hex(&cb[i], (datum_config.coinbase_unique_id&0xFF)); i+=2; cb_input_sz++; uchar_to_hex(&cb[i], ((datum_config.coinbase_unique_id>>8)&0xFF)); i+=2; cb_input_sz++; uchar_to_hex(&cb[i], (datum_config.prime_id&0xFF)); i+=2; cb_input_sz++; uchar_to_hex(&cb[i], ((datum_config.prime_id>>8)&0xFF)); i+=2; cb_input_sz++; uchar_to_hex(&cb[i], ((datum_config.prime_id>>16)&0xFF)); i+=2; cb_input_sz++; uchar_to_hex(&cb[i], ((datum_config.prime_id>>24)&0xFF)); i+=2; cb_input_sz++; } return cb_input_sz; } void generate_coinbase_txns_for_stratum_job_subtypebysize(T_DATUM_STRATUM_JOB *s, int coinbase_index, int remaining_size, bool space_for_en_in_coinbase, int *cb1idx, int *cb2idx, bool special_coinb1) { // This function finishes off the stratum coinb1+coinb2 using the available outputs in the job and other flags specified. // it does not attempt to maximize coinb1's size to any specific size int i, j, k, m, i2 = 0, c1cnt = 0; uint64_t mval = 0; bool c1full = false; bool en_done = false; // chicken and egg problem. we need to know the output count before we can close off coinb1 if !space_for_en_in_coinbase // either way, we want to start out coinb2 with outputs i = remaining_size; j = remaining_size; if (special_coinb1) { i2 = (300 - cb1idx[coinbase_index])>>1; if (i2 < 0) i2 = 0; space_for_en_in_coinbase = false; } m = 0; mval = 0; // technically an output script could be > 0x4B, meaning an extra byte would be eaten here... but that's not currently the standard // this needs to match the loop lower in this function, as the count will get thrown off if it does not. // TODO: Enforce max sigops! Note: This is not currently enforced in eloipool, either, so punting for now and will monitor network stats to determine priority. for(k=0;kavailable_coinbase_outputs_count;k++) { if (((s->available_coinbase_outputs[k].output_script_len+9) <= i) && ((mval + s->available_coinbase_outputs[k].value_sats) <= s->coinbase_value)) { if ((special_coinb1) && (!c1full) && ((s->available_coinbase_outputs[k].output_script_len+9) <= i2)) { i2 -= (s->available_coinbase_outputs[k].output_script_len+9); c1cnt++; } else { c1full = true; } i -= (s->available_coinbase_outputs[k].output_script_len+9); m++; mval += s->available_coinbase_outputs[k].value_sats; if (i < 30) break; if (mval >= s->coinbase_value) break; } } // "m" outputs fit if (space_for_en_in_coinbase) { // we'll start the empty coinb2 with the "sequence" m+=2; // pool addr + witness pk_u64le(s->coinbase[coinbase_index].coinb2, cb2idx[coinbase_index], 0x6666666666666666ULL); // "ffffffff" cb2idx[coinbase_index] = 8; cb2idx[coinbase_index] += append_bitcoin_varint_hex(m, &s->coinbase[coinbase_index].coinb2[cb2idx[coinbase_index]]); // us, witness, and "m" outputs } else { m+=3; cb1idx[coinbase_index] += append_bitcoin_varint_hex(m, &s->coinbase[coinbase_index].coinb1[cb1idx[coinbase_index]]); // extranonce, us, witness commit, and "m" outputs if (!special_coinb1) { // append extranonce op_return cb1idx[coinbase_index] += sprintf(&s->coinbase[coinbase_index].coinb1[cb1idx[coinbase_index]], "0000000000000000106a0e%04" PRIx16, s->enprefix); en_done = true; } } // append "m" payouts. find them the same way we did before mval = 0; for(k=0;kavailable_coinbase_outputs_count;k++) { if (((s->available_coinbase_outputs[k].output_script_len+9) <= j) && ((mval + s->available_coinbase_outputs[k].value_sats) <= s->coinbase_value)) { j -= (s->available_coinbase_outputs[k].output_script_len+9); m--; mval += s->available_coinbase_outputs[k].value_sats; if ((special_coinb1) && (k < c1cnt)) { // put in coinb1 cb1idx[coinbase_index] += sprintf(&s->coinbase[coinbase_index].coinb1[cb1idx[coinbase_index]], "%016llx", (unsigned long long)__builtin_bswap64(s->available_coinbase_outputs[k].value_sats)); // TODO: Profile a faster way to do this cb1idx[coinbase_index] += append_bitcoin_varint_hex(s->available_coinbase_outputs[k].output_script_len, &s->coinbase[coinbase_index].coinb1[cb1idx[coinbase_index]]); // Append script length for(i=0;iavailable_coinbase_outputs[k].output_script_len;i++) { uchar_to_hex(&s->coinbase[coinbase_index].coinb1[cb1idx[coinbase_index]], s->available_coinbase_outputs[k].output_script[i]); cb1idx[coinbase_index]+=2; } } else { if ((special_coinb1) && (k == c1cnt)) { // append extranonce op_return cb1idx[coinbase_index] += sprintf(&s->coinbase[coinbase_index].coinb1[cb1idx[coinbase_index]], "0000000000000000106a0e%04" PRIx16, s->enprefix); en_done = true; } // put in coinb2 cb2idx[coinbase_index] += sprintf(&s->coinbase[coinbase_index].coinb2[cb2idx[coinbase_index]], "%016llx", (unsigned long long)__builtin_bswap64(s->available_coinbase_outputs[k].value_sats)); // TODO: Profile a faster way to do this cb2idx[coinbase_index] += append_bitcoin_varint_hex(s->available_coinbase_outputs[k].output_script_len, &s->coinbase[coinbase_index].coinb2[cb2idx[coinbase_index]]); // Append script length for(i=0;iavailable_coinbase_outputs[k].output_script_len;i++) { uchar_to_hex(&s->coinbase[coinbase_index].coinb2[cb2idx[coinbase_index]], s->available_coinbase_outputs[k].output_script[i]); cb2idx[coinbase_index]+=2; } } if (!m) break; if (j < 30) break; if (mval >= s->coinbase_value) break; } } // this should never happen, but... if (mval > s->coinbase_value) { DLOG_ERROR("Attempting to pay more than we have available in the generation txn! --- %"PRIu64" sats available, %"PRIu64" sats to miners", s->coinbase_value, mval); } if ((!space_for_en_in_coinbase) && (!en_done)) { cb1idx[coinbase_index] += sprintf(&s->coinbase[coinbase_index].coinb1[cb1idx[coinbase_index]], "0000000000000000106a0e%04" PRIx16, s->enprefix); en_done = true; } if (s->coinbase_value > mval) { // append our payout output value and script, since there are leftover funds cb2idx[coinbase_index] += sprintf(&s->coinbase[coinbase_index].coinb2[cb2idx[coinbase_index]], "%016llx", (unsigned long long)__builtin_bswap64(s->coinbase_value - mval)); // TODO: Profile a faster way to do this cb2idx[coinbase_index] += append_bitcoin_varint_hex(s->pool_addr_script_len, &s->coinbase[coinbase_index].coinb2[cb2idx[coinbase_index]]); // Append script length for(i=0;ipool_addr_script_len;i++) { uchar_to_hex(&s->coinbase[coinbase_index].coinb2[cb2idx[coinbase_index]], s->pool_addr_script[i]); cb2idx[coinbase_index]+=2; } } else { // We paid every sat of the coinbase to miners... and saved an output. // HOWEVER....... we already locked in a number of outputs that presumes we would have a pool output // so tack on a dead output, sadly. // TODO: Make code smarter above, don't waste an output if we don't need it. // This is quite unlikely in practice, but, just in case let's make this a prunable OP_RETURN cb2idx[coinbase_index] += sprintf(&s->coinbase[coinbase_index].coinb2[cb2idx[coinbase_index]], "0000000000000000036a0100"); // TODO: Is a naked OP_RETURN without any bytes after safe? Above TODO is probably better than investigating. } // witness commit output costs 46 bytes // append the default_witness_commitment cb2idx[coinbase_index] += sprintf(&s->coinbase[coinbase_index].coinb2[cb2idx[coinbase_index]], "0000000000000000%2.2x%s", (unsigned int)strlen(s->block_template->default_witness_commitment)>>1, s->block_template->default_witness_commitment); // lock time cb2idx[coinbase_index] += sprintf(&s->coinbase[coinbase_index].coinb2[cb2idx[coinbase_index]], "00000000"); } int datum_stratum_coinbase_fit_to_template(int max_sz, int fixed_bytes, T_DATUM_STRATUM_JOB *s) { int j,i,msz1; i = fixed_bytes + max_sz; msz1 = max_sz+fixed_bytes; if ((i+s->block_template->txn_total_size+85+36) > s->block_template->sizelimit) { j = s->block_template->sizelimit - (s->block_template->txn_total_size+85+36) - fixed_bytes; if (j < 0) return 0; msz1 = j; } if (((i<<2)+s->block_template->txn_total_weight+340+36) > s->block_template->weightlimit) { j = ((s->block_template->weightlimit - (s->block_template->txn_total_weight+340+36))>>2) - fixed_bytes; if (j < 0) return 0; msz1 = j; } if (msz1 < 0) { msz1 = 0; } if (msz1 < (max_sz - fixed_bytes)) { return msz1; } else { return max_sz - fixed_bytes; } } void generate_base_coinbase_txns_for_stratum_job(T_DATUM_STRATUM_JOB *s, bool new_block) { char cb[512]; int cb_input_sz = 0; bool space_for_en_in_coinbase = false; int i, j, k; int cb1idx[1] = { 0 }; int cb2idx[1] = { 0 }; int target_pot_index; if (datum_protocol_is_active()) { // DATUM s->pool_addr_script_len = datum_config.override_mining_pool_scriptsig_len; memcpy(&s->pool_addr_script[0], datum_config.override_mining_pool_scriptsig, datum_config.override_mining_pool_scriptsig_len); s->is_datum_job = true; } else { // No pool s->pool_addr_script_len = addr_2_output_script(datum_config.mining_pool_address, &s->pool_addr_script[0], 64); s->is_datum_job = false; } if (!s->pool_addr_script_len) { DLOG_FATAL("Could not generate output script for pool addr! Perhaps invalid? This is bad."); panic_from_thread(__LINE__); } // copy beginning of the generation txn to the appropriate outputs j = strlen(cbstart_hex); memcpy(&s->coinbase[0].coinb1[0], cbstart_hex, j); cb1idx[0] = j; cb_input_sz = generate_coinbase_input(s->height, &cb[0], &target_pot_index); i = cb_input_sz << 1; // null terminate... probably not needed cb[i] = 0; if (cb_input_sz <= 85) { space_for_en_in_coinbase = true; } if (space_for_en_in_coinbase) { cb1idx[0] += append_bitcoin_varint_hex(cb_input_sz+15, &s->coinbase[0].coinb1[cb1idx[0]]); // 15 bytes for extranonce+uid push + data } else { cb1idx[0] += append_bitcoin_varint_hex(cb_input_sz, &s->coinbase[0].coinb1[cb1idx[0]]); } memcpy(&s->coinbase[0].coinb1[cb1idx[0]], &cb[0], cb_input_sz*2); s->target_pot_index = target_pot_index + (cb1idx[0]>>1); // adjust for placement in the txn. always safe for all types, since the varint will always be 1 byte. cb1idx[0] += cb_input_sz*2; if (space_for_en_in_coinbase) { // if we are doing extranonce in the coinbase, then this is ALMOST the end of coinbase1 // we need a PUSH 14 and our enprefix in the coinbase uchar_to_hex(&s->coinbase[0].coinb1[cb1idx[0]], 0x0E); cb1idx[0]+=2; // TODO: Profile a faster way to do this cb1idx[0] += sprintf(&s->coinbase[0].coinb1[cb1idx[0]], "%04" PRIx16, s->enprefix); } else { // if we are not, then we need to append the "sequence" pk_u64le(s->coinbase[0].coinb1, cb1idx[0], 0x6666666666666666ULL); // "ffffffff" cb1idx[0] += 8; } s->coinbase[0].coinb1[cb1idx[0]] = 0; ///////////////////////////// // 0 / EMPTY // empty should be easy. lets start there if (space_for_en_in_coinbase) { // we'll start the empty coinb2 with the "sequence" pk_u64le(s->coinbase[0].coinb2, 0, 0x6666666666666666ULL); // "ffffffff" cb2idx[0] = 8; cb2idx[0] += append_bitcoin_varint_hex(2, &s->coinbase[0].coinb2[cb2idx[0]]); // us and witness commit if (new_block) { // copy the beginning to the subsidy-only memcpy(&s->subsidy_only_coinbase.coinb1[0], &s->coinbase[0].coinb1[0], cb1idx[0]); pk_u64le(s->subsidy_only_coinbase.coinb2, 0, 0x6666666666666666ULL); // "ffffffff" append_bitcoin_varint_hex(1, &s->subsidy_only_coinbase.coinb2[8]); // just us! } } else { // we're already at the point in coinb1 where we need an output count, which will be 3 if (new_block) { j = cb1idx[0]; } cb1idx[0] += append_bitcoin_varint_hex(3, &s->coinbase[0].coinb1[cb1idx[0]]); // extranonce, us, and witness commit // append extranonce op_return cb1idx[0] += sprintf(&s->coinbase[0].coinb1[cb1idx[0]], "0000000000000000106a0e%04" PRIx16, s->enprefix); if (new_block) { // copy the beginning to the subsidy-only memcpy(&s->subsidy_only_coinbase.coinb1[0], &s->coinbase[0].coinb1[0], cb1idx[0]); k = append_bitcoin_varint_hex(2, &s->subsidy_only_coinbase.coinb1[j]); // extranonce and us s->subsidy_only_coinbase.coinb1[j+k] = s->coinbase[0].coinb1[j+k]; } } // finish off "empty" coinbase // append our payout output value and script if (new_block) { j = cb2idx[0]; } cb2idx[0] += sprintf(&s->coinbase[0].coinb2[cb2idx[0]], "%016llx", (unsigned long long)__builtin_bswap64(s->coinbase_value)); // TODO: Profile a faster way to do this cb2idx[0] += append_bitcoin_varint_hex(s->pool_addr_script_len, &s->coinbase[0].coinb2[cb2idx[0]]); // Append script length for(i=0;ipool_addr_script_len;i++) { uchar_to_hex(&s->coinbase[0].coinb2[cb2idx[0]], s->pool_addr_script[i]); cb2idx[0]+=2; } if (new_block) { k = cb2idx[0]; } // witness commit output costs 46 bytes // append the default_witness_commitment cb2idx[0] += sprintf(&s->coinbase[0].coinb2[cb2idx[0]], "0000000000000000%2.2x%s", (unsigned int)strlen(s->block_template->default_witness_commitment)>>1, s->block_template->default_witness_commitment); // lock time cb2idx[0] += sprintf(&s->coinbase[0].coinb2[cb2idx[0]], "00000000"); if (new_block) { // Append the subsidy-only payout to the subsidy_only_coinbase sprintf(&s->subsidy_only_coinbase.coinb2[j], "%016llx", (unsigned long long)__builtin_bswap64(block_reward(s->height))); // subsidy calc for height memcpy(&s->subsidy_only_coinbase.coinb2[j+16], &s->coinbase[0].coinb2[j+16], k-j-16); sprintf(&s->subsidy_only_coinbase.coinb2[k], "00000000"); } // End of 0 / Empty ////////////////////////////// // prep binary versions of the coinbase for speeding up later i = strlen(s->coinbase[0].coinb1); s->coinbase[0].coinb1_len = 0; for(j=0;jcoinbase[0].coinb1_bin[j>>1] = hex2bin_uchar(&s->coinbase[0].coinb1[j]); s->coinbase[0].coinb1_len++; } i = strlen(s->coinbase[0].coinb2); s->coinbase[0].coinb2_len = 0; for(j=0;jcoinbase[0].coinb2_bin[j>>1] = hex2bin_uchar(&s->coinbase[0].coinb2[j]); s->coinbase[0].coinb2_len++; } if (new_block) { i = strlen(s->subsidy_only_coinbase.coinb1); s->subsidy_only_coinbase.coinb1_len = 0; for(j=0;jsubsidy_only_coinbase.coinb1_bin[j>>1] = hex2bin_uchar(&s->subsidy_only_coinbase.coinb1[j]); s->subsidy_only_coinbase.coinb1_len++; } i = strlen(s->subsidy_only_coinbase.coinb2); s->subsidy_only_coinbase.coinb2_len = 0; for(j=0;jsubsidy_only_coinbase.coinb2_bin[j>>1] = hex2bin_uchar(&s->subsidy_only_coinbase.coinb2[j]); s->subsidy_only_coinbase.coinb2_len++; } } } void generate_coinbase_txns_for_stratum_job(T_DATUM_STRATUM_JOB *s, bool empty_only) { // Account for available vsize, sigops, size, weight, etc // Note: // With a minimum payout of 10 TBC, the largest likely coinbase as of height 840000 is around 16 KB if we paid every miner the minimum to a long address type. // This seems highly unlikely. 16KB is more than sufficient. int i, j, k; char cb[300]; int target_pot_index; int cb_input_sz = 0; bool space_for_en_in_coinbase = false; int cb1idx[MAX_COINBASE_TYPES] = { 0,0,0,0,0,0 }; int cb2idx[MAX_COINBASE_TYPES] = { 0,0,0,0,0,0 }; int cb_req_sz[MAX_COINBASE_TYPES] = { 0,0,0,0,0 }; //////////////// // Initial mainnet coinbaser if (datum_protocol_is_active()) { // DATUM s->pool_addr_script_len = datum_config.override_mining_pool_scriptsig_len; memcpy(&s->pool_addr_script[0], datum_config.override_mining_pool_scriptsig, datum_config.override_mining_pool_scriptsig_len); s->is_datum_job = true; if (s->available_coinbase_outputs_count == 0) { empty_only = true; } } else { // No pool s->pool_addr_script_len = addr_2_output_script(datum_config.mining_pool_address, &s->pool_addr_script[0], 64); s->is_datum_job = false; empty_only = true; } if (!s->pool_addr_script_len) { DLOG_FATAL("Could not generate output script for pool addr! Perhaps invalid? This is bad."); panic_from_thread(__LINE__); } // copy beginning of the generation txn to the appropriate outputs j = strlen(cbstart_hex); for(i=0;icoinbase[i].coinb1[0], cbstart_hex, j); cb1idx[i] = j; } cb_input_sz = generate_coinbase_input(s->height, &cb[0], &target_pot_index); s->target_pot_index = target_pot_index; i = cb_input_sz << 1; // null terminate... probably not needed cb[i] = 0; // do we have space in the coinbase for the extranonce for types that can do it this way? // we need 1 byte for the push, 2 for the enprefix, 4 for en1 and 8 for en2 = 15 bytes // coinbase max is 100 if (cb_input_sz <= 85) { space_for_en_in_coinbase = true; } // multiple coinbase options // 0 = "empty" --- just pays pool addr, and possibly TIDES data. extranonce in coinbase if fits, or in first output if not. // 1 = "nicehash" --- roughly 500 bytes total... smaller than antminer... has nothing before the extranonce OP_RETURN (or no extranonce OP_RETURN if enough space in the coinbase) // 2 = "antminer" --- roughly 730 bytes max size, using a larger coinb1 and UART sync bits. This also works as a good default. // 3 = "whatsminer" --- max 6500 bytes tested. does not need the extranonce OP_RETURN unless there's no space in the coinbase itself after tags // 4 = "huge" --- max 16kB --- this is probably the most we should reasonably attempt to do in the coinbase... something like 380 to 530 outputs, depending on the type of output // 5 = "antminer2" --- max 2250 bytes --- latest S21s appear to support this // only type 2 *needs* the OP_RETURN extranonce, unless the coinbase itself is too long // set the len, and copy over the rest of the coinbase for(i=0;icoinbase[i].coinb1[cb1idx[i]]); } else { cb1idx[i] += append_bitcoin_varint_hex(cb_input_sz, &s->coinbase[i].coinb1[cb1idx[i]]); } memcpy(&s->coinbase[i].coinb1[cb1idx[i]], &cb[0], cb_input_sz*2); // save this and adjust for placement in the txn... this is always safe because the coinbase input is always < 0xFD len // little silly to set this multiple times, but it's fine for consistency. s->target_pot_index = target_pot_index + (cb1idx[i]>>1); cb1idx[i] += cb_input_sz*2; if ((i!=2) && (space_for_en_in_coinbase)) { // if we are doing extranonce in the coinbase, then this is ALMOST the end of coinbase1 // we need a PUSH 14 and our enprefix in the coinbase uchar_to_hex(&s->coinbase[i].coinb1[cb1idx[i]], 0x0E); cb1idx[i]+=2; // TODO: Profile a faster way to do this cb1idx[i] += sprintf(&s->coinbase[i].coinb1[cb1idx[i]], "%04" PRIx16, s->enprefix); } else { // if we are not, then we need to append the "sequence" pk_u64le(s->coinbase[i].coinb1, cb1idx[i], 0x6666666666666666ULL); // "ffffffff" cb1idx[i] += 8; } s->coinbase[i].coinb1[cb1idx[i]] = 0; } // extranonce ends up at the end of coinb1 // for the antminer hack coinbaser, we want to cram an output or two in coinb1, which is tricky // would be much easier to just always use the OP_RETURN, but that's wasteful when not needed as it wastes 10 bytes (wastes 8 bytes for the value, 2 for the OP_RETURN and the PUSH...) // if extranonce in the coinbase, then we start coinb2 with the "sequence" // if extranonce not in the coinbase, then we already tacked the "sequence" on to coinb1 immediately // we need to know the output count for each type so we can figure out what to stuff in each one // this may be a bit wasteful, but needs to be done. only needs to happen once per work update, and only when doing non-empty. ///////////////////////////// // 0 / EMPTY // empty should be easy. lets start there if (space_for_en_in_coinbase) { // we'll start the empty coinb2 with the "sequence" pk_u64le(s->coinbase[0].coinb2, 0, 0x6666666666666666ULL); // "ffffffff" cb2idx[0] = 8; cb2idx[0] += append_bitcoin_varint_hex(2, &s->coinbase[0].coinb2[cb2idx[0]]); // us and witness commit if (empty_only) { // copy the beginning to the subsidy-only memcpy(&s->subsidy_only_coinbase.coinb1[0], &s->coinbase[0].coinb1[0], cb1idx[0]); pk_u64le(s->subsidy_only_coinbase.coinb2, 0, 0x6666666666666666ULL); // "ffffffff" append_bitcoin_varint_hex(1, &s->subsidy_only_coinbase.coinb2[8]); // just us! } } else { // we're already at the point in coinb1 where we need an output count, which will be 3 if (empty_only) { j = cb1idx[0]; } cb1idx[0] += append_bitcoin_varint_hex(3, &s->coinbase[0].coinb1[cb1idx[0]]); // extranonce, us, and witness commit // append extranonce op_return cb1idx[0] += sprintf(&s->coinbase[0].coinb1[cb1idx[0]], "0000000000000000106a0e%04" PRIx16, s->enprefix); if (empty_only) { // copy the beginning to the subsidy-only memcpy(&s->subsidy_only_coinbase.coinb1[0], &s->coinbase[0].coinb1[0], cb1idx[0]); k = append_bitcoin_varint_hex(2, &s->subsidy_only_coinbase.coinb1[j]); // extranonce and us s->subsidy_only_coinbase.coinb1[j+k] = s->coinbase[0].coinb1[j+k]; } } // finish off "empty" coinbase // append our payout output value and script if (empty_only) { j = cb2idx[0]; } cb2idx[0] += sprintf(&s->coinbase[0].coinb2[cb2idx[0]], "%016llx", (unsigned long long)__builtin_bswap64(s->coinbase_value)); // TODO: Profile a faster way to do this cb2idx[0] += append_bitcoin_varint_hex(s->pool_addr_script_len, &s->coinbase[0].coinb2[cb2idx[0]]); // Append script length for(i=0;ipool_addr_script_len;i++) { uchar_to_hex(&s->coinbase[0].coinb2[cb2idx[0]], s->pool_addr_script[i]); cb2idx[0]+=2; } if (empty_only) { k = cb2idx[0]; } // witness commit output costs 46 bytes // append the default_witness_commitment cb2idx[0] += sprintf(&s->coinbase[0].coinb2[cb2idx[0]], "0000000000000000%2.2x%s", (unsigned int)strlen(s->block_template->default_witness_commitment)>>1, s->block_template->default_witness_commitment); // lock time cb2idx[0] += sprintf(&s->coinbase[0].coinb2[cb2idx[0]], "00000000"); if (empty_only) { // Append the subsidy-only payout to the subsidy_only_coinbase sprintf(&s->subsidy_only_coinbase.coinb2[j], "%016llx", (unsigned long long)__builtin_bswap64(block_reward(s->height))); // subsidy calc for height memcpy(&s->subsidy_only_coinbase.coinb2[j+16], &s->coinbase[0].coinb2[j+16], k-j-16); sprintf(&s->subsidy_only_coinbase.coinb2[k], "00000000"); } // End of 0 / Empty ////////////////////////////// if (empty_only) { // copy empty coinbaser to the others for (i=1;icoinbase[i].coinb1, s->coinbase[0].coinb1); strcpy(s->coinbase[i].coinb2, s->coinbase[0].coinb2); } } else { // ok, let's figure out how much space, if any, we have for miner payout outputs // we first need to figure out how much space we are using for each type after required data, so let's do that // witness output = 46 bytes // pool output = pool_addr_script_len + 9 // coinbase itself = cb_input_sz // coinbase len = 1 // cbstart = 41 bytes // lock time = 4 bytes // "sequence" = 4 bytes // extranonce size = 15 bytes (w/len push needed for either coinbase or OP_RETURN formats) // output count... could technically be up to three bytes for types 3 + 4, most likely 1 byte for 0,1,2. // --- lets give ourselves the wiggle room and say 3 bytes // // total static bytes = 46+9+1+41+4+3+4+15 = 123 bytes // not-static bytes = pool_addr_script_len + cb_input_sz + (space_for_en_in_coinbase?0:10) // --- it costs 10 extra bytes to do the OP_RETURN based extranonce if (!space_for_en_in_coinbase) { cb_req_sz[1] = cb_req_sz[2] = cb_req_sz[3] = cb_req_sz[4] = cb_req_sz[5] = 119 + s->pool_addr_script_len + cb_input_sz + 10; } else { cb_req_sz[1] = cb_req_sz[2] = cb_req_sz[3] = cb_req_sz[4] = cb_req_sz[5] = 119 + s->pool_addr_script_len + cb_input_sz; cb_req_sz[2] += 10; // always OP_RETURN extranonce for type 2 } // TYPE 1 - "Nicehash" friendly, max 500 bytes i = datum_stratum_coinbase_fit_to_template(500, cb_req_sz[1], s); generate_coinbase_txns_for_stratum_job_subtypebysize(s, 1, i, space_for_en_in_coinbase, cb1idx, cb2idx, false); // TYPE 3 - "Whatsminer" friendly, max 6500 bytes i = datum_stratum_coinbase_fit_to_template(6500, cb_req_sz[3], s); generate_coinbase_txns_for_stratum_job_subtypebysize(s, 3, i, space_for_en_in_coinbase, cb1idx, cb2idx, false); // TYPE 4 - "YUGE", max 16KB i = datum_stratum_coinbase_fit_to_template(16000, cb_req_sz[4], s); generate_coinbase_txns_for_stratum_job_subtypebysize(s, 4, i, space_for_en_in_coinbase, cb1idx, cb2idx, false); // TYPE 5 - "Antminer 2", max 2250 bytes i = datum_stratum_coinbase_fit_to_template(2250, cb_req_sz[5], s); generate_coinbase_txns_for_stratum_job_subtypebysize(s, 5, i, space_for_en_in_coinbase, cb1idx, cb2idx, false); // TYPE 2 - Older Antminer stock (S19) i = datum_stratum_coinbase_fit_to_template(755, cb_req_sz[2], s); generate_coinbase_txns_for_stratum_job_subtypebysize(s, 2, i, false, cb1idx, cb2idx, true); } // prep binary versions of the coinbase for speeding up later for(k=0;kcoinbase[k].coinb1); s->coinbase[k].coinb1_len = 0; for(j=0;jcoinbase[k].coinb1_bin[j>>1] = hex2bin_uchar(&s->coinbase[k].coinb1[j]); s->coinbase[k].coinb1_len++; } i = strlen(s->coinbase[k].coinb2); s->coinbase[k].coinb2_len = 0; for(j=0;jcoinbase[k].coinb2_bin[j>>1] = hex2bin_uchar(&s->coinbase[k].coinb2[j]); s->coinbase[k].coinb2_len++; } } if (empty_only) { i = strlen(s->subsidy_only_coinbase.coinb1); s->subsidy_only_coinbase.coinb1_len = 0; for(j=0;jsubsidy_only_coinbase.coinb1_bin[j>>1] = hex2bin_uchar(&s->subsidy_only_coinbase.coinb1[j]); s->subsidy_only_coinbase.coinb1_len++; } i = strlen(s->subsidy_only_coinbase.coinb2); s->subsidy_only_coinbase.coinb2_len = 0; for(j=0;jsubsidy_only_coinbase.coinb2_bin[j>>1] = hex2bin_uchar(&s->subsidy_only_coinbase.coinb2[j]); s->subsidy_only_coinbase.coinb2_len++; } } } int datum_coinbaser_v2_parse(T_DATUM_STRATUM_JOB *s, unsigned char *coinbaser, int cblen, bool must_free) { // parse raw outputs from DATUM connection into a useful coinbaser uint64_t outval = 0; uint64_t tally = 0; int cidx = 0; int slen = 0; int cbvalid = 0; int datum_id; if (!coinbaser) { DLOG_WARN("Coinbaser is NULL Using default/empty"); s->available_coinbase_outputs_count = 0; return 0; } if (cblen < 9) { // 0 outputs possible DLOG_WARN("Coinbaser lentgh is invalid (too short). Using default/empty"); s->available_coinbase_outputs_count = 0; return 0; } DLOG_DEBUG("Coinbaser v2 size %d", cblen); datum_id = coinbaser[cidx]; cidx++; while (cidx < cblen) { outval = upk_u64le(coinbaser, cidx); cidx+=8; if ((outval + tally) > s->coinbase_value) { // we can't include this value, since it would put us over our total available! // this shouldn't happen, however... break; } slen = coinbaser[cidx]; cidx++; if ((slen < 2) || (slen > 64)) { // invalid script len?!? break; } tally += outval; memcpy(s->available_coinbase_outputs[cbvalid].output_script, &coinbaser[cidx], slen); cidx+=slen; // 64-bit value in sats is part of the output s->available_coinbase_outputs[cbvalid].value_sats = outval; if (s->available_coinbase_outputs[cbvalid].output_script[0] == 0x76) { // kludge for checking for P2PKH output s->available_coinbase_outputs[cbvalid].sigops = 4; } else { s->available_coinbase_outputs[cbvalid].sigops = 0; } s->available_coinbase_outputs[cbvalid].output_script_len = slen; cbvalid++; if (cbvalid >= 512) break; // limitation of datum for now } s->datum_coinbaser_id = datum_id; s->available_coinbase_outputs_count = cbvalid; if (coinbaser && must_free) free(coinbaser); return cbvalid; } void *datum_coinbaser_thread(void *ptr) { int sjob = -1; T_DATUM_STRATUM_JOB *s = NULL; bool need_coinbaser = false; int i; DLOG_DEBUG("Coinbaser thread active"); while(1) { // check if we need to fetch any new coinbasers // check if the stratum job has been updated pthread_rwlock_rdlock(&stratum_global_job_ptr_lock); if (global_latest_stratum_job_index != sjob) { s = global_cur_stratum_jobs[global_latest_stratum_job_index]; if (s) { sjob = global_latest_stratum_job_index; if (s->need_coinbaser) { need_coinbaser = true; } } else { need_coinbaser = false; } } pthread_rwlock_unlock(&stratum_global_job_ptr_lock); if (need_coinbaser) { // fetch remote coinbaser for job DLOG_DEBUG("Job %d needs a coinbaser!", sjob); if (datum_protocol_is_active()) { i = datum_protocol_coinbaser_fetch(s); } else { s->available_coinbase_outputs_count = 0; i = 0; } if (i>=0) { DLOG_DEBUG("Generating coinbases for up to %d outputs", i); generate_coinbase_txns_for_stratum_job(s, false); if (need_coinbaser_rwlocks_init_done) { pthread_rwlock_wrlock(&need_coinbaser_rwlocks[sjob]); s->need_coinbaser = false; pthread_rwlock_unlock(&need_coinbaser_rwlocks[sjob]); need_coinbaser = false; } DLOG_DEBUG("Generated and notified."); } } usleep(12000); } } int datum_coinbaser_init(void) { pthread_t pthread_datum_coinbaser_thread; int result = pthread_create(&pthread_datum_coinbaser_thread, NULL, datum_coinbaser_thread, NULL); if (result != 0) { DLOG_FATAL("datum_coinbaser_init: pthread_create failed with code %d", result); return -1; } return 0; } datum_gateway-0.4.1beta/src/datum_coinbaser.h000066400000000000000000000036071512710151500213060ustar00rootroot00000000000000/* * * DATUM Gateway * Decentralized Alternative Templates for Universal Mining * * This file is part of OCEAN's Bitcoin mining decentralization * project, DATUM. * * https://ocean.xyz * * --- * * Copyright (c) 2024 Bitcoin Ocean, LLC & Jason Hughes * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ #ifndef _DATUM_COINBASE_H_ #define _DATUM_COINBASE_H_ int datum_coinbaser_init(void); void generate_coinbase_txns_for_stratum_job_subtypebysize(T_DATUM_STRATUM_JOB *s, int coinbase_index, int remaining_size, bool space_for_en_in_coinbase, int *cb1idx, int *cb2idx, bool special_coinb1); void generate_coinbase_txns_for_stratum_job(T_DATUM_STRATUM_JOB *s, bool empty_only); void generate_base_coinbase_txns_for_stratum_job(T_DATUM_STRATUM_JOB *s, bool new_block); int datum_coinbaser_v2_parse(T_DATUM_STRATUM_JOB *s, unsigned char *coinbaser, int cblen, bool must_free); #endif datum_gateway-0.4.1beta/src/datum_conf.c000066400000000000000000001033111512710151500202520ustar00rootroot00000000000000/* * * DATUM Gateway * Decentralized Alternative Templates for Universal Mining * * This file is part of OCEAN's Bitcoin mining decentralization * project, DATUM. * * https://ocean.xyz * * --- * * Copyright (c) 2024-2025 Bitcoin Ocean, LLC & Jason Hughes * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ // Custom configurator and help output generator #include #include #include #include #include #include #include #include #include "datum_conf.h" #include "datum_jsonrpc.h" #include "datum_utils.h" #include "datum_sockets.h" global_config_t datum_config; const char *datum_conf_var_type_text[] = { "boolean", "integer", "string", "string_array", "{\"modname\":{\"address\":proportion,...},...}", }; const T_DATUM_CONFIG_ITEM datum_config_options[] = { // Bitcoind configs { .var_type = DATUM_CONF_STRING, .category = "bitcoind", .name = "rpccookiefile", .description = "Path to file to read RPC cookie from, for communication with local bitcoind.", .required = false, .ptr = datum_config.bitcoind_rpccookiefile, .default_string[0] = "", .max_string_len = sizeof(datum_config.bitcoind_rpccookiefile) }, { .var_type = DATUM_CONF_STRING, .category = "bitcoind", .name = "rpcuser", .description = "RPC username for communication with local bitcoind.", .example = "\"datum\"", .required = false, .ptr = datum_config.bitcoind_rpcuser, .default_string[0] = "", .max_string_len = sizeof(datum_config.bitcoind_rpcuser) }, { .var_type = DATUM_CONF_STRING, .category = "bitcoind", .name = "rpcpassword", .description = "RPC password for communication with local bitcoind.", .example = "\"something only you know\"", .required = false, .ptr = datum_config.bitcoind_rpcpassword, .default_string[0] = "", .max_string_len = sizeof(datum_config.bitcoind_rpcpassword) }, { .var_type = DATUM_CONF_STRING, .category = "bitcoind", .name = "rpcurl", .description = "RPC URL for communication with local bitcoind. (GBT Template Source)", .example = "\"http://localhost:8332\"", .required = true, .ptr = datum_config.bitcoind_rpcurl, .max_string_len = sizeof(datum_config.bitcoind_rpcurl) }, { .var_type = DATUM_CONF_INT, .category = "bitcoind", .name = "work_update_seconds", .description = "How many seconds between normal work updates? (5-120, 40 suggested)", .required = false, .ptr = &datum_config.bitcoind_work_update_seconds, .default_int = 40 }, { .var_type = DATUM_CONF_BOOL, .category = "bitcoind", .name = "notify_fallback", .description = "Fall back to less efficient methods for new block notifications. Can disable if you use blocknotify.", .example_default = true, .required = false, .ptr = &datum_config.bitcoind_notify_fallback, .default_bool = true }, // stratum v1 server configs { .var_type = DATUM_CONF_STRING, .category = "stratum", .name = "listen_addr", .description = "IP address to listen for Stratum Gateway connections", .required = false, .ptr = datum_config.stratum_v1_listen_addr, .default_string[0] = "", .max_string_len = sizeof(datum_config.stratum_v1_listen_addr) }, { .var_type = DATUM_CONF_INT, .category = "stratum", .name = "listen_port", .description = "Listening port for Stratum Gateway", .example_default = true, .required = false, .ptr = &datum_config.stratum_v1_listen_port, .default_int = 23334 }, { .var_type = DATUM_CONF_INT, .category = "stratum", .name = "max_clients_per_thread", .description = "Maximum clients per Stratum server thread", .required = false, .ptr = &datum_config.stratum_v1_max_clients_per_thread, .default_int = 128 }, { .var_type = DATUM_CONF_INT, .category = "stratum", .name = "max_threads", .description = "Maximum Stratum server threads", .required = false, .ptr = &datum_config.stratum_v1_max_threads, .default_int = 8 }, { .var_type = DATUM_CONF_INT, .category = "stratum", .name = "max_clients", .description = "Maximum total Stratum clients before rejecting connections", .required = false, .ptr = &datum_config.stratum_v1_max_clients, .default_int = 1024 }, { .var_type = DATUM_CONF_INT, .category = "stratum", .name = "trust_proxy", .description = "Enable support for the PROXY protocol, trusting up to the specified number of levels deep of proxies (-1 to disable entirely)", .required = false, .ptr = &datum_config.stratum_v1_trust_proxy, .default_int = -1 }, { .var_type = DATUM_CONF_INT, .category = "stratum", .name = "vardiff_min", .description = "Work difficulty floor", .required = false, .ptr = &datum_config.stratum_v1_vardiff_min, .default_int = 16384 }, { .var_type = DATUM_CONF_INT, .category = "stratum", .name = "vardiff_target_shares_min",.description = "Adjust work difficulty to target this many shares per minute", .required = false, .ptr = &datum_config.stratum_v1_vardiff_target_shares_min, .default_int = 8 }, { .var_type = DATUM_CONF_INT, .category = "stratum", .name = "vardiff_quickdiff_count", .description = "How many shares before considering a quick diff update", .required = false, .ptr = &datum_config.stratum_v1_vardiff_quickdiff_count, .default_int = 8 }, { .var_type = DATUM_CONF_INT, .category = "stratum", .name = "vardiff_quickdiff_delta", .description = "How many times faster than our target does the miner have to be before we enforce a quick diff bump", .required = false, .ptr = &datum_config.stratum_v1_vardiff_quickdiff_delta, .default_int = 8 }, { .var_type = DATUM_CONF_INT, .category = "stratum", .name = "share_stale_seconds", .description = "How many seconds after a job is generated before a share submission is considered stale?", .required = false, .ptr = &datum_config.stratum_v1_share_stale_seconds, .default_int = 120 }, { .var_type = DATUM_CONF_BOOL, .category = "stratum", .name = "fingerprint_miners", .description = "Attempt to fingerprint miners for better use of coinbase space", .required = false, .ptr = &datum_config.stratum_v1_fingerprint_miners, .default_bool = true }, { .var_type = DATUM_CONF_INT, .category = "stratum", .name = "idle_timeout_no_subscribe",.description = "Seconds we allow a connection to be idle without seeing a work subscription? (0 disables)", .required = false, .ptr = &datum_config.stratum_v1_idle_timeout_no_subscribe, .default_int = 15 }, { .var_type = DATUM_CONF_INT, .category = "stratum", .name = "idle_timeout_no_shares", .description = "Seconds we allow a subscribed connection to be idle without seeing at least one accepted share? (0 disables)", .required = false, .ptr = &datum_config.stratum_v1_idle_timeout_no_share, .default_int = 7200 }, { .var_type = DATUM_CONF_INT, .category = "stratum", .name = "idle_timeout_max_last_work", .description = "Seconds we allow a subscribed connection to be idle since its last accepted share? (0 disables)", .required = false, .ptr = &datum_config.stratum_v1_idle_timeout_max_last_work, .default_int = 0 }, { .var_type = DATUM_CONF_USERNAME_MODS, .category = "stratum", .name = "username_modifiers", .description = "Modifiers to redirect some portion of shares to alternate usernames", .required = false, .ptr = &datum_config.stratum_username_mod, }, // mining settings { .var_type = DATUM_CONF_STRING, .category = "mining", .name = "pool_address", .description = "Bitcoin address used for mining rewards.", .example = "\"put your own Bitcoin invoice address here\"", .required = true, .ptr = datum_config.mining_pool_address, .max_string_len = sizeof(datum_config.mining_pool_address) }, { .var_type = DATUM_CONF_STRING, .category = "mining", .name = "coinbase_tag_primary", .description = "Text to have in the primary coinbase tag when not using pool (overridden by DATUM Pool)", .example_default = true, .required = false, .ptr = datum_config.mining_coinbase_tag_primary, .default_string[0] = "DATUM Gateway", .max_string_len = sizeof(datum_config.mining_coinbase_tag_primary) }, { .var_type = DATUM_CONF_STRING, .category = "mining", .name = "coinbase_tag_secondary", .description = "Text to have in the secondary coinbase tag (Short name/identifier)", .example_default = true, .required = false, .ptr = datum_config.mining_coinbase_tag_secondary, .default_string[0] = "DATUM User", .max_string_len = sizeof(datum_config.mining_coinbase_tag_secondary) }, { .var_type = DATUM_CONF_INT, .category = "mining", .name = "coinbase_unique_id", .description = "A unique ID between 1 and 65535. This is appended to the coinbase. Make unique per instance of datum with the same coinbase tags.", .required = false, .ptr = &datum_config.coinbase_unique_id, .default_int = 4242 }, { .var_type = DATUM_CONF_STRING, .category = "mining", .name = "save_submitblocks_dir", .description = "Directory to save all submitted blocks to as submitblock JSON files", .required = false, .ptr = datum_config.mining_save_submitblocks_dir, .default_string[0] = "", .max_string_len = sizeof(datum_config.mining_save_submitblocks_dir) }, // API/dashboard { .var_type = DATUM_CONF_STRING, .category = "api", .name = "admin_password", .description = "API password for actions/changes (username 'admin'; disabled if blank)", .example = "\"\"", .required = false, .ptr = datum_config.api_admin_password, .default_string[0] = "", .max_string_len = sizeof(datum_config.api_admin_password) }, { .var_type = DATUM_CONF_BOOL, .category = "api", .name = "allow_insecure_auth", .description = "Allow insecure authentication (required for Safari)", .required = false, .ptr = &datum_config.api_allow_insecure_auth, .default_bool = false }, { .var_type = DATUM_CONF_STRING, .category = "api", .name = "listen_addr", .description = "IP address to listen for API/dashboard requests", .required = false, .ptr = datum_config.api_listen_addr, .default_string[0] = "", .max_string_len = sizeof(datum_config.api_listen_addr) }, { .var_type = DATUM_CONF_INT, .category = "api", .name = "listen_port", .description = "Port to listen for API/dashboard requests (0=disabled)", .example = "7152", .required = false, .ptr = &datum_config.api_listen_port, .default_int = 0 }, { .var_type = DATUM_CONF_BOOL, .category = "api", .name = "modify_conf", .description = "Enable modifying the config file from API/dashboard", .example_default = true, .required = false, .ptr = &datum_config.api_modify_conf, .default_bool = false }, // extra block submissions list { .var_type = DATUM_CONF_STRING_ARRAY, .category = "extra_block_submissions", .name = "urls", .description = "Array of bitcoind RPC URLs to submit our blocks to directly. Include auth info: http://user:pass@IP", .required = false, .ptr = datum_config.extra_block_submissions_urls[0], .max_string_len = sizeof(*datum_config.extra_block_submissions_urls) }, // logger { .var_type = DATUM_CONF_BOOL, .category = "logger", .name = "log_to_console", .description = "Enable logging of messages to the console", .example_default = true, .required = false, .ptr = &datum_config.clog_to_console, .default_bool = true }, { .var_type = DATUM_CONF_BOOL, .category = "logger", .name = "log_to_stderr", .description = "Log console messages to stderr *instead* of stdout", .required = false, .ptr = &datum_config.clog_to_stderr, .default_bool = false }, { .var_type = DATUM_CONF_BOOL, .category = "logger", .name = "log_to_file", .description = "Enable logging of messages to a file", .example_default = true, .required = false, .ptr = &datum_config.clog_to_file, .default_bool = false }, { .var_type = DATUM_CONF_STRING, .category = "logger", .name = "log_file", .description = "Path to file to write log messages, when enabled", .example = "\"/var/log/datum.log\"", .required = false, .ptr = datum_config.clog_file, .default_string[0] = "", .max_string_len = sizeof(datum_config.clog_file) }, { .var_type = DATUM_CONF_BOOL, .category = "logger", .name = "log_rotate_daily", .description = "Rotate the message log file at midnight", .example_default = true, .required = false, .ptr = &datum_config.clog_rotate_daily, .default_bool = true }, { .var_type = DATUM_CONF_BOOL, .category = "logger", .name = "log_calling_function", .description = "Log the name of the calling function when logging", .required = false, .ptr = &datum_config.clog_calling_function, .default_bool = true }, { .var_type = DATUM_CONF_INT, .category = "logger", .name = "log_level_console", .description = "Minimum log level for console messages (0=All, 1=Debug, 2=Info, 3=Warn, 4=Error, 5=Fatal)", .example_default = true, .required = false, .ptr = &datum_config.clog_level_console, .default_int = 2 }, { .var_type = DATUM_CONF_INT, .category = "logger", .name = "log_level_file", .description = "Minimum log level for log file messages (0=All, 1=Debug, 2=Info, 3=Warn, 4=Error, 5=Fatal)", .example_default = true, .required = false, .ptr = &datum_config.clog_level_file, .default_int = 1 }, // datum options { .var_type = DATUM_CONF_STRING, .category = "datum", .name = "pool_host", .description = "Remote DATUM server host/ip to use for decentralized pooled mining (set to \"\" to disable pooled mining)", .required = false, .ptr = datum_config.datum_pool_host, .default_string[0] = "datum-beta1.mine.ocean.xyz", .max_string_len = sizeof(datum_config.datum_pool_host) }, { .var_type = DATUM_CONF_INT, .category = "datum", .name = "pool_port", .description = "Remote DATUM server port", .required = false, .ptr = &datum_config.datum_pool_port, .default_int = 28915 }, { .var_type = DATUM_CONF_STRING, .category = "datum", .name = "pool_pubkey", .description = "Public key of the DATUM server for initiating encrypted connection. Get from secure location, or set to empty to auto-fetch.", .required = false, .ptr = datum_config.datum_pool_pubkey, .default_string[0] = "f21f2f0ef0aa1970468f22bad9bb7f4535146f8e4a8f646bebc93da3d89b1406f40d032f09a417d94dc068055df654937922d2c89522e3e8f6f0e649de473003", .max_string_len = sizeof(datum_config.datum_pool_pubkey) }, { .var_type = DATUM_CONF_BOOL, .category = "datum", .name = "pool_pass_workers", .description = "Pass stratum miner usernames as sub-worker names to the pool (pool_username.miner's username)", .example_default = true, .required = false, .ptr = &datum_config.datum_pool_pass_workers, .default_bool = true }, { .var_type = DATUM_CONF_BOOL, .category = "datum", .name = "pool_pass_full_users", .description = "Pass stratum miner usernames as raw usernames to the pool (use if putting multiple payout addresses on miners behind this gateway)", .example_default = true, .required = false, .ptr = &datum_config.datum_pool_pass_full_users, .default_bool = true }, { .var_type = DATUM_CONF_BOOL, .category = "datum", .name = "always_pay_self", .description = "Always include my datum.pool_username payout in my blocks if possible", .required = false, .ptr = &datum_config.datum_always_pay_self, .default_bool = true }, { .var_type = DATUM_CONF_BOOL, .category = "datum", .name = "pooled_mining_only", .description = "If the DATUM pool server becomes unavailable, terminate miner connections (otherwise, 100% of any blocks you find pay mining.pool_address)", .example_default = true, .required = false, .ptr = &datum_config.datum_pooled_mining_only, .default_bool = true }, { .var_type = DATUM_CONF_INT, .category = "datum", .name = "protocol_global_timeout", .description = "If no valid messages are received from the DATUM server in this many seconds, give up and try to reconnect", .required = false, .ptr = &datum_config.datum_protocol_global_timeout, .default_int = 60 }, }; #define NUM_CONFIG_ITEMS (sizeof(datum_config_options) / sizeof(datum_config_options[0])) const T_DATUM_CONFIG_ITEM *datum_config_get_option_info(const char * const category, const size_t category_len, const char * const name, const size_t name_len) { for (size_t i = 0; i < NUM_CONFIG_ITEMS; ++i) { if (strncmp(category, datum_config_options[i].category, category_len)) continue; if (datum_config_options[i].category[category_len]) continue; if (strncmp(name, datum_config_options[i].name, name_len)) continue; if (datum_config_options[i].name[name_len]) continue; return &datum_config_options[i]; } return NULL; } const T_DATUM_CONFIG_ITEM *datum_config_get_option_info2(const char * const category, const char * const name) { return datum_config_get_option_info(category, strlen(category), name, strlen(name)); } json_t *load_json_from_file(const char *file_path) { json_error_t error; json_t *root = json_load_file(file_path, 0, &error); if(!root) { DLOG_ERROR("Error parsing JSON file: %s", error.text); return NULL; } return root; } void datum_config_set_default(const T_DATUM_CONFIG_ITEM *c) { // set the default switch(c->var_type) { case DATUM_CONF_INT: { *((int *)c->ptr) = c->default_int; break; } case DATUM_CONF_BOOL: { *((bool *)c->ptr) = c->default_bool; break; } case DATUM_CONF_STRING: { strncpy((char *)c->ptr, c->default_string[0], c->max_string_len-1); ((char *)c->ptr)[c->max_string_len-1] = 0; break; } case DATUM_CONF_STRING_ARRAY: { // TODO: For now, these won't have a default. the first string will just be empty ((char *)c->ptr)[0] = 0; break; } case DATUM_CONF_USERNAME_MODS: { struct datum_username_mod ** const umods_p = c->ptr; free(*umods_p); *umods_p = NULL; break; } } } int datum_config_parse_username_mods(struct datum_username_mod ** const umods_p, json_t * const item, const bool log_errors) { if (!json_object_size(item)) { if (json_is_null(item) || json_is_object(item)) { free(*umods_p); *umods_p = NULL; return 1; } return -1; } size_t sz = (sizeof(**umods_p) + _Alignof(struct datum_username_mod) - 1) * (json_object_size(item) + 1); const char *modname, *addr; json_t *moddefn, *proportion_j; bool at_least_one_mod = false; json_object_foreach(item, modname, moddefn) { if (json_is_null(moddefn)) continue; if (!json_is_object(moddefn)) return -1; at_least_one_mod = true; sz += strlen(modname) + 1 + (sizeof(*((*umods_p)->ranges)) * (json_object_size(moddefn) + 1)); json_object_foreach(moddefn, addr, proportion_j) { if (json_is_null(proportion_j)) continue; if (!json_is_number(proportion_j)) return -1; if (json_number_value(proportion_j) < 0) return -1; sz += strlen(addr) + 1; } } free(*umods_p); if (!at_least_one_mod) { *umods_p = NULL; return 1; } uint8_t *p = malloc(sz); assert(p); *umods_p = (struct datum_username_mod*)p; json_object_foreach(item, modname, moddefn) { if (json_is_null(moddefn)) continue; struct datum_username_mod * const umod = (struct datum_username_mod*)p; umod->sz = sizeof(*umod) + (sizeof(*umod->ranges) * (json_object_size(moddefn) + 1)); p += umod->sz; umod->modname_len = strlen(modname); umod->modname = memcpy(p, modname, umod->modname_len); p += umod->modname_len; double sum = 0; struct datum_addr_range *addr_range = umod->ranges; json_object_foreach(moddefn, addr, proportion_j) { if (json_is_null(proportion_j)) continue; sum += json_number_value(proportion_j); const double nonce_max_d = ceil(sum * 0x10000) - 1; if (nonce_max_d < 0) continue; const uint16_t nonce_max = (nonce_max_d > 0xffff) ? (uint16_t)0xffff : (uint16_t)nonce_max_d; addr_range->addr_len = strlen(addr); addr_range->addr = memcpy(p, addr, addr_range->addr_len + 1); addr_range->max = nonce_max; p = &p[addr_range->addr_len + 1]; ++addr_range; if (nonce_max == 0xffff) break; } if (log_errors && (addr_range == umod->ranges || addr_range[-1].max != 0xffff)) { double missing_percent = 100 * (1 - sum); const unsigned int missing_percent_precision = datum_double_precision(&missing_percent); DLOG_ERROR("Username modifier '%s' is configured to not distribute %.*f%% of shares!", modname, missing_percent_precision, missing_percent); } addr_range[0].addr = NULL; assert((uint8_t*)addr_range <= &((uint8_t*)umod)[umod->sz]); // otherwise we overwrote strings! assert(p <= &((uint8_t*)*umods_p)[sz]); // otherwise we overran the buffer! umod->sz = datum_align_sz(p - (uint8_t*)umod, _Alignof(struct datum_username_mod)); p = &((uint8_t*)umod)[umod->sz]; } ((struct datum_username_mod*)p)->sz = 0; return 1; } struct datum_username_mod *datum_username_mods_next(struct datum_username_mod * const prev_umod) { struct datum_username_mod * const next_umod = (struct datum_username_mod *)&(((uint8_t*)prev_umod)[prev_umod->sz]); return next_umod->sz ? next_umod : NULL; } struct datum_username_mod *datum_username_mods_find(struct datum_username_mod *umod, const char * const modname, const size_t modname_len) { for ( ; umod; umod = datum_username_mods_next(umod)) { if (modname_len != umod->modname_len) continue; if (strncmp(modname, umod->modname, modname_len)) continue; return umod; } return NULL; } int datum_config_parse_value(const T_DATUM_CONFIG_ITEM *c, json_t *item) { switch(c->var_type) { case DATUM_CONF_INT: { if (json_is_null(item)) { *((int *)c->ptr) = 0; // set to zero if "NULL" ... probably not ideal, but better than failing return 1; } if (!json_is_integer(item)) return -1; const json_int_t value = json_integer_value(item); if (value > INT_MAX || value < INT_MIN) return -1; *((int *)c->ptr) = value; return 1; } case DATUM_CONF_BOOL: { if (json_is_null(item)) { *((bool *)c->ptr) = false; // set to false if "NULL" ... probably not ideal, but better than failing return 1; } if (!json_is_boolean(item)) return -1; *((bool *)c->ptr) = json_boolean_value(item)?true:false; return 1; } case DATUM_CONF_STRING: { if (json_is_null(item)) { ((char *)c->ptr)[0] = 0; // Set c->ptr to an empty string if the js is actually NULL return 1; } if (!json_is_string(item)) return -1; // check for overflow int written = snprintf((char *)c->ptr, c->max_string_len, "%s", json_string_value(item)); if (written >= c->max_string_len) { return -2; } return 1; } case DATUM_CONF_STRING_ARRAY: { if (!json_is_array(item)) return -1; size_t index; json_t *value; int i = 0; json_array_foreach(item, index, value) { if (!json_is_string(value)) return -1; if (i < (DATUM_CONFIG_MAX_ARRAY_ENTRIES-1)) { strncpy(((char (*)[1024])c->ptr)[i], json_string_value(value), c->max_string_len-1); ((char (*)[1024])c->ptr)[i][c->max_string_len-1] = 0; i++; } } ((char (*)[1024])c->ptr)[i][0] = 0; return 1; } case DATUM_CONF_USERNAME_MODS: { return datum_config_parse_username_mods(c->ptr, item, true); } } return -1; } static void datum_config_opt_missing_error(const T_DATUM_CONFIG_ITEM * const opt) { DLOG_ERROR("Required configuration option (%s.%s) not found in config file:", opt->category, opt->name); DLOG_ERROR("--- Config description: \"%s\"", opt->description); } int datum_read_config(const char *conffile) { json_t *config = NULL; json_t *cat, *item; unsigned int i; int j; memset(&datum_config, 0, sizeof(global_config_t)); config = load_json_from_file(conffile); if (!json_is_object(config)) { DLOG_FATAL("Could not read configuration JSON file!"); return -1; } for (i=0;i 120) { datum_config.bitcoind_work_update_seconds = 120; } if (datum_config.bitcoind_rpcuser[0]) { if (!datum_config.bitcoind_rpcpassword[0]) { datum_config_opt_missing_error(datum_config_get_option_info2("bitcoind", "rpcpassword")); return 0; } snprintf(datum_config.bitcoind_rpcuserpass, sizeof(datum_config.bitcoind_rpcuserpass), "%s:%s", datum_config.bitcoind_rpcuser, datum_config.bitcoind_rpcpassword); } else if (datum_config.bitcoind_rpccookiefile[0]) { update_rpc_cookie(&datum_config); } else { const T_DATUM_CONFIG_ITEM *opt; DLOG_ERROR("Either bitcoind.rpcuser (and bitcoind.rpcpassword) or bitcoind.rpccookiefile is required."); opt = datum_config_get_option_info2("bitcoind", "rpcuser"); DLOG_ERROR("--- Config description for %s.%s: \"%s\"", opt->category, opt->name, opt->description); opt = datum_config_get_option_info2("bitcoind", "rpccookiefile"); DLOG_ERROR("--- Config description for %s.%s: \"%s\"", opt->category, opt->name, opt->description); return 0; } #ifndef ENABLE_API if (datum_config.api_listen_port) { DLOG_WARN("API is enabled in configuration, but this build was compiled without API support"); } #else datum_config.api_admin_password_len = strlen(datum_config.api_admin_password); if (datum_config.api_admin_password_len) { static const char hash_tag[] = "DATUM Anti-CSRF Token"; const size_t data_max_sz = sizeof(hash_tag) + sizeof(datum_config.api_listen_port) + sizeof(datum_config.api_admin_password); const size_t data_sz = sizeof(hash_tag) + sizeof(datum_config.api_listen_port) + datum_config.api_admin_password_len; char data[data_max_sz]; strcpy(data, hash_tag); memcpy(&data[sizeof(hash_tag)], &datum_config.api_listen_port, sizeof(datum_config.api_listen_port)); strcpy(&data[sizeof(hash_tag)+sizeof(datum_config.api_listen_port)], datum_config.api_admin_password); unsigned char hash[32]; my_sha256(hash, data, data_sz); hash2hex(hash, datum_config.api_csrf_token); } #endif if (datum_config.stratum_v1_max_threads > MAX_THREADS) { DLOG_FATAL("Maximum threads must be less than %d.", MAX_THREADS); return 0; } if (datum_config.stratum_v1_max_clients_per_thread > MAX_CLIENTS_THREAD) { DLOG_FATAL("Maximum clients per thread must be less than %d.",MAX_CLIENTS_THREAD); return 0; } if ((strlen(datum_config.mining_coinbase_tag_primary)+strlen(datum_config.mining_coinbase_tag_secondary)) > 88) { DLOG_FATAL("Length of coinbase tags can not exceed 88 bytes total."); return 0; } if ((strlen(datum_config.mining_coinbase_tag_primary) > 60) || (strlen(datum_config.mining_coinbase_tag_secondary) > 60)) { DLOG_FATAL("Length of coinbase tags can not exceed 88 bytes total or 60 bytes each."); return 0; } if (datum_config.stratum_v1_vardiff_target_shares_min < 1) { DLOG_FATAL("Stratum server stratum.vardiff_target_shares_min must be at least 1"); return 0; } if (datum_config.stratum_v1_vardiff_quickdiff_count < 4) { DLOG_FATAL("Stratum server stratum.vardiff_quickdiff_count must be at least 4"); return 0; } if (datum_config.stratum_v1_vardiff_quickdiff_delta < 3) { DLOG_FATAL("Stratum server stratum.vardiff_quickdiff_delta must be at least 3"); return 0; } if (roundDownToPowerOfTwo_64(datum_config.stratum_v1_vardiff_min) != datum_config.stratum_v1_vardiff_min) { const int nv = roundDownToPowerOfTwo_64(datum_config.stratum_v1_vardiff_min); DLOG_WARN("stratum.vardiff_min MUST be a power of two. adjusting from %d to %d", datum_config.stratum_v1_vardiff_min, nv); datum_config.stratum_v1_vardiff_min = nv; } if (datum_config.stratum_v1_vardiff_min < 1) { DLOG_FATAL("Stratum server stratum.vardiff_min must be at least 1 (suggest at least 1024, but more likely 32768)"); return 0; } if (datum_config.stratum_v1_max_clients > (datum_config.stratum_v1_max_clients_per_thread*datum_config.stratum_v1_max_threads)) { DLOG_FATAL("Stratum server configuration error. Max clients too high for thread settings"); return 0; } if (datum_config.stratum_v1_share_stale_seconds < 60) { DLOG_FATAL("Stratum server stratum.share_stale_seconds must be at least 60 (suggest 120)"); return 0; } if (datum_config.stratum_v1_share_stale_seconds > 150) { DLOG_FATAL("Stratum server stratum.share_stale_seconds must not exceed 150 (suggest 120)"); return 0; } if (datum_config.datum_protocol_global_timeout < (datum_config.bitcoind_work_update_seconds+5)) { DLOG_FATAL("DATUM protocol global timeout must be at least the work update interval plus 5 seconds."); return 0; } if (datum_config.datum_pool_host[0] == '\0' && datum_config.datum_pooled_mining_only == true) { DLOG_FATAL("Pooled mining only is set to true, but pool host is not specified."); return 0; } // Save some multiplication later datum_config.datum_protocol_global_timeout_ms = datum_config.datum_protocol_global_timeout * 1000; // Just in case strcpy(datum_config.override_mining_coinbase_tag_primary, datum_config.mining_coinbase_tag_primary); return 1; } void datum_gateway_help(const char * const argv0) { int p; const char *lastcat = ""; static const char * const paddots = ".............................................................."; printf("Usage: %s [OPTION]...\n\n", argv0); puts("Command line options:\n"); puts(" -c, --config=FILE ..................... Path to configuration JSON file (default: ./datum_gateway_config.json)"); puts(" -?, --help ............................ Print this help"); puts(" --example-conf ........................ Print an example configuration JSON file"); puts(" --version ............................. Print this software's name and version"); puts(""); puts("Configuration file options:\n\n{"); for (unsigned int i = 0; i < NUM_CONFIG_ITEMS; ++i) { const T_DATUM_CONFIG_ITEM * const opt = &datum_config_options[i]; if (strcmp(opt->category, lastcat)) { if (i) { puts(" },"); } printf(" \"%s\": {\n", opt->category); lastcat = opt->category; } p = 30 - strlen(opt->name); if (p < 0) p = 0; printf(" \"%s\": %.*s %s (%s", opt->name, p, paddots, opt->description, datum_conf_var_type_text[opt->var_type]); if (opt->required) { puts(", REQUIRED)"); } else { switch (opt->var_type) { case DATUM_CONF_INT: { printf(", default: %d)\n", opt->default_int); break; } case DATUM_CONF_BOOL: { printf(", default: %s)\n", opt->default_bool ? "true" : "false"); break; } case DATUM_CONF_STRING: { printf(", default: \"%s\")\n", opt->default_string[0]); break; } default: { puts(")"); break; } } } } puts(" }\n}\n"); } void datum_gateway_example_conf(void) { const char *lastcat = ""; bool first = true; puts("{"); for (unsigned int i = 0; i < NUM_CONFIG_ITEMS; ++i) { const T_DATUM_CONFIG_ITEM * const opt = &datum_config_options[i]; if (!(opt->example || opt->example_default)) { continue; } if (strcmp(opt->category, lastcat)) { if (*lastcat) { puts("\n\t},"); } printf("\t\"%s\": {\n", opt->category); lastcat = opt->category; first = true; } if (first) { first = false; } else { puts(","); } printf("\t\t\"%s\": ", opt->name); if (opt->example) { printf("%s", opt->example); } else if (opt->example_default) { switch (opt->var_type) { case DATUM_CONF_INT: { printf("%d", opt->default_int); break; } case DATUM_CONF_BOOL: { printf("%s", opt->default_bool ? "true" : "false"); break; } case DATUM_CONF_STRING: { printf("\"%s\"", opt->default_string[0]); break; } case DATUM_CONF_STRING_ARRAY: { puts("[]"); break; } case DATUM_CONF_USERNAME_MODS: { puts("{}"); break; } } } } puts("\n\t}\n}"); } datum_gateway-0.4.1beta/src/datum_conf.h000066400000000000000000000120621512710151500202610ustar00rootroot00000000000000/* * * DATUM Gateway * Decentralized Alternative Templates for Universal Mining * * This file is part of OCEAN's Bitcoin mining decentralization * project, DATUM. * * https://ocean.xyz * * --- * * Copyright (c) 2024-2025 Bitcoin Ocean, LLC & Jason Hughes * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ #ifndef _DATUM_CONF_H_ #define _DATUM_CONF_H_ #define DATUM_CONFIG_MAX_ARRAY_ENTRIES 32 #define DATUM_MAX_BLOCK_SUBMITS DATUM_CONFIG_MAX_ARRAY_ENTRIES #define DATUM_MAX_SUBMIT_URL_LEN 512 #include #include #include enum datum_conf_vartype { // NOTE: Keep in sync with datum_conf_var_type_text DATUM_CONF_BOOL, DATUM_CONF_INT, DATUM_CONF_STRING, DATUM_CONF_STRING_ARRAY, DATUM_CONF_USERNAME_MODS, }; typedef struct { char category[32]; char name[64]; char description[512]; const char *example; bool example_default; enum datum_conf_vartype var_type; union { int default_int; bool default_bool; struct { int max_string_len; const char *default_string[DATUM_CONFIG_MAX_ARRAY_ENTRIES]; }; }; void *ptr; bool required; } T_DATUM_CONFIG_ITEM; const T_DATUM_CONFIG_ITEM *datum_config_get_option_info(const char *category, size_t category_len, const char *name, size_t name_len); const T_DATUM_CONFIG_ITEM *datum_config_get_option_info2(const char *category, const char *name); struct datum_addr_range { char *addr; size_t addr_len; uint16_t max; }; struct datum_username_mod { size_t sz; char *modname; size_t modname_len; struct datum_addr_range ranges[]; }; int datum_config_parse_username_mods(struct datum_username_mod **umods_p, json_t *item, bool log_errors); struct datum_username_mod *datum_username_mods_next(struct datum_username_mod *prev_umod); struct datum_username_mod *datum_username_mods_find(struct datum_username_mod *umod, const char *modname, size_t modname_len); // Globally accessable config options typedef struct { char bitcoind_rpcuserpass[256]; char bitcoind_rpccookiefile[1024]; char bitcoind_rpcuser[128]; char bitcoind_rpcpassword[128]; char bitcoind_rpcurl[256]; int bitcoind_work_update_seconds; bool bitcoind_notify_fallback; char stratum_v1_listen_addr[128]; int stratum_v1_listen_port; int stratum_v1_max_clients; int stratum_v1_max_threads; int stratum_v1_max_clients_per_thread; int stratum_v1_trust_proxy; int stratum_v1_vardiff_min; int stratum_v1_vardiff_target_shares_min; int stratum_v1_vardiff_quickdiff_count; int stratum_v1_vardiff_quickdiff_delta; int stratum_v1_share_stale_seconds; bool stratum_v1_fingerprint_miners; int stratum_v1_idle_timeout_no_subscribe; int stratum_v1_idle_timeout_no_share; int stratum_v1_idle_timeout_max_last_work; void *stratum_username_mod; char mining_pool_address[256]; char mining_coinbase_tag_primary[64]; char mining_coinbase_tag_secondary[64]; char mining_save_submitblocks_dir[256]; int coinbase_unique_id; char api_admin_password[72]; size_t api_admin_password_len; char api_csrf_token[65]; char api_listen_addr[128]; int api_listen_port; bool api_allow_insecure_auth; bool api_modify_conf; json_t *config_json; int extra_block_submissions_count; char extra_block_submissions_urls[DATUM_MAX_BLOCK_SUBMITS][DATUM_MAX_SUBMIT_URL_LEN]; bool clog_to_file; bool clog_to_console; int clog_level_console; int clog_level_file; bool clog_calling_function; bool clog_to_stderr; bool clog_rotate_daily; char clog_file[1024]; char datum_pool_host[1024]; int datum_pool_port; bool datum_pool_pass_workers; bool datum_pool_pass_full_users; bool datum_always_pay_self; bool datum_pooled_mining_only; char datum_pool_pubkey[1024]; int datum_protocol_global_timeout; uint64_t datum_protocol_global_timeout_ms; uint32_t prime_id; unsigned char override_mining_pool_scriptsig[256]; int override_mining_pool_scriptsig_len; char override_mining_coinbase_tag_primary[256]; uint64_t override_vardiff_min; } global_config_t; extern global_config_t datum_config; int datum_read_config(const char *conffile); void datum_gateway_help(const char *argv0); void datum_gateway_example_conf(void); #endif datum_gateway-0.4.1beta/src/datum_conf_tests.c000066400000000000000000000167311512710151500215050ustar00rootroot00000000000000/* * * DATUM Gateway * Decentralized Alternative Templates for Universal Mining * * This file is part of OCEAN's Bitcoin mining decentralization * project, DATUM. * * https://ocean.xyz * * --- * * Copyright (c) 2025 Bitcoin Ocean, LLC & Luke Dashjr * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ #include #include #include "datum_conf.h" #include "datum_jsonrpc.h" #include "datum_utils.h" struct datum_test_username_mods_range { const char *addr; uint16_t max; }; struct datum_test_username_mods { const char *modname; struct datum_test_username_mods_range *ranges; }; void datum_conf_test_parse_username_mods__(const unsigned int code_line, json_t * const j_input, const char * const input, const int expected_ret, const struct datum_test_username_mods *expected_umods) { struct datum_username_mod *umods = NULL; int ret = datum_config_parse_username_mods(&umods, j_input, false); json_decref(j_input); datum_test_(ret == expected_ret, input, code_line, "return value"); if (ret != 1) { assert(!umods); assert(!expected_umods); return; } datum_test_(!datum_username_mods_find(umods, "MISSING", 7), input, code_line, "modname search: non-existent"); struct datum_username_mod *umod = umods; while (expected_umods && expected_umods->modname) { if (!datum_test_(umod, input, code_line, "premature end of result umods")) { break; } if (datum_test_(strlen(expected_umods->modname) == umod->modname_len, input, code_line, "modname length")) { datum_test_(strncmp(expected_umods->modname, umod->modname, umod->modname_len) == 0, input, code_line, "modname content"); } datum_test_(datum_username_mods_find(umods, expected_umods->modname, umod->modname_len) == umod, input, code_line, "modname search"); struct datum_addr_range *range = umod->ranges; assert(range); char *last_p = &umod->modname[umod->modname_len]; for (struct datum_test_username_mods_range *expected_range = expected_umods->ranges; expected_range->addr; ++expected_range) { if (!datum_test_(range->addr, input, code_line, "premature end of range list")) { break; } datum_test_(expected_range->max == range->max, input, code_line, "range max"); if (datum_test_(strlen(expected_range->addr) == range->addr_len, input, code_line, "range addr length")) { datum_test_(strncmp(expected_range->addr, range->addr, range->addr_len) == 0, input, code_line, "range addr content"); datum_test_(range->addr[range->addr_len] == '\0', input, code_line, "range addr trailing null"); datum_test_(range->addr == last_p, input, code_line, "expected range addr location"); last_p = &range->addr[range->addr_len + 1]; } ++range; } datum_test_(!range->addr, input, code_line, "extra range"); datum_test_(umod->sz == datum_align_sz((uint8_t*)last_p - (uint8_t*)umod, _Alignof(struct datum_username_mod)), input, code_line, "umod sz mismatch"); umod = datum_username_mods_next(umod); ++expected_umods; } datum_test_(!umod, input, code_line, "extra result umods"); free(umods); } void datum_conf_test_parse_username_mods_(const unsigned int code_line, const char * const input, const int expected_ret, const struct datum_test_username_mods *expected_umods) { json_error_t err; json_t * const j_input = JSON_LOADS(input, &err); assert(j_input); datum_conf_test_parse_username_mods__(code_line, j_input, input, expected_ret, expected_umods); } #define datum_conf_test_parse_username_mods(...) datum_conf_test_parse_username_mods_(__LINE__, __VA_ARGS__) #define MODS (struct datum_test_username_mods[]) #define RANGES (struct datum_test_username_mods_range[]) #define NO_RANGES_AT_ALL RANGES{{NULL}} void datum_conf_username_mods_tests() { datum_conf_test_parse_username_mods__(__LINE__, json_null(), "null", 1, NULL); datum_conf_test_parse_username_mods("{}", 1, NULL); datum_conf_test_parse_username_mods("[]", -1, NULL); datum_conf_test_parse_username_mods("{\"y\":[]}", -1, NULL); datum_conf_test_parse_username_mods("{\"y\":42}", -1, NULL); datum_conf_test_parse_username_mods("{\"y\":\"z\"}", -1, NULL); datum_conf_test_parse_username_mods("{\"y\":true}", -1, NULL); datum_conf_test_parse_username_mods("{\"y\":false}", -1, NULL); datum_conf_test_parse_username_mods("{\"z\":{\"a\":[]}}", -1, NULL); datum_conf_test_parse_username_mods("{\"z\":{\"a\":{}}}", -1, NULL); datum_conf_test_parse_username_mods("{\"z\":{\"a\":\"z\"}}", -1, NULL); datum_conf_test_parse_username_mods("{\"z\":{\"a\":true}}", -1, NULL); datum_conf_test_parse_username_mods("{\"z\":{\"a\":false}}", -1, NULL); datum_conf_test_parse_username_mods("{\"x\":null}", 1, MODS{ {NULL} }); datum_conf_test_parse_username_mods("{\"x\":{}}", 1, MODS{ {"x", NO_RANGES_AT_ALL}, {NULL} }); datum_conf_test_parse_username_mods("{\"x\":{\"addr\": 0}}", 1, MODS{ {"x", NO_RANGES_AT_ALL}, {NULL}, }); datum_conf_test_parse_username_mods("{\"x\":{\"addr\": 0.00001}}", 1, MODS{ {"x", RANGES{{"addr", 0}, {NULL}}}, {NULL}, }); datum_conf_test_parse_username_mods("{\"x\":{\"addr\": 1}}", 1, MODS{ {"x", RANGES{{"addr", 0xffff}, {NULL}}}, {NULL} }); datum_conf_test_parse_username_mods("{\"x\":{\"addr\": -1}}", -1, NULL); datum_conf_test_parse_username_mods("{\"x\":{\"addr\": 2.3}}", 1, MODS{ {"x", RANGES{{"addr", 0xffff}, {NULL}}}, {NULL} }); datum_conf_test_parse_username_mods("{\"x\":{\"addrA\": 0.3, \"addrB\":0.7}}", 1, MODS{ {"x", RANGES{ {"addrA", 0x4ccc}, {"addrB", 0xffff}, {NULL} }}, {NULL} }); datum_conf_test_parse_username_mods("{\"x\":{\"addrA\": 0.3, \"addrB\":0.3,\"addrC\":0.3}}", 1, MODS{ {"x", RANGES{ {"addrA", 0x4ccc}, {"addrB", 0x9999}, {"addrC", 0xe666}, {NULL} }}, {NULL} }); datum_conf_test_parse_username_mods("{\"x\":{\"addrA\": 0.3, \"\":0.3,\"addrC\":0.3}}", 1, MODS{ {"x", RANGES{ {"addrA", 0x4ccc}, {"", 0x9999}, {"addrC", 0xe666}, {NULL} }}, {NULL} }); datum_conf_test_parse_username_mods("{\"x\":{\"addrA\": 0.3, \"\":null,\"addrC\":0.3}}", 1, MODS{ {"x", RANGES{ {"addrA", 0x4ccc}, {"addrC", 0x9999}, {NULL} }}, {NULL} }); datum_conf_test_parse_username_mods("{\"x\":{\"addrA\": null, \"\":null,\"addrC\":null}}", 1, MODS{ {"x", NO_RANGES_AT_ALL}, {NULL} }); datum_conf_test_parse_username_mods("{\"x\":{\"addrA\": 0.3}, \"abc\":{\"addrB\":0.3,\"addrC\":0.3}}", 1, MODS{ {"x", RANGES{ {"addrA", 0x4ccc}, {NULL} }}, {"abc", RANGES{ {"addrB", 0x4ccc}, {"addrC", 0x9999}, {NULL} }}, {NULL} }); } void datum_conf_tests(void) { datum_conf_username_mods_tests(); } datum_gateway-0.4.1beta/src/datum_gateway.c000066400000000000000000000211761512710151500207760ustar00rootroot00000000000000/* * * DATUM Gateway * Decentralized Alternative Templates for Universal Mining * * This file is part of OCEAN's Bitcoin mining decentralization * project, DATUM. * * https://ocean.xyz * * --- * * Copyright (c) 2024-2025 Bitcoin Ocean, LLC & Jason Hughes * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ // NOTE: Everything about this software assumes compilation for little endian underlying hardware operations. // This *will* break on big endian hardware and not perform expected operations correctly. #include #include #include #include #include #include #include #include #include #include #include #include #include #include "datum_gateway.h" #include "datum_jsonrpc.h" #include "datum_utils.h" #include "datum_blocktemplates.h" #include "datum_stratum.h" #include "datum_conf.h" #include "datum_sockets.h" #include "datum_api.h" #include "datum_coinbaser.h" #include "datum_protocol.h" const char *datum_gateway_config_filename = NULL; // ARGP stuff const char *argp_program_version = "datum_gateway " DATUM_PROTOCOL_VERSION; const char *argp_program_bug_address = ""; static char doc[] = "Decentralized Alternative Templates for Universal Mining - Pool Gateway"; static char args_doc[] = ""; static struct argp_option options[] = { {"help", '?', 0, 0, "Show custom help", 0}, {"example-conf", 0x100, NULL, 0, "Print an example configuration JSON file", 0}, {"test", 0x101, NULL, 0, "Run tests only", 0}, {"usage", '?', 0, 0, "Show custom help", 0}, {"config", 'c', "FILE", 0, "Configuration JSON file"}, {0} }; struct arguments { char *config_file; }; void datum_stratum_tests(void); void datum_conf_tests(void); void datum_utils_tests(void); static error_t parse_opt(int key, char *arg, struct argp_state *state) { struct arguments *arguments = state->input; switch (key) { case '?': { datum_print_banner(); datum_gateway_help(state->argv[0]); exit(0); break; } case 'c': { arguments->config_file = arg; break; } case 0x100: // example-conf datum_gateway_example_conf(); exit(0); break; case 0x101: // test datum_utils_tests(); datum_conf_tests(); datum_stratum_tests(); exit(datum_test_failed); default: return ARGP_ERR_UNKNOWN; } return 0; } static struct argp argp = {options, parse_opt, args_doc, doc}; // END ARGP Stuff void datum_print_banner(void) { puts(""); puts(" *****************************************************************"); puts(" * DATUM Gateway --- Copyright (c) 2024-2025 Bitcoin Ocean, LLC, *"); puts(" * Jason Hughes, and individual contributors *"); printf(" * git commit: %-49s *\n", GIT_COMMIT_HASH); puts(" *****************************************************************"); puts(""); fflush(stdout); } void handle_sigusr1(int sig) { datum_blocktemplates_notifynew_sighandler(); } const char * const *datum_argv; int main(const int argc, const char * const * const argv) { datum_argv = argv; struct arguments arguments; pthread_t pthread_datum_stratum_v1; pthread_t pthread_datum_gateway_template; int i; int fail_retries=0; struct sigaction sa; uint64_t last_datum_protocol_connect_tsms = 0; bool rejecting_stratum = false; uint32_t next_reconnect_attempt_ms = 5000; // listen for block notifications // set this up early so a notification doesn't break our init sa.sa_handler = handle_sigusr1; sa.sa_flags = 0; sigemptyset(&sa.sa_mask); if (sigaction(SIGUSR1, &sa, NULL) == -1) { datum_print_banner(); DLOG_FATAL("Could not setup signal handler!"); perror("sigaction"); usleep(100000); exit(1); } // Ignore SIGPIPE. This is instead handled gracefully by datum_sockets signal(SIGPIPE, SIG_IGN); srand(time(NULL)); // Not used for anything secure, so this is fine. curl_global_init(CURL_GLOBAL_ALL); datum_utils_init(); arguments.config_file = "datum_gateway_config.json"; // Default config file if (argp_parse(&argp, argc, datum_deepcopy_charpp(argv), 0, 0, &arguments) != 0) { datum_print_banner(); DLOG_FATAL("Error parsing arguments. Check --help"); exit(1); } datum_print_banner(); if (datum_read_config(arguments.config_file) != 1) { DLOG_FATAL("Error reading config file. Check --help"); exit(1); } datum_gateway_config_filename = arguments.config_file; // Initialize logger thread datum_logger_init(); if (datum_protocol_init()) { DLOG_FATAL("Error initializing the DATUM protocol!"); usleep(100000); exit(1); } last_datum_protocol_connect_tsms = current_time_millis(); #ifdef ENABLE_API if (datum_api_init()) { DLOG_FATAL("Error initializing API interface"); usleep(100000); exit(1); } #endif if (datum_coinbaser_init()) { DLOG_FATAL("Error initializing coinbaser thread"); usleep(100000); exit(1); } // Try to connect to the DATUM server, if setup to do so. if (datum_config.datum_pool_host[0] != 0) { while((current_time_millis()-15000 < last_datum_protocol_connect_tsms) && (!datum_protocol_is_active())) { DLOG_INFO("Waiting on DATUM server... %d", (int)((last_datum_protocol_connect_tsms-(current_time_millis()-15000))/1000)); sleep(1); if ((datum_config.datum_pool_host[0] != 0) && (!datum_protocol_thread_is_active())) { datum_protocol_start_connector(); } } } // TODO: Churn and continue to try and connect while leaving the Stratum server down if pooled mining only if (datum_config.datum_pooled_mining_only && (!datum_protocol_is_active())) { DLOG_ERROR("DATUM server connection could not be established."); fflush(stdout); } DLOG_DEBUG("Starting template fetcher thread"); pthread_create(&pthread_datum_gateway_template, NULL, datum_gateway_template_thread, NULL); // Note: The stratum thread will wait for a template to be available for some time before panicking. DLOG_DEBUG("Starting Stratum v1 server"); pthread_create(&pthread_datum_stratum_v1, NULL, datum_stratum_v1_socket_server, NULL); // Randomize the reconnect delay from 5 to 20 seconds to prevent hammering the server next_reconnect_attempt_ms = ( 5000 + (rand() % 15001) ); i=0; while(1) { if (panic_mode) { DLOG_FATAL("*** PANIC TRIGGERED: EXITING IMMEDIATELY ***"); printf("PANIC EXIT.\n"); sleep(1); // almost immediately, wait a second for the logger! fflush(stdout); usleep(2000); exit(1); } usleep(500000); i++; if (i>=600) { // Roughly every 5 minutes spit out some stats to the log i = datum_stratum_v1_global_subscriber_count(); DLOG_INFO("Server stats: %d client%s / %.2f Th/s", i, (i!=1)?"s":"", datum_stratum_v1_est_total_th_sec()); i=0; } if (fail_retries > 0) { if (datum_protocol_is_active()) { fail_retries = 0; } } if (datum_config.datum_pooled_mining_only && (fail_retries >= 2) && (!datum_protocol_is_active())) { if (!rejecting_stratum) { DLOG_WARN("Configured for pooled mining only, and connection lost to DATUM server! Shutting down Stratum v1 server until DATUM connection reestablished."); rejecting_stratum = true; datum_stratum_v1_shutdown_all(); } } else { rejecting_stratum = false; } if ((datum_config.datum_pool_host[0] != 0) && (!datum_protocol_thread_is_active())) { // DATUM thread is dead, and it shouldn't be. if (last_datum_protocol_connect_tsms < (current_time_millis()-next_reconnect_attempt_ms)) { datum_protocol_start_connector(); last_datum_protocol_connect_tsms = current_time_millis(); fail_retries++; // Randomize the reconnect delay from 5 to 20 seconds to prevent hammering the server next_reconnect_attempt_ms = ( 5000 + (rand() % 15001) ); } } } return 0; } datum_gateway-0.4.1beta/src/datum_gateway.h000066400000000000000000000040431512710151500207750ustar00rootroot00000000000000/* * * DATUM Gateway * Decentralized Alternative Templates for Universal Mining * * This file is part of OCEAN's Bitcoin mining decentralization * project, DATUM. * * https://ocean.xyz * * --- * * Copyright (c) 2024 Bitcoin Ocean, LLC & Jason Hughes * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ #ifndef _DATUM_GATEWAY_H_ #define _DATUM_GATEWAY_H_ #include "git_version.h" #ifndef GIT_COMMIT_HASH #define GIT_COMMIT_HASH "UNKNOWN_GIT_HASH" #endif // For SV1 // client buffer must be large enough to hold entire coinbase in hex at max size // TODO: Make somewhat more dynamic without having to hammer [cm]alloc #define CLIENT_BUFFER ((16384*3)+1024) // in ascii hex #define STRATUM_COINBASE1_MAX_LEN 1024 #define STRATUM_COINBASE2_MAX_LEN 32768 #define MAX_COINBASE_TXN_SIZE_BYTES (((STRATUM_COINBASE1_MAX_LEN+STRATUM_COINBASE2_MAX_LEN)>>1)+64) #define STRATUM_JOB_INDEX_XOR ((uint16_t)0xC0DE) void datum_print_banner(void); extern const char *datum_gateway_config_filename; extern const char * const *datum_argv; #endif datum_gateway-0.4.1beta/src/datum_jsonrpc.c000066400000000000000000000201041512710151500210010ustar00rootroot00000000000000/* * * DATUM Gateway * Decentralized Alternative Templates for Universal Mining * * This file is part of OCEAN's Bitcoin mining decentralization * project, DATUM. * * https://ocean.xyz * * --- * * Copyright (c) 2024-2025 Bitcoin Ocean, LLC & Jason Hughes * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ #include #include #include #include #include #include #include #include #include "datum_conf.h" #include "datum_jsonrpc.h" #include "datum_utils.h" // TODO: Clean this up. Most of this is very old code from other parts of Eligius/OCEAN internal tools and needs // a solid makeover. // However, it's all quite functional, so not a top priority. static void databuf_free(struct data_buffer *db) { if (!db) { return; } free(db->buf); memset(db, 0, sizeof(*db)); } static size_t all_data_cb(const void *ptr, size_t size, size_t nmemb, void *user_data) { struct data_buffer *db = user_data; size_t len, oldlen, newlen; void *newmem; if (SIZE_MAX / size < nmemb) abort(); len = size * nmemb; oldlen = db->len; if (SIZE_MAX - oldlen < len) abort(); newlen = oldlen + len; newmem = realloc(db->buf, newlen + 1); if (!newmem) { return 0; } db->buf = newmem; db->len = newlen; memcpy(&((char *)db->buf)[oldlen], ptr, len); ((char *)db->buf)[newlen] = 0; return len; } static size_t upload_data_cb(void *ptr, size_t size, size_t nmemb, void *user_data) { struct upload_buffer *ub = user_data; size_t len; if (SIZE_MAX / size < nmemb) nmemb = SIZE_MAX / size; len = size * nmemb; if (len > ub->len) len = ub->len; if (len) { memcpy(ptr, ub->buf, len); ub->buf = &((const char *)ub->buf)[len]; ub->len -= len; } return len; } char *basic_http_call(CURL *curl, const char *url) { CURLcode rc; struct data_buffer all_data = { }; char curl_err_str[CURL_ERROR_SIZE]; char *out; curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_ENCODING, ""); curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L); curl_easy_setopt(curl, CURLOPT_TCP_NODELAY, 1L); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, all_data_cb); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &all_data); curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_err_str); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 5L); // quick timeout! curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L); // quick timeout! curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); rc = curl_easy_perform(curl); if (rc) { DLOG_DEBUG("HTTP request failed: %s", curl_err_str); goto err_out; } out = calloc(strlen(all_data.buf)+20,1); if (!out) goto err_out; strcpy(out, all_data.buf); databuf_free(&all_data); curl_easy_reset(curl); return out; err_out: databuf_free(&all_data); curl_easy_reset(curl); return NULL; } json_t *json_rpc_call_full(CURL *curl, const char *url, const char *userpass, const char *rpc_req, const char *extra_header, long * const http_resp_code_out) { json_t *val, *err_val, *res_val; CURLcode rc; struct data_buffer all_data = { }; struct upload_buffer upload_data; json_error_t err = { }; struct curl_slist *headers = NULL; char len_hdr[64]; char curl_err_str[CURL_ERROR_SIZE]; bool check_for_result = true; curl_easy_setopt(curl, CURLOPT_URL, url); curl_easy_setopt(curl, CURLOPT_ENCODING, ""); curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L); curl_easy_setopt(curl, CURLOPT_TCP_NODELAY, 1L); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, all_data_cb); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &all_data); curl_easy_setopt(curl, CURLOPT_READFUNCTION, upload_data_cb); curl_easy_setopt(curl, CURLOPT_READDATA, &upload_data); curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_err_str); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 5L); // quick timeout! curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L); // quick timeout! curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); if (userpass) { curl_easy_setopt(curl, CURLOPT_USERPWD, userpass); curl_easy_setopt(curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); } curl_easy_setopt(curl, CURLOPT_POST, 1L); upload_data.buf = rpc_req; upload_data.len = strlen(rpc_req); sprintf(len_hdr, "Content-Length: %lu",(unsigned long) upload_data.len); headers = curl_slist_append(headers, "Content-type: application/json"); headers = curl_slist_append(headers, len_hdr); headers = curl_slist_append(headers, "Expect:"); if (extra_header) { headers = curl_slist_append(headers, extra_header); check_for_result = false; } curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); rc = curl_easy_perform(curl); if (rc) { if (http_resp_code_out) curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, http_resp_code_out); DLOG_DEBUG("json_rpc_call: HTTP request failed: %s", curl_err_str); DLOG_DEBUG("json_rpc_call: Request was: %s",rpc_req); goto err_out; } val = JSON_LOADS(all_data.buf, &err); if (!val) { DLOG_DEBUG("JSON decode failed(%d): %s", err.line, err.text); goto err_out; } if (check_for_result) { res_val = json_object_get(val, "result"); err_val = json_object_get(val, "error"); if (!res_val || json_is_null(res_val) || (err_val && !json_is_null(err_val))) { char *s; if (err_val) { s = json_dumps(err_val, JSON_INDENT(3)); } else { s = strdup("(unknown reason)"); } DLOG_DEBUG("JSON-RPC call failed: %s", s); free(s); goto err_out; } } databuf_free(&all_data); curl_slist_free_all(headers); curl_easy_reset(curl); return val; err_out: databuf_free(&all_data); curl_slist_free_all(headers); curl_easy_reset(curl); return NULL; } json_t *json_rpc_call(CURL *curl, const char *url, const char *userpass, const char *rpc_req) { return json_rpc_call_full(curl, url, userpass, rpc_req, NULL, NULL); } bool update_rpc_cookie(global_config_t * const cfg) { assert(!cfg->bitcoind_rpcuser[0]); FILE * const F = fopen(cfg->bitcoind_rpccookiefile, "r"); if (!F) { DLOG_ERROR("Cannot %s cookie file %s", "open", datum_config.bitcoind_rpccookiefile); return false; } if (!(fgets(cfg->bitcoind_rpcuserpass, sizeof(cfg->bitcoind_rpcuserpass), F) && cfg->bitcoind_rpcuserpass[0])) { DLOG_ERROR("Cannot %s cookie file %s", "read", datum_config.bitcoind_rpccookiefile); return false; } return true; } void update_rpc_auth(global_config_t * const cfg) { if (datum_config.bitcoind_rpccookiefile[0] && !cfg->bitcoind_rpcuser[0]) { update_rpc_cookie(cfg); } else { snprintf(datum_config.bitcoind_rpcuserpass, sizeof(datum_config.bitcoind_rpcuserpass), "%s:%s", datum_config.bitcoind_rpcuser, datum_config.bitcoind_rpcpassword); } } json_t *bitcoind_json_rpc_call(CURL * const curl, global_config_t * const cfg, const char * const rpc_req) { long http_resp_code = -1; json_t *j = json_rpc_call_full(curl, cfg->bitcoind_rpcurl, cfg->bitcoind_rpcuserpass, rpc_req, NULL, &http_resp_code); if (j) return j; if (cfg->bitcoind_rpcuser[0]) return NULL; if (http_resp_code != 401) return NULL; // Authentication failure using cookie; reload cookie file and try again if (!update_rpc_cookie(cfg)) return NULL; return json_rpc_call(curl, cfg->bitcoind_rpcurl, cfg->bitcoind_rpcuserpass, rpc_req); } datum_gateway-0.4.1beta/src/datum_jsonrpc.h000066400000000000000000000044571512710151500210230ustar00rootroot00000000000000/* * * DATUM Gateway * Decentralized Alternative Templates for Universal Mining * * This file is part of OCEAN's Bitcoin mining decentralization * project, DATUM. * * https://ocean.xyz * * --- * * Copyright (c) 2024 Bitcoin Ocean, LLC & Jason Hughes * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ #ifndef _DATUM_JSONRPC_H_ #define _DATUM_JSONRPC_H_ #include #include #include "datum_conf.h" #ifndef JSON_INTEGER_IS_LONG_LONG # error "Jansson 2.0 with long long support required!" #endif #if JANSSON_MAJOR_VERSION >= 2 #define JSON_LOADS(str, err_ptr) json_loads((str), 0, (err_ptr)) #else #define JSON_LOADS(str, err_ptr) json_loads((str), (err_ptr)) #endif // Legacy functions #define bitcoin_rpc_url global_config.bitcoind_url #define bitcoin_rpc_userpass global_config.bitcoind_userpass struct data_buffer { void *buf; size_t len; }; struct upload_buffer { const void *buf; size_t len; }; json_t *json_rpc_call(CURL *curl, const char *url, const char *userpass, const char *rpc_req); char *basic_http_call(CURL *curl, const char *url); bool update_rpc_cookie(global_config_t *cfg); void update_rpc_auth(global_config_t *cfg); json_t *bitcoind_json_rpc_call(CURL *curl, global_config_t *cfg, const char *rpc_req); #endif datum_gateway-0.4.1beta/src/datum_logger.c000066400000000000000000000351541512710151500206150ustar00rootroot00000000000000/* * * DATUM Gateway * Decentralized Alternative Templates for Universal Mining * * This file is part of OCEAN's Bitcoin mining decentralization * project, DATUM. * * https://ocean.xyz * * --- * * Copyright (c) 2024-2025 Bitcoin Ocean, LLC & Jason Hughes * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ // Multithread-safe logger // TODO: Add additional output types such as for system logging. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "datum_logger.h" #include "datum_utils.h" const char *level_text[] = { " ALL", "DEBUG", " INFO", " WARN", "ERROR", "FATAL" }; volatile bool datum_logger_initialized = false; volatile bool log_reopen_signal = false; // configurable options bool log_to_file = false; bool log_to_console = true; int log_level_console = DLOG_LEVEL_INFO; int log_level_file = DLOG_LEVEL_ALL; bool log_calling_function = true; bool log_to_stderr = false; bool log_rotate_daily = true; char log_file[1024] = { 0 }; int dlog_queue_max_entries = 0; int msg_buf_maxsz = DLOG_MSG_BUF_SIZE; pthread_rwlock_t dlog_active_buffer_rwlock = PTHREAD_RWLOCK_INITIALIZER; int dlog_active_buffer = 0; uint64_t dlog_active_buffer_version = 0; pthread_rwlock_t dlog_buffer_rwlock[2] = { PTHREAD_RWLOCK_INITIALIZER, PTHREAD_RWLOCK_INITIALIZER }; DLOG_MSG *dlog_queue[2]; int dlog_queue_next[2] = { 0, 0 }; uint64_t dlog_queue_version[2] = { 0, 10 }; int msg_buf_idx[2] = { 0, 0 }; char *msg_buffer[2] = { NULL, NULL }; void datum_logger_config( bool clog_to_file, bool clog_to_console, int clog_level_console, int clog_level_file, bool clog_calling_function, bool clog_to_stderr, bool clog_rotate_daily, char *clog_file ) { log_to_file = clog_to_file; log_to_console = clog_to_console; log_level_console = clog_level_console; log_level_file = clog_level_file; log_calling_function = clog_calling_function; log_to_stderr = clog_to_stderr; log_rotate_daily = clog_rotate_daily; strncpy(log_file, clog_file, 1023); log_file[1023] = 0; if (log_level_console < 0) log_level_console = 0; if (log_level_console > DLOG_LEVEL_FATAL) log_level_console = DLOG_LEVEL_FATAL; if (log_level_file < 0) log_level_file = 0; if (log_level_file > DLOG_LEVEL_FATAL) log_level_file = DLOG_LEVEL_FATAL; } int datum_logger_queue_msg(const char *func, int level, const char *format, ...) { int buffer_id, i; uint64_t buffer_version; DLOG_MSG *msg = NULL; uint64_t tsms; va_list args; struct timeval tv; struct tm tm_info; char time_buffer[20]; if ((level < log_level_console) && (level < log_level_file)) { return 0; } // get timestamp before messing with locks and such gettimeofday(&tv, NULL); tsms = (tv.tv_sec * 1000LL) + (tv.tv_usec / 1000LL); if (level > 5) level = 5; if (level < 0) level = 0; if (__builtin_expect(!datum_logger_initialized,0)) { // not initialized yet, so we're just going to print this to console with default settings if ((log_to_console) && (level >= log_level_console)) { va_start(args, format); localtime_r(&tv.tv_sec, &tm_info); strftime(time_buffer, sizeof(time_buffer), "%Y-%m-%d %H:%M:%S", &tm_info); if (log_calling_function) { fprintf(log_to_stderr?stderr:stdout, "%s.%03ld [%44s] %s: ", time_buffer, tv.tv_usec / 1000, func, level_text[level]); } else { fprintf(log_to_stderr?stderr:stdout, "%s.%03ld %s: ", time_buffer, tv.tv_usec / 1000, level_text[level]); } vfprintf(log_to_stderr?stderr:stdout, format, args); fprintf(log_to_stderr?stderr:stdout, "\n"); va_end(args); } return 0; } // Add the msg to the logger queue // this is probably overkill... for (i=0;i<10000000;i++) { if (i < 99999990) { // ensure we don't get the lock on the last try and forget to unlock and crash // get the active buffer ID pthread_rwlock_rdlock(&dlog_active_buffer_rwlock); buffer_id = dlog_active_buffer; buffer_version = dlog_active_buffer_version; pthread_rwlock_unlock(&dlog_active_buffer_rwlock); // get a write lock for that buffer pthread_rwlock_wrlock(&dlog_buffer_rwlock[buffer_id]); // check for race condition on buffer swap if (buffer_version != dlog_queue_version[buffer_id]) { // Race condition! pthread_rwlock_unlock(&dlog_buffer_rwlock[buffer_id]); } else { // no race condition, we're good break; } } } if (i >= 10000000) { // we have no locks, but we also couldn't sync up on the rare race condition after 10000000 attempts // means something very bad is probably happening panic_from_thread(__LINE__); return -1; } if (dlog_queue_next[buffer_id] >= dlog_queue_max_entries) { // TODO: Delay quickly, hope the writer thread catches up before panicking pthread_rwlock_unlock(&dlog_buffer_rwlock[buffer_id]); panic_from_thread(__LINE__); return -1; } // store the log data! msg = &dlog_queue[buffer_id][dlog_queue_next[buffer_id]]; // Construct and store the msg.... memset(msg, 0, sizeof(DLOG_MSG)); msg->level = level; msg->tsms = tsms; strncpy(msg->calling_function, func, 47); msg->calling_function[47] = 0; msg->msg = &msg_buffer[buffer_id][msg_buf_idx[buffer_id]]; va_start(args, format); i = vsnprintf(msg->msg, 1023, format, args); // clamp i to actual written value in order not to waste buffer space if (i >= 1023) { i = 1022; } va_end(args); if (((msg_buf_idx[buffer_id]+i+2) > msg_buf_maxsz) || (dlog_queue_next[buffer_id] >= dlog_queue_max_entries)) { // this is ok, since we overallocate by more than 1KB on purpose // but not great for logging! // we won't bump things, so the next line to the logger will overwrite this one pthread_rwlock_unlock(&dlog_buffer_rwlock[buffer_id]); if ((log_to_console) && (level >= log_level_console)) { localtime_r(&tv.tv_sec, &tm_info); strftime(time_buffer, sizeof(time_buffer), "%Y-%m-%d %H:%M:%S", &tm_info); if (log_calling_function) { fprintf(log_to_stderr?stderr:stdout, "LOGGER OVERRUN:%s.%03ld [%44s] %s: %s\n", time_buffer, tv.tv_usec / 1000, func, level_text[level], msg->msg); } else { fprintf(log_to_stderr?stderr:stdout, "LOGGER OVERRUN:%s.%03ld %s: %s\n", time_buffer, tv.tv_usec / 1000, level_text[level], msg->msg); } } return -1; } msg_buf_idx[buffer_id] += i+2; // increment the index dlog_queue_next[buffer_id]++; // bounds check is above, since we can potentially delay to wait for the writer instead of failing here pthread_rwlock_unlock(&dlog_buffer_rwlock[buffer_id]); return 0; } time_t get_midnight_timestamp(void) { time_t now = time(NULL); struct tm tm_now; localtime_r(&now, &tm_now); tm_now.tm_hour = 0; tm_now.tm_min = 0; tm_now.tm_sec = 0; tm_now.tm_mday += 1; time_t midnight = mktime(&tm_now); return midnight; } void * datum_logger_thread(void *ptr) { int buffer_id,offline_buffer_id; int i,j; uint64_t sts,ets,lflush; time_t seconds; int millis; struct tm tm_info_storage; struct tm *tm_info; DLOG_MSG *msg; char time_buffer[20]; char log_line[1200]; FILE *log_handle = NULL; time_t next_log_rotate = get_midnight_timestamp(); time_t log_file_opened = time(NULL); msg_buffer[0] = calloc((DLOG_MSG_BUF_SIZE * 2) + (1024*8),1); if (!msg_buffer[0]) { DLOG(DLOG_LEVEL_FATAL, "Could not allocate memory for logger queue!"); panic_from_thread(__LINE__); } // split the allocation in half for the double buffering msg_buffer[1] = &msg_buffer[0][DLOG_MSG_BUF_SIZE + (1024*4)]; dlog_queue_max_entries = (DLOG_MSG_BUF_SIZE / sizeof(DLOG_MSG)) - 1; if (dlog_queue_max_entries < 1024) dlog_queue_max_entries = 1024; dlog_queue[0] = calloc(dlog_queue_max_entries * 2 * sizeof(DLOG_MSG),1); if (!dlog_queue[0]) { DLOG(DLOG_LEVEL_FATAL, "Could not allocate memory for logger queue list!"); panic_from_thread(__LINE__); } dlog_queue[1] = &dlog_queue[0][dlog_queue_max_entries]; if ((log_to_file) && (log_file[0] != 0)) { log_handle = fopen(log_file,"a"); if (!log_handle) { DLOG(DLOG_LEVEL_FATAL, "Could not open log file (%s): %s!", log_file, strerror(errno)); panic_from_thread(__LINE__); } } // alert the masses. datum_logger_initialized = true; DLOG(DLOG_LEVEL_DEBUG, "Logging thread started! (Approximately %d MB of RAM allocated for up to %d entries per cycle)", (DLOG_MSG_BUF_SIZE * 4)/1024/1024, dlog_queue_max_entries); lflush = 0; while(1) { sts = current_time_micros(); // wait for there to be msgs to log, and log them! // We don't need to read lock to read this, as we're the only thread that writes to it. buffer_id = dlog_active_buffer; pthread_rwlock_rdlock(&dlog_buffer_rwlock[buffer_id]); i = dlog_queue_next[buffer_id]; pthread_rwlock_unlock(&dlog_buffer_rwlock[buffer_id]); if (i) { // there are msgs to write. // switch the writers over to the other buffer, and then work on that // TODO: Think through the process here, ensure no race conditions with locking all at once. // this lock prevents msgs from being queued and holds up all other threads // we need to release it ASAP pthread_rwlock_wrlock(&dlog_active_buffer_rwlock); // we'll get a lock on writing to the current buffer. pthread_rwlock_wrlock(&dlog_buffer_rwlock[buffer_id]); // at this point we could have threads waiting on the buffer ID, and // we also could have threads waiting to write to the buffer we just got a // write lock on if the beat the race to lock the buffer_id // so we must increment the version of the current buffer, which will signal it's stale dlog_queue_version[buffer_id]++; // no one should be waiting to write the other buffer offline_buffer_id = buffer_id?0:1; pthread_rwlock_wrlock(&dlog_buffer_rwlock[offline_buffer_id]); // we now have write locks on everything // increment version again, just in case dlog_queue_version[offline_buffer_id]++; // store the new offline buffer ID as the active dlog_active_buffer_version = dlog_queue_version[offline_buffer_id]; // make the offline buffer the active one dlog_active_buffer = offline_buffer_id; // just in case dlog_queue_next[offline_buffer_id] = 0; // release the lock on the offline pthread_rwlock_unlock(&dlog_buffer_rwlock[offline_buffer_id]); // release the lock on the buffer index... which releases any threads waiting to write pthread_rwlock_unlock(&dlog_active_buffer_rwlock); for(i=0;itsms / 1000; millis = msg->tsms % 1000; tm_info = localtime_r(&seconds, &tm_info_storage); strftime(time_buffer, sizeof(time_buffer), "%Y-%m-%d %H:%M:%S", tm_info); if (log_calling_function) { j = snprintf(log_line, sizeof(log_line), "%s.%03d [%44s] %s: %s\n", time_buffer, millis, msg->calling_function, level_text[msg->level], msg->msg); } else { j = snprintf(log_line, sizeof(log_line), "%s.%03d %s: %s\n", time_buffer, millis, level_text[msg->level], msg->msg); } log_line[1199] = 0; if ((log_to_console) && (msg->level >= log_level_console)) { fprintf(log_to_stderr?stderr:stdout, "%s", log_line); } if ((log_to_file) && (msg->level >= log_level_file)) { if (log_handle) { // TODO: Error handling here. fwrite(log_line, j, 1, log_handle); } } } // all done msg_buf_idx[buffer_id] = 0; dlog_queue_next[buffer_id] = 0; pthread_rwlock_unlock(&dlog_buffer_rwlock[buffer_id]); fflush(stdout); fflush(stderr); } ets = current_time_micros(); if ((log_to_file) && (log_handle)) { if ((sts - lflush) > 1000000) { fflush(log_handle); lflush = sts; } } if (log_reopen_signal && log_to_file && log_handle) { log_reopen_signal = false; DLOG(DLOG_LEVEL_DEBUG, "Reopening log file"); fclose(log_handle); log_handle = fopen(log_file, "a"); if (!log_handle) { DLOG(DLOG_LEVEL_FATAL, "Could not reopen log file (%s): %s!", log_file, strerror(errno)); panic_from_thread(__LINE__); } log_file_opened = time(NULL); } if ((log_rotate_daily) && (log_to_file) && (log_handle)) { if (next_log_rotate < (ets/1000000ULL)) { DLOG(DLOG_LEVEL_INFO, "Rotating log file!"); tm_info = localtime_r(&log_file_opened, &tm_info_storage); strftime(time_buffer, sizeof(time_buffer), "%Y-%m-%d", tm_info); snprintf(log_line, sizeof(log_line), "%s.%s", log_file, time_buffer); fclose(log_handle); if (rename(log_file, log_line) != 0) { DLOG(DLOG_LEVEL_ERROR, "Could not rename log file (%s) for rotation: %s!", log_file, strerror(errno)); } log_handle = fopen(log_file,"a"); if (!log_handle) { DLOG(DLOG_LEVEL_FATAL, "Could not open log file (%s) after rotation: %s!", log_file, strerror(errno)); panic_from_thread(__LINE__); } next_log_rotate = get_midnight_timestamp()+1; log_file_opened = time(NULL); } } if (ets > sts) { ets = ets - sts; } else { ets = 0; } if (ets < 56999) { j = (57000 - ets) / 1000; j++; for(i=0;i #include // approximately this times 3 will be used // NOTE: With huge debug logging, this CAN potentially overrun and fail #define DLOG_MSG_BUF_SIZE (1024*1024*8) typedef struct { int level; uint64_t tsms; char calling_function[48]; char *msg; } DLOG_MSG; #define DLOG_LEVEL_ALL 0 #define DLOG_LEVEL_DEBUG 1 #define DLOG_LEVEL_INFO 2 #define DLOG_LEVEL_WARN 3 #define DLOG_LEVEL_ERROR 4 #define DLOG_LEVEL_FATAL 5 int datum_logger_queue_msg(const char *func, int level, const char *format, ...) __attribute__((format(printf, 3, 4))); // Generic for dynamic log level messages #define DLOG(level, format, ...) datum_logger_queue_msg(__func__, level, format __VA_OPT__(,) __VA_ARGS__) // for leftover logging code, default to debug level #define LOG_PRINTF(format, ...) datum_logger_queue_msg(__func__, DLOG_LEVEL_DEBUG, format __VA_OPT__(,) __VA_ARGS__) // macros for various log levels #define DLOG_DEBUG(format, ...) datum_logger_queue_msg(__func__, DLOG_LEVEL_DEBUG, format __VA_OPT__(,) __VA_ARGS__) #define DLOG_INFO(format, ...) datum_logger_queue_msg(__func__, DLOG_LEVEL_INFO, format __VA_OPT__(,) __VA_ARGS__) #define DLOG_WARN(format, ...) datum_logger_queue_msg(__func__, DLOG_LEVEL_WARN, format __VA_OPT__(,) __VA_ARGS__) #define DLOG_ERROR(format, ...) datum_logger_queue_msg(__func__, DLOG_LEVEL_ERROR, format __VA_OPT__(,) __VA_ARGS__) #define DLOG_FATAL(format, ...) datum_logger_queue_msg(__func__, DLOG_LEVEL_FATAL, format __VA_OPT__(,) __VA_ARGS__) int datum_logger_init(void); void datum_logger_config( bool clog_to_file, bool clog_to_console, int clog_level_console, int clog_level_file, bool clog_calling_function, bool clog_to_stderr, bool clog_rotate_daily, char *clog_file ); #endif datum_gateway-0.4.1beta/src/datum_protocol.c000066400000000000000000001717341512710151500212040ustar00rootroot00000000000000/* * * DATUM Gateway * Decentralized Alternative Templates for Universal Mining * * This file is part of OCEAN's Bitcoin mining decentralization * project, DATUM. * * https://ocean.xyz * * --- * * Copyright (c) 2024-2025 Bitcoin Ocean, LLC & Jason Hughes * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ // DATUM Client protocol implementation // Encrypted and on the wire has 7.999 bits of entropy per byte in testing. completely uncompressable. // TODO: Clean this up and break up various functions // TODO: Generalize encryption related operations vs repeated code // TODO: Implement versioning on the protocol for feature lists // TODO: Add pool-side assistance with startup to ensure that the client's node is fully sync'd with the network // TODO: Optionally allow pool to suggest node peers // TODO: Implement graceful negotiation of chain forks // TODO: Implement preciousblock for pool blocks not found by the client // TODO: Handle network failures that aren't immediately obvious more gracefully (like not receiving responses to server commands) // TODO: Implement resuiming of work without allowing one client to cause duplicate work for another #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "datum_utils.h" #include "datum_protocol.h" #include "datum_conf.h" #include "datum_stratum.h" #include "datum_blocktemplates.h" #include "datum_coinbaser.h" #include "datum_queue.h" #include "git_version.h" atomic_int datum_protocol_client_active = 0; DATUM_ENC_KEYS local_datum_keys; DATUM_ENC_KEYS session_datum_keys; DATUM_ENC_KEYS session_remote_datum_keys; DATUM_ENC_KEYS pool_keys; DATUM_ENC_PRECOMP session_precomp; unsigned char datum_state = 0; int server_out_buf = 0; int server_in_buf = 0; int protocol_state = 0; unsigned char server_send_buffer[DATUM_PROTOCOL_BUFFER_SIZE]; unsigned char server_recv_buffer[DATUM_PROTOCOL_BUFFER_SIZE]; uint32_t sending_header_key = 0xDC871829; // initial send header key ... changed by handshake function uint32_t receiving_header_key = 0; // set by handshake function unsigned char session_nonce_sender[crypto_box_NONCEBYTES]; unsigned char session_nonce_receiver[crypto_box_NONCEBYTES]; pthread_mutex_t datum_protocol_sender_stage1_lock = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t datum_protocol_send_buffer_lock = PTHREAD_MUTEX_INITIALIZER; unsigned char datum_protocol_next_job_idx = 0; pthread_mutex_t datum_protocol_next_job_idx_lock = PTHREAD_MUTEX_INITIALIZER; T_DATUM_PROTOCOL_JOB datum_jobs[MAX_DATUM_PROTOCOL_JOBS]; pthread_rwlock_t datum_jobs_rwlock = PTHREAD_RWLOCK_INITIALIZER; // Share tallies for decentralized mining uint64_t datum_accepted_share_count = 0; uint64_t datum_accepted_share_diff = 0; uint64_t datum_rejected_share_count = 0; uint64_t datum_rejected_share_diff = 0; uint64_t datum_last_accepted_share_tsms = 0; uint64_t datum_last_accepted_share_local_tsms = 0; uint64_t datum_protocol_mainloop_tsms = 0; uint64_t latest_server_msg_tsms = 0; // may be used by this thread when crafting replies to server commands unsigned char temp_data[DATUM_PROTOCOL_MAX_CMD_DATA_SIZE + 16384]; unsigned char datum_protocol_setup_new_job_idx(void *sx) { // Called by the stratum job updater. Must be thread safe. // give the stratum job updater a new job ID for us to work with // The server will track up to 8 unique jobs. T_DATUM_STRATUM_JOB *s = (T_DATUM_STRATUM_JOB *)sx; unsigned char a; pthread_mutex_lock(&datum_protocol_next_job_idx_lock); a = datum_protocol_next_job_idx; datum_protocol_next_job_idx++; if (datum_protocol_next_job_idx >= MAX_DATUM_PROTOCOL_JOBS) { datum_protocol_next_job_idx = 0; } pthread_mutex_unlock(&datum_protocol_next_job_idx_lock); pthread_rwlock_wrlock(&datum_jobs_rwlock); memset(&datum_jobs[a], 0, sizeof(T_DATUM_PROTOCOL_JOB)); datum_jobs[a].sjob = s; datum_jobs[a].datum_job_id = a; pthread_rwlock_unlock(&datum_jobs_rwlock); return a; } static inline void datum_xor_header_key(void *h, uint32_t key) { *((uint32_t *)h) ^= key; } uint32_t datum_header_xor_feedback(const uint32_t i) { uint32_t s = 0xb10cfeed; uint32_t h = s; uint32_t k = i; k *= 0xcc9e2d51; k = (k << 15) | (k >> 17); k *= 0x1b873593; h ^= k; h = (h << 13) | (h >> 19); h = h * 5 + 0xe6546b64; h ^= 4; h ^= h >> 16; h *= 0x85ebca6b; h ^= h >> 13; h *= 0xc2b2ae35; h ^= h >> 16; return h; } // Take the hexidecimal public key string and store it in a DATUM_ENC_KEYS int datum_pubkey_to_struct(const char *input, DATUM_ENC_KEYS *key) { int i; if (input[0] == 0) return -1; if (strlen(input) != 128) { DLOG_FATAL("Pool public key is not the correct length!"); return -1; } for(i=0;i<32;i++) { key->pk_ed25519[i] = hex2bin_uchar(&input[i<<1]); } for(i=0;i<32;i++) { key->pk_x25519[i] = hex2bin_uchar(&input[64+(i<<1)]); } return 0; } // Prepare session encryption precomputation void datum_encrypt_prep_precomp(DATUM_ENC_KEYS *remote, DATUM_ENC_KEYS *local, DATUM_ENC_PRECOMP *precomp) { precomp->local = local; precomp->remote = remote; if (crypto_box_beforenm(precomp->precomp_remote, remote->pk_x25519, local->sk_x25519) != 0) { DLOG_ERROR("Could not precompute encryption keys."); } } // Buffer data to the server. Raw, already encrypted and part of the protocol. int datum_protocol_chars_to_server(unsigned char *s, int len) { if (!len) return 0; pthread_mutex_lock(&datum_protocol_send_buffer_lock); if ((server_out_buf + len) >= DATUM_PROTOCOL_BUFFER_SIZE) { pthread_mutex_unlock(&datum_protocol_send_buffer_lock); return -1; } if (len > (DATUM_PROTOCOL_BUFFER_SIZE-(server_out_buf)-1)) { len = DATUM_PROTOCOL_BUFFER_SIZE-(server_out_buf)-1; } if ((server_out_buf+len) >= DATUM_PROTOCOL_BUFFER_SIZE) { DLOG_ERROR("DATUM Server send overrun!"); pthread_mutex_unlock(&datum_protocol_send_buffer_lock); return -1; } memcpy(&server_send_buffer[server_out_buf], s, len); server_out_buf += len; pthread_mutex_unlock(&datum_protocol_send_buffer_lock); return len; } int datum_protocol_mining_cmd(void *data, int len) { // protocol cmd 5 // encypt and send a standard mining sub-command // this can be called from other threads so must be thread safe! T_DATUM_PROTOCOL_HEADER h; int i; memset(&h, 0, sizeof(T_DATUM_PROTOCOL_HEADER)); h.is_encrypted_channel = true; h.proto_cmd = 5; h.cmd_len = len; h.cmd_len += crypto_box_MACBYTES; // sends of encrypted data must remain ordered // we have to lock here for both the header obfuscation and the nonce increment pthread_mutex_lock(&datum_protocol_sender_stage1_lock); crypto_box_easy_afternm(data, data, len, session_nonce_sender, session_precomp.precomp_remote); //DLOG_DEBUG("mining cmd 5--- len %d, send header key %8.8x, raw %8.8lx", h.cmd_len, sending_header_key, (unsigned long)upk_u32le(h, 0)); datum_xor_header_key(&h, sending_header_key); sending_header_key = datum_header_xor_feedback(sending_header_key); datum_increment_session_nonce(session_nonce_sender); i = datum_protocol_chars_to_server((unsigned char *)&h, sizeof(T_DATUM_PROTOCOL_HEADER)); if (i < 1) { pthread_mutex_unlock(&datum_protocol_sender_stage1_lock); return -1; } i = datum_protocol_chars_to_server((unsigned char *)data, len + crypto_box_MACBYTES); if (i < 1) { pthread_mutex_unlock(&datum_protocol_sender_stage1_lock); return -1; } pthread_mutex_unlock(&datum_protocol_sender_stage1_lock); return 0; } pthread_mutex_t datum_protocol_coinbaser_fetch_mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t datum_protocol_coinbaser_fetch_cond = PTHREAD_COND_INITIALIZER; unsigned char datum_coinbaser_v2_response_buf[2][32768] = { 0 }; unsigned char *datum_coinbaser_v2_response = NULL; unsigned char datum_coinbaser_v2_response_buf_idx = 0; uint64_t datum_coinbaser_v2_response_value[2] = { 0, 0 }; int datum_coinbaser_v2_response_len[2] = { 0, 0 }; int datum_protocol_coinbaser_fetch_response(int len, unsigned char *data) { if (len < 12) { DLOG_DEBUG("Invalid coinbaser received!"); return 0; } // Coinbaser response from server. stash appropriately! struct timespec ts; int rc; clock_gettime(CLOCK_REALTIME, &ts); ts.tv_sec += 5; // Set timeout to 5 seconds from now uint32_t x; uint64_t v; v = upk_u64le(data, 0); x = upk_u32le(data, 8); if ((x > 32768-1) || (x<1) || x > (unsigned int)(len - 12)) { DLOG_DEBUG("Invalid coinbaser received! %lu %lu", (unsigned long)x, (unsigned long)(len-12)); return 0; } rc = pthread_mutex_timedlock(&datum_protocol_coinbaser_fetch_mutex, &ts); if (rc != 0) { DLOG_DEBUG("Could not get a lock on the coinbaser reception mutex after 5 seconds... bug?"); return 0; } // mutex is locked if (datum_coinbaser_v2_response_buf_idx == 0) { datum_coinbaser_v2_response_buf_idx = 1; } else { datum_coinbaser_v2_response_buf_idx = 0; } datum_coinbaser_v2_response = datum_coinbaser_v2_response_buf[datum_coinbaser_v2_response_buf_idx]; memcpy(datum_coinbaser_v2_response, &data[12], x); datum_coinbaser_v2_response_value[datum_coinbaser_v2_response_buf_idx] = v; datum_coinbaser_v2_response_len[datum_coinbaser_v2_response_buf_idx] = x; pthread_cond_signal(&datum_protocol_coinbaser_fetch_cond); // Signal the condition variable pthread_mutex_unlock(&datum_protocol_coinbaser_fetch_mutex); return 1; } int datum_protocol_coinbaser_fetch(void *sptr) { // Called by the coinbaser thread to request a coinbase split // The coinbaser thread expects this to actually result in a processed coinbase split, so we need to churn // here until that's ready or times out. T_DATUM_STRATUM_JOB *s = (T_DATUM_STRATUM_JOB *)sptr; uint64_t value = s->coinbase_value; unsigned char msg[128 + crypto_box_MACBYTES]; int i = 0, j; int rc; struct timespec ts; s->available_coinbase_outputs_count = 0; if (value < 31250000) { // mainnet epoch V return 0; } msg[0] = 0x10; i++; // Fetch Coinbaser subcmd pk_u64le(msg, 1, value); i += 8; // value we have available with this job // the job's previous block hash. this ensures that the remote end knows which block this payout is related to // in the event of a chain split. memcpy(&msg[i], s->prevhash_bin, 32); i+=32; msg[i] = 0xFE; i++; // pad j = 1 + (rand() % 80); memset(&msg[i], rand(), j); i+=j; if (datum_protocol_client_active != 3) { return 0; } datum_protocol_mining_cmd(msg, i); // spin here for up to 5 seconds while awaiting a coinbaser response from DATUM Prime clock_gettime(CLOCK_REALTIME, &ts); ts.tv_sec += 5; // Set timeout to 5 seconds pthread_mutex_lock(&datum_protocol_coinbaser_fetch_mutex); rc = pthread_cond_timedwait(&datum_protocol_coinbaser_fetch_cond, &datum_protocol_coinbaser_fetch_mutex, &ts); if (rc == ETIMEDOUT) { pthread_mutex_unlock(&datum_protocol_coinbaser_fetch_mutex); DLOG_DEBUG("Timeout waiting for coinbaser response from DATUM Prime"); return 0; } if (rc != 0) { DLOG_DEBUG("Error waiting for coinbaser response from DATUM Prime"); pthread_mutex_unlock(&datum_protocol_coinbaser_fetch_mutex); return 0; } i = 0; // process received coinbase if ((datum_coinbaser_v2_response) && (datum_coinbaser_v2_response_value[datum_coinbaser_v2_response_buf_idx] == value)) { i = datum_coinbaser_v2_parse(s, datum_coinbaser_v2_response, datum_coinbaser_v2_response_len[datum_coinbaser_v2_response_buf_idx], false); } pthread_mutex_unlock(&datum_protocol_coinbaser_fetch_mutex); return i; } int datum_protocol_ping_response(T_DATUM_PROTOCOL_HEADER *h, unsigned char *data) { // TODO: Not implemented, but should be done for keepalive return 1; } int datum_protocol_client_configure(int len, unsigned char *data) { // Server->Client configuration changes. This can be called at any time by the server // to make updates to these important variables. int i=0; unsigned char a; DLOG_DEBUG("client configuration cmd received from DATUM server"); char msg[1024]; if (i >= len || data[i] != 1) { err: DLOG_ERROR("Bad configuration version from server. Is this client up to date?"); return 0; } i++; // read pool addr script if (i >= len) goto err; a = data[i]; i++; if (i + a > len) goto err; memcpy(datum_config.override_mining_pool_scriptsig, &data[i], a); i+=a; datum_config.override_mining_pool_scriptsig_len = a; // prime ID if (i + 4 > len) goto err; datum_config.prime_id = upk_u32le(data, i); i+=4; // pool coinbase tag if (i >= len) goto err; a = data[i]; i++; if (i + a > len) goto err; memcpy(datum_config.override_mining_coinbase_tag_primary, &data[i], a); i+=a; datum_config.override_mining_coinbase_tag_primary[a] = 0; if (i + 8 > len) goto err; datum_config.override_vardiff_min = upk_u64le(data, i); i+=8; if (datum_config.override_vardiff_min != roundDownToPowerOfTwo_64(datum_config.override_vardiff_min)) { DLOG_WARN("Server specified a minimum difficulty that is not a power of two! Is your client up to date? Rounding up to a power of two! (%"PRIu64" to %"PRIu64")", datum_config.override_vardiff_min, roundDownToPowerOfTwo_64(datum_config.override_vardiff_min)<<1); datum_config.override_vardiff_min = roundDownToPowerOfTwo_64(datum_config.override_vardiff_min)<<1; } if (i + 2 > len) goto err; if ((data[i] != 0) || (data[i+1] != 0xFE)) { DLOG_ERROR("Invalid data structure in configuration :( Is this client up to date???"); return 0; } memset(msg, 0, (datum_config.override_mining_pool_scriptsig_len<<1)+2); for(i=0;i= 8) { // error response to 0x50 0x10 msg[i] = 0x50; i++; msg[i] = 0x90; i++; msg[i] = 0xFF; i++; msg[i] = 0xF3; i++; // pad with some randomness j = 1 + (rand() % 100); memset(&msg[i], rand(), j); i+=j; datum_protocol_mining_cmd(msg, i); return 1; } pthread_rwlock_rdlock(&datum_jobs_rwlock); dj = &datum_jobs[job_index]; sj = dj->sjob; if (!sj) { pthread_rwlock_unlock(&datum_jobs_rwlock); // error response to 0x50 0x10 msg[i] = 0x50; i++; msg[i] = 0x90; i++; msg[i] = job_index; i++; msg[i] = 0xF0; i++; // pad with some randomness j = 1 + (rand() % 100); memset(&msg[i], rand(), j); i+=j; datum_protocol_mining_cmd(msg, i); return 1; } block_template = sj->block_template; if (!block_template) { pthread_rwlock_unlock(&datum_jobs_rwlock); // error response to 0x50 0x10 msg[i] = 0x50; i++; msg[i] = 0x90; i++; msg[i] = job_index; i++; msg[i] = 0xF1; i++; // pad with some randomness j = 1 + (rand() % 100); memset(&msg[i], rand(), j); i+=j; datum_protocol_mining_cmd(msg, i); return 1; } if (block_template->txn_count == 0) { pthread_rwlock_unlock(&datum_jobs_rwlock); // there are no transactions in this block... // normal response, except our tx count is 0 // since tx count = 0, we dont need anything else msg[i] = 0x50; i++; msg[i] = 0x90; i++; msg[i] = job_index; i++; msg[i] = 0x01; i++; msg[i++] = 0; msg[i++] = 0; // no need to send the crosscheck... there's nothing to crosscheck! // pad with some randomness j = 1 + (rand() % 100); memset(&msg[i], rand(), j); i+=j; datum_protocol_mining_cmd(msg, i); return 1; } if (block_template->txn_count > 16383) { pthread_rwlock_unlock(&datum_jobs_rwlock); // error response to 0x50 0x10 msg[i] = 0x50; i++; msg[i] = 0x90; i++; msg[i] = job_index; i++; msg[i] = 0xF2; i++; // pad with some randomness j = 1 + (rand() % 100); memset(&msg[i], rand(), j); i+=j; datum_protocol_mining_cmd(msg, i); return 1; } // ok, we're good msg[i] = 0x50; i++; msg[i] = 0x90; i++; msg[i] = job_index; i++; msg[i] = 0x01; i++; pk_u16le(msg, i, block_template->txn_count); i += 2; // we don't have the benefit that compact blocks have of fully having something unknown to an attacker before an attack // like the block hash. so we'll do the next best thing and use the client's public signing key mixed with the pool's // as the "key" for siphash for(j=0;j<16;j++) { siphash_key[j] = (local_datum_keys.pk_ed25519[j] ^ pool_keys.pk_ed25519[j]) ^ 0x55; } for(j=0;jtxn_count;j++) { siphash = datum_siphash_mod8(block_template->txns[j].hash_bin, 32, siphash_key); // store 48-bits of the hash in the message pk_u32le(msg, i, siphash & 0xFFFFFFFF); i += 4; pk_u16le(msg, i, (siphash >> 32) & 0xFFFF); i += 2; // keep a running xor of all hashes for a final crosscheck // not intended to be secure, but is fast enough for an initial server side pass/fail // should be virtually impossible, due to the search space, to attack anyway for (int k = 0; k < 0x20; ++k) crosscheck[k] ^= block_template->txns[j].hash_bin[k]; } // we dont need the template data anymore, unlock! pthread_rwlock_unlock(&datum_jobs_rwlock); // cap off the message with the running XOR memcpy(&msg[i], &crosscheck[0], 0x20); i += 0x20; msg[i] = 0xFE; i++; // pad with some randomness j = 1 + (rand() % 111); memset(&msg[i], rand(), j); i+=j; datum_protocol_mining_cmd(msg, i); // let 'er rip DLOG_DEBUG("sent short txn list to server for job %d (%d bytes)",job_index,i); return 1; } int datum_protocol_job_validation_stxlist_byid(unsigned char *data) { // the server is requesting missing transactions // send them unsigned char job_index = data[0]; uint16_t req_count = upk_u16le(data, 1); T_DATUM_PROTOCOL_JOB *dj; T_DATUM_STRATUM_JOB *sj; T_DATUM_TEMPLATE_DATA *block_template; unsigned char *msg = &temp_data[0]; // global temp var for constructing messages within the datum protocol thread uint16_t req_id; int i = 0, j,k=3; if (job_index >= 8) { // error response to 0x50 0x11 msg[i] = 0x50; i++; msg[i] = 0x91; i++; msg[i] = 0xFF; i++; msg[i] = 0xF3; i++; // pad with some randomness j = 1 + (rand() % 100); memset(&msg[i], rand(), j); i+=j; datum_protocol_mining_cmd(msg, i); return 1; } pthread_rwlock_rdlock(&datum_jobs_rwlock); dj = &datum_jobs[job_index]; sj = dj->sjob; if (!sj) { pthread_rwlock_unlock(&datum_jobs_rwlock); // error response to 0x50 0x11 msg[i] = 0x50; i++; msg[i] = 0x91; i++; msg[i] = job_index; i++; msg[i] = 0xF0; i++; // pad with some randomness j = 1 + (rand() % 100); memset(&msg[i], rand(), j); i+=j; datum_protocol_mining_cmd(msg, i); return 1; } block_template = sj->block_template; if (!block_template) { pthread_rwlock_unlock(&datum_jobs_rwlock); // error response to 0x50 0x11 msg[i] = 0x50; i++; msg[i] = 0x91; i++; msg[i] = job_index; i++; msg[i] = 0xF1; i++; // pad with some randomness j = 1 + (rand() % 100); memset(&msg[i], rand(), j); i+=j; datum_protocol_mining_cmd(msg, i); return 1; } if ((req_count == 0) || (req_count > block_template->txn_count)) { pthread_rwlock_unlock(&datum_jobs_rwlock); // error response to 0x50 0x11 msg[i] = 0x50; i++; msg[i] = 0x91; i++; msg[i] = job_index; i++; msg[i] = 0xF4; i++; // pad with some randomness j = 1 + (rand() % 100); memset(&msg[i], rand(), j); i+=j; datum_protocol_mining_cmd(msg, i); return 1; } // ok, make it happen. msg[i] = 0x50; i++; msg[i] = 0x91; i++; msg[i] = job_index; i++; msg[i] = 0x01; i++; pk_u16le(msg, i, req_count); i += 2; for(j=0;j= block_template->txn_count) { // error.... pthread_rwlock_unlock(&datum_jobs_rwlock); // error response to 0x50 0x11 i = 0; // reset index msg[i] = 0x50; i++; msg[i] = 0x91; i++; msg[i] = job_index; i++; msg[i] = 0xF4; i++; // pad with some randomness j = 1 + (rand() % 100); memset(&msg[i], rand(), j); i+=j; datum_protocol_mining_cmd(msg, i); return 1; } // size is stored as 3 bytes for consistency. // this is technically redundant, as the server can derive this by decoding the transaction // however, we're future-proofing just a little here for a tiny bit of overhead. pk_u16le(msg, i, ((uint32_t)(block_template->txns[req_id].size) & 0xFFFF)); i += 2; msg[i] = (uint16_t)((block_template->txns[req_id].size >> 16) & 0xFF); i++; memcpy(&msg[i], block_template->txns[req_id].txn_data_binary, block_template->txns[req_id].size); i += block_template->txns[req_id].size; } // done with the job. pthread_rwlock_unlock(&datum_jobs_rwlock); msg[i] = 0xFE; i++; // pad with some randomness j = 1 + (rand() % 111); memset(&msg[i], rand(), j); i+=j; datum_protocol_mining_cmd(msg, i); // let 'er rip DLOG_DEBUG("sent full txns to server for job %d (%d bytes for %d txns)",job_index,i,(int)req_count); return 1; } int datum_protocol_job_validation_sblock(unsigned char *data) { // the server decided our template probably is too unique from what it knows about, or was // otherwise not able to validate the block using faster negotiations. // It would like us to just send the entire transaction blob for validation as-is. // This is reasonable and required. The server should already have the rest of the data needed // to construct a block to fully validate. unsigned char job_index = data[0]; T_DATUM_PROTOCOL_JOB *dj; T_DATUM_STRATUM_JOB *sj; T_DATUM_TEMPLATE_DATA *block_template; unsigned char *msg = &temp_data[0]; // global temp var for constructing messages within the datum protocol thread int i = 0, j; if (job_index >= 8) { // error response to 0x50 0x12 msg[i] = 0x50; i++; msg[i] = 0x92; i++; msg[i] = 0xFF; i++; msg[i] = 0xF3; i++; // pad with some randomness j = 1 + (rand() % 100); memset(&msg[i], rand(), j); i+=j; datum_protocol_mining_cmd(msg, i); return 1; } pthread_rwlock_rdlock(&datum_jobs_rwlock); dj = &datum_jobs[job_index]; sj = dj->sjob; if (!sj) { pthread_rwlock_unlock(&datum_jobs_rwlock); // error response to 0x50 0x12 msg[i] = 0x50; i++; msg[i] = 0x92; i++; msg[i] = job_index; i++; msg[i] = 0xF0; i++; // pad with some randomness j = 1 + (rand() % 100); memset(&msg[i], rand(), j); i+=j; datum_protocol_mining_cmd(msg, i); return 1; } block_template = sj->block_template; if (!block_template) { pthread_rwlock_unlock(&datum_jobs_rwlock); // error response to 0x50 0x12 msg[i] = 0x50; i++; msg[i] = 0x92; i++; msg[i] = job_index; i++; msg[i] = 0xF1; i++; // pad with some randomness j = 1 + (rand() % 100); memset(&msg[i], rand(), j); i+=j; datum_protocol_mining_cmd(msg, i); return 1; } msg[i] = 0x50; i++; msg[i] = 0x92; i++; msg[i] = job_index; i++; msg[i] = 0x01; i++; pk_u16le(msg, i, block_template->txn_count); i += 2; for(j=0;jtxn_count;j++) { // size is stored as 3 bytes for consistency. // this is technically redundant, as the server can derive this by decoding the transaction // however, we're future-proofing just a little here for a tiny bit of overhead. // since the block data must be < 4MB, max 16384 txns, this adds at most 50KB, which fits in our 2^22 byte send max pk_u16le(msg, i, ((uint32_t)(block_template->txns[j].size) & 0xFFFF)); i += 2; msg[i] = (uint16_t)((block_template->txns[j].size >> 16) & 0xFF); i++; // dump the raw txn in memcpy(&msg[i], block_template->txns[j].txn_data_binary, block_template->txns[j].size); i += block_template->txns[j].size; } pthread_rwlock_unlock(&datum_jobs_rwlock); msg[i] = 0xFE; i++; // no random data here for size safety. this is also already expensive, potentially taking seconds to transmit. datum_protocol_mining_cmd(msg, i); // let 'er rip DLOG_DEBUG("sent full block of txns to server for job %d (%d bytes)",job_index,i); return 1; } int datum_protocol_job_validation_cmd(int len, unsigned char *data) { unsigned char cmd = data[0]; unsigned char *p = data; if (len < 2) return 0; p++; // sub sub cmd switch (cmd) { case 0x10: { // send short txn list return datum_protocol_job_validation_stxlist(p); break; } case 0x11: { // send the requested txns // 16-bit indexes return datum_protocol_job_validation_stxlist_byid(p); break; } case 0x12: { // send the entire block, except the coinbase txn return datum_protocol_job_validation_sblock(p); break; } // TODO: Implement a job differences mechanism to save bandwidth on new work vs stxids default: break; } return 1; } // TODO: Ensure all shares are responded to! Currently this has no bearing on anything, just logging int datum_protocol_share_response(int len, unsigned char *data) { if (len < 9) { DLOG_DEBUG("Invalid share response received!"); return 0; } if (data[0] == DATUM_POW_SHARE_RESPONSE_REJECTED) { DLOG_DEBUG("DATUM server rejected our share! Reason code: %d / TargetPOT: %2.2x / Job ID: %d / Nonce: %8.8x", (int)upk_u16le(data, 1), data[7], (int)data[8], upk_u32le(data, 3)); datum_rejected_share_count++; if (data[7] != 0xFF) { datum_rejected_share_diff += 1<cmd_len) return 0; switch(*data) { case 0x99: { if (!h->is_signed) { DLOG_ERROR("Received unsigned client configuration from DATUM server!"); return 0; } return datum_protocol_client_configure(h->cmd_len-1, &data[1]); break; } case 0x11: { // Coinbaser response! return datum_protocol_coinbaser_fetch_response(h->cmd_len-1, &data[1]); break; } case 0x50: { // Job validation commands return datum_protocol_job_validation_cmd(h->cmd_len-1, &data[1]); break; } case 0x8F: { // share response return datum_protocol_share_response(h->cmd_len-1, &data[1]); break; } case 0xF9: { // Server says we should check for a new block template immediately DLOG_DEBUG("DATUM server blocknotify"); datum_blocktemplates_notifynew(NULL, 0); return 1; break; } default: { DLOG_WARN("Received unknown mining command %2.2X from DATUM Server. Perhaps you need to upgrade this DATUM Gateway?", *data); return 0; } } return 0; } int datum_protocol_send_hello(int sockfd) { T_DATUM_PROTOCOL_HEADER h; unsigned char hello_msg[1024]; unsigned char enc_hello_msg[1024]; int i = 0; int j; uint32_t nk; memset(&h, 0, sizeof(T_DATUM_PROTOCOL_HEADER)); h.is_signed = true; h.is_encrypted_pubkey = true; h.proto_cmd = 1; // handshake init if (datum_encrypt_generate_keys(&session_datum_keys) != 0) { DLOG_FATAL("Could not generate our session keys!"); return -1; } // send over a message that gives the server our encryption public key, our signing public key, our session encryption public key, and our session signing key. // we should sign this message with our signing public key, then seal it in a message to the server. // we should pad it with some random number of bytes also, not that the purpose is lost at the packet level here i = 0; memcpy(&hello_msg[i], local_datum_keys.pk_ed25519, crypto_sign_PUBLICKEYBYTES); i+=crypto_sign_PUBLICKEYBYTES; memcpy(&hello_msg[i], local_datum_keys.pk_x25519, crypto_box_PUBLICKEYBYTES); i+=crypto_box_PUBLICKEYBYTES; memcpy(&hello_msg[i], session_datum_keys.pk_ed25519, crypto_sign_PUBLICKEYBYTES); i+=crypto_sign_PUBLICKEYBYTES; memcpy(&hello_msg[i], session_datum_keys.pk_x25519, crypto_box_PUBLICKEYBYTES); i+=crypto_box_PUBLICKEYBYTES; strncpy((char *)&hello_msg[i], DATUM_PROTOCOL_VERSION, 127); hello_msg[i+127] = 0; i += strlen((char *)&hello_msg[i]); hello_msg[i] = '/'; i++; strncpy((char *)&hello_msg[i], GIT_COMMIT_HASH, 127); hello_msg[i+127] = 0; i += strlen((char *)&hello_msg[i]); #ifdef BUILD_GIT_TAG hello_msg[i] = '('; i++; strncpy((char *)&hello_msg[i], BUILD_GIT_TAG, 127); hello_msg[i+127] = 0; i += strlen((char *)&hello_msg[i]); hello_msg[i] = ')'; i++; #endif hello_msg[i] = 0; i++; hello_msg[i] = 0xFE; i++; // pick our initial sending_header_key hello_msg[i] = rand(); i++; hello_msg[i] = rand(); i++; hello_msg[i] = rand(); i++; hello_msg[i] = rand(); i++; nk = upk_u32le(hello_msg, i - 4); // TODO: maybe tack on other useful data here at some point // ... // pad with some randomness j = 1 + (rand() % 200); memset(&hello_msg[i], rand(), j); i+=j; // tack the signature on to the message DLOG_DEBUG("Signing handshake %d bytes",i); crypto_sign_detached(&hello_msg[i], NULL, hello_msg, i, local_datum_keys.sk_ed25519); i+=crypto_sign_BYTES; // seal it up crypto_box_seal(&enc_hello_msg[sizeof(T_DATUM_PROTOCOL_HEADER)], hello_msg, i, pool_keys.pk_x25519); i+=crypto_box_SEALBYTES; h.cmd_len = i; memcpy(enc_hello_msg, &h, sizeof(T_DATUM_PROTOCOL_HEADER)); // apply our initial xor key to the header, just to obfuscate it a tiny bit // kinda pointless, but ok datum_xor_header_key(&enc_hello_msg[0], sending_header_key); DLOG_DEBUG("Sending handshake init (%d bytes)", h.cmd_len); // from here on out, we're going to send our headers to the server XOR'd with the header feedback mechanism for each header to protect that data // generally, packet alignment will help with some analysis, but overall can't be certain about values. sending_header_key = datum_header_xor_feedback(nk); receiving_header_key = datum_header_xor_feedback(~nk); // setup a somewhat deterministic nonce memset(session_nonce_receiver, 0, crypto_box_NONCEBYTES); nk -= 42; nk = nk ^ upk_u32le(session_datum_keys.pk_ed25519, 7); for(j=0;jcmd_len < crypto_box_SEALBYTES) { DLOG_ERROR("Couldn't decrypt too-small DATUM command from server! (%d bytes)", h->cmd_len); return -1; } int i; memcpy(temp_data, data, h->cmd_len); // attempt to decode with our session key i = crypto_box_seal_open(data, temp_data, h->cmd_len, session_datum_keys.pk_x25519, session_datum_keys.sk_x25519); if (i!=0) { DLOG_ERROR("Couldn't decrypt DATUM command from server with our session key! (%d bytes)", h->cmd_len); return -1; } h->cmd_len -= crypto_box_SEALBYTES; return 1; } void datum_increment_session_nonce(void *s) { uint32_t *x = s; int i; for(i=0;icmd_len < crypto_box_MACBYTES) { DLOG_ERROR("Couldn't decrypt too-small DATUM command from server! (%d bytes)", h->cmd_len); return -1; } int i; // supposedly this can be done in place, according to docs! i = crypto_box_open_easy_afternm(data, data, h->cmd_len, session_nonce_receiver, session_precomp.precomp_remote); if (i!=0) { DLOG_ERROR("Couldn't decrypt DATUM command from server with our session key! (%d bytes)", h->cmd_len); return -1; } h->cmd_len -= crypto_box_MACBYTES; datum_increment_session_nonce(session_nonce_receiver); return 0; } int datum_protocol_compare_data(unsigned char *a, unsigned char *b, int len) { int i; for (i=0;iis_signed) { // handshake must have passed a sig check return -1; } i = 0; if (datum_protocol_compare_data(&data[i], local_datum_keys.pk_ed25519, crypto_sign_PUBLICKEYBYTES) != 0) { DLOG_WARN("Our public signing key echoed by the DATUM server did NOT match."); return -1; } i+=crypto_sign_PUBLICKEYBYTES; if (datum_protocol_compare_data(&data[i], local_datum_keys.pk_x25519, crypto_box_PUBLICKEYBYTES) != 0) { DLOG_WARN("Our public encryption key echoed by the DATUM server did NOT match."); return -1; } i+=crypto_box_PUBLICKEYBYTES; if (datum_protocol_compare_data(&data[i], session_datum_keys.pk_ed25519, crypto_sign_PUBLICKEYBYTES) != 0) { DLOG_WARN("Our session public signing key echoed by the DATUM server did NOT match."); return -1; } i+=crypto_sign_PUBLICKEYBYTES; if (datum_protocol_compare_data(&data[i], session_datum_keys.pk_x25519, crypto_box_PUBLICKEYBYTES) != 0) { DLOG_WARN("Our session public encryption key echoed by the DATUM server did NOT match."); return -1; } i+=crypto_box_PUBLICKEYBYTES; // ok, let's save the pool's session keys memcpy(session_remote_datum_keys.pk_ed25519, &data[i], crypto_sign_PUBLICKEYBYTES); i+=crypto_sign_PUBLICKEYBYTES; memcpy(session_remote_datum_keys.pk_x25519, &data[i], crypto_box_PUBLICKEYBYTES); i+=crypto_box_PUBLICKEYBYTES; // Server MOTD strncpy(motd, (char *)&data[i], 511); motd[511] = 0; session_remote_datum_keys.is_remote = true; datum_encrypt_prep_precomp(&session_remote_datum_keys, &session_datum_keys, &session_precomp); datum_state = 2; //we're handshaked with encryption setup! DLOG_DEBUG("Handshake response received."); DLOG_INFO("DATUM Server MOTD: %s", motd); return 1; } int datum_protocol_server_msg(T_DATUM_PROTOCOL_HEADER *h, unsigned char *data) { int i; //DLOG_DEBUG("Server msg: %d bytes cmd %d", h->cmd_len, h->proto_cmd); if ((h->is_encrypted_pubkey) && (!h->is_encrypted_channel)) { // this is a sealed message to our session pubkey // decrypt the message i = datum_protocol_decrypt_sealed(h, data); if (i < 0) { DLOG_ERROR("Could not decrypt sealed message from DATUM server!"); return -1; } } if ((!h->is_encrypted_pubkey) && (h->is_encrypted_channel)) { // this is a message encrypted for our session i = datum_protocol_decrypt_standard(h, data); if (i < 0) { DLOG_ERROR("Could not decrypt standard message from DATUM server!"); return -1; } } // message is decrypted by now if (h->is_signed) { if (h->cmd_len < crypto_sign_BYTES) { DLOG_ERROR("Could not validate too-small signature of message from server! (%d bytes)", h->cmd_len); return -1; } // validate the signature // if we're already handshaked, signatures are with the pool-side session key. if not, they're with the pool's key if (datum_state >= 2) { i = crypto_sign_verify_detached(&data[h->cmd_len-crypto_sign_BYTES], data, h->cmd_len-crypto_sign_BYTES, session_remote_datum_keys.pk_ed25519); } else { i = crypto_sign_verify_detached(&data[h->cmd_len-crypto_sign_BYTES], data, h->cmd_len-crypto_sign_BYTES, pool_keys.pk_ed25519); } if (i!=0) { DLOG_ERROR("Could not validate signature of message from server! (%d bytes)", h->cmd_len); return -1; } // signature good... strip it! h->cmd_len -= crypto_sign_BYTES; } latest_server_msg_tsms = datum_protocol_mainloop_tsms; // NOTE: Keep in mind protocol command is limited to 5 bits switch(h->proto_cmd) { case 2: { // handshake response return datum_protocol_handshake_response(h, data); } case 5: { return datum_protocol_mining_cmd5(h, data); } case 7: { // display INFO in log if (h->cmd_len) { DLOG_INFO("DATUM Server message: %s", (char *)data); } return 1; } case 1: { // PING return datum_protocol_ping_response(h, data); } default: { DLOG_WARN("Unknown protocol command from server 0x%2.2x. It this client up to date???", h->proto_cmd); return 1; } } return -1; } // Work submission multithreaded queue DATUM_QUEUE pow_queue; void datum_protocol_pow_queue_submits(void) { datum_queue_process(&pow_queue); } int datum_protocol_pow_submit( const T_DATUM_CLIENT_DATA *c, const T_DATUM_STRATUM_JOB *job, const char *username, const bool was_block, const bool subsidy_only, const bool quickdiff, const unsigned char *block_header, const uint64_t target_diff, const unsigned char *full_cb_tx, const T_DATUM_STRATUM_COINBASE *cb, unsigned char *extranonce, unsigned char coinbase_index) { // called by other threads to submit new POW T_DATUM_PROTOCOL_POW pow; pow.datum_job_id = job->datum_job_idx; memcpy(pow.extranonce, extranonce, 12); strncpy(pow.username, username, 383); pow.username[383] = 0; pow.coinbase_id = coinbase_index; pow.subsidy_only = subsidy_only; pow.is_block = was_block; pow.quickdiff = quickdiff; pow.target_byte_index = job->target_pot_index; // just a sanity check on the server side. server hunts for this in the correct place anyway. pow.target_byte = full_cb_tx[job->target_pot_index]; pow.ntime = upk_u32le(block_header, 68); pow.nonce = upk_u32le(block_header, 76); pow.version = upk_u32le(block_header, 0); //DLOG_DEBUG("ADD: DATUM POW: time %d nonce %8.8X", pow.ntime, pow.nonce); return datum_queue_add_item(&pow_queue, &pow); } // {"params": ["mzjP9Hn7aqaCLM5pSgMSQzgs3gnxSFv91B", "662599770700", "f40c000000000000", "66259976", "48220d13", "00d30000"], "id": 182, "method": "mining.submit"} int datum_protocol_pow(void *arg) { T_DATUM_PROTOCOL_POW *pow = arg; T_DATUM_STRATUM_JOB *sjob; unsigned char msg[32768 + crypto_box_MACBYTES]; int i = 0, j; bool w=false; // this is called when processing queued shares in our thread //DLOG_DEBUG("DATUM POW @ %p: time %d nonce %8.8X", pow, pow->ntime, pow->nonce); if ((pow->coinbase_id > 7) && (!(pow->coinbase_id == 0xff) && pow->subsidy_only)) { DLOG_ERROR("Could not process POW to DATUM server! Bad coinbase ID."); return 0; } msg[0] = 0x27; i++; // submit POW msg[i]=pow->datum_job_id; i++; // job ID 0 msg[i]=pow->coinbase_id; i++; // which coinbase 1 msg[i]=((pow->is_block?1:0)|(pow->subsidy_only?2:0)|(pow->quickdiff?4:0)); i++; // other flags that are useful for constructing the block header 2 msg[i]=pow->target_byte; i++; // target byte we used for this work (PoT target diff) 3 pk_u32le(msg, i, pow->ntime); i += 4; // ntime 4 pk_u32le(msg, i, pow->nonce); i += 4; // nonce 8 pk_u32le(msg, i, pow->version); i += 4; // version 12 msg[i] = 12; i++; // extranonce size... DO NOT CHANGE. Server support for other sizes is not likely any time soon. But, planning ahead. This should be plenty for everyone. :) 16 memcpy(&msg[i], pow->extranonce, 12); i+=12; // extranonce1+2 17 char * const username = (char *)&msg[i]; if (((!datum_config.datum_pool_pass_full_users) && (!datum_config.datum_pool_pass_workers)) || pow->username[0] == '\0') { i+=snprintf(username, 385, "%s", datum_config.mining_pool_address); } else if (datum_config.datum_pool_pass_full_users && pow->username[0] != '.') { // TODO: Make sure the usernames are addresses, and if not use one of the configured addresses i+=snprintf(username, 385, "%s", pow->username); } else if (datum_config.datum_pool_pass_full_users || datum_config.datum_pool_pass_workers) { // append the miner's username to the configured address as .workername i+=snprintf(username, 385, "%s%s%s", datum_config.mining_pool_address, (pow->username[0] == '.') ? "" : ".", pow->username); } i++; // already 0 from snprintf // reserve 4 bytes for future use memset(&msg[i], 0, 4); i+=4; pthread_rwlock_rdlock(&datum_jobs_rwlock); sjob = datum_jobs[pow->datum_job_id].sjob; if (!sjob) { pthread_rwlock_unlock(&datum_jobs_rwlock); return 0; } if (!datum_jobs[pow->datum_job_id].server_has_merkle_branches) { // we need to send the merkle branches with this job // also send the prevblockhash msg[i] = 0x01; i++; memcpy(&msg[i], sjob->prevhash_bin, 32); i+=32; pk_u16le(msg, i, pow->target_byte_index); i += 2; // target byte location in coinb1 memcpy(&msg[i], &sjob->nbits_bin[0], sizeof(sjob->nbits_bin)); i += sizeof(sjob->nbits_bin); // nbits! msg[i] = sjob->datum_coinbaser_id; i++; pk_u32le(msg, i, sjob->height); i += 4; pk_u64le(msg, i, sjob->coinbase_value); i += 8; pk_u32le(msg, i, sjob->block_template->txn_count); i += 4; pk_u32le(msg, i, sjob->block_template->txn_total_weight); i += 4; pk_u32le(msg, i, sjob->block_template->txn_total_size); i += 4; pk_u32le(msg, i, sjob->block_template->txn_total_sigops); i += 4; msg[i] = sjob->merklebranch_count; i++; memcpy(&msg[i], &sjob->merklebranches_bin[0][0], sjob->merklebranch_count * 32); i+=sjob->merklebranch_count * 32; // switch us to a write lock pthread_rwlock_unlock(&datum_jobs_rwlock); pthread_rwlock_wrlock(&datum_jobs_rwlock); w = true; datum_jobs[pow->datum_job_id].server_has_merkle_branches = true; } if (pow->subsidy_only) { if (!datum_jobs[pow->datum_job_id].server_has_coinbase_empty) { msg[i] = 0x02; i++; msg[i] = 0xFF; i++; // subsidy only coinbase! yes, I know we specified above in the flags as well pk_u16le(msg, i, sjob->subsidy_only_coinbase.coinb1_len); i += 2; // len1 pk_u16le(msg, i, sjob->subsidy_only_coinbase.coinb2_len); i += 2; // len2 memcpy(&msg[i], sjob->subsidy_only_coinbase.coinb1_bin, sjob->subsidy_only_coinbase.coinb1_len); i+=sjob->subsidy_only_coinbase.coinb1_len; memcpy(&msg[i], sjob->subsidy_only_coinbase.coinb2_bin, sjob->subsidy_only_coinbase.coinb2_len); i+=sjob->subsidy_only_coinbase.coinb2_len; if (!w) { pthread_rwlock_unlock(&datum_jobs_rwlock); pthread_rwlock_wrlock(&datum_jobs_rwlock); w = true; } datum_jobs[pow->datum_job_id].server_has_coinbase_empty = true; } } else { if (!datum_jobs[pow->datum_job_id].server_has_coinbase[pow->coinbase_id]) { msg[i] = 0x02; i++; msg[i] = pow->coinbase_id; i++; pk_u16le(msg, i, sjob->coinbase[pow->coinbase_id].coinb1_len); i += 2; // len1 pk_u16le(msg, i, sjob->coinbase[pow->coinbase_id].coinb2_len); i += 2; // len2 memcpy(&msg[i], sjob->coinbase[pow->coinbase_id].coinb1_bin, sjob->coinbase[pow->coinbase_id].coinb1_len); i+=sjob->coinbase[pow->coinbase_id].coinb1_len; memcpy(&msg[i], sjob->coinbase[pow->coinbase_id].coinb2_bin, sjob->coinbase[pow->coinbase_id].coinb2_len); i+=sjob->coinbase[pow->coinbase_id].coinb2_len; if (!w) { pthread_rwlock_unlock(&datum_jobs_rwlock); pthread_rwlock_wrlock(&datum_jobs_rwlock); w = true; } datum_jobs[pow->datum_job_id].server_has_coinbase[pow->coinbase_id] = true; } } pthread_rwlock_unlock(&datum_jobs_rwlock); // cap message msg[i] = 0xFE; i++; // pad with some randomness // TODO: Make this dependant on the number of shares we have in our queue to submit, since they can share space in a packet further obfuscating the nature of the data j = 1 + (rand() % 80); memset(&msg[i], rand(), j); i+=j; datum_protocol_mining_cmd(msg, i); if ((datum_protocol_mainloop_tsms - datum_last_accepted_share_local_tsms) > 25000) { // we don't want to trigger a connection timeout just because we are mining very slowly... // so we'll fake this in that case. // There's better ways to do this, but we'll worry about those later // this is just a network hiccup kludge for now. datum_last_accepted_share_tsms = datum_protocol_mainloop_tsms; } datum_last_accepted_share_local_tsms = datum_protocol_mainloop_tsms; return 0; } bool datum_protocol_thread_is_active(void) { if (datum_protocol_client_active != 0) return true; return false; } bool datum_protocol_is_active(void) { if (datum_protocol_client_active == 3) return true; return false; } void *datum_protocol_client(void *args) { struct addrinfo hints, *res, *p; int sockfd = -1; int epollfd, nfds; int flag = 1; struct epoll_event ev, events[MAX_DATUM_CLIENT_EVENTS]; struct timeval start, now; int ret,i,n; datum_protocol_client_active = 1; memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; char port_str[7]; // To hold the port number as a string bool break_again = false; int sent = 0; T_DATUM_PROTOCOL_HEADER s_header; pthread_rwlock_wrlock(&datum_jobs_rwlock); for(i=0;iai_next) { if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1) { DLOG_ERROR("socket(...) error: %s",strerror(errno)); continue; } // Set socket to non-blocking int flags = fcntl(sockfd, F_GETFL, 0); if (flags == -1 || fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) == -1) { DLOG_ERROR("fcntl(...) error: %s",strerror(errno)); close(sockfd); sockfd = -1; continue; } // TCP_NODELAY! Probably not needed since we group sends, but can't hurt. if (setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(int)) < 0) { DLOG_FATAL("setsockopt(TCP_NODELAY) failed: %s", strerror(errno)); close(sockfd); sockfd = -1; continue; } // Start the connection process. gettimeofday(&start, NULL); while (1) { ret = connect(sockfd, p->ai_addr, p->ai_addrlen); if (ret == 0 || errno == EINPROGRESS) { break; // Either connected immediately or in progress } if (errno != EINPROGRESS && errno != EALREADY) { DLOG_ERROR("connect(...) error: %s",strerror(errno)); close(sockfd); sockfd = -1; continue; } // Check for timeout gettimeofday(&now, NULL); if (now.tv_sec - start.tv_sec >= DATUM_PROTOCOL_CONNECT_TIMEOUT) { // TODO: Make configurable DLOG_ERROR("Connection timed out!"); close(sockfd); sockfd = -1; continue; } usleep(100000); // Sleep 100ms before retrying } if (sockfd != -1) { break; // Successfully connected } } freeaddrinfo(res); if (sockfd == -1) { DLOG_FATAL("Could not connect to DATUM server!"); datum_protocol_client_active = 0; return NULL; } // Set up epoll if ((epollfd = epoll_create1(0)) == -1) { DLOG_FATAL("epoll_create1(...) error: %s",strerror(errno)); close(sockfd); datum_protocol_client_active = 0; return NULL; } ev.events = EPOLLIN | EPOLLERR | EPOLLHUP; ev.data.fd = sockfd; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev) == -1) { DLOG_FATAL("epoll_ctl(...) error: %s",strerror(errno)); close(sockfd); close(epollfd); datum_protocol_client_active = 0; return NULL; } i = 0; datum_last_accepted_share_tsms = 0; datum_last_accepted_share_local_tsms = 0; latest_server_msg_tsms = current_time_millis(); while (1) { i++; datum_protocol_mainloop_tsms = current_time_millis(); // Sanity check. If we haven't received anything at all from the server in the set time, then it's pretty likely there's a connection issue. if ((datum_protocol_mainloop_tsms - latest_server_msg_tsms) >= datum_config.datum_protocol_global_timeout_ms) { // Pretty safe bet that the connection is dead. DLOG_WARN("No data received from server in over %d seconds. Exiting protocol thread to retry.", datum_config.datum_protocol_global_timeout); break; } // Sanity check. If we've been sending shares but getting no acceptance for > 30 seconds, something is wrong and we should fail and start over if ((datum_last_accepted_share_tsms != 0) && (datum_last_accepted_share_local_tsms != 0)) { if (datum_last_accepted_share_local_tsms > datum_last_accepted_share_tsms) { if ((datum_last_accepted_share_local_tsms - datum_last_accepted_share_tsms) >= 30000) { // no response to our latest share for > 30 seconds DLOG_WARN("No share acceptance response for > 30 seconds. Exiting protocol thread to retry."); break; } } } // Queue up sends for PoW submissions datum_protocol_pow_queue_submits(); pthread_mutex_lock(&datum_protocol_send_buffer_lock); if (server_out_buf) { sent = send(sockfd, server_send_buffer, server_out_buf, MSG_DONTWAIT); if (sent > 0) { if (sent < server_out_buf) { // not a full send. shift remaining data to beginning of w_buffer memmove(server_send_buffer, server_send_buffer + sent, server_out_buf - sent); } if (sent <= server_out_buf) { server_out_buf -= sent; } else { // should never happen server_out_buf = 0; } } else { if (!(errno == EAGAIN || errno == EWOULDBLOCK || errno == ENOTCONN)) { pthread_mutex_unlock(&datum_protocol_send_buffer_lock); break; } } } pthread_mutex_unlock(&datum_protocol_send_buffer_lock); break_again = false; // Basic state machine for connection setup switch(datum_state) { case 0: { // Hello, server! if (datum_protocol_send_hello(sockfd) < 0) { DLOG_FATAL("Error sending handshake start message."); break_again = true; break; } datum_state = 1; break; } case 1: { // waiting on server response // Global nothing-from-server timeout applies break; } case 2: { // we're online! datum_protocol_client_active = 2; break; } case 3: { // we're configured! datum_protocol_client_active = 3; break; } default: break; } if (break_again) break; nfds = epoll_wait(epollfd, events, MAX_DATUM_CLIENT_EVENTS, 5); // Wait for 5ms if (nfds == -1 && errno != EINTR) { DLOG_FATAL("epoll_wait(...) error: %s",strerror(errno)); break; } if (nfds <= 0) { continue; // Timeout, nothing happened } if (events[0].events & (EPOLLERR | EPOLLHUP)) { int err = 0; socklen_t errlen = sizeof(err); if (getsockopt(events[0].data.fd, SOL_SOCKET, SO_ERROR, &err, &errlen) == 0) { if (err != 0) { DLOG_ERROR("Socket error: %s", strerror(err)); } else { DLOG_ERROR("Socket hangup with no error."); } } else { DLOG_ERROR("Socket error, but failed to get the specific error: %s", strerror(errno)); } break; } if (events[0].events & EPOLLIN) { // data to receive break_again = false; // Receive the header, followed by any data specified by the header switch(protocol_state) { // order matters because of fall throughs case 1: case 2: case 3: { n = recv(sockfd, ((unsigned char *)&s_header) + (sizeof(T_DATUM_PROTOCOL_HEADER) - protocol_state), protocol_state, MSG_DONTWAIT); if (n <= 0) { if ((n < 0) && ((errno == EAGAIN || errno == EWOULDBLOCK))) { continue; } DLOG_DEBUG("recv() issue. protocol_state=%d, n=%d, errno=%d (%s)", protocol_state, n, errno, strerror(errno)); break_again = true; break; } if ((n+(sizeof(T_DATUM_PROTOCOL_HEADER) - protocol_state)) != sizeof(T_DATUM_PROTOCOL_HEADER)) { if ((n+protocol_state) > 4) { DLOG_DEBUG("recv() issue. too many header bytes. protocol_state=%d, n=%d, errno=%d (%s)", protocol_state, n, errno, strerror(errno)); break_again = true; break; } protocol_state = sizeof(T_DATUM_PROTOCOL_HEADER) - n - (sizeof(T_DATUM_PROTOCOL_HEADER) - protocol_state); // should give us a state equal to the number of. consoluted to show the process. (compiler optimizes) continue; } protocol_state = 4; continue; // cant fall through to 0, so loop around back to this to jump to 4 } case 0: { n = recv(sockfd, &s_header, sizeof(T_DATUM_PROTOCOL_HEADER), MSG_DONTWAIT); if (n <= 0) { if ((n < 0) && ((errno == EAGAIN || errno == EWOULDBLOCK))) { continue; } DLOG_DEBUG("recv() issue. protocol_state=%d, n=%d, errno=%d (%s)", protocol_state, n, errno, strerror(errno)); break_again = true; break; } if (n != sizeof(T_DATUM_PROTOCOL_HEADER)) { if (n > 4) { DLOG_DEBUG("recv() issue. too many header bytes (B). protocol_state=%d, n=%d, errno=%d (%s)", protocol_state, n, errno, strerror(errno)); break_again = true; break; } protocol_state = sizeof(T_DATUM_PROTOCOL_HEADER)-n; continue; } protocol_state = 4; // Fall through to 4 now! [[fallthrough]]; } case 4: { datum_xor_header_key(&s_header, receiving_header_key); //DLOG_DEBUG("Server CMD: cmd=%u, len=%u, raw = %8.8x ... rkey = %8.8x", s_header.proto_cmd, s_header.cmd_len, upk_u32le(s_header, 0), receiving_header_key); receiving_header_key = datum_header_xor_feedback(receiving_header_key); protocol_state = 5; server_in_buf = 0; if (!s_header.cmd_len) { // fall through to 5 server_in_buf = 0; } else { n = recv(sockfd, server_recv_buffer, s_header.cmd_len, MSG_DONTWAIT); if (n <= 0) { if ((n < 0) && ((errno == EAGAIN || errno == EWOULDBLOCK))) { continue; } DLOG_DEBUG("recv() issue. protocol_state=%d, n=%d, errno=%d (%s)", protocol_state, n, errno, strerror(errno)); break_again = true; break; } if (n > s_header.cmd_len) { DLOG_DEBUG("recv() issue. too many header bytes (C). protocol_state=%d, n=%d, errno=%d (%s)", protocol_state, n, errno, strerror(errno)); break_again = true; break; } protocol_state = 5; server_in_buf = n; if (n < s_header.cmd_len) { continue; } // fall through to 5 } [[fallthrough]]; } case 5: { if (server_in_buf < s_header.cmd_len) { n = recv(sockfd, &server_recv_buffer[server_in_buf], s_header.cmd_len-server_in_buf, MSG_DONTWAIT); if (n <= 0) { if ((n < 0) && ((errno == EAGAIN || errno == EWOULDBLOCK))) { continue; } DLOG_DEBUG("recv() issue. protocol_state=%d, n=%d, errno=%d (%s)", protocol_state, n, errno, strerror(errno)); break_again = true; break; } if (n+server_in_buf > s_header.cmd_len) { DLOG_DEBUG("recv() issue. too many data bytes. cmd_len=%d, server_in_buf=%d, protocol_state=%d, n=%d, errno=%d (%s)", s_header.cmd_len, server_in_buf, protocol_state, n, errno, strerror(errno)); break_again = true; break; } server_in_buf += n; if (server_in_buf < s_header.cmd_len) { continue; } } if (server_in_buf == s_header.cmd_len) { n = datum_protocol_server_msg(&s_header, server_recv_buffer); if (n < 0) { DLOG_DEBUG("datum_protocol_server_msg returned %d",n); break_again = true; break; } protocol_state = 0; server_in_buf = 0; continue; } break; } default: { // Should never happen! DLOG_DEBUG("unknown protocol_state %d!",protocol_state); break_again = true; break; } } if (break_again) break; } } // Must clean up this thread, as it can be restarted by the main thread. DLOG_DEBUG("DATUM Protocol Thread is exiting."); close(sockfd); close(epollfd); datum_protocol_client_active = 0; datum_queue_free(&pow_queue); // Wait up to 5 seconds for another thread to reconnect for (i = 2000; i; --i) { if (datum_protocol_client_active >= 3) break; usleep(2500); } // ...then force a new job datum_blocktemplates_notify_othercause(); return 0; } void datum_encrypt_log_pubkeys(DATUM_ENC_KEYS *keys) { char s[512]; int i; for(i=0;ipk_ed25519[i]); } s[i<<1] = 0; DLOG_INFO("Signing Public Key: %s", s); for(i=0;ipk_x25519[i]); } s[i<<1] = 0; DLOG_INFO("Encryption Public Key: %s", s); } void datum_protocol_start_connector(void) { pthread_t pthread_datum_protocol_client; if (!datum_protocol_client_active) { datum_protocol_client_active = 1; // no delay! DLOG_DEBUG("Starting DATUM " DATUM_PROTOCOL_VERSION " client..."); if (pthread_create(&pthread_datum_protocol_client, NULL, datum_protocol_client, NULL) != 0) { DLOG_ERROR("Could not start thread for DATUM Protocol"); datum_protocol_client_active = 0; return; } pthread_detach(pthread_datum_protocol_client); } else { DLOG_DEBUG("DATUM client already running."); } } int datum_protocol_init(void) { if (datum_config.datum_pool_host[0] == 0) { DLOG_WARN("****************************************************"); DLOG_WARN("*** DATUM pool host is blank. NON-POOLED MINING! ***"); DLOG_WARN("****************************************************"); return 0; } if (sodium_init() < 0) { DLOG_FATAL("libsodium initialization failed"); return -1; } memset(&local_datum_keys, 0, sizeof(DATUM_ENC_KEYS)); memset(&pool_keys, 0, sizeof(DATUM_ENC_KEYS)); memset(&session_datum_keys, 0, sizeof(DATUM_ENC_KEYS)); if (datum_encrypt_generate_keys(&local_datum_keys) != 0) { DLOG_FATAL("Could not generate our keys"); return -1; } DLOG_INFO("Our public keys:"); datum_encrypt_log_pubkeys(&local_datum_keys); if (datum_pubkey_to_struct(datum_config.datum_pool_pubkey, &pool_keys) != 0) { DLOG_WARN("Pool pubkey not specified or invalid."); return -1; } pool_keys.is_remote = true; DLOG_INFO("Pool's public keys: (You should periodically verify that these are what you expect!)"); datum_encrypt_log_pubkeys(&pool_keys); datum_protocol_start_connector(); return 0; } int datum_encrypt_generate_keys(DATUM_ENC_KEYS *keys) { int i; // generate an Ed25519 key pair i = crypto_sign_keypair(keys->pk_ed25519, keys->sk_ed25519); if (i != 0) return i; // generate an X25519 key pair i = crypto_box_keypair(keys->pk_x25519, keys->sk_x25519); if (i != 0) return i; keys->is_remote = false; return 0; } datum_gateway-0.4.1beta/src/datum_protocol.h000066400000000000000000000134561512710151500212050ustar00rootroot00000000000000/* * * DATUM Gateway * Decentralized Alternative Templates for Universal Mining * * This file is part of OCEAN's Bitcoin mining decentralization * project, DATUM. * * https://ocean.xyz * * --- * * Copyright (c) 2024 Bitcoin Ocean, LLC & Jason Hughes * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ #ifndef _DATUM_PROTOCOL_H_ #define _DATUM_PROTOCOL_H_ #include #include #include "datum_stratum.h" // This is a protocol limit! server will truncate down to 8, unless a new spec is done that permits more. // Works out to over 5 minutes of jobs at 30-40 second work change intervals. No miner should be holding on to work this long. #define MAX_DATUM_PROTOCOL_JOBS 8 #define DATUM_PROTOCOL_VERSION "v0.4.1-beta" // this is sent to the server as a UA #define DATUM_PROTOCOL_CONNECT_TIMEOUT 30 #define DATUM_PROTOCOL_MAX_CMD_DATA_SIZE 4194304 // 2^22 - protocol limit! #define DATUM_PROTOCOL_BUFFER_SIZE (DATUM_PROTOCOL_MAX_CMD_DATA_SIZE*3) #define MAX_DATUM_CLIENT_EVENTS 32 // Header is only XOR'd with a rotating key. This is NOT 100% secure, and makes the cmd# and length of the handshake message decipherable. // This is not an issue for security, however, as all following packets use a negotiated XOR key. // It's likely possible to brute force the XOR key to break packets down into individual commands, but the contents and nature of the // cmd is still obfuscated and unrecoverable without the session keys. typedef struct __attribute__((packed)) T_DATUM_PROTOCOL_HEADER { uint32_t cmd_len:22; // max cmd size is 2^22 (~4MB), which is roughly the max block size for a raw submission or a raw template validation uint8_t reserved:2; // save for later use bool is_signed:1; bool is_encrypted_pubkey:1; bool is_encrypted_channel:1; uint8_t proto_cmd:5; // 32 protocol level commands } T_DATUM_PROTOCOL_HEADER; typedef struct { bool is_remote; // ed25519 key pair (signing) unsigned char pk_ed25519[crypto_sign_PUBLICKEYBYTES]; unsigned char sk_ed25519[crypto_sign_SECRETKEYBYTES]; // x25519 key pair (encyption) unsigned char pk_x25519[crypto_box_PUBLICKEYBYTES]; unsigned char sk_x25519[crypto_box_SECRETKEYBYTES]; } DATUM_ENC_KEYS; typedef struct { DATUM_ENC_KEYS *local; DATUM_ENC_KEYS *remote; unsigned char precomp_remote[crypto_box_BEFORENMBYTES]; } DATUM_ENC_PRECOMP; typedef struct T_DATUM_PROTOCOL_JOB { unsigned char datum_job_id; T_DATUM_STRATUM_JOB *sjob; bool server_has_merkle_branches; bool server_has_coinbase[8]; bool server_has_coinbase_empty; bool server_has_short_txnlist; bool server_has_validated_block; } T_DATUM_PROTOCOL_JOB; typedef struct { unsigned char datum_job_id; unsigned char extranonce[12]; char username[384]; unsigned char coinbase_id; bool subsidy_only; bool is_block; bool quickdiff; unsigned char target_byte; uint16_t target_byte_index; uint32_t ntime; uint32_t nonce; uint32_t version; } T_DATUM_PROTOCOL_POW; int datum_protocol_init(void); int datum_encrypt_generate_keys(DATUM_ENC_KEYS *keys); bool datum_protocol_is_active(void); void datum_increment_session_nonce(void *s); int datum_protocol_fetch_coinbaser(uint64_t value); int datum_protocol_coinbaser_fetch(void *s); int datum_protocol_pow_submit( const T_DATUM_CLIENT_DATA *c, const T_DATUM_STRATUM_JOB *job, const char *username, const bool was_block, const bool subsidy_only, const bool quickdiff, const unsigned char *block_header, const uint64_t target_diff, const unsigned char *full_cb_tx, const T_DATUM_STRATUM_COINBASE *cb, unsigned char *extranonce, unsigned char coinbase_index ); bool datum_protocol_thread_is_active(void); void datum_protocol_start_connector(void); unsigned char datum_protocol_setup_new_job_idx(void *sx); extern uint64_t datum_accepted_share_count; extern uint64_t datum_accepted_share_diff; extern uint64_t datum_rejected_share_count; extern uint64_t datum_rejected_share_diff; #define DATUM_REJECT_BAD_JOB_ID 10 #define DATUM_REJECT_BAD_COINBASE_ID 11 #define DATUM_REJECT_BAD_EXTRANONCE_SIZE 12 #define DATUM_REJECT_BAD_TARGET 13 #define DATUM_REJECT_BAD_USERNAME 14 #define DATUM_REJECT_BAD_COINBASER_ID 15 #define DATUM_REJECT_BAD_MERKLE_COUNT 16 #define DATUM_REJECT_BAD_COINBASE_TOO_LARGE 17 #define DATUM_REJECT_COINBASE_MISSING 18 #define DATUM_REJECT_TARGET_MISMATCH 19 #define DATUM_REJECT_H_NOT_ZERO 20 #define DATUM_REJECT_HIGH_HASH 21 #define DATUM_REJECT_COINBASE_ID_MISMATCH 22 #define DATUM_REJECT_BAD_NTIME 23 #define DATUM_REJECT_BAD_VERSION 24 #define DATUM_REJECT_STALE_BLOCK 25 #define DATUM_REJECT_BAD_COINBASE 26 #define DATUM_REJECT_BAD_COINBASE_OUTPUTS 27 #define DATUM_REJECT_MISSING_POOL_TAG 28 #define DATUM_REJECT_DUPLICATE_WORK 29 #define DATUM_REJECT_OTHER 30 #define DATUM_POW_SHARE_RESPONSE_ACCEPTED 0x50 #define DATUM_POW_SHARE_RESPONSE_ACCEPTED_TENTATIVELY 0x55 #define DATUM_POW_SHARE_RESPONSE_REJECTED 0x66 #endif datum_gateway-0.4.1beta/src/datum_queue.c000066400000000000000000000172171512710151500204620ustar00rootroot00000000000000/* * * DATUM Gateway * Decentralized Alternative Templates for Universal Mining * * This file is part of OCEAN's Bitcoin mining decentralization * project, DATUM. * * https://ocean.xyz * * --- * * Copyright (c) 2024 Bitcoin Ocean, LLC & Jason Hughes * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ // Generic threaded queue implementation. // Used for DATUM Protocol share submissions. // TODO: Use for share logger? #include #include #include #include #include #include #include #include #include #include #include #include #include #include "datum_queue.h" #include "datum_logger.h" #include "datum_utils.h" int datum_queue_free(DATUM_QUEUE *q) { if (!q->initialized) return -1; pthread_rwlock_wrlock(&q->active_buffer_rwlock); pthread_rwlock_wrlock(&q->buffer_rwlock[0]); pthread_rwlock_wrlock(&q->buffer_rwlock[1]); if (q->buffer[0]) { free(q->buffer[0]); } q->initialized = false; q->buffer[0] = 0; pthread_rwlock_unlock(&q->buffer_rwlock[1]); pthread_rwlock_unlock(&q->buffer_rwlock[0]); pthread_rwlock_unlock(&q->active_buffer_rwlock); pthread_rwlock_destroy(&q->active_buffer_rwlock); pthread_rwlock_destroy(&q->buffer_rwlock[0]); pthread_rwlock_destroy(&q->buffer_rwlock[1]); memset(q, 0, sizeof(DATUM_QUEUE)); return 0; } int datum_queue_prep(DATUM_QUEUE *q, const int max_items, const int item_size, int (*item_handler)(void *)) { memset(q, 0, sizeof(DATUM_QUEUE)); q->initialized = false; if (pthread_rwlock_init(&q->active_buffer_rwlock, NULL) != 0) { DLOG_FATAL("Could not initialize lock 1"); return -1; } if (pthread_rwlock_init(&q->buffer_rwlock[0], NULL) != 0) { DLOG_FATAL("Could not initialize lock 2"); return -1; } if (pthread_rwlock_init(&q->buffer_rwlock[1], NULL) != 0) { DLOG_FATAL("Could not initialize lock 3"); return -1; } q->max_entries = max_items; q->queue_version[1] = 10; q->buffer[0] = calloc((max_items + 16)*2, item_size); if (!q->buffer[0]) { DLOG_FATAL("Could not allocate memory for queue items! (%d bytes)", (max_items + 16)*2*item_size); return -1; } q->buffer[1] = ((char *)q->buffer[0]) + ((max_items + 16) * item_size); q->item_size = item_size; // handler function pointer q->item_handler = item_handler; q->initialized = true; return 0; } int datum_queue_add_item(DATUM_QUEUE *q, void *item) { int buffer_id, i; uint64_t buffer_version; void *out; if (!q->initialized) return -1; // Add the msg to the logger queue // this is probably overkill... for (i=0;i<10000000;i++) { if (i < 9999999) { // ensure we don't get the lock on the last try and forget to unlock and crash // get the active buffer ID pthread_rwlock_rdlock(&q->active_buffer_rwlock); buffer_id = q->active_buffer; buffer_version = q->active_buffer_version; pthread_rwlock_unlock(&q->active_buffer_rwlock); // get a write lock for that buffer pthread_rwlock_wrlock(&q->buffer_rwlock[buffer_id]); // check for race condition on buffer swap if (buffer_version != q->queue_version[buffer_id]) { // Race condition! pthread_rwlock_unlock(&q->buffer_rwlock[buffer_id]); } else { // no race condition, we're good break; } } } if (i >= 10000000) { // we have no locks, but we also couldn't sync up on the rare race condition after 10000000 attempts // means something very bad is probably happening. DLOG_ERROR("Could not satisfy queue race condition. Is there anything consuming this queue? Likely a bug!"); return -1; } if (q->queue_next[buffer_id] >= q->max_entries) { pthread_rwlock_unlock(&q->buffer_rwlock[buffer_id]); DLOG_ERROR("Queue overflow! Is there anything consuming this queue? Likely a bug!"); return -1; } out = ((char *)q->buffer[buffer_id]) + (q->queue_next[buffer_id] * q->item_size); memcpy(out, item, q->item_size); q->queue_next[buffer_id]++; // bounds check is above, since we can potentially delay to wait for the writer instead of failing here pthread_rwlock_unlock(&q->buffer_rwlock[buffer_id]); //DLOG_DEBUG("QUEUE ADD @ %p", out); return 0; } int datum_queue_process(DATUM_QUEUE *q) { // process any items in the specified queue // only one thread should ever call this, realistically. // if more than one thread needs to process a queue, this will need a good bit of modification. int buffer_id,offline_buffer_id; int i; void *item; if (!q->initialized) return -1; // We don't need to read lock to read this, as we're the only thread that writes to it. buffer_id = q->active_buffer; pthread_rwlock_rdlock(&q->buffer_rwlock[buffer_id]); i = q->queue_next[buffer_id]; pthread_rwlock_unlock(&q->buffer_rwlock[buffer_id]); if (!i) { // nothing in queue return 0; } // there are msgs to write. // switch the writers over to the other buffer, and then work on that // this lock prevents msgs from being queued and holds up all other threads // we need to release it ASAP pthread_rwlock_wrlock(&q->active_buffer_rwlock); // we'll get a lock on writing to the current buffer. pthread_rwlock_wrlock(&q->buffer_rwlock[buffer_id]); // at this point we could have threads waiting on the buffer ID, and // we also could have threads waiting to write to the buffer we just got a // write lock on if the beat the race to lock the buffer_id // so we must increment the version of the current buffer, which will signal it's stale q->queue_version[buffer_id]++; // no one should be waiting to write the other buffer offline_buffer_id = buffer_id?0:1; pthread_rwlock_wrlock(&q->buffer_rwlock[offline_buffer_id]); // we now have write locks on everything // increment version again, just in case q->queue_version[offline_buffer_id]++; // store the new offline buffer ID as the active q->active_buffer_version = q->queue_version[offline_buffer_id]; // make the offline buffer the active one q->active_buffer = offline_buffer_id; // just in case q->queue_next[offline_buffer_id] = 0; // release the lock on the offline pthread_rwlock_unlock(&q->buffer_rwlock[offline_buffer_id]); // release the lock on the buffer index... which releases any threads waiting to write pthread_rwlock_unlock(&q->active_buffer_rwlock); for(i=0;iqueue_next[buffer_id];i++) { // process items item = ((char *)q->buffer[buffer_id]) + (i * q->item_size); q->item_handler(item); // TODO: Handle errors from handler? // If such a thing is needed in the future, implement it here so as not to break other things using these queues. } // all done q->queue_next[buffer_id] = 0; pthread_rwlock_unlock(&q->buffer_rwlock[buffer_id]); return i; } datum_gateway-0.4.1beta/src/datum_queue.h000066400000000000000000000040171512710151500204610ustar00rootroot00000000000000/* * * DATUM Gateway * Decentralized Alternative Templates for Universal Mining * * This file is part of OCEAN's Bitcoin mining decentralization * project, DATUM. * * https://ocean.xyz * * --- * * Copyright (c) 2024 Bitcoin Ocean, LLC & Jason Hughes * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ #ifndef _DATUM_QUEUE_H_ #define _DATUM_QUEUE_H_ #include #include typedef struct { volatile bool initialized; int max_entries; pthread_rwlock_t active_buffer_rwlock; int active_buffer; uint64_t active_buffer_version; pthread_rwlock_t buffer_rwlock[2]; int queue_next[2]; uint64_t queue_version[2]; size_t item_size; int buf_idx[2]; void *buffer[2]; // pointer to processor function int (*item_handler)(void *); } DATUM_QUEUE; int datum_queue_prep(DATUM_QUEUE *q, const int max_items, const int item_size, int (*item_handler)(void *)); int datum_queue_process(DATUM_QUEUE *q); int datum_queue_add_item(DATUM_QUEUE *q, void *item); int datum_queue_free(DATUM_QUEUE *q); #endif datum_gateway-0.4.1beta/src/datum_sockets.c000066400000000000000000000711771512710151500210160ustar00rootroot00000000000000/* * * DATUM Gateway * Decentralized Alternative Templates for Universal Mining * * This file is part of OCEAN's Bitcoin mining decentralization * project, DATUM. * * https://ocean.xyz * * --- * * Copyright (c) 2024-2025 Bitcoin Ocean, LLC & Jason Hughes * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "datum_conf.h" #include "datum_gateway.h" #include "datum_protocol.h" #include "datum_utils.h" #include "datum_sockets.h" int datum_active_threads = 0; int datum_active_clients = 0; int get_remote_ip(int fd, char *ip, size_t max_len) { struct sockaddr_storage addr; socklen_t addr_len = sizeof(addr); // Get the address of the peer if (getpeername(fd, (struct sockaddr*)&addr, &addr_len) == -1) { strncpy(ip, "0.0.0.0", max_len); return -1; } // Check if the address is IPv4 or IPv6 if (addr.ss_family == AF_INET) { struct sockaddr_in *s = (struct sockaddr_in *)&addr; if (inet_ntop(AF_INET, &s->sin_addr, ip, max_len) == NULL) { strncpy(ip, "0.0.0.0", max_len); return -1; } } else if (addr.ss_family == AF_INET6) { struct sockaddr_in6 *s = (struct sockaddr_in6 *)&addr; if (inet_ntop(AF_INET6, &s->sin6_addr, ip, max_len) == NULL) { strncpy(ip, "0.0.0.0", max_len); return -1; } } else { strncpy(ip, "0.0.0.0", max_len); return -1; } return 0; } void *datum_threadpool_thread(void *arg) { T_DATUM_THREAD_DATA *my = (T_DATUM_THREAD_DATA *)arg; int i, nfds, n, cidx, j; size_t leftover = 0; if (!my->app->client_cmd_func) { DLOG_FATAL("Thread pool thread started with no client command function pointer. :("); panic_from_thread(__LINE__); return 0; } my->epollfd = epoll_create1(EPOLL_CLOEXEC); if (my->epollfd < 0) { DLOG_FATAL("could not epoll_create!"); panic_from_thread(__LINE__); return 0; } // Call application specific thread init if (my->app->init_func) my->app->init_func(my); while(1) { pthread_mutex_lock(&my->thread_data_lock); if (!my->connected_clients) { // no clients to serve // shutdown this thread after some kind of timeout? pthread_mutex_unlock(&my->thread_data_lock); // the loop doesn't care if we have no clients... if (my->app->loop_func) my->app->loop_func(my); my->has_client_kill_request = false; my->empty_request = false; usleep(10000); continue; } // check if any new clients, handle them if so if (my->has_new_clients) { for(i=0;iapp->max_clients_thread;i++) { if (my->client_data[i].new_connection) { my->client_data[i].new_connection = false; my->client_data[i].in_buf = 0; my->client_data[i].out_buf = 0; my->client_data[i].proxy_line_read = 0; // add to epoll for this thread my->ev.events = EPOLLIN | EPOLLONESHOT | EPOLLERR; // | EPOLLRDHUP my->ev.data.u64 = i; // store client index... duh if (epoll_ctl(my->epollfd, EPOLL_CTL_ADD, my->client_data[i].fd, &my->ev) < 0) { DLOG_ERROR("epoll_ctl add failed: %s", strerror(errno)); close(my->client_data[i].fd); // Close the file descriptor on error // call closed client function, if any if (my->app->closed_client_func) my->app->closed_client_func(&my->client_data[i], "epoll_ctl add failed @ new connection"); // we already have a lock on this thread's data here, so can (must) decrement wo/locking again datum_socket_thread_client_count_decrement(my, i, false); continue; } // call new client handler, if any if (my->app->new_client_func) my->app->new_client_func(&my->client_data[i]); } } my->has_new_clients = false; } pthread_mutex_unlock(&my->thread_data_lock); if (__builtin_expect(my->empty_request,0)) { // We got a request to empty all clients from our thread! DLOG_WARN("Executing command to empty thread (%d clients)",my->connected_clients); for (j = 0; j < my->app->max_clients_thread; j++) { if (my->client_data[j].fd != 0) { epoll_ctl(my->epollfd, EPOLL_CTL_DEL, my->client_data[j].fd, NULL); close(my->client_data[j].fd); // call closed client function, if any if (my->app->closed_client_func) my->app->closed_client_func(&my->client_data[j], "empty thread command"); datum_socket_thread_client_count_decrement(my, j, true); } } } else if (__builtin_expect(my->has_client_kill_request,0)) { // the API has requested we kill a specific client for (j = 0; j < my->app->max_clients_thread; j++) { if ((my->client_data[j].fd != 0) && (my->client_data[j].kill_request)) { my->client_data[j].kill_request = false; epoll_ctl(my->epollfd, EPOLL_CTL_DEL, my->client_data[j].fd, NULL); close(my->client_data[j].fd); // call closed client function, if any if (my->app->closed_client_func) my->app->closed_client_func(&my->client_data[j], "client kill command"); datum_socket_thread_client_count_decrement(my, j, true); } } } my->has_client_kill_request = false; my->empty_request = false; // Call application specific thread preloop if (my->app->loop_func) my->app->loop_func(my); // TODO: make this smarter // See if there's anything to write for any of our clients before looping through all potential clients. // Will need profiling, as this is pretty cheap to do with reasonable max_clients_thread. // If there's data, attempt to send it. for (j = 0; j < my->app->max_clients_thread; j++) { if ((my->client_data[j].fd != 0) && (my->client_data[j].out_buf > 0)) { int sent = send(my->client_data[j].fd, my->client_data[j].w_buffer, my->client_data[j].out_buf, MSG_DONTWAIT); if (sent > 0) { if (sent < my->client_data[j].out_buf) { // not a full send. shift remaining data to beginning of w_buffer memmove(my->client_data[j].w_buffer, my->client_data[j].w_buffer + sent, my->client_data[j].out_buf - sent); } if (sent <= my->client_data[j].out_buf) { my->client_data[j].out_buf -= sent; } else { // should never happen my->client_data[j].out_buf = 0; } } else { if (!(errno == EAGAIN || errno == EWOULDBLOCK)) { epoll_ctl(my->epollfd, EPOLL_CTL_DEL, my->client_data[j].fd, NULL); close(my->client_data[j].fd); // call closed client function, if any if (my->app->closed_client_func) my->app->closed_client_func(&my->client_data[j], "send error"); datum_socket_thread_client_count_decrement(my, j, true); } } } } // check if we have any data to read from any existing clients nfds = epoll_wait(my->epollfd, my->events, MAX_EVENTS, 7); if (nfds < 0) { if (errno != EINTR) { DLOG_ERROR("epoll_wait returned %d", nfds); sleep(1); continue; } } if (nfds) { for(i=0;ievents[i].data.u64; if (cidx >= 0) { n = recv(my->client_data[cidx].fd, &my->client_data[cidx].buffer[my->client_data[cidx].in_buf], CLIENT_BUFFER - 1 - my->client_data[cidx].in_buf, MSG_DONTWAIT); if (n <= 0) { if ((n < 0) && ((errno == EAGAIN || errno == EWOULDBLOCK))) { // we epoll'd without edge triggering. this shouldn't happen! DLOG_DEBUG("recv returned would block or again! shouldn't happen?"); continue; // continue for loop } else { // an error occurred or the client closed the connection DLOG_DEBUG("Thread %03d epoll --- Closing fd %d (n=%d) errno=%d (%s) (req bytes: %d)", my->thread_id, my->client_data[cidx].fd, n, errno, strerror(errno), CLIENT_BUFFER - 1 - my->client_data[cidx].in_buf); epoll_ctl(my->epollfd, EPOLL_CTL_DEL, my->client_data[cidx].fd, NULL); close(my->client_data[cidx].fd); // call closed client function, if any if (my->app->closed_client_func) my->app->closed_client_func(&my->client_data[cidx], "client closed connection"); datum_socket_thread_client_count_decrement(my, cidx, true); } } else { // null terminate the buffer for simplicity // this set of functions is currently only used for stratum v1-like protocols, but can easily be adopted to others. my->client_data[cidx].buffer[my->client_data[cidx].in_buf+n] = 0; char *start_line = my->client_data[cidx].buffer; char *end_line = strchr(start_line, '\n'); while (end_line != NULL) { *end_line = 0; // null terminate the line if (datum_config.stratum_v1_trust_proxy != -1 && my->client_data[cidx].proxy_line_read != -1) { if (strncmp(start_line, "PROXY ", 6) == 0) { my->client_data[cidx].proxy_line_read += 1; if (my->client_data[cidx].proxy_line_read <= datum_config.stratum_v1_trust_proxy) { char src_ip[DATUM_MAX_IP_LEN + 1]; int matched = sscanf(start_line, "PROXY TCP4 %15s", src_ip); if (matched != 1) { matched = sscanf(start_line, "PROXY TCP6 %45s", src_ip); } if (matched == 1 && src_ip[0] != 0) { DLOG_DEBUG("New proxy IP detected: %s on TID: %d, CID: %d", src_ip, my->thread_id, my->client_data[cidx].cid); strcpy(my->client_data[cidx].rem_host, src_ip); } else { DLOG_DEBUG("PROXY line present but no valid IP found, keeping original source IP: %s on TID: %d, CID: %d", my->client_data[cidx].rem_host, my->thread_id, my->client_data[cidx].cid); } } start_line = end_line + 1; end_line = strchr(start_line, '\n'); continue; } else { DLOG_DEBUG("Received non-PROXY line from client %d/%d", my->thread_id, my->client_data[cidx].cid); my->client_data[cidx].proxy_line_read = -1; } } // this function can not be NULL j = my->app->client_cmd_func(&my->client_data[cidx], start_line); if (j < 0) { //LOG_PRINTF("Thread %03d --- Closing fd %d (client_cmd_func returned %d)", my->thread_id, my->client_data[cidx].fd, j); epoll_ctl(my->epollfd, EPOLL_CTL_DEL, my->client_data[cidx].fd, NULL); close(my->client_data[cidx].fd); // call closed client function, if any if (my->app->closed_client_func) my->app->closed_client_func(&my->client_data[cidx], "client_cmd_func returned error"); datum_socket_thread_client_count_decrement(my, cidx, true); start_line[0] = 0; break; } start_line = end_line + 1; end_line = strchr(start_line, '\n'); } // If any data is leftover, shift it to the beginning of the buffer // TODO: Implement a buffer type that doesn't require memmove on a partial read if (start_line[0] != 0) { leftover = strlen(start_line); // we null terminate the buffer above if (leftover) { memmove(my->client_data[cidx].buffer, start_line, leftover+1); // we null terminated the read above, remember? } } else { leftover = 0; } my->client_data[cidx].in_buf = leftover; if (my->client_data[cidx].in_buf >= (CLIENT_BUFFER - 1)) { // buffer overrun. lose the data. will probably break things, so punt the client. this shouldn't happen with sane clients. my->client_data[cidx].in_buf = 0; my->client_data[cidx].buffer[0] = 0; epoll_ctl(my->epollfd, EPOLL_CTL_DEL, my->client_data[cidx].fd, NULL); close(my->client_data[cidx].fd); // call closed client function, if any if (my->app->closed_client_func) my->app->closed_client_func(&my->client_data[cidx], "read buffer overrun before client command break"); datum_socket_thread_client_count_decrement(my, cidx, true); } } } if (my->client_data[cidx].fd > 0) { // re-add to epoll for this client my->ev.events = EPOLLIN | EPOLLONESHOT; my->ev.data.u64 = cidx; // store client index... duh if (epoll_ctl(my->epollfd, EPOLL_CTL_MOD, my->client_data[cidx].fd, &my->ev) < 0) { // if this fails, there's probably some bad things happening. In any case, we can't continue serving this client so we should punt them. DLOG_ERROR("epoll_ctl mod for client %d", cidx); close(my->client_data[cidx].fd); // Close the file descriptor on error // call closed client function, if any if (my->app->closed_client_func) my->app->closed_client_func(&my->client_data[cidx], "epoll_ctl error re-upping client polling"); datum_socket_thread_client_count_decrement(my, cidx, true); continue; } } } } } return NULL; } void clean_thread_data(T_DATUM_THREAD_DATA *d, T_DATUM_SOCKET_APP *app) { int i,ret; // clean up clients, just in case for(i=0;imax_clients_thread;i++) { d->client_data[i].new_connection = false; d->client_data[i].fd = 0; d->client_data[i].in_buf = 0; d->client_data[i].out_buf = 0; } d->connected_clients = 0; d->next_open_client_index = 0; d->has_new_clients = false; // clear polling events // TODO: dynamic allocation of buffers memset(&d->ev, 0, sizeof(struct epoll_event)); memset(d->events, 0, sizeof(struct epoll_event) * MAX_CLIENTS_THREAD*2); // init the mutex ret = pthread_mutex_init(&d->thread_data_lock, NULL); if (ret) { DLOG_FATAL("Could not init mutex for thread data: %s", strerror(ret)); panic_from_thread(__LINE__); return; } // fix the app pointer d->app = app; } int assign_to_thread(T_DATUM_SOCKET_APP *app, int fd) { // Only one thread will be calling this function for a particular "app" // under the current design. Safe to assume that multiple clients will // not cause overlap here. // Check how many threads are active. // If < max, put the client on a new thread. // If all threads are active, then it should find the one with the fewest clients and place the new client there. int i,j,ret,tc=0; int tid=-1,cid=-1; if (app->datum_active_threads < app->max_threads) { // we have not launched all threads yet, or somehow a thread has become inactive // place this connection on it's own new thread // let's assume, for now, that if we don't have all threads active that we're not above max_clients // find the first inactive thread for(i=0;imax_threads;i++) { // safe to read this without locking, as we're the only one that should be updating it if (!app->datum_threads[i].is_active) { tid = i; break; } } if (tid == -1) { DLOG_ERROR("Possible bug in thread handler. Could not find an inactive thread. datum_active_threads = %d; max_threads = %d", app->datum_active_threads, app->max_threads); return 0; } // clean up thread starting data clean_thread_data(&app->datum_threads[tid], app); app->datum_threads[tid].thread_id = tid; app->datum_threads[tid].is_active = true; if (pthread_create(&app->datum_threads[i].pthread, NULL, datum_threadpool_thread, &app->datum_threads[i]) != 0) { DLOG_ERROR("Could not start new thread for TID %d", tid); return 0; } app->datum_active_threads++; } else { // active threads are maxed already. find one with the fewest clients // in general, it should be safe to read the client count without locking, since // we don't particularly care _right here_ if it's higher than expected from a client // disconnection. We're the only one that increments it. // TODO: Profile if locking/unlocking here is sufficiently slow to care or not on the performance side // We don't want to make a clean path to a DoS, even though this is intended as a local service for local miners. j = app->max_clients_thread; // find the thread with the lowest client count // also tally up the total clients for(i=0;imax_threads;i++) { if (app->datum_threads[i].connected_clients < j) { j = app->datum_threads[i].connected_clients; tid = i; } tc+=app->datum_threads[i].connected_clients; } if (tid == -1) { DLOG_INFO("All threads have max clients! Rejecting connection. :("); return 0; } if (tc >= app->max_clients) { DLOG_INFO("Sum of clients on all threads at configured global maximum (%d) Rejecting connection. :(", app->max_clients); return 0; } } // lock the thread's data for a moment ret = pthread_mutex_lock(&app->datum_threads[tid].thread_data_lock); if (ret != 0) { DLOG_FATAL("Could not lock mutex for thread data on TID %d: %s", tid, strerror(ret)); panic_from_thread(__LINE__); // Is this panic worthy? should never happen return 0; } // sanity check if (app->datum_threads[tid].connected_clients >= app->max_clients_thread) { pthread_mutex_unlock(&app->datum_threads[tid].thread_data_lock); DLOG_ERROR("Attempted to assign client to thread %d, which already has MAX CLIENTS %d >= %d", tid, app->datum_threads[tid].connected_clients, app->max_clients_thread); return 0; } // get the client's cid cid = app->datum_threads[tid].next_open_client_index; // sanity check: confirm this cid is usable if (app->datum_threads[tid].client_data[cid].fd != 0) { DLOG_ERROR("Possible bug: Desync with next_open_client_index. Expected open client slot @ %d on non-maxed thread %d! (shows fd = %d)", cid, tid, app->datum_threads[tid].client_data[cid].fd); // let's try the hard way to find an open slot cid = -1; for(i=0;imax_clients_thread;i++) { if (app->datum_threads[tid].client_data[i].fd == 0) { cid = i; break; } } if (cid != -1) { DLOG_ERROR("Possible bug: Found an open client slot the hard way. Recovering. TID=%d CID=%d", tid, cid); } else { DLOG_ERROR("Possible bug: Could not find an open client slot the hard way! Rejecting client for TID=%d (%d clients)", tid, app->datum_threads[tid].connected_clients); pthread_mutex_unlock(&app->datum_threads[tid].thread_data_lock); return 0; } } // prep the next open CID by finding the next open slot app->datum_threads[tid].next_open_client_index = cid + 1; if (app->datum_threads[tid].next_open_client_index == app->max_clients_thread) app->datum_threads[tid].next_open_client_index = 0; // prep the next open CID for(i=app->datum_threads[tid].next_open_client_index; i != cid;) { if (app->datum_threads[tid].client_data[i].fd == 0) { // i is good app->datum_threads[tid].next_open_client_index = i; break; } // loop i around i++; if (i >= app->max_clients_thread) i = 0; } if (i == cid) { // we couldn't find an open client slot for the next client :( DLOG_DEBUG("Placing client on maxed out thread TID=%d CID=%d ... Thread is now FULL!",tid,cid); app->datum_threads[tid].next_open_client_index = app->max_clients_thread-1; } // bump connected client count app->datum_threads[tid].connected_clients++; // clear up and prep slot's client data without clobbering app_client_data app->datum_threads[tid].client_data[cid].fd = fd; app->datum_threads[tid].client_data[cid].cid = cid; app->datum_threads[tid].client_data[cid].new_connection = true; app->datum_threads[tid].client_data[cid].datum_thread = (void *)&app->datum_threads[tid]; app->datum_threads[tid].client_data[cid].in_buf = 0; app->datum_threads[tid].client_data[cid].out_buf = 0; app->datum_threads[tid].has_new_clients = true; pthread_mutex_unlock(&app->datum_threads[tid].thread_data_lock); if (!tc) { // tally clients for our debug for(i=0;imax_threads;i++) { tc+=app->datum_threads[i].connected_clients; } } else { tc++; } get_remote_ip(fd, app->datum_threads[tid].client_data[cid].rem_host, DATUM_MAX_IP_LEN); DLOG_DEBUG("New client (%s) on TID %d, CID %d with fd %d. clients: %d / clients on thread: %d", app->datum_threads[tid].client_data[cid].rem_host, tid, cid, fd, tc, app->datum_threads[tid].connected_clients); DLOG_DEBUG("app->datum_threads[tid].next_open_client_index = %d", app->datum_threads[tid].next_open_client_index); return 1; } const char *datum_sockets_setup_listen_sock(const int listen_sock, const struct sockaddr * const sa, const size_t sa_len) { if (-1 == listen_sock) { return "Could not create listening socket"; } datum_socket_setoptions(listen_sock); static const int reuse = 1; if (setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse, sizeof(reuse)) < 0) { return "setsockopt(SO_REUSEADDR) failed"; } if (bind(listen_sock, sa, sa_len) < 0) { return "bind failed"; } if (listen(listen_sock, 10) < 0) { return "listen failed"; } return NULL; } bool datum_sockets_setup_listening_sockets(const char * const purpose, const char * const addr, const uint16_t port, int * const out_socks, size_t * const inout_socks_n) { assert(*inout_socks_n > 0); if (addr && addr[0]) { char port_str[6]; snprintf(port_str, sizeof(port_str), "%u", (unsigned int)port); const struct addrinfo hints = { .ai_family = AF_UNSPEC, .ai_socktype = SOCK_STREAM, .ai_protocol = 0, .ai_flags = AI_PASSIVE | AI_NUMERICHOST | AI_NUMERICSERV, }; struct addrinfo *res; int err = getaddrinfo(addr, port_str, &hints, &res); if (err) { DLOG_FATAL("Failed to resolve listen address '%s' (%s): %s", purpose, addr, gai_strerror(err)); panic_from_thread(__LINE__); return false; } *out_socks = socket(res->ai_family, res->ai_socktype, res->ai_protocol); const char *errstr = datum_sockets_setup_listen_sock(*out_socks, res->ai_addr, res->ai_addrlen); const int errno_saved = errno; freeaddrinfo(res); if (errstr) { DLOG_FATAL("%s (%s): %s", errstr, purpose, strerror(errno_saved)); panic_from_thread(__LINE__); return false; } *inout_socks_n = 1; } else { const struct sockaddr_in6 anyaddr6 = { .sin6_family = AF_INET6, .sin6_port = htons(port), .sin6_addr = IN6ADDR_ANY_INIT, }; out_socks[0] = socket(AF_INET6, SOCK_STREAM, 0); #if defined(IPPROTO_IPV6) && defined(IPV6_V6ONLY) if (out_socks[0] != -1) { static const int zero = 0; setsockopt(out_socks[0], IPPROTO_IPV6, IPV6_V6ONLY, &zero, sizeof(zero)); } #endif const char * const errstr6 = datum_sockets_setup_listen_sock(out_socks[0], (const struct sockaddr *)&anyaddr6, sizeof(anyaddr6)); const int errno6 = errno; unsigned int socks_n = 1; if (errstr6 && out_socks[0] != -1) { close(out_socks[0]); out_socks[0] = -1; --socks_n; } if (*inout_socks_n > socks_n) { const struct sockaddr_in anyaddr4 = { .sin_family = AF_INET, .sin_port = htons(port), .sin_addr.s_addr = INADDR_ANY, }; out_socks[socks_n] = socket(AF_INET, SOCK_STREAM, 0); const char *errstr = datum_sockets_setup_listen_sock(out_socks[socks_n], (const struct sockaddr *)&anyaddr4, sizeof(anyaddr4)); if (errstr && errstr6) { const int errno4 = errno; DLOG_FATAL("%s (IPv6): %s", errstr6, strerror(errno6)); DLOG_FATAL("%s (IPv4): %s", errstr, strerror(errno4)); panic_from_thread(__LINE__); return false; } if (errstr && out_socks[socks_n] != -1) { close(out_socks[socks_n]); out_socks[socks_n] = -1; } else { ++socks_n; } } *inout_socks_n = socks_n; } return true; } void *datum_gateway_listener_thread(void *arg) { int i, ret; bool rejecting_now = false; uint64_t last_reject_msg_tsms = 0, curtime_tsms = 0; uint64_t reject_count = 0; T_DATUM_SOCKET_APP *app = (T_DATUM_SOCKET_APP *)arg; struct epoll_event ev, events[MAX_EVENTS]; int listen_socks[2], conn_sock, nfds, epollfd; if (!app) { DLOG_FATAL("Called without application data structure. :("); panic_from_thread(__LINE__); return NULL; } DLOG_DEBUG("Setting up app '%s' on address %s port %d. (T:%d/TC:%d/C:%d)", app->name, datum_config.stratum_v1_listen_addr[0] ? datum_config.stratum_v1_listen_addr : "(any)", app->listen_port, app->max_threads, app->max_clients_thread, app->max_clients); // we assume the caller sets up the thread data in some way // don't clobber those pointers for(i=0;imax_threads;i++) { ret = pthread_mutex_init(&app->datum_threads[i].thread_data_lock, NULL); if (ret) { DLOG_FATAL("Could not init mutex for thread data: %s", strerror(ret)); panic_from_thread(__LINE__); return NULL; } // set app data pointer app->datum_threads[i].app = app; app->datum_threads[i].thread_id = i; app->datum_threads[i].connected_clients = 0; app->datum_threads[i].next_open_client_index = 0; } app->datum_active_threads = 0; size_t listen_socks_len = 2; if (!datum_sockets_setup_listening_sockets("stratum", datum_config.stratum_v1_listen_addr, app->listen_port, listen_socks, &listen_socks_len)) { return NULL; } if (listen_socks_len < 2) listen_socks[1] = -1; epollfd = epoll_create1(0); if (epollfd < 0) { DLOG_FATAL("epoll_create1 failed: %s", strerror(errno)); panic_from_thread(__LINE__); return NULL; } for (i = 0; i < 2; ++i) { if (listen_socks[i] == -1) continue; ev.events = EPOLLIN; ev.data.fd = listen_socks[i]; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, ev.data.fd, &ev) < 0) { DLOG_FATAL("epoll_ctl failed: %s", strerror(errno)); panic_from_thread(__LINE__); return NULL; } } DLOG_INFO("DATUM Socket listener thread active for '%s'", app->name); for (;;) { nfds = epoll_wait(epollfd, events, MAX_EVENTS, 100); if (nfds) { if (datum_config.datum_pooled_mining_only && (!datum_protocol_is_active())) { curtime_tsms = current_time_millis(); // we only need this if we're rejecting connections if (!rejecting_now) { last_reject_msg_tsms = curtime_tsms - 5000; // first disconnect triggers msg } rejecting_now = true; } else { rejecting_now = false; } } for (int n = 0; n < nfds; ++n) { if (events[n].data.fd == listen_socks[0] || events[n].data.fd == listen_socks[1]) { conn_sock = accept(events[n].data.fd, NULL, NULL); if (conn_sock < 0) { DLOG_ERROR("accept failed: %s", strerror(errno)); continue; } if (rejecting_now) { reject_count++; if ((curtime_tsms - last_reject_msg_tsms) > 5000) { DLOG_INFO("DATUM not connected and configured for pooled mining only! Rejecting connection. (%llu connections rejected since last noted)", (unsigned long long)reject_count); last_reject_msg_tsms = curtime_tsms; reject_count = 0; } close(conn_sock); continue; } DLOG_DEBUG("Accepted socket to fd %d", conn_sock); datum_socket_setoptions(conn_sock); // assign socket to a thread i = assign_to_thread(app, conn_sock); if (!i) { // error finding a thread (too many connections?) DLOG_DEBUG("Closing socket we couldn't assign %d", conn_sock); close(conn_sock); } } } } return NULL; } void datum_socket_setoptions(int sock) { int opts; int flag = 1; opts = fcntl(sock,F_GETFL); if (opts < 0) { DLOG_FATAL("fcntl(F_GETFL) failed: %s", strerror(errno)); panic_from_thread(__LINE__); } opts = (opts | O_NONBLOCK); if (fcntl(sock,F_SETFL,opts) < 0) { DLOG_FATAL("fcntl(F_SETFL) failed: %s", strerror(errno)); panic_from_thread(__LINE__); } // Set the TCP_NODELAY option if (setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (char *)&flag, sizeof(int)) < 0) { DLOG_FATAL("setsockopt(TCP_NODELAY) failed: %s", strerror(errno)); panic_from_thread(__LINE__); } } int datum_socket_send_string_to_client(T_DATUM_CLIENT_DATA *c, char *s) { int len = strlen(s); if (!len) return 0; if ((c->out_buf + len) >= CLIENT_BUFFER) return -1; strncpy(&c->w_buffer[c->out_buf], s, CLIENT_BUFFER-(c->out_buf)-1); c->out_buf += len; return len; } int datum_socket_send_chars_to_client(T_DATUM_CLIENT_DATA *c, char *s, int len) { if (!len) return 0; if ((c->out_buf + len) >= CLIENT_BUFFER) return -1; if (len > (CLIENT_BUFFER-(c->out_buf)-1)) { len = CLIENT_BUFFER-(c->out_buf)-1; } memcpy(&c->w_buffer[c->out_buf], s, len); c->out_buf += len; return len; } datum_gateway-0.4.1beta/src/datum_sockets.h000066400000000000000000000134601512710151500210120ustar00rootroot00000000000000/* * * DATUM Gateway * Decentralized Alternative Templates for Universal Mining * * This file is part of OCEAN's Bitcoin mining decentralization * project, DATUM. * * https://ocean.xyz * * --- * * Copyright (c) 2024 Bitcoin Ocean, LLC & Jason Hughes * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ #ifndef _DATUM_SOCKETS_H_ #define _DATUM_SOCKETS_H_ #ifndef T_DATUM_TEMPLATE_DATA #include "datum_blocktemplates.h" #endif #include #include typedef struct T_DATUM_THREAD_DATA T_DATUM_THREAD_DATA; typedef struct T_DATUM_CLIENT_DATA T_DATUM_CLIENT_DATA; typedef void (*DATUM_ThreadPool_Init_Func)(T_DATUM_THREAD_DATA *); typedef void (*DATUM_ThreadPool_Loop_Func)(T_DATUM_THREAD_DATA *); typedef int (*DATUM_ThreadPool_ClientCmd_Func)(T_DATUM_CLIENT_DATA *, char *); typedef void (*DATUM_ThreadPool_ClientClosed_Func)(T_DATUM_CLIENT_DATA *, const char *); typedef void (*DATUM_ThreadPool_ClientNew_Func)(T_DATUM_CLIENT_DATA *); #define DATUM_MAX_IP_LEN 64 // TODO: Make these dynamic // These are hard coded buffer related values, not directly related to the config file values. // We avoid dynamic memory allocation to prevent fragmentation and other hassles, currently. #define MAX_CLIENTS_THREAD 4096 #define MAX_THREADS 64 #define MAX_EVENTS (MAX_CLIENTS_THREAD*2) typedef struct T_DATUM_CLIENT_DATA { bool new_connection; int fd; int cid; char buffer[CLIENT_BUFFER]; int in_buf; char w_buffer[CLIENT_BUFFER]; int out_buf; char rem_host[DATUM_MAX_IP_LEN+1]; bool kill_request; void *app_client_data; int proxy_line_read; T_DATUM_THREAD_DATA *datum_thread; } T_DATUM_CLIENT_DATA; typedef struct { char name[32]; // Called when a new threadpool thread is started DATUM_ThreadPool_Init_Func init_func; // Called at the beginning of each loop of the threadpool thread DATUM_ThreadPool_Loop_Func loop_func; // Called for each command the threadpool thread receives from a client DATUM_ThreadPool_ClientCmd_Func client_cmd_func; // Called each time a client is disconnected (either on purpose or via an error) DATUM_ThreadPool_ClientClosed_Func closed_client_func; // Called each time a new client connects and is assigned to a threadpool thread DATUM_ThreadPool_ClientNew_Func new_client_func; // TCP port this server will listen on int listen_port; // Maximum clients each thread can handle int max_clients_thread; // Maximum threads in the thread pool for this server int max_threads; // Maximum number of total clients for the server int max_clients; // Memory allocated by the app before starting the listener for max_threads worth of thread data // TODO: Dynamically allocate client_data and events T_DATUM_THREAD_DATA *datum_threads; int datum_active_threads; } T_DATUM_SOCKET_APP; typedef struct { // app functions and global data for socket app T_DATUM_SOCKET_APP *config; // application specific data for this thread void *data; } T_DATUM_SOCKET_APP_THREAD_DATA; typedef struct T_DATUM_THREAD_DATA { pthread_t pthread; bool is_active; bool has_new_clients; bool empty_request; bool has_client_kill_request; int thread_id; //int newBlockCount; // each client slot should have a pre-allocated chunk of memory // do not clear this entire structure! T_DATUM_CLIENT_DATA client_data[MAX_CLIENTS_THREAD]; pthread_mutex_t thread_data_lock; int connected_clients; int next_open_client_index; struct epoll_event ev, events[MAX_CLIENTS_THREAD*2]; int epollfd; // information for this socket application // this is global to the socket application T_DATUM_SOCKET_APP *app; // Socket application data for threadpool // remember, this is per thread void *app_thread_data; } T_DATUM_THREAD_DATA; bool datum_sockets_setup_listening_sockets(const char *purpose, const char *addr, uint16_t port, int *out_socks, size_t *inout_socks_n); void *datum_gateway_listener_thread(void *arg); void datum_socket_setoptions(int sock); int datum_socket_send_string_to_client(T_DATUM_CLIENT_DATA *c, char *s); int datum_socket_send_chars_to_client(T_DATUM_CLIENT_DATA *c, char *s, int len); int assign_to_thread(T_DATUM_SOCKET_APP *app, int fd); void *datum_threadpool_thread(void *arg); static inline void datum_socket_thread_client_count_decrement(T_DATUM_THREAD_DATA *my, int cid_who_left, bool not_already_locked) { // compiler will optimize the if's away in most cases, since this is inline if (not_already_locked) pthread_mutex_lock(&my->thread_data_lock); // decrement connected client count for the thread my->connected_clients--; // if the ID we dropped is less than the expected next, drop it down to speed that up if (cid_who_left < my->next_open_client_index) { my->next_open_client_index = cid_who_left; } my->client_data[cid_who_left].fd = 0; if (not_already_locked) pthread_mutex_unlock(&my->thread_data_lock); } #endif datum_gateway-0.4.1beta/src/datum_stratum.c000066400000000000000000002314001512710151500210250ustar00rootroot00000000000000/* * * DATUM Gateway * Decentralized Alternative Templates for Universal Mining * * This file is part of OCEAN's Bitcoin mining decentralization * project, DATUM. * * https://ocean.xyz * * --- * * Copyright (c) 2024-2025 Bitcoin Ocean, LLC & Jason Hughes * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ // Stratum V1 server for providing work to mining hardware supporting Stratum V1 #include #include #include #include #include #include #include #include #include #include #include #include "datum_gateway.h" #include "datum_stratum.h" #include "datum_stratum_dupes.h" #include "datum_jsonrpc.h" #include "datum_utils.h" #include "datum_blocktemplates.h" #include "datum_sockets.h" #include "datum_conf.h" #include "datum_coinbaser.h" #include "datum_submitblock.h" #include "datum_protocol.h" T_DATUM_SOCKET_APP *global_stratum_app = NULL; int stratum_job_next = 0; T_DATUM_STRATUM_JOB stratum_job_list[MAX_STRATUM_JOBS]; int global_latest_stratum_job_index = -1; T_DATUM_STRATUM_JOB *global_cur_stratum_jobs[MAX_STRATUM_JOBS] = { 0 }; pthread_rwlock_t stratum_global_job_ptr_lock = PTHREAD_RWLOCK_INITIALIZER; uint16_t stratum_enprefix = 0; pthread_rwlock_t stratum_global_latest_empty_stat = PTHREAD_RWLOCK_INITIALIZER; uint64_t stratum_latest_empty_complete_count = 0; bool stratum_latest_empty_ready_for_full = 0; uint64_t stratum_latest_empty_job_index = 0; uint64_t stratum_latest_empty_sent_count = 0; pthread_rwlock_t need_coinbaser_rwlocks[MAX_STRATUM_JOBS]; bool need_coinbaser_rwlocks_init_done = false; void stratum_latest_empty_increment_complete(uint64_t index, int clients_notified) { pthread_rwlock_wrlock(&stratum_global_latest_empty_stat); if ((stratum_latest_empty_job_index == index) && (!stratum_latest_empty_ready_for_full)) { stratum_latest_empty_complete_count++; stratum_latest_empty_sent_count += clients_notified; } pthread_rwlock_unlock(&stratum_global_latest_empty_stat); } bool stratum_latest_empty_check_ready_for_full(void) { bool a = false; pthread_rwlock_rdlock(&stratum_global_latest_empty_stat); if (stratum_latest_empty_ready_for_full) { a = true; } pthread_rwlock_unlock(&stratum_global_latest_empty_stat); return a; } void datum_stratum_v1_shutdown_all(void) { int ret; unsigned int shutdown_threads = 0; if (!global_stratum_app) { DLOG_DEBUG("Disconnect request for all stratum clients, but stratum thread is not ready."); return; } for (int tid = 0; tid < global_stratum_app->max_threads; ++tid) { if (!global_stratum_app->datum_threads[tid].is_active) continue; ret = pthread_mutex_lock(&global_stratum_app->datum_threads[tid].thread_data_lock); if (ret != 0) { DLOG_FATAL("Could not lock mutex for thread data on TID %d: %s", tid, strerror(ret)); panic_from_thread(__LINE__); // Is this panic worthy? should never happen return; } // Send request to gracefully boot all clients from the thread global_stratum_app->datum_threads[tid].empty_request = true; shutdown_threads++; pthread_mutex_unlock(&global_stratum_app->datum_threads[tid].thread_data_lock); } DLOG_INFO("Sent disconnect request for all stratum clients to %u threads.", shutdown_threads); return; } // Started as its own pthread during startup void *datum_stratum_v1_socket_server(void *arg) { // setup the stratum v1 DATUM socket server T_DATUM_SOCKET_APP *app; pthread_t pthread_datum_stratum_socket_server; int ret; int i,j; struct rlimit rlimit; uint64_t ram_allocated = 0; DLOG_DEBUG("Stratum V1 server startup"); // Setup the socket "app" for Stratum V1 app = (T_DATUM_SOCKET_APP *)calloc(1,sizeof(T_DATUM_SOCKET_APP)); if (!app) { DLOG_FATAL("Could not allocate memory for Stratum V1 server app metadata! (%lu bytes)", (unsigned long)sizeof(T_DATUM_SOCKET_APP)); panic_from_thread(__LINE__); return NULL; } ram_allocated += sizeof(T_DATUM_SOCKET_APP); memset(app, 0, sizeof(T_DATUM_SOCKET_APP)); strcpy(app->name, "Stratum V1 Server"); // setup callbacks app->init_func = datum_stratum_v1_socket_thread_init; app->loop_func = datum_stratum_v1_socket_thread_loop; app->client_cmd_func = datum_stratum_v1_socket_thread_client_cmd; app->closed_client_func = datum_stratum_v1_socket_thread_client_closed; app->new_client_func = datum_stratum_v1_socket_thread_client_new; // set listen port app->listen_port = datum_config.stratum_v1_listen_port; // setup limits app->max_clients_thread = datum_config.stratum_v1_max_clients_per_thread; app->max_threads = datum_config.stratum_v1_max_threads; app->max_clients = datum_config.stratum_v1_max_clients; // Our memory rationale here is to do as few dynamic allocations as possible. // We'll also never give up this memory, so no heap fragmentation risk. // allocate memory for DATUM socket thread data app->datum_threads = (T_DATUM_THREAD_DATA *) calloc(app->max_threads + 1, sizeof(T_DATUM_THREAD_DATA)); if (!app->datum_threads) { DLOG_FATAL("Could not allocate memory for Stratum V1 server thread pool data! (%lu bytes)", (unsigned long)(sizeof(T_DATUM_THREAD_DATA) * (app->max_threads + 1))); panic_from_thread(__LINE__); return NULL; } ram_allocated += (sizeof(T_DATUM_THREAD_DATA) * (app->max_threads + 1)); // allocate memory for our per-thread data // allocate once for the whole chunk, and set the pointers. no need to do tons of calls for a static block of data app->datum_threads[0].app_thread_data = calloc(app->max_threads + 1, sizeof(T_DATUM_STRATUM_THREADPOOL_DATA)); if (!app->datum_threads[0].app_thread_data) { DLOG_FATAL("Could not allocate memory for Stratum V1 server thread pool app data! (%lu bytes)", (unsigned long)(sizeof(T_DATUM_STRATUM_THREADPOOL_DATA) * (app->max_threads + 1))); panic_from_thread(__LINE__); return NULL; } for(i=1;imax_threads;i++) { app->datum_threads[i].app_thread_data = &((char *)app->datum_threads[0].app_thread_data)[sizeof(T_DATUM_STRATUM_THREADPOOL_DATA)*i]; } ram_allocated += (sizeof(T_DATUM_STRATUM_THREADPOOL_DATA) * (app->max_threads + 1)); // allocate memory for our per-client data // we need to allocate this per thread, since max clients could be lower // so our RAM usage will be based on app->max_threads*app->max_clients_thread, sadly, even if this is higher // T_DATUM_MINER_DATA app->datum_threads[0].client_data[0].app_client_data = calloc(((app->max_threads*app->max_clients_thread)+1), sizeof(T_DATUM_MINER_DATA)); if (!app->datum_threads[0].client_data[0].app_client_data) { DLOG_FATAL("Could not allocate memory for Stratum V1 server per-client data! (%lu bytes)", (unsigned long)(((app->max_threads*app->max_clients_thread)+1) * sizeof(T_DATUM_MINER_DATA))); panic_from_thread(__LINE__); return NULL; } ram_allocated += ((app->max_threads*app->max_clients_thread)+1) * sizeof(T_DATUM_MINER_DATA); for(i=0;imax_threads;i++) { for(j=0;jmax_clients_thread;j++) { if (!((i == 0) && (j == 0))) { app->datum_threads[i].client_data[j].app_client_data = &((char *)app->datum_threads[0].client_data[0].app_client_data)[((i*app->max_clients_thread)+j) * sizeof(T_DATUM_MINER_DATA)]; } } } // init locks for each job for (i = 0; i < MAX_STRATUM_JOBS; i++) { pthread_rwlock_init(&need_coinbaser_rwlocks[i], NULL); } need_coinbaser_rwlocks_init_done = true; // Backup thread for submitting blocks found to our node and additional nodes. DLOG_DEBUG("Starting submitblock thread"); datum_submitblock_init(); pthread_rwlock_rdlock(&stratum_global_job_ptr_lock); i = global_latest_stratum_job_index; pthread_rwlock_unlock(&stratum_global_job_ptr_lock); // we wait for the block template thread to have work for us before moving on. if (i < 0) { DLOG_DEBUG("Waiting for our first job before starting listening server..."); j = 0; i = global_latest_stratum_job_index; while(i<0) { usleep(50000); pthread_rwlock_rdlock(&stratum_global_job_ptr_lock); i = global_latest_stratum_job_index; pthread_rwlock_unlock(&stratum_global_job_ptr_lock); j++; if (j > 500 && j % 100 == 1) { DLOG_ERROR("Did not see an initial stratum job after ~%d seconds. Is your node properly setup?", j / 20); } } } // start the DATUM socket server DLOG_DEBUG("Starting listener thread %p",app); ret = pthread_create(&pthread_datum_stratum_socket_server, NULL, datum_gateway_listener_thread, app); if (ret != 0) { DLOG_FATAL("Could not pthread_create for DATUM socket listener!: %s", strerror(ret)); panic_from_thread(__LINE__); return NULL; } DLOG_INFO("Stratum V1 Server Init complete."); DLOG_DEBUG("%"PRIu64" MB of RAM allocated for Stratum V1 server data.", ram_allocated>>20); // TODO: If limits are too low, attempt to set our ulimits in case we're allowed to do so but it hasn't been done before executing. if (!getrlimit(RLIMIT_NOFILE, &rlimit)) { if (app->max_clients > rlimit.rlim_max) { DLOG_WARN("*** NOTE *** Max Stratum clients (%llu) exceeds hard open file limit (Soft: %llu / Hard: %llu)", (unsigned long long)app->max_clients, (unsigned long long)rlimit.rlim_cur, (unsigned long long)rlimit.rlim_max); DLOG_WARN("*** NOTE *** Adjust max open file hard limit or you WILL run into issues before reaching max clients!"); } else if (app->max_clients > rlimit.rlim_cur) { DLOG_WARN("*** NOTE *** Max Stratum clients (%llu) exceeds open file soft limit (Soft: %llu / Hard: %llu)", (unsigned long long)app->max_clients, (unsigned long long)rlimit.rlim_cur, (unsigned long long)rlimit.rlim_max); DLOG_WARN("*** NOTE *** You should increase the soft open file limit to prevent issues as you approach max clients!"); } } global_stratum_app = app; while (1) { // do periodic global stratum things here // If we're on an empty block waiting for a full one, handle that state transition here. pthread_rwlock_wrlock(&stratum_global_latest_empty_stat); if (!stratum_latest_empty_ready_for_full) { // we're still on an empty wait-for-full if (stratum_latest_empty_complete_count >= app->datum_active_threads) { // we are done! stratum_latest_empty_ready_for_full = true; DLOG_INFO("Empty work send completed. Sent to %llu clients across %llu threads", (unsigned long long)stratum_latest_empty_sent_count, (unsigned long long)stratum_latest_empty_complete_count); } } pthread_rwlock_unlock(&stratum_global_latest_empty_stat); usleep(11000); } return NULL; } int datum_stratum_v1_global_subscriber_count(void) { int j, kk, ii; T_DATUM_MINER_DATA *m; if (!global_stratum_app) return 0; kk = 0; for(j=0;jmax_threads;j++) { for(ii=0;iimax_clients_thread;ii++) { if (global_stratum_app->datum_threads[j].client_data[ii].fd > 0) { m = global_stratum_app->datum_threads[j].client_data[ii].app_client_data; if (m->subscribed) kk++; } } } return kk; } // TODO: Make this more accurate by tracking work over a longer period of time per user double datum_stratum_v1_est_total_th_sec(void) { double hr; unsigned char astat; double thr = 0.0; T_DATUM_MINER_DATA *m = NULL; uint64_t tsms; int j,ii; if (!global_stratum_app) return 0; tsms = current_time_millis(); for(j=0;jmax_threads;j++) { for(ii=0;iimax_clients_thread;ii++) { if (global_stratum_app->datum_threads[j].client_data[ii].fd > 0) { m = global_stratum_app->datum_threads[j].client_data[ii].app_client_data; if (m->subscribed) { astat = m->stats.active_index?0:1; // inverted hr = 0.0; if ((m->stats.last_swap_ms > 0) && (m->stats.diff_accepted[astat] > 0)) { hr = ((double)m->stats.diff_accepted[astat] / (double)((double)m->stats.last_swap_ms/1000.0)) * 0.004294967296; // Th/sec based on shares/sec } if (((double)(tsms - m->stats.last_swap_tsms)/1000.0) < 180.0) { thr += hr; } } } } } return thr; } void datum_stratum_v1_socket_thread_client_closed(T_DATUM_CLIENT_DATA *c, const char *msg) { DLOG_DEBUG("Stratum client connection closed. (%s)", msg); } void datum_stratum_v1_socket_thread_client_new(T_DATUM_CLIENT_DATA *c) { T_DATUM_MINER_DATA * const m = c->app_client_data; DLOG_DEBUG("New Stratum client connected. %d",c->fd); // clear miner data for connection memset(m, 0, sizeof(T_DATUM_MINER_DATA)); m->sdata = (T_DATUM_STRATUM_THREADPOOL_DATA *)c->datum_thread->app_thread_data; m->stats.last_swap_tsms = m->stats.last_share_tsms; static uint64_t unique_id_ctr = 0; m->unique_id = unique_id_ctr++; // set initial connection time // if this is the first client on the thread, we won't have a loop_tsms yet if (m->sdata->loop_tsms > 0) { m->connect_tsms = m->sdata->loop_tsms; } else { m->connect_tsms = current_time_millis(); } } void datum_stratum_v1_socket_thread_init(T_DATUM_THREAD_DATA *my) { T_DATUM_STRATUM_THREADPOOL_DATA *sdata = (T_DATUM_STRATUM_THREADPOOL_DATA *)my->app_thread_data; pthread_rwlock_rdlock(&stratum_global_job_ptr_lock); sdata->latest_stratum_job_index = global_latest_stratum_job_index; sdata->cur_stratum_job = global_cur_stratum_jobs[global_latest_stratum_job_index]; pthread_rwlock_unlock(&stratum_global_job_ptr_lock); sdata->new_job = false; sdata->last_sent_job_state = 0; sdata->next_kick_check_tsms = current_time_millis() + 10000; // initialize the dupe checker system datum_stratum_dupes_init(sdata); } int datum_stratum_v1_get_thread_subscriber_count(T_DATUM_THREAD_DATA *my) { int i,c=0; T_DATUM_MINER_DATA *m; for(i=0;iapp->max_clients_thread;i++) { m = my->client_data[i].app_client_data; if (my->client_data[i].fd && m->subscribed) { c++; } } return c; } bool stratum_job_coinbaser_ready(T_DATUM_STRATUM_THREADPOOL_DATA *sdata, T_DATUM_STRATUM_JOB *job) { bool a = false; // backup timeout for coinbaser on these jobs if ((sdata->loop_tsms > job->tsms) && (sdata->loop_tsms - job->tsms) > 5000) { // enforce a timeout of 5 seconds on waiting on a coinbaser... sdata->full_coinbase_ready = false; return true; } pthread_rwlock_rdlock(&need_coinbaser_rwlocks[job->global_index]); if (!job->need_coinbaser) { a = true; } pthread_rwlock_unlock(&need_coinbaser_rwlocks[job->global_index]); if (a) { sdata->full_coinbase_ready = true; } return a; } void datum_stratum_v1_socket_thread_loop(T_DATUM_THREAD_DATA *my) { T_DATUM_STRATUM_THREADPOOL_DATA *sdata = (T_DATUM_STRATUM_THREADPOOL_DATA *)my->app_thread_data; T_DATUM_STRATUM_JOB *job = NULL; T_DATUM_MINER_DATA *m = NULL; int i; bool change_ready = true; int cnt = 0; uint64_t tsms,tsms2,tsms3; // check if the stratum job has been updated pthread_rwlock_rdlock(&stratum_global_job_ptr_lock); if (global_latest_stratum_job_index != sdata->latest_stratum_job_index) { change_ready = true; if ((sdata->last_was_empty) && (global_cur_stratum_jobs[global_latest_stratum_job_index]->job_state >= JOB_STATE_FULL_PRIORITY_WAIT_COINBASER)) { // we went from an empty to a coinbaser wait job somehow... // we don't want to delay our full work blast, however... if (!stratum_job_coinbaser_ready(sdata,global_cur_stratum_jobs[global_latest_stratum_job_index])) { // yeah, it's not ready. let's just pretend we didn't see this job yet... // ... unless it's a different block height and we're somehow _that_ far behind on processing. // should never happen, but let's be careful. if (sdata->last_job_height == global_cur_stratum_jobs[global_latest_stratum_job_index]->height) { change_ready = false; } } } if (change_ready) { sdata->cur_stratum_job = global_cur_stratum_jobs[global_latest_stratum_job_index]; sdata->latest_stratum_job_index = global_latest_stratum_job_index; sdata->new_job = true; if (sdata->cur_stratum_job->job_state == 2) { // make sure we dont skip our empty work if job type 2 sdata->last_was_empty = false; } sdata->notify_remaining_count = 0; // this is new work sdata->full_coinbase_ready = false; sdata->last_job_height = sdata->cur_stratum_job->height; } } pthread_rwlock_unlock(&stratum_global_job_ptr_lock); sdata->loop_tsms = current_time_millis(); job = sdata->cur_stratum_job; if (sdata->new_job) { switch (job->job_state) { case 1: { // this is an empty work job. it should be followed up by a full priority job sdata->full_coinbase_ready = false; DLOG_DEBUG("Blasting empty work type 1 for thread %d",my->thread_id); for(i=0;iapp->max_clients_thread;i++) { m = my->client_data[i].app_client_data; if (my->client_data[i].fd && m->subscribed) { send_mining_notify(&my->client_data[i],true,false,true); cnt++; } } sdata->new_job = false; // we're waiting on a completely new job for the next blast wave sdata->last_was_empty = true; stratum_latest_empty_increment_complete(sdata->latest_stratum_job_index, cnt); // we do want to make sure everyone got an empty before a full still sdata->last_sent_job_state = 1; break; } case 2: { sdata->full_coinbase_ready = false; // this is an empty+ job. blast the empty work the first time around if (!sdata->last_was_empty) { // blast empty DLOG_DEBUG("Blasting empty work type 2 for thread %d",my->thread_id); for(i=0;iapp->max_clients_thread;i++) { m = my->client_data[i].app_client_data; if (my->client_data[i].fd && m->subscribed) { send_mining_notify(&my->client_data[i],true,false,true); cnt++; } } sdata->last_was_empty = true; stratum_latest_empty_increment_complete(sdata->latest_stratum_job_index, cnt); sdata->last_sent_job_state = 2; break; } else { if (stratum_latest_empty_check_ready_for_full()) { // blast full when all threads ready or timed out DLOG_DEBUG("Blasting full work for type 2 job thread %d",my->thread_id); for(i=0;iapp->max_clients_thread;i++) { m = my->client_data[i].app_client_data; if (my->client_data[i].fd && m->subscribed) { send_mining_notify(&my->client_data[i],false,false,false); } } sdata->new_job = false; sdata->last_was_empty = false; sdata->last_sent_job_state = 2; // probably should be different, but not sure if needed. } } break; } case 3: { // This is a full work job, no coinbaser wait, and has priority blasting // it's possible this job gets skipped straight to 4 sdata->full_coinbase_ready = false; DLOG_DEBUG("Blasting full work for type 3 job thread %d",my->thread_id); for(i=0;iapp->max_clients_thread;i++) { m = my->client_data[i].app_client_data; if (my->client_data[i].fd && m->subscribed) { send_mining_notify(&my->client_data[i],false,false,false); } } sdata->new_job = false; sdata->last_was_empty = false; sdata->last_sent_job_state = 3; break; } case 4: { // this is a full work job with blast priority once we get our coinbaser // the coinbaser readiness needs to hide behind a lock specific to the job sdata->full_coinbase_ready = false; if (stratum_job_coinbaser_ready(sdata,job)) { // will set sdata->full_coinbase_ready = true if it's really ready. if it times out, it will not. DLOG_DEBUG("Blasting full work for type 4 job thread %d",my->thread_id); for(i=0;iapp->max_clients_thread;i++) { m = my->client_data[i].app_client_data; if (my->client_data[i].fd && m->subscribed) { send_mining_notify(&my->client_data[i],false,false,false); } } sdata->new_job = false; sdata->last_was_empty = false; sdata->last_sent_job_state = 4; } break; } case 5: { sdata->full_coinbase_ready = false; if (stratum_job_coinbaser_ready(sdata,job)) { // this is a normal job that normally gets slowly sent out over the course of the work change time until interupted by a new block if (sdata->last_was_empty) { // HOWEVER... // if the last work this thread sent out was empty work, likely due to load or whatever // then we don't want to delay the sending of full work. // up until this point, this wasn't a concern, but it could be if load is high and the socket side processing // takes a long time // blast out the work NOW DLOG_DEBUG("Blasting full work for type 5 job thread %d",my->thread_id); for(i=0;iapp->max_clients_thread;i++) { m = my->client_data[i].app_client_data; if (my->client_data[i].fd && m->subscribed) { send_mining_notify(&my->client_data[i],false,false,false); } } } else { // last work was not empty, so we can safely slow things up a bit. sdata->notify_remaining_count = datum_stratum_v1_get_thread_subscriber_count(my); if (sdata->notify_remaining_count > 0) { sdata->notify_last_cid = -1; sdata->notify_start_time = sdata->loop_tsms; sdata->notify_delay_per_slot_tsms = ((datum_config.bitcoind_work_update_seconds - 3)*1000) / sdata->notify_remaining_count; if (!sdata->notify_delay_per_slot_tsms) { sdata->notify_delay_per_slot_tsms = 1; } // loosely stagger based on thread ID as well sdata->notify_last_time = sdata->loop_tsms - ((sdata->notify_delay_per_slot_tsms / my->app->max_threads) * my->thread_id); DLOG_DEBUG("Pacing job update for thread %d to %d clients @ %"PRIu64" ms",my->thread_id, sdata->notify_remaining_count, sdata->notify_delay_per_slot_tsms); } } sdata->new_job = false; sdata->last_was_empty = false; sdata->last_sent_job_state = 5; // technically might not be "sent" yet, but last processed for sure. } break; } case 0: default: { // Unknown job state... break; } } } // slowly send out non-critical work changes // this prevents bandwidth spikes from the server sending notifies to all clients at once. // that would be quite wasteful and hard on remote connections. if (sdata->notify_remaining_count > 0) { // we have notifies to send tsms = sdata->loop_tsms - sdata->notify_last_time; if ((!tsms) || (tsms >= sdata->notify_delay_per_slot_tsms)) { tsms = tsms / sdata->notify_delay_per_slot_tsms; if (!tsms) tsms = 1; for(i=(sdata->notify_last_cid+1);iapp->max_clients_thread;i++) { m = my->client_data[i].app_client_data; if (my->client_data[i].fd && m->subscribed && m->subscribe_tsms <= sdata->notify_start_time) { send_mining_notify(&my->client_data[i],false,false,false); sdata->notify_remaining_count--; sdata->notify_last_cid = i; tsms--; if (!tsms) break; } } if (i==my->app->max_clients_thread) { sdata->notify_remaining_count = 0; } sdata->notify_last_time = sdata->loop_tsms; } } if (sdata->loop_tsms >= sdata->next_kick_check_tsms) { if ((datum_config.stratum_v1_idle_timeout_no_subscribe > 0) || (datum_config.stratum_v1_idle_timeout_no_share > 0) || (datum_config.stratum_v1_idle_timeout_max_last_work)) { tsms = 1; tsms2 = 1; tsms3 = 1; if (datum_config.stratum_v1_idle_timeout_no_subscribe > 0) { tsms = sdata->loop_tsms - (datum_config.stratum_v1_idle_timeout_no_subscribe * 1000); } if (datum_config.stratum_v1_idle_timeout_no_share > 0) { tsms2 = sdata->loop_tsms - (datum_config.stratum_v1_idle_timeout_no_share * 1000); } if (datum_config.stratum_v1_idle_timeout_max_last_work > 0) { tsms3 = sdata->loop_tsms - (datum_config.stratum_v1_idle_timeout_max_last_work * 1000); } for(i=0;iapp->max_clients_thread;i++) { if (my->client_data[i].fd) { m = my->client_data[i].app_client_data; if (m->subscribed) { // subscribed if (m->share_count_accepted > 0) { // has accepted shares if (m->stats.last_share_tsms < tsms3) { DLOG_DEBUG("Kicking client %d/%d (%s) for being idle > %d seconds without submitting any new shares. (connected %.2f, currently %.2f, delta %.2f)",my->thread_id, i, my->client_data[i].rem_host, datum_config.stratum_v1_idle_timeout_max_last_work, (double)m->connect_tsms / (double)1000.0, (double)sdata->loop_tsms/ (double)1000.0, (double)(sdata->loop_tsms - m->connect_tsms) / (double)1000.0); // boot them! my->client_data[i].kill_request = true; my->has_client_kill_request = true; } } else { // no accepted shares if (m->connect_tsms < tsms2) { DLOG_DEBUG("Kicking client %d/%d (%s) for being idle > %d seconds without submitting any shares. (connected %.2f, currently %.2f, delta %.2f)",my->thread_id, i, my->client_data[i].rem_host, datum_config.stratum_v1_idle_timeout_no_share, (double)m->connect_tsms / (double)1000.0, (double)sdata->loop_tsms/ (double)1000.0, (double)(sdata->loop_tsms - m->connect_tsms) / (double)1000.0); // boot them! my->client_data[i].kill_request = true; my->has_client_kill_request = true; } } } else { // not subscribed if (m->connect_tsms < tsms) { // boot them! DLOG_DEBUG("Kicking client %d/%d (%s) for being idle > %d seconds without subscribing. (connected %.2f, currently %.2f, delta %.2f)",my->thread_id, i, my->client_data[i].rem_host, datum_config.stratum_v1_idle_timeout_no_subscribe, (double)m->connect_tsms / (double)1000.0, (double)sdata->loop_tsms/ (double)1000.0, (double)(sdata->loop_tsms - m->connect_tsms) / (double)1000.0); my->client_data[i].kill_request = true; my->has_client_kill_request = true; } } } } } sdata->next_kick_check_tsms = sdata->loop_tsms + 11150; } } void send_error_to_client(T_DATUM_CLIENT_DATA *c, uint64_t id, char *e) { // "e" must be valid JSON string char s[1024]; snprintf(s, sizeof(s), "{\"error\":%s,\"id\":%"PRIu64",\"result\":null}\n", e, id); datum_socket_send_string_to_client(c, s); } static inline void send_unknown_work_error(T_DATUM_CLIENT_DATA *c, uint64_t id) { send_error_to_client(c, id, "[20,\"unknown-work\",null]"); } static inline void send_rejected_high_hash_error(T_DATUM_CLIENT_DATA *c, uint64_t id) { send_error_to_client(c, id, "[23,\"high-hash\",null]"); } static inline void send_rejected_stale(T_DATUM_CLIENT_DATA *c, uint64_t id) { send_error_to_client(c, id, "[21,\"stale-work\",null]"); } static inline void send_rejected_time_too_old(T_DATUM_CLIENT_DATA *c, uint64_t id) { send_error_to_client(c, id, "[21,\"time-too-old\",null]"); } static inline void send_rejected_time_too_new(T_DATUM_CLIENT_DATA *c, uint64_t id) { send_error_to_client(c, id, "[21,\"time-too-new\",null]"); } static inline void send_rejected_stale_block(T_DATUM_CLIENT_DATA *c, uint64_t id) { send_error_to_client(c, id, "[21,\"stale-prevblk\",null]"); } static inline void send_rejected_hnotzero_error(T_DATUM_CLIENT_DATA *c, uint64_t id) { send_error_to_client(c, id, "[23,\"H-not-zero\",null]"); } static inline void send_bad_version_error(T_DATUM_CLIENT_DATA *c, uint64_t id) { send_error_to_client(c, id, "[23,\"bad-version\",null]"); } static inline void send_rejected_duplicate(T_DATUM_CLIENT_DATA *c, uint64_t id) { send_error_to_client(c, id, "[22,\"duplicate\",null]"); } uint32_t get_new_session_id(T_DATUM_CLIENT_DATA *c) { // K.I.S.S. --- Session ID is just the thread ID and client ID, XOR with our constant. // This will always be unique for every client connected to the server. // We end up "limited" a little, but sanely: // --- Max threads: 1,024 // --- Max clients per thread: 4,194,304 // // Downside to this is it prevents stratum v1 resume, however almost nothing appears to implement this correctly anymore anyway // TODO: Potentially implement stratum resume if a requested session ID is unique and available uint32_t i; i = ((uint32_t)c->cid) & (uint32_t)0x003FFFFF; i |= ((((uint32_t)c->datum_thread->thread_id)<<22) & (uint32_t)0xFFC00000); return i ^ 0xB10CF00D; // Feed us the blocks. } void reset_vardiff_stats(T_DATUM_CLIENT_DATA *c) { T_DATUM_MINER_DATA * const m = c->app_client_data; m->share_count_since_snap = 0; m->share_diff_since_snap = 0; m->share_snap_tsms = m->sdata->loop_tsms; } void stratum_update_vardiff(T_DATUM_CLIENT_DATA *c, bool no_quick) { // Should be called at/around a share being accepted? // before processing a mining notify? (for downward T_DATUM_MINER_DATA * const m = c->app_client_data; uint64_t delta_tsms; uint64_t ms_per_share; uint64_t target_ms_share; // if we already have a diff change pending, don't do calcs again if (m->current_diff != m->last_sent_diff) return; // don't even bother until we have at least X shares to work with for quick diff if ((!no_quick) && (m->share_count_since_snap < datum_config.stratum_v1_vardiff_quickdiff_count)) { return; } delta_tsms = m->sdata->loop_tsms - m->share_snap_tsms; if (!m->share_count_since_snap) { // no shares since last snap // is it because our diff is way too high? if (delta_tsms > 60000) { // 60s with no shares seems sufficient to bump diff down next round. m->current_diff = m->current_diff >> 1; if (m->current_diff < m->forced_high_min_diff) { m->current_diff = m->forced_high_min_diff; } if (m->current_diff < datum_config.stratum_v1_vardiff_min) { m->current_diff = datum_config.stratum_v1_vardiff_min; } reset_vardiff_stats(c); } // return either way, since with 0 shares the math below doesn't work. return; } // first, let's check if we're wayyyy out of line on what we want for diff, and respond accordingly // we need at least 1 second of data if (delta_tsms < 1000) return; ms_per_share = delta_tsms / m->share_count_since_snap; if (!ms_per_share) ms_per_share = 1; target_ms_share = (uint64_t)60000/(uint64_t)datum_config.stratum_v1_vardiff_target_shares_min; // we want to target X shares/minute // that would be 60000/X ms per share on average // if we're *significantly* faster than this, we'll want to bump diff immediately if ((!m->quickdiff_active) && (!no_quick) && (ms_per_share < (target_ms_share/(uint64_t)datum_config.stratum_v1_vardiff_quickdiff_delta))) { // let's say if we're at 64/shares/min or higher, we'll do a quick bump // reusing this var... // try to set the difficulty quickly to a value that makes some sense based on how many shares we just saw delta_tsms = roundDownToPowerOfTwo_64((target_ms_share / ms_per_share) * m->current_diff); if (delta_tsms < (m->current_diff << 2)) { delta_tsms = (m->current_diff << 2); } m->current_diff = delta_tsms; // send a special clean=true stratum job to the client // this will send the new diff also send_mining_notify(c, true, true, false); // reset the vardiff stats to start this process over again reset_vardiff_stats(c); // nothing else to do return; } // check if we need a diff bump downward if (ms_per_share > (target_ms_share*2)) { // adjust diff downward a tick m->current_diff = m->current_diff >> 1; if (m->current_diff < m->forced_high_min_diff) { m->current_diff = m->forced_high_min_diff; } if (m->current_diff < datum_config.stratum_v1_vardiff_min) { m->current_diff = datum_config.stratum_v1_vardiff_min; } reset_vardiff_stats(c); return; } // don't bother with looking to bump unless we have 16 shares to work with if (m->share_count_since_snap < 16) return; if (ms_per_share < (target_ms_share/2)) { // adjust diff upward a tick m->current_diff = m->current_diff << 1; reset_vardiff_stats(c); return; } // nothing to do yet return; } #define STAT_CYCLE_MS 60000 void stratum_update_miner_stats_accepted(T_DATUM_CLIENT_DATA *c, uint64_t diff_accepted) { T_DATUM_MINER_DATA * const m = c->app_client_data; m->stats.diff_accepted[m->stats.active_index?1:0] += diff_accepted; m->stats.last_share_tsms = m->sdata->loop_tsms; if (m->sdata->loop_tsms >= (m->stats.last_swap_tsms+STAT_CYCLE_MS)) { m->stats.last_swap_ms = m->sdata->loop_tsms - m->stats.last_swap_tsms; m->stats.last_swap_tsms = m->sdata->loop_tsms; if (m->stats.active_index) { m->stats.active_index = 0; m->stats.diff_accepted[0] = 0; } else { m->stats.active_index = 1; m->stats.diff_accepted[1] = 0; } } } // CAUTION: modname MUST be part of username_s following a tilde const char *datum_stratum_mod_username(const char *username_s, char * const username_buf, const size_t username_buf_sz, const uint16_t share_rnd, const char * const modname, const size_t modname_len) { struct datum_username_mod * const umod = datum_username_mods_find(datum_config.stratum_username_mod, modname, modname_len); if (!umod) return username_s; struct datum_addr_range *range; for (range = umod->ranges; ; ++range) { if (!range->addr) return datum_config.mining_pool_address; if (share_rnd <= range->max) break; } const char * const tilde = &modname[-1]; if (range->addr_len == 0) { size_t len = tilde - username_s; if (len >= username_buf_sz) len = username_buf_sz - 1; memcpy(username_buf, username_s, len); username_buf[len] = '\0'; return username_buf; } const char * const period = strchr(username_s, '.'); if (range->addr_len >= username_buf_sz || !period) { return range->addr; } memcpy(username_buf, range->addr, range->addr_len); size_t len = tilde - period; const size_t max_len = username_buf_sz - range->addr_len - 1; if (len > max_len) len = max_len; memcpy(&username_buf[range->addr_len], period, len); username_buf[range->addr_len + len] = '\0'; return username_buf; } int client_mining_submit(T_DATUM_CLIENT_DATA *c, uint64_t id, json_t *params_obj) { // {"params": ["username", "job", "extranonce2", "time", "nonce", "version"], "id": 1, "method": "mining.submit"} // 0 = username // 1 = jobid // 2 = extranonce2 // 3 = ntime // 4 = nonce // 5 = version roll (OR with version) json_t *username; json_t *job_id; json_t *extranonce2; json_t *ntime; json_t *nonce; json_t *vroll; T_DATUM_STRATUM_JOB *job = NULL; const char *job_id_s; const char *vroll_s; const char *username_s; char username_buf[0x100]; const char *extranonce2_s; const char *ntime_s; const char *nonce_s; uint32_t vroll_uint; uint16_t g_job_index; uint32_t bver; uint32_t ntime_val; uint32_t nonce_val; unsigned char coinbase_index = 0; T_DATUM_STRATUM_COINBASE *cb = NULL; unsigned char extranonce_bin[12]; unsigned char block_header[80]; unsigned char digest_temp[40]; unsigned char share_hash[40]; unsigned char full_cb_txn[MAX_COINBASE_TXN_SIZE_BYTES]; T_DATUM_MINER_DATA * const m = c->app_client_data; int i; bool quickdiff = false; bool empty_work = false; bool was_block = false; char new_notify_blockhash[65]; // 0 = version 4 bytes // 4 = previous block hash 32 bytes // 36 = merkle root 32 bytes // 68 = ntime // 72 = nbits // 76 = nonce // see if this is a real job job_id = json_array_get(params_obj, 1); if (!job_id) { send_unknown_work_error(c,id); m->share_count_rejected++; m->share_diff_rejected+=m->last_sent_diff; // guestimate here return 0; } job_id_s = json_string_value(job_id); if (!job_id_s) { send_unknown_work_error(c,id); m->share_count_rejected++; m->share_diff_rejected+=m->last_sent_diff; // guestimate here return 0; } if (strlen(job_id_s) != 16) { if ((strlen(job_id_s) == 17) && (job_id_s[0] == 'Q')) { // was a quick diff change job. discard the Q at the front job_id_s++; quickdiff = true; } else if ((strlen(job_id_s) == 17) && (job_id_s[0] == 'N')) { // new block empty work. means we use coinbase 0 and we have no merkle leafs job_id_s++; empty_work = true; } else { send_unknown_work_error(c,id); m->share_count_rejected++; m->share_diff_rejected+=m->last_sent_diff; // guestimate here return 0; } } // jobID is // 4 bytes time (who cares) // 1 byte raw index kinda (useless) // 2 bytes global ptr index // 1 byte coinbase index used // 6625a3d53cc0e500 // 0123456789ABCDEF g_job_index = (hex2bin_uchar(&job_id_s[0xA])<<8) | hex2bin_uchar(&job_id_s[0xC]); g_job_index ^= STRATUM_JOB_INDEX_XOR; if (g_job_index >= MAX_STRATUM_JOBS) { send_unknown_work_error(c,id); m->share_count_rejected++; m->share_diff_rejected+=m->last_sent_diff; // guestimate here return 0; } job = global_cur_stratum_jobs[g_job_index]; if (!job) { send_unknown_work_error(c,id); m->share_count_rejected++; m->share_diff_rejected+=m->last_sent_diff; // guestimate here return 0; } if (upk_u64le(job->job_id, 0) != upk_u64le(job_id_s, 0)) { //LOG_PRINTF("DEBUG: Job ID for index %u doesn't match expected in RAM. (%s vs %s)", g_job_index, job->job_id, job_id_s); send_unknown_work_error(c,id); m->share_count_rejected++; m->share_diff_rejected+=m->last_sent_diff; // guestimate here return 0; } const uint64_t job_diff = quickdiff ? m->quickdiff_value : m->stratum_job_diffs[g_job_index]; // construct block header bver = job->version_uint; if (m->extension_version_rolling) { vroll = json_array_get(params_obj, 5); if (!vroll) { // version rolling requested, but missing from this work submission send_bad_version_error(c,id); m->share_count_rejected++; m->share_diff_rejected += job_diff; return 0; } vroll_s = json_string_value(vroll); if (!vroll_s) { // couldn't get string send_bad_version_error(c,id); m->share_count_rejected++; m->share_diff_rejected += job_diff; return 0; } vroll_uint = strtoul(vroll_s, NULL, 16); if ((vroll_uint & m->extension_version_rolling_mask) != vroll_uint) { // tried to roll bits we didn't approve send_bad_version_error(c,id); m->share_count_rejected++; m->share_diff_rejected += job_diff; return 0; } bver |= vroll_uint; } // 0 - 4 = version pk_u32le(block_header, 0, bver); // 4 - 35 = previous block hash memcpy(&block_header[4], job->prevhash_bin, 32); // 36 - 67 = merkle root // need to get the extranonce together pk_u32le(extranonce_bin, 0, m->sid_inv); extranonce2 = json_array_get(params_obj, 2); if (!extranonce2) { send_unknown_work_error(c, id); m->share_count_rejected++; m->share_diff_rejected += job_diff; return 0; } extranonce2_s = json_string_value(extranonce2); if (!extranonce2_s) { send_unknown_work_error(c, id); m->share_count_rejected++; m->share_diff_rejected += job_diff; return 0; } if (strlen(extranonce2_s) != 16) { send_unknown_work_error(c, id); m->share_count_rejected++; m->share_diff_rejected += job_diff; return 0; } for(i=0;i<8;i++) { extranonce_bin[i+4] = hex2bin_uchar(&extranonce2_s[i<<1]); } // need to build the full coinbase txn coinbase_index = hex2bin_uchar(&job_id_s[0xE]); if (coinbase_index >= MAX_COINBASE_TYPES) { if (!(empty_work && coinbase_index == 255)) { send_unknown_work_error(c, id); m->share_count_rejected++; m->share_diff_rejected += job_diff; return 0; } } if (empty_work) { cb = &job->subsidy_only_coinbase; } else { cb = &job->coinbase[coinbase_index]; } if (!cb) { send_unknown_work_error(c, id); m->share_count_rejected++; m->share_diff_rejected += job_diff; return 0; } memcpy(&full_cb_txn[0], cb->coinb1_bin, cb->coinb1_len); memcpy(&full_cb_txn[cb->coinb1_len], extranonce_bin, 12); memcpy(&full_cb_txn[cb->coinb1_len+12], cb->coinb2_bin, cb->coinb2_len); // if we did a quickdiff work, we need to change our extra data just a little so it's unique. // if we don't do this, we're forcing the miner to redo work its already done, which is wasteful // and the miner would potentially see these as rejected duplicate shares. // // we only need to tweak the binary version here. // this is saved for the block submission and all below, also, so is safe // we also need to apply our target diff byte, which could be different depending on if quickdiff or not // we must encode the current diff directly into the PoW. This allows remote DATUM servers to accept // our variable difficulty work (subject to the DATUM server provided global minimum) if (quickdiff) { if (upk_u16le(full_cb_txn, cb->coinb1_len - 2) != 0x5144) { pk_u16le(full_cb_txn, cb->coinb1_len - 2, 0x5144); } else { pk_u16le(full_cb_txn, cb->coinb1_len - 2, 0xAEBB); } full_cb_txn[job->target_pot_index] = floorPoT(m->quickdiff_value); } else { full_cb_txn[job->target_pot_index] = floorPoT(m->stratum_job_diffs[g_job_index]); } if ((job->merklebranch_count) && (!empty_work)) { // hash the CB txn double_sha256(digest_temp, full_cb_txn, cb->coinb1_len+12+cb->coinb2_len); // calc root stratum_job_merkle_root_calc(job, digest_temp, &block_header[36]); } else { // empty block means coinbase txn hash is the merkleroot double_sha256(&block_header[36], full_cb_txn, cb->coinb1_len+12+cb->coinb2_len); } // 68 - 71 = ntime ntime = json_array_get(params_obj, 3); if (!ntime) { send_unknown_work_error(c, id); m->share_count_rejected++; m->share_diff_rejected += job_diff; return 0; } ntime_s = json_string_value(ntime); if (!ntime_s) { send_unknown_work_error(c, id); m->share_count_rejected++; m->share_diff_rejected += job_diff; return 0; } ntime_val = strtoul(ntime_s, NULL, 16); pk_u32le(block_header, 68, ntime_val); // 72 - 75 = bits memcpy(&block_header[72], &job->nbits_bin[0], sizeof(uint32_t)); // 76 - 79 = nonce nonce = json_array_get(params_obj, 4); if (!nonce) { send_unknown_work_error(c, id); m->share_count_rejected++; m->share_diff_rejected += job_diff; return 0; } nonce_s = json_string_value(nonce); if (!nonce_s) { send_unknown_work_error(c, id); m->share_count_rejected++; m->share_diff_rejected += job_diff; return 0; } nonce_val = strtoul(nonce_s, NULL, 16); pk_u32le(block_header, 76, nonce_val); my_sha256(digest_temp, block_header, 80); my_sha256(share_hash, digest_temp, 32); if (upk_u32le(share_hash, 28) != 0) { // H-not-zero //LOG_PRINTF("HIGH HASH: %8.8lx", (unsigned long)upk_u32le(share_hash, 28)); send_rejected_hnotzero_error(c, id); m->share_count_rejected++; m->share_diff_rejected += job_diff; return 0; } username = json_array_get(params_obj, 0); if (!username) { username_s = (const char *)"NULL"; } else { username_s = json_string_value(username); if (!username_s) { username_s = (const char *)"NULL"; } } if (datum_config.stratum_username_mod) { const char * const tilde = strchr(username_s, '~'); if (tilde) { const char * const modname = &tilde[1]; const size_t modname_len = json_string_length(username) - (modname - username_s); const uint16_t share_rnd = upk_u16le(share_hash, 0); username_s = datum_stratum_mod_username(username_s, username_buf, sizeof(username_buf), share_rnd, modname, modname_len); } } // most important thing to do right here is to check if the share is a block // there's some downstream failures that can impact the share being valid, but at this point it's // possible for this block to be valid. even if it's stale or something we're going to try it. if (compare_hashes(share_hash, job->block_target) <= 0) { // BLOCK // since we check this early, it's possible a duplicate share submission could trigger this twice... but that's alright. // it won't hurt to re-submit a block. was_block = true; new_notify_blockhash[64] = 0; for(i=0;i<32;i++) { uchar_to_hex((char *)&new_notify_blockhash[(31-i)<<1], share_hash[i]); } DLOG_WARN("************************************************************************************************"); DLOG_WARN("******** BLOCK FOUND - %s ********",new_notify_blockhash); DLOG_WARN("************************************************************************************************"); i = assembleBlockAndSubmit(block_header, full_cb_txn, cb->coinb1_len+12+cb->coinb2_len, job, m->sdata, new_notify_blockhash, empty_work); if (i) { // successfully submitted datum_blocktemplates_notifynew(new_notify_blockhash, job->height + 1); } if (job->is_datum_job) { // submit via DATUM datum_protocol_pow_submit(c, job, username_s, was_block, empty_work, quickdiff, block_header, job_diff, full_cb_txn, cb, extranonce_bin, coinbase_index); } } // we check this after checking if the share is a valid block because... well, we want to try and build on our own block even on the off chance it's late. // we'll still reject the share, though, even if it's a block. *trollface* if (job->is_stale_prevblock) { // share is from a stale job send_rejected_stale_block(c, id); m->share_count_rejected++; m->share_diff_rejected += job_diff; return 0; } // check if ntime is within bounds for a valid block // we'll do this after we try and potential blocks found with bad times, just in case if (ntime_val < job->block_template->mintime) { send_rejected_time_too_old(c, id); m->share_count_rejected++; m->share_diff_rejected += job_diff; return 0; } if (ntime_val > (job->block_template->curtime + 7200)) { send_rejected_time_too_new(c, id); m->share_count_rejected++; m->share_diff_rejected += job_diff; return 0; } // check if share beats miner's work target if (!quickdiff) { // check against job+connection target if (compare_hashes(share_hash, m->stratum_job_targets[g_job_index]) > 0) { // bad target diff send_rejected_high_hash_error(c, id); m->share_count_rejected++; m->share_diff_rejected += job_diff; return 0; } } else { // check against quickdiff target instead if (compare_hashes(share_hash, m->quickdiff_target) > 0) { // bad target diff send_rejected_high_hash_error(c, id); m->share_count_rejected++; m->share_diff_rejected += job_diff; return 0; } } // check if stale if (m->sdata->loop_tsms > (job->tsms + ((datum_config.stratum_v1_share_stale_seconds + datum_config.bitcoind_work_update_seconds) * 1000))) { // share is from a stale job send_rejected_stale(c, id); m->share_count_rejected++; m->share_diff_rejected += job_diff; return 0; } // check if duplicate submission // if this is a quickdiff share, invert ntime here as a way to prevent unlikely collisions. if (datum_stratum_check_for_dupe(m->sdata, nonce_val, g_job_index, quickdiff?(~ntime_val):(ntime_val), bver, &extranonce_bin[0])) { send_rejected_duplicate(c, id); m->share_count_rejected++; m->share_diff_rejected += job_diff; return 0; } // work accepted if (!was_block) { if (job->is_datum_job) { // submit via DATUM datum_protocol_pow_submit(c, job, username_s, was_block, empty_work, quickdiff, block_header, job_diff, full_cb_txn, cb, extranonce_bin, coinbase_index); } } char s[256]; snprintf(s, sizeof(s), "{\"error\":null,\"id\":%"PRIu64",\"result\":true}\n", id); datum_socket_send_string_to_client(c, s); // update connection totals m->share_diff_accepted += job_diff; m->share_count_accepted++; // update since-snap totals m->share_count_since_snap++; m->share_diff_since_snap += job_diff; stratum_update_miner_stats_accepted(c, job_diff); stratum_update_vardiff(c,false); return 0; } int client_mining_configure(T_DATUM_CLIENT_DATA *c, uint64_t id, json_t *params_obj) { // {"id":0,"method":"mining.configure","params":[["version-rolling"],{"version-rolling.mask":"1fffe000","version-rolling.min-bit-count":16}]} // {"id": 9966, "method": "mining.configure", "params": [["version-rolling", "subscribe-extranonce"], {"version-rolling.mask": "1fffe000", "version-rolling.min-bit-count": 16}]} // {"id":1,"method":"mining.configure","params":[["version-rolling","minimum-difficulty","subscribe-extranonce"],{"version-rolling.mask":"1fffe000","version-rolling.min-bit-count":16,"minimum-difficulty.value":2048}]} // prompts the following responses: // {"error": null, "id": 0, "result": {"version-rolling": true, "version-rolling.mask": "1fffe000", "minimum-difficulty": false}} // {"id": null, "method": "mining.set_version_mask", "params": ["1fffe000"]} // need to parse params... json_t *p1, *p2, *t; const char *s, *s2; char sx[1024]; char sa[1024]; int sxl = 0; sx[0] = 0; int i; T_DATUM_MINER_DATA * const m = c->app_client_data; bool new_vroll = false; bool new_mdiff = false; if (!json_is_array(params_obj)) { return -1; } p1 = json_array_get(params_obj, 0); p2 = json_array_get(params_obj, 1); if ((!p1) || (!p2)) return -1; size_t index; json_t *value; json_array_foreach(p1, index, value) { if (json_is_string(value)) { s = json_string_value(value); switch(s[0]) { case 'v': { if (!strcmp("version-rolling", s)) { new_vroll = true; m->extension_version_rolling = true; m->extension_version_rolling_mask = 0x1fffe000; m->extension_version_rolling_bits = 16; t = json_object_get(p2, "version-rolling.mask"); if (t) { s2 = json_string_value(t); if (s2) { m->extension_version_rolling_mask = strtoul(s2, NULL, 16) & m->extension_version_rolling_mask; } } sxl = sprintf(&sx[sxl], "{\"id\":null,\"method\":\"mining.set_version_mask\",\"params\":[\"%08x\"]}\n", m->extension_version_rolling_mask); } break; } case 'm': { if (!strcmp("minimum-difficulty", s)) { new_mdiff = true; } break; } default: break; } } } i = snprintf(sa, sizeof(sa), "{\"error\":null,\"id\":%"PRIu64",\"result\":{", id); if (new_vroll) { i+= snprintf(&sa[i], sizeof(sa)-i, "\"version-rolling\":true,\"version-rolling.mask\":\"%08x\"", m->extension_version_rolling_mask); } if (new_mdiff) { // we don't currently support miner specified minimum difficulty. i+= snprintf(&sa[i], sizeof(sa)-i, ",\"minimum-difficulty\":false"); } i+= snprintf(&sa[i], sizeof(sa)-i, "}}\n"); datum_socket_send_string_to_client(c, sa); if (sxl) { datum_socket_send_string_to_client(c, sx); } return 0; } int client_mining_authorize(T_DATUM_CLIENT_DATA *c, uint64_t id, json_t *params_obj) { char s[256]; const char *username_s; json_t *username; T_DATUM_MINER_DATA * const m = c->app_client_data; username = json_array_get(params_obj, 0); if (!username) { username_s = (const char *)"NULL"; } else { username_s = json_string_value(username); if (!username_s) { username_s = (const char *)"NULL"; } } strncpy(m->last_auth_username, username_s, sizeof(m->last_auth_username) - 1); m->last_auth_username[sizeof(m->last_auth_username)-1] = 0; snprintf(s, sizeof(s), "{\"error\":null,\"id\":%"PRIu64",\"result\":true}\n", id); datum_socket_send_string_to_client(c, s); m->authorized = true; return 0; } int send_mining_notify(T_DATUM_CLIENT_DATA *c, bool clean, bool quickdiff, bool new_block) { // send the current job to the miner T_DATUM_THREAD_DATA *t = (T_DATUM_THREAD_DATA *)c->datum_thread; T_DATUM_STRATUM_JOB *j = ((T_DATUM_STRATUM_THREADPOOL_DATA *)t->app_thread_data)->cur_stratum_job; T_DATUM_MINER_DATA * const m = c->app_client_data; T_DATUM_STRATUM_COINBASE *cb; char cb1[STRATUM_COINBASE1_MAX_LEN+2]; unsigned int cbselect = 0; bool full_coinbase = false; char s[512]; unsigned char tdiff = 0xFF; if (!j) { return -1; } //job_id - ID of the job. Use this ID while submitting share generated from this job. //prevhash - Hash of previous block. //coinb1 - Initial part of coinbase transaction. //coinb2 - Final part of coinbase transaction. //merkle_branch - List of hashes, will be used for calculation of merkle root. //version - Bitcoin block version. //nbits - Encoded current network difficulty //ntime - Current ntime/ //clean_jobs // { // "id": null, // "method": "mining.notify", // "params": [ // "17137173556511577", // job_id // "275476ad65c63568bfea24935f56ecbb4cafe90100030dcf0000000000000000", // prevhash // "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff110326d20c0c004f4345414e2e58595a0026ffffffff020000000000000000106a0e7795", // coinb1 // "205fa0120000000017a914413b5901fe4e591c95405fd446b5b002da575bf08700000000", // coinb2 // [], // merkle_branch // "20000000", // version // "17034219", // nbits // "6625406b", // ntime // true // clean_jobs // ] // } // let's not conflict the two special types of work. empty block is more important than changing vardiff quickly if (new_block) { quickdiff = false; } // we always set difficulty before the first notify on the connection, so last_sent_diff should always be set here // compute the target for this job for the client if (!quickdiff) { // check for a vardiff change. call with "no_quick" set to true to prevent recursion or double notifies stratum_update_vardiff(c, true); } if (j->is_datum_job) { // check if our client meets of exceeds the minimum datum diff if (m->current_diff < datum_config.override_vardiff_min) { m->current_diff = datum_config.override_vardiff_min; } } // if we have an updated difficulty to send, send it before we send the notify // applies to quick and normal diff changes if (m->last_sent_diff != m->current_diff) { send_mining_set_difficulty(c); } // if this is a quick diff change, the job is likely identical to one we've already sent // in which case, we don't want to clobber the normal target table and reject shares that we shouldn't if (!quickdiff) { get_target_from_diff(m->stratum_job_targets[j->global_index], m->last_sent_diff); m->stratum_job_diffs[j->global_index] = m->last_sent_diff; m->quickdiff_active = false; } else { m->quickdiff_active = true; m->quickdiff_value = m->last_sent_diff; get_target_from_diff(m->quickdiff_target, m->quickdiff_value); } // We'll use the client's send buffer for sanity, since in this environment it wont result in a partial send and we can just build up the string in the output buffer datum_socket_send_string_to_client(c, "{\"id\":null,\"method\":\"mining.notify\",\"params\":["); if (j->job_state >= JOB_STATE_FULL_PRIORITY_WAIT_COINBASER) { if (((T_DATUM_STRATUM_THREADPOOL_DATA *)t->app_thread_data)->full_coinbase_ready) { full_coinbase = true; } } // coinbase selection is tacked on to the job ID // prepending a Q means it's a duplicate job, but with a new diff (needed per stratum protocol "spec") // prepending an N means this is an empty (subsidy-only) block with a small coinbase and has coinbase ID 255/0xff if (new_block) { cbselect = 0; } else { if (full_coinbase) { cbselect = m->coinbase_selection; } else { cbselect = 0; } } cb = &j->coinbase[cbselect]; // new block work always is just a blank coinbase, for now if (quickdiff) { snprintf(s, sizeof(s), "\"Q%s%2.2x\",\"%s\",\"", j->job_id, cbselect, j->prevhash); } else { if (!new_block) { snprintf(s, sizeof(s), "\"%s%2.2x\",\"%s\",\"", j->job_id, cbselect, j->prevhash); } else { snprintf(s, sizeof(s), "\"N%s%2.2x\",\"%s\",\"", j->job_id, (unsigned int)255, j->prevhash); // empty coinbase for new block cb = &j->subsidy_only_coinbase; } } // this may look silly, but the send buffer doesn't get emptied until this thread's loop runs. so might as well just utilize it // for code readability purposes at the expense of a few extra calls. datum_socket_send_string_to_client(c, s); memcpy(cb1, cb->coinb1, cb->coinb1_len<<1); // copy coinb1 to temp buffer for user-specific modifications cb1[cb->coinb1_len<<1] = 0; // the miner's PoT diff needs to be encoded here. // TODO: Rework job ID to include the target byte. This is gateway side, and the server doesn't care at all about the SV1 job ID. tdiff = floorPoT(m->last_sent_diff); uchar_to_hex(&cb1[j->target_pot_index<<1], tdiff); if (quickdiff) { // in a quickdiff, we need to replace the last two bytes of coinb1 to make the work unique // while the quickdiff value here is non-unique per user in the case of multiple quickdiffs for the same job, the extranonce1 // is still unique per user and mitigates this. // NOTE: These constants are also used by the DATUM server. DO NOT CHANGE THEM. datum_socket_send_chars_to_client(c, cb1, (cb->coinb1_len<<1)-4); if (upk_u16le(cb->coinb1_bin, cb->coinb1_len - 2) != 0x5144) { datum_socket_send_string_to_client(c, "4451"); } else { datum_socket_send_string_to_client(c, "BBAE"); } } else { datum_socket_send_string_to_client(c, cb1); } datum_socket_send_string_to_client(c, "\",\""); datum_socket_send_string_to_client(c, cb->coinb2); datum_socket_send_string_to_client(c, "\","); if (!new_block) { // send job merkle leafs datum_socket_send_string_to_client(c, j->merklebranches_full); } else { // send empty merkle leafs datum_socket_send_string_to_client(c, "[]"); } snprintf(s, sizeof(s), ",\"%s\",\"%s\",\"%s\",", j->version, j->nbits, j->ntime); datum_socket_send_string_to_client(c, s); // bunch of reasons we may need to discard old work if ((clean) || (quickdiff) || (new_block)) { datum_socket_send_string_to_client(c, "true]}\n"); } else { datum_socket_send_string_to_client(c, "false]}\n"); } m->last_sent_stratum_job_index = j->global_index; return 0; } int send_mining_set_difficulty(T_DATUM_CLIENT_DATA *c) { char s[256]; T_DATUM_MINER_DATA * const m = c->app_client_data; if (!m->current_diff) { m->current_diff = datum_config.stratum_v1_vardiff_min; } snprintf(s, sizeof(s), "{\"id\":null,\"method\":\"mining.set_difficulty\",\"params\":[%"PRIu64"]}\n", (uint64_t)m->current_diff); datum_socket_send_string_to_client(c, s); m->last_sent_diff = m->current_diff; return 0; } void datum_stratum_fingerprint_by_UA(T_DATUM_MINER_DATA *m) { // TODO: Make this a little more efficient. perhaps move to a loadable definitions file of some kind. // S21 tested to handle 2.25KB coinbase work on all versions released // UA starts with: Antminer S21/ // S21 Pro NOT confirmed to work this way (yet)... so keep the / if (strstr(m->useragent, "Antminer S21/") == m->useragent) { m->coinbase_selection = 5; // ANTMAIN2 return; } // the ePIC control boards can handle almost any size coinbase // UA starts with: PowerPlay-BM/ if (strstr(m->useragent, "PowerPlay-BM/") == m->useragent) { m->coinbase_selection = 4; // YUGE return; } // "vinsh" reports as xminer // Tested to handle up to 16KB if (strstr(m->useragent, "xminer-1.") == m->useragent) { m->coinbase_selection = 4; // YUGE return; } // whatsminer works fine with about a 6.5 KB coinbase // UA starts with: whatsminer/v1 if (strstr(m->useragent, "whatsminer/v1") == m->useragent) { m->coinbase_selection = 3; // RESPECTABLE return; } // Braiins firmware // Appears to handle arbitrary coinbase sizes, however not extensively tested on all firmware versions // feed the S21-like coinbase for now, which is at least moderately sized // UA contains: bosminer-plus-tuner if (strstr(m->useragent, "bosminer-plus-tuner") != NULL) { // match anywhere in string, not just beginning m->coinbase_selection = 5; // ANTMAIN2 return; } // Nicehash, sadly needs a smaller coinbase than even antminer s19s // they also need a high minimum difficulty if (strstr(m->useragent, "NiceHash/") == m->useragent) { m->current_diff=524288; m->forced_high_min_diff=524288; m->coinbase_selection = 1; // TINY return; } // The Bitaxe is tested to work with a large coinbase // However, it does slow work changes slightly when they're YUGE, so we'll go with // the whatsminer tested size as a compromise. also should save some bandwidth, which // is probably not a bad plan, given the low odds of a bitaxe finding a block. if (strstr(m->useragent, "bitaxe") == m->useragent) { m->coinbase_selection = 3; // RESPECTABLE return; } } int client_mining_subscribe(T_DATUM_CLIENT_DATA *c, uint64_t id, json_t *params_obj) { uint32_t sid; char s[1024]; T_DATUM_MINER_DATA * const m = c->app_client_data; json_t *useragent; // params = // 0 = UA // 1 = session ID to resume // 2 = host/port // 3 = ??? if (m->subscribed) { // don't resubscribe them... that'd be dumb. return 0; } // set default diff m->current_diff = datum_config.stratum_v1_vardiff_min; // default to the antminer workaround, which appears to be universally compatible // except for NiceHash. m->coinbase_selection = 2; m->useragent[0] = 0; if (params_obj) { if (json_is_array(params_obj)) { useragent = json_array_get(params_obj, 0); if (json_is_string(useragent)) { strncpy_uachars(m->useragent, json_string_value(useragent), 127); // strip some chars m->useragent[127] = 0; } } } if ((datum_config.stratum_v1_fingerprint_miners) && (m->useragent[0])) { datum_stratum_fingerprint_by_UA(m); if (m->current_diff < datum_config.stratum_v1_vardiff_min) { m->current_diff = datum_config.stratum_v1_vardiff_min; } } // get a new unique session ID for this connection (extranonce1) sid = get_new_session_id(c); m->sid = sid; // store the inverted endian version for faster share checking later m->sid_inv = ((sid>>24)&0xff) | (((sid>>16)&0xff)<<8) | (((sid>>8)&0xff)<<16) | ((sid&0xff)<<24); // tell them about all of this snprintf(s, sizeof(s), "{\"error\":null,\"id\":%"PRIu64",\"result\":[[[\"mining.notify\",\"%8.8x1\"],[\"mining.set_difficulty\",\"%8.8x2\"]],\"%8.8x\",8]}\n", id, sid, sid, sid); datum_socket_send_string_to_client(c, s); // send them their current difficulty before sending a job send_mining_set_difficulty(c); // mark them as subscribed so that notifies actually work m->subscribed = true; // clean work on connect, not quickdiff, doesn't matter if new block or not (don't need empty work speedup on connect) send_mining_notify(c,true,false,false); // reset vardiff tallies m->share_count_since_snap = 0; m->share_diff_since_snap = 0; m->share_snap_tsms = m->sdata->loop_tsms; m->subscribe_tsms = m->sdata->loop_tsms; return 0; } int datum_stratum_v1_socket_thread_client_cmd(T_DATUM_CLIENT_DATA *c, char *line) { json_t *j ,*method_obj, *id_obj, *params_obj; json_error_t err = { }; const char *method; int i; uint64_t id; if (line[0] == 0) return 0; if (line[0] != '{') { return -1; } j = json_loads(line, JSON_REJECT_DUPLICATES, &err); if (!j) { return -2; } if (!(method_obj = json_object_get(j, "method"))) { json_decref(j); return -3; } if (!json_is_string(method_obj)) { json_decref(j); return -6; } // id can technically be anything, but we should enforce some sanity... if (!(id_obj = json_object_get(j, "id"))) { json_decref(j); return -4; } // enforce that id must be an integer. might not be 100% to spec, but is sane and nothing known violates this. // allowing arbitrary non-integer things here is a potential DoS vector. if (!json_is_integer(id_obj)) { json_decref(j); return -4; } id = json_integer_value(id_obj); if (!(params_obj = json_object_get(j, "params"))) { json_decref(j); return -5; } method = json_string_value(method_obj); if (method[0] == 0) { json_decref(j); return -7; } switch (method[0]) { case 'm': { if (!strcmp(method, "mining.submit")) { i = client_mining_submit(c, id, params_obj); json_decref(j); return i; } if (!strcmp(method, "mining.configure")) { i = client_mining_configure(c, id, params_obj); json_decref(j); return i; } if (!strcmp(method, "mining.subscribe")) { i = client_mining_subscribe(c, id, params_obj); json_decref(j); return i; } if (!strcmp(method, "mining.authorize")) { i = client_mining_authorize(c, id, params_obj); json_decref(j); return i; } [[fallthrough]]; } default: { send_error_to_client(c, id, "[-3,\"Method not found\",null]"); json_decref(j); return 0; } } } void stratum_job_merkle_root_calc(T_DATUM_STRATUM_JOB *s, unsigned char *coinbase_txn_hash, unsigned char *merkle_root_output) { int i; unsigned char combined[64]; unsigned char next[32]; if (!s->merklebranch_count) { memcpy(merkle_root_output, coinbase_txn_hash, 32); return; } memcpy(&combined[0], coinbase_txn_hash, 32); memcpy(&combined[32], s->merklebranches_bin[0], 32); double_sha256(next, combined, 64); for(i=1;imerklebranch_count;i++) { memcpy(&combined[0], next, 32); memcpy(&combined[32], s->merklebranches_bin[i], 32); double_sha256(next, combined, 64); } memcpy(merkle_root_output, next, 32); return; } void stratum_calculate_merkle_branches(T_DATUM_STRATUM_JOB *s) { // NOTE: This uses a static varaible for temp space. Do not call concurrently from multiple threads. bool level_needs_dupe = false; int current_level_size = 0, next_level_size = 0; int q,i,j; // 64 byte combined hashes for inputs to merkle hashes unsigned char combined[64]; // pointers for hash lists const unsigned char (*current_level)[32]; unsigned char (*next_level)[32]; // scratch RAM static unsigned char templist[16384][32]; // dev sanity check for thread concurrency static int safety_check; int marker = ++safety_check; if (s->block_template->txn_count > 16383) { DLOG_FATAL("BUG: stratum_calculate_merkle_branches does not support templates with more than 16383 transactions! %d transactions in template.",(int)s->block_template->txn_count); panic_from_thread(__LINE__); return; } if (!s->block_template->txn_count) { // no transactions s->merklebranch_count = 0; s->merklebranches_full[0] = '['; s->merklebranches_full[1] = ']'; s->merklebranches_full[2] = 0; return; } current_level_size = s->block_template->txn_count+1; next_level = &templist[0]; current_level = next_level; level_needs_dupe = false; q = 0; while (current_level_size > 1) { if (current_level_size % 2 != 0) { current_level_size++; level_needs_dupe = true; } else { level_needs_dupe = false; } next_level_size = current_level_size >> 1; for(i=0;imerklebranches_bin[0], s->block_template->txns[0].txid_bin, 32); for(j=0;j<32;j++) { pk_u16le(s->merklebranches_hex[0], j << 1, upk_u16le(s->block_template->txns[0].txid_hex, (31 - j) << 1)); } s->merklebranches_hex[0][64] = 0; } else { // second+ level branch if (level_needs_dupe && (i==(next_level_size-1))) { memcpy(s->merklebranches_bin[q], ¤t_level[i<<1][0], 32); } else { memcpy(s->merklebranches_bin[q], ¤t_level[(i<<1)+1][0], 32); } hash2hex(s->merklebranches_bin[q], s->merklebranches_hex[q]); } } else { if (!q) { // getting from txn list // we're pretending we have a coinbase txn that we don't know about yet at index -1, so // we need to pretend we're one txn ahead of where we really are. // That means expected index-1 is our first, and index is our second. This is not a typo! memcpy(&combined[0], s->block_template->txns[(i<<1)-1].txid_bin, 32); if (level_needs_dupe && (i==(next_level_size-1))) { memcpy(&combined[32], s->block_template->txns[(i<<1)-1].txid_bin, 32); } else { memcpy(&combined[32], s->block_template->txns[(i<<1)].txid_bin, 32); } } else { // getting from "current_level" memcpy(&combined[0], ¤t_level[i<<1][0], 32); if (level_needs_dupe && (i==(next_level_size-1))) { memcpy(&combined[32], ¤t_level[i<<1][0], 32); } else { memcpy(&combined[32], ¤t_level[(i<<1)+1][0], 32); } } double_sha256(next_level[i], combined, 64); } } current_level = next_level; next_level+=i+1; current_level_size = next_level_size; q++; } s->merklebranch_count = q; // Pre-construct stratum v1 job field s->merklebranches_full[0] = '['; j=1; for(i=0;imerklebranches_full[j] = ','; j++; } j += sprintf(&s->merklebranches_full[j], "\"%s\"", s->merklebranches_hex[i]); } s->merklebranches_full[j] = ']'; s->merklebranches_full[j+1] = 0; if (safety_check != marker) { DLOG_FATAL("BUG: stratum_calculate_merkle_branches is NOT thread safe and appears to have been called concurrently!"); panic_from_thread(__LINE__); } } void update_stratum_job(T_DATUM_TEMPLATE_DATA *block_template, bool new_block, int job_state) { T_DATUM_STRATUM_JOB *s = &stratum_job_list[stratum_job_next]; int i; // clear the job memory memset(s, 0, sizeof(T_DATUM_STRATUM_JOB)); // come up with a prefix for the job // this ensures it is unique even if nothing else about the job has changed for some reason s->enprefix = stratum_enprefix ^ 0xB10C; stratum_enprefix++; // copy the template's previous block hash in 32-bit LE hex for(i=0;i<8;i++) { pk_u64le(s->prevhash, i << 3, upk_u64le(block_template->previousblockhash, (7 - i) << 3)); } s->prevhash[64] = 0; snprintf(s->version, sizeof(s->version), "%8.8x", block_template->version); s->version_uint = block_template->version; strncpy(s->nbits, block_template->bits, sizeof(s->nbits) - 1); // TODO: Should we use local time, and just verify is valid for the block? // Perhaps as an option. // The template's time is 100% safe, so we'll use that for now. snprintf(s->ntime, sizeof(s->ntime), "%8.8llx", (unsigned long long)block_template->curtime); // Set the coinbase value of this job based on the template s->coinbase_value = block_template->coinbasevalue; s->height = block_template->height; s->block_template = block_template; // stash useful binary versions of prevblockhash and nbits memcpy(s->prevhash_bin, block_template->previousblockhash_bin, 32); memcpy(s->nbits_bin, block_template->bits_bin, 4); s->nbits_uint = upk_u32le(s->nbits_bin, 0); // calculate block target from nbits nbits_to_target(s->nbits_uint, s->block_target); // if this is to be a clean job, remember that s->is_new_block = new_block; // we're new, not stale s->is_stale_prevblock = false; // start as not a datum job. if we have coinbase data and such for it down the line, this will get updated. s->is_datum_job = false; // prep the coinbase txn(s) for this job generate_base_coinbase_txns_for_stratum_job(s, s->is_new_block); s->job_state = job_state; if ((job_state == JOB_STATE_FULL_PRIORITY_WAIT_COINBASER) || (job_state == JOB_STATE_FULL_NORMAL_WAIT_COINBASER)) { s->need_coinbaser = true; } else { s->need_coinbaser = false; } // if this is a new block, invalidate all old work if (new_block) { for(i=0;itsms = current_time_millis(); // calculate the stratum merkle branches and store them on this job stratum_calculate_merkle_branches(s); // update the latest empty data before we update the global job // this way, this info is here when all of the threads switch jobs if (new_block) { pthread_rwlock_wrlock(&stratum_global_latest_empty_stat); stratum_latest_empty_complete_count = 0; // reset # of threads completed stratum_latest_empty_ready_for_full = false; // reset ready-for-full. stratum_latest_empty_sent_count = 0; // reset client count stratum_latest_empty_job_index = global_latest_stratum_job_index+1; if (stratum_latest_empty_job_index == MAX_STRATUM_JOBS) { stratum_latest_empty_job_index = 0; } pthread_rwlock_unlock(&stratum_global_latest_empty_stat); } if (s->is_datum_job) { s->datum_job_idx = datum_protocol_setup_new_job_idx(s); } // update and sync the current global job index pthread_rwlock_wrlock(&stratum_global_job_ptr_lock); global_latest_stratum_job_index++; if (global_latest_stratum_job_index == MAX_STRATUM_JOBS) { global_latest_stratum_job_index = 0; } s->global_index = global_latest_stratum_job_index; snprintf(s->job_id, sizeof(s->job_id), "%8.8x%2.2x%4.4x", (uint32_t)time(NULL), (uint8_t)stratum_job_next, (unsigned int)((uint16_t)s->global_index ^ STRATUM_JOB_INDEX_XOR)); global_cur_stratum_jobs[global_latest_stratum_job_index] = s; pthread_rwlock_unlock(&stratum_global_job_ptr_lock); DLOG_DEBUG("Updated to job %d, ncb = %d, state = %d", s->global_index, s->need_coinbaser?1:0, s->job_state); return; } int assembleBlockAndSubmit(uint8_t *block_header, uint8_t *coinbase_txn, size_t coinbase_txn_size, T_DATUM_STRATUM_JOB *job, T_DATUM_STRATUM_THREADPOOL_DATA *sdata, const char *block_hash_hex, bool empty_work) { // TODO: Also submit directly to bitcoin P2P char *submitblock_req = NULL; char *ptr = NULL; size_t i; json_t *r; CURL *tcurl; int ret = 0; bool free_submitblock_req = false; char *s = NULL; // each thread has a chunk of RAM dedicated to prepping block submissions. use it. submitblock_req = sdata->submitblock_req; if (!submitblock_req) { // This should NEVER happen and likely indicates something is terribly wrong with the state of things... but we'll try our best to salvage this block. DLOG_ERROR("For some reason no pointer available for submitting the block we just found! Attempting to allocate new memory for this, but we're probably in for a bad time..."); submitblock_req = malloc(8500000); // worst case if (!submitblock_req) { // this would be really bad DLOG_FATAL("Could not allocate RAM for submitblock! This is REALLY bad."); // TODO: dump what we can to disk to preserve the block for any watchdog available there // This should never happen, however, so super low priority... but to cover every contingency when a block is involved is eventually important to do. panic_from_thread(__LINE__); return 0; } DLOG_ERROR("We were able to allocate a new block of RAM for submitting this block. But look into this issue. May be a hardware or OS problem!"); free_submitblock_req = true; } ptr = submitblock_req; ptr += sprintf(ptr, "{\"jsonrpc\":\"1.0\",\"id\":\"%llu\",\"method\":\"submitblock\",\"params\":[\"",(unsigned long long)time(NULL)); for(i=0;i<80;i++) { ptr += sprintf(ptr, "%2.2x", block_header[i]); } // txn count if (!empty_work) { ptr += append_bitcoin_varint_hex(job->block_template->txn_count + 1, ptr); } else { ptr += append_bitcoin_varint_hex(1, ptr); } // copy coinbase txn for(i=0;iblock_template->txn_count;i++) { memcpy(ptr, job->block_template->txns[i].txn_data_hex, job->block_template->txns[i].size*2); ptr += job->block_template->txns[i].size*2; } } // close the submitblock *ptr = '"'; ptr++; *ptr = ']'; ptr++; *ptr = '}'; ptr++; *ptr = 0; // logging function will truncate the output DLOG_DEBUG("Block Payload: %s", submitblock_req); // Trigger our redundant submission thread datum_submitblock_trigger(submitblock_req, block_hash_hex); // while this may induce a tiny delay for writing to disk, that seems favorable to losing the block entirely // if something below were to fail/crash/etc // this way we can have a (future) external watchdog monitoring the folder as a backup to submit the blocks if need be // for added security. The thread above should already be submitting this block anyway. if (datum_config.mining_save_submitblocks_dir[0] != 0) { // save the block submission to a file named by the block's hash char submitblockpath[384]; int n = snprintf(submitblockpath, sizeof(submitblockpath), "%s/datum_submitblock_%s.json", datum_config.mining_save_submitblocks_dir, block_hash_hex); if (n >= sizeof(submitblockpath)) { DLOG_ERROR("Overflow in construction of submitblock path!"); } else { FILE *f; f = fopen(submitblockpath, "w"); if (!f) { DLOG_ERROR("Could not open %s for writing submitblock record to disk: %s!", submitblockpath, strerror(errno)); } else { if (!fwrite(submitblock_req, ptr-submitblock_req, 1, f)) { DLOG_ERROR("Could not write to %s when writing submitblock record to disk: %s!", submitblockpath, strerror(errno)); } fclose(f); } } } tcurl = curl_easy_init(); if (!tcurl) { DLOG_FATAL("Could not initialize cURL for submitblock!!! This is REALLY REALLY BAD."); // we're not going to panic here because our other thread might still pull off submitting it... // these are cosmic ray rarity situations that should just never happen. usleep(100000); return 0; } // make the call! r = bitcoind_json_rpc_call(tcurl, &datum_config, submitblock_req); curl_easy_cleanup(tcurl); if (!r) { // oddly, this means success here. DLOG_INFO("Block %s submitted to upstream node successfully!",block_hash_hex); ret = 1; } else { s = json_dumps(r, JSON_ENCODE_ANY); if (!s) { DLOG_WARN("Upstream node rejected our block! (unknown)"); } else { DLOG_WARN("Upstream node rejected our block! (%s)",s); free(s); } json_decref(r); ret = 0; } // cleanup if (free_submitblock_req) { // let's not free until our thread is done with it usleep(10000); datum_submitblock_waitfree(); free(submitblock_req); } return ret; } datum_gateway-0.4.1beta/src/datum_stratum.h000066400000000000000000000242441512710151500210400ustar00rootroot00000000000000/* * * DATUM Gateway * Decentralized Alternative Templates for Universal Mining * * This file is part of OCEAN's Bitcoin mining decentralization * project, DATUM. * * https://ocean.xyz * * --- * * Copyright (c) 2024 Bitcoin Ocean, LLC & Jason Hughes * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ #ifndef _DATUM_STRATUM_H_ #define _DATUM_STRATUM_H_ #include #ifndef T_DATUM_CLIENT_DATA #include "datum_sockets.h" #endif #ifndef T_DATUM_TEMPLATE_DATA #include "datum_blocktemplates.h" #endif #define MAX_STRATUM_JOBS 256 #define MAX_COINBASE_TYPES 6 #define COINBASE_TYPE_TINY 0 // "empty", just pays pool #define COINBASE_TYPE_SMALL 1 // Nicehash needs a tiny coinb1, among other things. Max 500 bytes. #define COINBASE_TYPE_ANTMAIN 2 // Hack for antminer stock firmware to 750 bytes #define COINBASE_TYPE_RESPECTABLE 3 // 6500 byte max (whatsminers) #define COINBASE_TYPE_YUGE 4 // 16KB max (ePIC, bitaxe) #define COINBASE_TYPE_ANTMAIN2 5 // 2.25KB max (S21, +?) // Submitblock json rpc command max size is max block size * 2 for ascii plus some breathing room #define MAX_SUBMITBLOCK_SIZE 8500000 ///////////////////////////////// // Stratum job types ///////////////////////////////// // Potential job paths are: // 1 -> 3 -> 4 -> 5 -> 5 ... // 2E -> 2F -> 4 -> 5 -> 5 ... // Unknown job state. don't use this job. #define JOB_STATE_UNKNOWN 0 // This job is empty only. No template data available to switch to. // No template is expected to end up on this job and we should IMMEDIATELY change to the next job when seen #define JOB_STATE_EMPTY_ONLY 1 // this job is the result of a GBT call that got us the latest full template, but we need to do an empty first // use for the empty. wait for other threads. immediately send the full work with the "blank" coinbase // template is max sized for a "blank" coinbase. other coinbases are not expected to be used // this is the fastest empty->full work setup #define JOB_STATE_EMPTY_PLUS 2 // this job is a full GBT wo/coinbaser which we're expected to immediately broadcast to miners after a JOB_STATE_EMPTY_ONLY #define JOB_STATE_FULL_PRIORITY 3 // this is a normal job that waits for a full coinbaser setup after either JOB_STATE_FULL_PRIORITY or JOB_STATE_EMPTY_PLUS's full template // it's broadcast immediately when ready #define JOB_STATE_FULL_PRIORITY_WAIT_COINBASER 4 // // all of the above jobs will use a GBT with the least common denominator for a block size/coinbaser combo. coinbaser gets truncated. // // this is a normal job // a GBT call is made, and the coinbaser is queued // once the coinbaser returns (or fails) this job is gently broadcasted to all miners across the work change interval // TODO: Fit the multiple coinbasers to multiple templates sized specifically for them #define JOB_STATE_FULL_NORMAL_WAIT_COINBASER 5 //////////////////////////////////////////////////////// //////////////////////////////////////////////////////// //////////////////////////////////////////////////////// typedef struct { char coinb1[STRATUM_COINBASE1_MAX_LEN]; char coinb2[STRATUM_COINBASE2_MAX_LEN]; unsigned char coinb1_bin[STRATUM_COINBASE1_MAX_LEN>>1]; unsigned char coinb2_bin[STRATUM_COINBASE2_MAX_LEN>>1]; int coinb1_len; int coinb2_len; } T_DATUM_STRATUM_COINBASE; typedef struct { unsigned char output_script[64]; int output_script_len; uint64_t value_sats; int sigops; } T_DATUM_TXN_OUTPUT; typedef struct { int global_index; char job_id[24]; char prevhash[68]; unsigned char prevhash_bin[32]; char version[10]; uint32_t version_uint; char nbits[10]; unsigned char nbits_bin[4]; uint32_t nbits_uint; char ntime[10]; unsigned char block_target[32]; T_DATUM_TEMPLATE_DATA *block_template; unsigned char merklebranch_count; char merklebranches_hex[24][72]; unsigned char merklebranches_bin[24][32]; char merklebranches_full[4096]; // when fetching the coinbaser, we'll just stash all of the possible and valid output scripts here T_DATUM_TXN_OUTPUT available_coinbase_outputs[512]; int available_coinbase_outputs_count; unsigned char pool_addr_script[64]; int pool_addr_script_len; // multiple coinbase options // 0 = "empty" --- just pays pool addr, and possibly TIDES data. extranonce in coinbase if fits, or in first output if not. // 1 = "nicehash" --- roughly 500 bytes total... smaller than antminer... has nothing before the extranonce OP_RETURN (or no extranonce OP_RETURN if enough space in the coinbase) // 2 = "antminer" --- roughly 730 bytes max size, using a larger coinb1 and UART sync bits. This also works as a good default. // 3 = "whatsminer" --- max 6500 bytes tested. does not need the extranonce OP_RETURN unless there's no space in the coinbase itself after tags // 4 = "huge" --- max 16kB --- this is probably the most we should reasonably attempt to do in the coinbase... something like 380 to 530 outputs, depending on the type of output // 5 = "antminer2" --- max 2250 bytes --- latest S21s appear to support this T_DATUM_STRATUM_COINBASE coinbase[MAX_COINBASE_TYPES]; T_DATUM_STRATUM_COINBASE subsidy_only_coinbase; int target_pot_index; // where in coinb1 do we put our per-user vardiff pot value? uint64_t coinbase_value; uint64_t height; uint16_t enprefix; uint64_t tsms; // local timestamp for when job was created. can differ from the bitcoin network timestamp. bool is_new_block; bool is_stale_prevblock; int job_state; bool need_coinbaser; bool is_datum_job; unsigned char datum_job_idx; unsigned char datum_coinbaser_id; } T_DATUM_STRATUM_JOB; typedef struct T_DATUM_STRATUM_THREADPOOL_DATA { T_DATUM_STRATUM_JOB *cur_stratum_job; int latest_stratum_job_index; bool new_job; bool last_was_empty; int last_sent_job_state; uint64_t loop_tsms; bool full_coinbase_ready; int notify_remaining_count; uint64_t notify_start_time; uint64_t notify_last_time; uint64_t notify_delay_per_slot_tsms; int notify_last_cid; uint64_t last_job_height; uint64_t next_kick_check_tsms; char submitblock_req[MAX_SUBMITBLOCK_SIZE]; void *dupes; } T_DATUM_STRATUM_THREADPOOL_DATA; typedef struct { unsigned char active_index; // the one we're adding to. use the other for stats uint64_t last_swap_tsms; // timestamp of last swap uint64_t last_swap_ms; // length of time for the last uint64_t diff_accepted[2]; uint64_t last_share_tsms; } T_DATUM_STRATUM_USER_STATS; typedef struct { uint32_t sid, sid_inv; uint64_t unique_id; uint64_t connect_tsms; char useragent[128]; char last_auth_username[192]; bool extension_version_rolling; uint32_t extension_version_rolling_mask; unsigned char extension_version_rolling_bits; bool extension_minimum_difficulty; double extension_minimum_difficulty_value; bool authorized; bool subscribed; uint64_t subscribe_tsms; uint64_t last_sent_diff; uint64_t current_diff; uint8_t stratum_job_targets[MAX_STRATUM_JOBS][32]; uint64_t stratum_job_diffs[MAX_STRATUM_JOBS]; unsigned char coinbase_selection; uint64_t share_diff_accepted; uint64_t share_count_accepted; uint64_t share_diff_rejected; uint64_t share_count_rejected; // for vardiff uint64_t share_count_since_snap; uint64_t share_diff_since_snap; uint64_t share_snap_tsms; bool quickdiff_active; uint64_t quickdiff_value; uint8_t quickdiff_target[32]; uint64_t forced_high_min_diff; int last_sent_stratum_job_index; T_DATUM_STRATUM_USER_STATS stats; T_DATUM_STRATUM_THREADPOOL_DATA *sdata; } T_DATUM_MINER_DATA; extern int global_latest_stratum_job_index; extern pthread_rwlock_t stratum_global_job_ptr_lock; extern T_DATUM_STRATUM_JOB *global_cur_stratum_jobs[MAX_STRATUM_JOBS]; const char *datum_stratum_mod_username(const char *username_s, char *username_buf, size_t username_buf_sz, uint16_t share_rnd, const char *modname, size_t modname_len); int send_mining_notify(T_DATUM_CLIENT_DATA *c, bool clean, bool quickdiff, bool new_block); void update_stratum_job(T_DATUM_TEMPLATE_DATA *block_template, bool new_block, int job_state); void stratum_job_merkle_root_calc(T_DATUM_STRATUM_JOB *s, unsigned char *coinbase_txn_hash, unsigned char *merkle_root_output); int assembleBlockAndSubmit(uint8_t *block_header, uint8_t *coinbase_txn, size_t coinbase_txn_size, T_DATUM_STRATUM_JOB *job, T_DATUM_STRATUM_THREADPOOL_DATA *sdata, const char *block_hash_hex, bool empty_work); void generate_coinbase_txns_for_stratum_job(T_DATUM_STRATUM_JOB *s, bool empty_only); int send_mining_set_difficulty(T_DATUM_CLIENT_DATA *c); bool stratum_latest_empty_check_ready_for_full(void); // Server thread main loop void *datum_stratum_v1_socket_server(void *arg); // DATUM socket callbacks void datum_stratum_v1_socket_thread_init(T_DATUM_THREAD_DATA *my); void datum_stratum_v1_socket_thread_loop(T_DATUM_THREAD_DATA *my); int datum_stratum_v1_socket_thread_client_cmd(T_DATUM_CLIENT_DATA *c, char *line); void datum_stratum_v1_socket_thread_client_closed(T_DATUM_CLIENT_DATA *c, const char *msg); void datum_stratum_v1_socket_thread_client_new(T_DATUM_CLIENT_DATA *c); int datum_stratum_v1_global_subscriber_count(void); double datum_stratum_v1_est_total_th_sec(void); void datum_stratum_v1_shutdown_all(void); extern T_DATUM_SOCKET_APP *global_stratum_app; extern pthread_rwlock_t need_coinbaser_rwlocks[MAX_STRATUM_JOBS]; extern bool need_coinbaser_rwlocks_init_done; #endif datum_gateway-0.4.1beta/src/datum_stratum_dupes.c000066400000000000000000000355731512710151500222420ustar00rootroot00000000000000/* * * DATUM Gateway * Decentralized Alternative Templates for Universal Mining * * This file is part of OCEAN's Bitcoin mining decentralization * project, DATUM. * * https://ocean.xyz * * --- * * Copyright (c) 2024-2025 Bitcoin Ocean, LLC & Jason Hughes * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ #include #include #include #include #include #include "datum_stratum_dupes.h" #include "datum_stratum.h" #include "datum_conf.h" #include "datum_utils.h" // TODO: Refactor to just use the block header sanely. // This is more contrived than it needs to be, although it profiles quite well void datum_stratum_dupes_init(void *sdata_v) { T_DATUM_STRATUM_THREADPOOL_DATA *sdata = sdata_v; T_DATUM_STRATUM_DUPES *dupes = NULL; sdata->dupes = calloc( sizeof(T_DATUM_STRATUM_DUPES) + 16, 1 ); if (!sdata->dupes) { DLOG_FATAL("Could not allocate RAM for dupe struct (small one, %lu bytes)", (unsigned long)sizeof(T_DATUM_STRATUM_DUPES) + 16); panic_from_thread(__LINE__); return; } dupes = sdata->dupes; dupes->ptr = calloc((datum_config.stratum_v1_max_clients_per_thread * datum_config.stratum_v1_vardiff_target_shares_min * (datum_config.stratum_v1_share_stale_seconds/60) * 16), sizeof(T_DATUM_STRATUM_DUPE_ITEM) ); if (!dupes->ptr) { DLOG_FATAL("Could not allocate RAM for dupe struct (big one, %lu bytes)",(unsigned long)(datum_config.stratum_v1_max_clients_per_thread * datum_config.stratum_v1_vardiff_target_shares_min * (datum_config.stratum_v1_share_stale_seconds/60) * 16) * sizeof(T_DATUM_STRATUM_DUPE_ITEM)); panic_from_thread(__LINE__); return; } dupes->max_items = (datum_config.stratum_v1_max_clients_per_thread * datum_config.stratum_v1_vardiff_target_shares_min * (datum_config.stratum_v1_share_stale_seconds/60) * 16); dupes->current_items = 0; DLOG_DEBUG("Initialized dupe check thread data. %"PRIu64" bytes of RAM used for %d max entries @ %p for %p", (uint64_t)dupes->max_items * (uint64_t)sizeof(T_DATUM_STRATUM_DUPE_ITEM), dupes->max_items, dupes, sdata); return; } int datum_stratum_dupes_cleanup_sort_compare(const void *a, const void *b) { const T_DATUM_STRATUM_DUPE_ITEM *item1 = a; const T_DATUM_STRATUM_DUPE_ITEM *item2 = b; if (item1 == NULL && item2 == NULL) return 0; if (item1 == NULL) return 1; if (item2 == NULL) return -1; if (item1->job_index >= MAX_STRATUM_JOBS) return 1; if (item2->job_index >= MAX_STRATUM_JOBS) return -1; if (global_cur_stratum_jobs[item1->job_index] == NULL && global_cur_stratum_jobs[item2->job_index] == NULL) return 0; if (global_cur_stratum_jobs[item1->job_index] == NULL) return 1; if (global_cur_stratum_jobs[item2->job_index] == NULL) return -1; uint64_t tsms1 = global_cur_stratum_jobs[item1->job_index]->tsms; uint64_t tsms2 = global_cur_stratum_jobs[item2->job_index]->tsms; if (tsms1 > tsms2) return -1; if (tsms1 < tsms2) return 1; return 0; } int find_first_less_than(T_DATUM_STRATUM_DUPE_ITEM *ptr, size_t max_items, uint64_t given_tsms) { int low = 0; int high = max_items - 1; int result = -1; uint64_t tsms; while (low <= high) { int mid = low + (high - low) / 2; // sanity if (mid < 0) mid = 0; if (mid > (max_items-1)) mid = max_items-1; // more sanity if (ptr[mid].job_index < 0 || ptr[mid].job_index > MAX_STRATUM_JOBS) { tsms = 0; } else if (global_cur_stratum_jobs[ptr[mid].job_index] != NULL) { tsms = global_cur_stratum_jobs[ptr[mid].job_index]->tsms; } else { tsms = 0; // Treat NULL as the smallest possible value... we can trim NULLs } // bsearch until we find the first entry < given if (tsms < given_tsms) { result = mid; high = mid - 1; } else { low = mid + 1; } } return result; } void datum_stratum_dupes_expand(T_DATUM_STRATUM_DUPES *dupes) { T_DATUM_STRATUM_DUPE_ITEM *new_ptr; int new_max = ((dupes->max_items * 125)/100); new_ptr = realloc(dupes->ptr, sizeof(T_DATUM_STRATUM_DUPE_ITEM) * new_max); if (!new_ptr) { DLOG_FATAL("Could not reallocate dupes ptr %p of %d items to %d items!", dupes->ptr, dupes->max_items, new_max); panic_from_thread(__LINE__); return; } memset(&new_ptr[dupes->max_items], 0, sizeof(T_DATUM_STRATUM_DUPE_ITEM) * (new_max - dupes->max_items)); DLOG_DEBUG("INFO: Had to allocate more RAM to duplicate share checking for thread. %d to %d items (%"PRIu64" bytes)", dupes->max_items, new_max, (uint64_t)sizeof(T_DATUM_STRATUM_DUPE_ITEM) * (uint64_t)new_max); dupes->max_items = new_max; dupes->ptr = new_ptr; // always needs reoganizing after this... do externally. } void datum_stratum_dupes_reorganize(T_DATUM_STRATUM_DUPES *dupes) { int i; T_DATUM_STRATUM_DUPE_ITEM *q,*p=NULL; for(i=0;imax_items;i++) { // we'll use ntime as an indicator, since obvious ntime cant be zero if (dupes->ptr[i].ntime == 0) break; if (!dupes->index[dupes->ptr[i].nonce_high]) { // easy. this is the first dupes->index[dupes->ptr[i].nonce_high] = &dupes->ptr[i]; dupes->ptr[i].next = NULL; continue; } q = dupes->index[dupes->ptr[i].nonce_high]; p = NULL; do { if (q->nonce_low > dupes->ptr[i].nonce_low) { if (p) { // insert after p p->next = &dupes->ptr[i]; } else { // insert as first entry, before this one dupes->index[dupes->ptr[i].nonce_high] = &dupes->ptr[i]; } dupes->ptr[i].next = q; break; } // this should be safe here, even though we haven't cleaned up all the old pointers // the reason is that nothing we having cleaned should have ended up in the index yet p = q; if (!q->next) { // ended up at the last entry without finding one greater than me... add to the end q->next = &dupes->ptr[i]; dupes->ptr[i].next = NULL; q = NULL; break; } else { q = q->next; } } while(q); } dupes->current_items = i; // should be all straightened out now } void datum_stratum_dupes_cleanup(T_DATUM_STRATUM_DUPES *dupes, bool full_wipe) { int i; if (full_wipe) { // we're just cleaning up after a new block or whatever memset(dupes->ptr, 0, sizeof(T_DATUM_STRATUM_DUPE_ITEM) * dupes->max_items); dupes->current_items = 0; return; } // Somewhat expensive cleanup of dupes... // first, sort the full dupe list by the dupe's stratum job timestamp, decending... which is trickyish // then, bsearch find the index of the first item with a job timestamp that is from a stale job // if there are entries after it, we're good and we can prune those // fix the linked list // this breaks all links. we'll need to reconstruct them! qsort(dupes->ptr, dupes->max_items, sizeof(T_DATUM_STRATUM_DUPE_ITEM), datum_stratum_dupes_cleanup_sort_compare); // links all broken, so wipe out the starting table memset(dupes->index, 0, sizeof(T_DATUM_STRATUM_DUPE_ITEM *) * 65536); // find the first stale index i = find_first_less_than(dupes->ptr, dupes->max_items, current_time_millis() - (datum_config.stratum_v1_share_stale_seconds*1000)); if ((i == -1) || (i == dupes->max_items-1)) { // none of the items are stale... datum_stratum_dupes_expand(dupes); } else { // ok, we want to free up at least 5% of the entries, otherwise we'll be right back here wasting CPU time if (i < ((dupes->max_items * 95)/100)) { // at least 5% are good // clear out the stales... dupes->current_items = i; memset(&dupes->ptr[i], 0, sizeof(T_DATUM_STRATUM_DUPE_ITEM) * (dupes->max_items - i)); } else { // < 5% are freeable... we just need more RAM. datum_stratum_dupes_expand(dupes); } } // fix all the links in our now either expanded or cleaned dupe list datum_stratum_dupes_reorganize(dupes); } T_DATUM_STRATUM_DUPE_ITEM *datum_stratum_add_new_dupe(T_DATUM_STRATUM_DUPES *dupes, unsigned int nonce, unsigned short job_index, unsigned int ntime_val, unsigned int version_bits, unsigned char *extranonce_bin, T_DATUM_STRATUM_DUPE_ITEM *insert_after) { T_DATUM_STRATUM_DUPE_ITEM *i; i = &dupes->ptr[dupes->current_items]; if (!i) { DLOG_FATAL("Could not add entry to dupe table!"); panic_from_thread(__LINE__); return NULL; } i->nonce_high = nonce&0xFFFF; i->nonce_low = (nonce>>16) & 0xFFFF; i->job_index = job_index; i->ntime = ntime_val; i->version_bits = version_bits; i->extra_nonce_a = *((uint64_t *)&extranonce_bin[0]); i->extra_nonce_b = *((uint32_t *)&extranonce_bin[8]); if (!insert_after) { // is a new entry i->next = NULL; } else { i->next = insert_after->next; insert_after->next = i; } dupes->current_items++; if (dupes->current_items >= dupes->max_items) { datum_stratum_dupes_cleanup(dupes, false); } return i; } bool datum_stratum_check_for_dupe(T_DATUM_STRATUM_THREADPOOL_DATA *t, unsigned int nonce, unsigned short job_index, unsigned int ntime_val, unsigned int version_bits, unsigned char *extranonce_bin) { // check if a share is a dupe // if so, say so // if not, add to the T_DATUM_STRATUM_DUPES *dupes; unsigned short nonce_high = nonce&0xFFFF; unsigned short nonce_low; T_DATUM_STRATUM_DUPE_ITEM *i, *p = NULL; if (!t) { DLOG_FATAL("Threadpool data not available?!"); panic_from_thread(__LINE__); return true; } dupes = t->dupes; if (dupes->index[nonce_high] == NULL) { // first nonce of its kind! // not a duplicate // add the new first entry! dupes->index[nonce_high] = datum_stratum_add_new_dupe(dupes, nonce, job_index, ntime_val, version_bits, extranonce_bin, NULL); return false; } // ok, there's an entry. go through the list i = dupes->index[nonce_high]; nonce_low = (nonce>>16) & 0xFFFF; do { if (i->nonce_low > nonce_low) { // we've reached a nonce higher than ours, so we can't be a dupe // we need to keep the list in order, so we need to insert ourselves before this entry (so, the previous entry) if (p) { datum_stratum_add_new_dupe(dupes, nonce, job_index, ntime_val, version_bits, extranonce_bin, p); } else { // we need to replace the first item in a list, so... let's make a new entry p = datum_stratum_add_new_dupe(dupes, nonce, job_index, ntime_val, version_bits, extranonce_bin, NULL); dupes->index[nonce_high] = p; p->next = i; } //LOG_PRINTF("DEBUG: Not dupe, nonce_low higher --- %d > %d", i->nonce_low, nonce_low); return false; } // there can be more than one nonce that's equal, so can't just assume until we pass it or if (i->nonce_low == nonce_low) { // same nonce as us, so need to do the slow checks if (job_index == i->job_index) { // same job index... if (ntime_val == i->ntime) { // same ntime....! if (version_bits == i->version_bits) { // same version bits?!?!?!? if (i->extra_nonce_a == *((uint64_t *)&extranonce_bin[0])) { // same extra nonce 1?!?!?!??! if (i->extra_nonce_b == *((uint32_t *)&extranonce_bin[8])) { // ok, this is a duplicate :( return true; } } } } } } // store the current ptr for the next loop p = i; // setup i to be the next link // if it's the end of the chain, this will be NULL and the loop will break i = i->next; } while (i); // we reached the end of the list, and haven't found a dupe // means that all of the nonces in the list are lower than us, or the last nonce is equal but doesn't match us // so we should be safe to insert ourselves on to the end of the list and return datum_stratum_add_new_dupe(dupes, nonce, job_index, ntime_val, version_bits, extranonce_bin, p); return false; } #if 0 void datum_stratum_dupes_codetest(void) { int i; uint64_t t; bool r; unsigned char en[12] = { 0 }; int stratum_job_next = 0; T_DATUM_STRATUM_JOB *stratum_job_list; unsigned int nonce; // make a fake thread pool T_DATUM_STRATUM_THREADPOOL_DATA tp; datum_stratum_dupes_init(&tp); T_DATUM_STRATUM_DUPES *dupes = tp.dupes; stratum_job_list = calloc(MAX_STRATUM_JOBS,sizeof(T_DATUM_STRATUM_JOB)); t = current_time_millis() - (datum_config.stratum_v1_share_stale_seconds*2000); // fill stratum jobs with garbage tsms for(i=0;itsms = t; t+=1000000000; } for(i=0;i<(datum_config.stratum_v1_max_clients_per_thread * datum_config.stratum_v1_vardiff_target_shares_min * (datum_config.stratum_v1_share_stale_seconds/60) * 16)*80;i++) { en[0]=i%256; en[7]=en[0]^0xAA; nonce = ((i&0xFFFF)<<16)|(((i>>2)&0xFFFF)^0xFFFF); r = datum_stratum_check_for_dupe(&tp, nonce, (i^0x69) % MAX_STRATUM_JOBS, t/1000, 0x20000000 | i, &en[0]); r = datum_stratum_check_for_dupe(&tp, nonce, (i^0x69) % MAX_STRATUM_JOBS, t/1000, 0x20000000 | i, &en[0]); if (!r) { DLOG_DEBUG("MISSED A DUPE 1 - %d - %8.8x - %d / %4.4x",i, nonce , nonce & 0xFFFF, nonce & 0xFFFF); } r = datum_stratum_check_for_dupe(&tp, nonce, (i^0x69) % MAX_STRATUM_JOBS, t/1000, 0x20000000 | i, &en[0]); if (!r) { DLOG_DEBUG("MISSED A DUPE 2 - %d - %8.8x - %d / %4.4x",i, nonce , nonce & 0xFFFF, nonce & 0xFFFF); } } r = datum_stratum_check_for_dupe(&tp, 0xdeadc0de, 12, t, 0x20000001, &en[0]); DLOG_DEBUG("B %d %d %d",r?1:0, dupes->current_items, dupes->max_items); uint64_t starttsms, endtsms; starttsms = current_time_millis(); for(i=0;i<(datum_config.stratum_v1_max_clients_per_thread * datum_config.stratum_v1_vardiff_target_shares_min * (datum_config.stratum_v1_share_stale_seconds/60) * 16)*8;i++) { en[0]=i%256; en[7]=en[0]^0xAA; nonce = ((i&0xFFFF)<<16)|(((i>>2)&0xFFFF)^0xFFFF); r = datum_stratum_check_for_dupe(&tp, nonce, (i^0x69) % MAX_STRATUM_JOBS, t/1000, 0x20000000 | i, &en[0]); if (!r) { DLOG_DEBUG("MISSED A DUPE 3 - %d - %8.8x - %d / %4.4x",i, nonce , nonce & 0xFFFF, nonce & 0xFFFF); } } endtsms = current_time_millis(); DLOG_DEBUG("%d dupe checks took %"PRIu64" miliseconds", (datum_config.stratum_v1_max_clients_per_thread * datum_config.stratum_v1_vardiff_target_shares_min * (datum_config.stratum_v1_share_stale_seconds/60) * 16)*80, endtsms-starttsms); free(stratum_job_list); } #endif datum_gateway-0.4.1beta/src/datum_stratum_dupes.h000066400000000000000000000054171512710151500222410ustar00rootroot00000000000000/* * * DATUM Gateway * Decentralized Alternative Templates for Universal Mining * * This file is part of OCEAN's Bitcoin mining decentralization * project, DATUM. * * https://ocean.xyz * * --- * * Copyright (c) 2024-2025 Bitcoin Ocean, LLC & Jason Hughes * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ #ifndef _DATUM_STRATUM_DUPES_H_ #define _DATUM_STRATUM_DUPES_H_ #ifndef uint64_t #include #endif #ifndef bool #include #endif typedef struct T_DATUM_STRATUM_DUPE_ITEM { // things to compare against, in order, to check our list // in most cases, we shouldn't get beyond nonce_low, but we need the rest for completeness unsigned short nonce_high; unsigned short nonce_low; unsigned short job_index; unsigned int ntime; unsigned int version_bits; uint64_t extra_nonce_a; // extranonce1 + first 32 bits of extranonce2 uint32_t extra_nonce_b; // last 32 bits of extranonce2 struct T_DATUM_STRATUM_DUPE_ITEM *next; } T_DATUM_STRATUM_DUPE_ITEM; typedef struct T_DATUM_STRATUM_DUPES { // high bits of nonce = index T_DATUM_STRATUM_DUPE_ITEM *index[65536]; // memory - we target 8 shares per minute per connection. // suggested items: datum_config.stratum_v1_max_clients_per_thread * datum_config.stratum_v1_vardiff_target_shares_min * (datum_config.stratum_v1_share_stale_seconds/60) * 16 T_DATUM_STRATUM_DUPE_ITEM *ptr; int max_items; int current_items; } T_DATUM_STRATUM_DUPES; void datum_stratum_dupes_init(void *vsdata); #include "datum_stratum.h" bool datum_stratum_check_for_dupe(T_DATUM_STRATUM_THREADPOOL_DATA *t, unsigned int nonce, unsigned short job_index, unsigned int ntime_val, unsigned int bver, unsigned char *extranonce_bin); void datum_stratum_dupes_cleanup(T_DATUM_STRATUM_DUPES *dupes, bool full_wipe); #endif datum_gateway-0.4.1beta/src/datum_stratum_tests.c000066400000000000000000000161451512710151500222560ustar00rootroot00000000000000/* * * DATUM Gateway * Decentralized Alternative Templates for Universal Mining * * This file is part of OCEAN's Bitcoin mining decentralization * project, DATUM. * * https://ocean.xyz * * --- * * Copyright (c) 2025 Bitcoin Ocean, LLC & Luke Dashjr * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ #include #include #include "datum_jsonrpc.h" #include "datum_stratum.h" #include "datum_utils.h" void datum_stratum_mod_username_tests() { const char * const s_umods = "{\"x\":{\"addrA\": 0.3}, \"abc\":{\"addrB\":0.3,\"addrC\":0.3},\":)\":{\"\":0.5}}"; json_error_t err; json_t * const j_umods = JSON_LOADS(s_umods, &err); assert(j_umods); struct datum_username_mod *umods = NULL; int ret = datum_config_parse_username_mods(&umods, j_umods, false); assert(ret == 1); json_decref(j_umods); datum_config.stratum_username_mod = umods; char buf[0x100]; char * const pool_addr = datum_config.mining_pool_address; char *s, *modname; const char *res, *a1, *a2; strcpy(pool_addr, "dummy"); s = "def~G"; modname = &s[4]; datum_test(datum_stratum_mod_username(s, buf, sizeof(buf), 0, modname, 1) == s); s = "def~x"; modname = &s[4]; res = datum_stratum_mod_username(s, buf, sizeof(buf), 0, modname, 1); datum_test(0 == strcmp(res, "addrA")); memset(buf, 0, 5); res = datum_stratum_mod_username(s, buf, sizeof(buf), 0x4ccc, modname, 1); datum_test(0 == strcmp(res, "addrA")); datum_test(datum_stratum_mod_username(s, buf, sizeof(buf), 0x4ccd, modname, 1) == pool_addr); datum_test(datum_stratum_mod_username(s, buf, sizeof(buf), 0xffff, modname, 1) == pool_addr); s = "def~abc"; modname = &s[4]; res = datum_stratum_mod_username(s, buf, sizeof(buf), 0, modname, 3); if (0 == strcmp(res, "addrB")) { // jansson doesn't order keys' a1 = "addrB"; a2 = "addrC"; } else { a1 = "addrC"; a2 = "addrB"; } datum_test(0 == strcmp(res, a1)); memset(buf, 0, 5); res = datum_stratum_mod_username(s, buf, sizeof(buf), 0x4ccc, modname, 3); datum_test(0 == strcmp(res, a1)); memset(buf, 0, 5); res = datum_stratum_mod_username(s, buf, sizeof(buf), 0x4ccd, modname, 3); datum_test(0 == strcmp(res, a2)); memset(buf, 0, 5); res = datum_stratum_mod_username(s, buf, sizeof(buf), 0x9999, modname, 3); datum_test(0 == strcmp(res, a2)); datum_test(datum_stratum_mod_username(s, buf, sizeof(buf), 0x999a, modname, 3) == pool_addr); datum_test(datum_stratum_mod_username(s, buf, sizeof(buf), 0xffff, modname, 3) == pool_addr); s = "def.ghi~abc"; modname = &s[8]; datum_test(datum_stratum_mod_username(s, buf, sizeof(buf), 0, modname, 3) == buf); datum_test(0 == strncmp(buf, a1, 5)); datum_test(0 == strcmp(&buf[5], ".ghi")); memset(buf, 0, 8); datum_test(datum_stratum_mod_username(s, buf, sizeof(buf), 0x4ccc, modname, 3) == buf); datum_test(0 == strncmp(buf, a1, 5)); datum_test(0 == strcmp(&buf[5], ".ghi")); memset(buf, 0, 8); datum_test(datum_stratum_mod_username(s, buf, sizeof(buf), 0x4ccd, modname, 3) == buf); datum_test(0 == strncmp(buf, a2, 5)); datum_test(0 == strcmp(&buf[5], ".ghi")); memset(buf, 0, 8); datum_test(datum_stratum_mod_username(s, buf, sizeof(buf), 0x9999, modname, 3) == buf); datum_test(0 == strncmp(buf, a2, 5)); datum_test(0 == strcmp(&buf[5], ".ghi")); datum_test(datum_stratum_mod_username(s, buf, sizeof(buf), 0x999a, modname, 3) == pool_addr); datum_test(datum_stratum_mod_username(s, buf, sizeof(buf), 0xffff, modname, 3) == pool_addr); s = "def.ghi~:)"; modname = &s[8]; datum_test(datum_stratum_mod_username(s, buf, sizeof(buf), 0, modname, 2) == buf); datum_test(0 == strcmp(buf, "def.ghi")); memset(buf, 0, 7); datum_test(datum_stratum_mod_username(s, buf, sizeof(buf), 0x7fff, modname, 2) == buf); datum_test(0 == strcmp(buf, "def.ghi")); datum_test(datum_stratum_mod_username(s, buf, sizeof(buf), 0x8000, modname, 2) == pool_addr); datum_test(datum_stratum_mod_username(s, buf, sizeof(buf), 0xffff, modname, 2) == pool_addr); // Intentionally overflow buf with address: we lose the worker name, but get the full address via its umod buffer s = "def.ghi~x"; modname = &s[8]; memset(buf, 0x0e, 8); res = datum_stratum_mod_username(s, buf, 2, 0, modname, 1); datum_test(res != buf); datum_test(res != pool_addr); datum_test(buf[2] == 0x0e); datum_test(0 == strcmp(res, "addrA")); res = datum_stratum_mod_username(s, buf, 2, 0x4ccc, modname, 1); datum_test(0 == strcmp(res, "addrA")); datum_test(datum_stratum_mod_username(s, buf, 2, 0x4ccd, modname, 1) == pool_addr); datum_test(datum_stratum_mod_username(s, buf, 2, 0xffff, modname, 1) == pool_addr); datum_test(buf[2] == 0x0e); datum_test(buf[6] == 0x0e); res = datum_stratum_mod_username(s, buf, 6, 0, modname, 1); datum_test(res == buf); datum_test(res != pool_addr); datum_test(buf[6] == 0x0e); datum_test(0 == strcmp(res, "addrA")); memset(buf, 0x0e, 9); datum_test(datum_stratum_mod_username(s, buf, 7, 0, modname, 1) == buf); datum_test(buf[8] == 0x0e); datum_test(0 == strcmp(res, "addrA.")); memset(buf, 0x0e, 10); datum_test(datum_stratum_mod_username(s, buf, 8, 0, modname, 1) == buf); datum_test(buf[9] == 0x0e); datum_test(0 == strcmp(res, "addrA.g")); memset(buf, 0x0e, 11); datum_test(datum_stratum_mod_username(s, buf, 9, 0, modname, 1) == buf); datum_test(buf[10] == 0x0e); datum_test(0 == strcmp(res, "addrA.gh")); memset(buf, 0x0e, 12); datum_test(datum_stratum_mod_username(s, buf, 10, 0, modname, 1) == buf); datum_test(buf[11] == 0x0e); datum_test(0 == strcmp(res, "addrA.ghi")); s = "def.ghi~:)"; modname = &s[8]; memset(buf, 0x0e, 9); datum_test(datum_stratum_mod_username(s, buf, 2, 0, modname, 2) == buf); datum_test(buf[2] == 0x0e); datum_test(0 == strcmp(res, "d")); datum_test(datum_stratum_mod_username(s, buf, 6, 0, modname, 2) == buf); datum_test(buf[6] == 0x0e); datum_test(0 == strcmp(res, "def.g")); datum_test(datum_stratum_mod_username(s, buf, 7, 0, modname, 2) == buf); datum_test(buf[7] == 0x0e); datum_test(0 == strcmp(res, "def.gh")); datum_test(datum_stratum_mod_username(s, buf, 8, 0, modname, 2) == buf); datum_test(buf[8] == 0x0e); datum_test(0 == strcmp(res, "def.ghi")); } void datum_stratum_tests(void) { datum_stratum_mod_username_tests(); } datum_gateway-0.4.1beta/src/datum_submitblock.c000066400000000000000000000120411512710151500216420ustar00rootroot00000000000000/* * * DATUM Gateway * Decentralized Alternative Templates for Universal Mining * * This file is part of OCEAN's Bitcoin mining decentralization * project, DATUM. * * https://ocean.xyz * * --- * * Copyright (c) 2024-2025 Bitcoin Ocean, LLC & Jason Hughes * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ #include #include #include #include #include #include "datum_utils.h" #include "datum_conf.h" #include "datum_jsonrpc.h" pthread_mutex_t submitblock_mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t submitblock_cond = PTHREAD_COND_INITIALIZER; int submit_block_triggered = 0; const char *submitblock_ptr = NULL; char submitblock_hash[256] = { 0 }; void preciousblock(CURL *curl, char *blockhash) { json_t *json; char rpc_data[384]; snprintf(rpc_data, sizeof(rpc_data), "{\"method\":\"preciousblock\",\"params\":[\"%s\"],\"id\":1}", blockhash); json = bitcoind_json_rpc_call(curl, &datum_config, rpc_data); if (!json) return; json_decref(json); return; } void datum_submitblock_doit(CURL *tcurl, char *url, const char *submitblock_req, const char *block_hash_hex) { json_t *r; char *s = NULL; // TODO: Move these types of things to the conf file if (!url) { r = bitcoind_json_rpc_call(tcurl, &datum_config, submitblock_req); } else { r = json_rpc_call(tcurl, url, NULL, submitblock_req); } if (!r) { // oddly, this means success here. DLOG_INFO("Block %s submitted to upstream node successfully!",block_hash_hex); } else { s = json_dumps(r, JSON_ENCODE_ANY); if (!s) { DLOG_WARN("Upstream node rejected our block! (unknown)"); } else { DLOG_WARN("Upstream node rejected our block! (%s)",s); free(s); } json_decref(r); } // precious block! preciousblock(tcurl, submitblock_hash); } void *datum_submitblock_thread(void *ptr) { CURL *tcurl = NULL; int i; tcurl = curl_easy_init(); if (!tcurl) { DLOG_FATAL("Could not initialize cURL for submitblock!!! This is REALLY REALLY BAD. Like accidentally calling your wife your ex-girlfriend's name bad."); panic_from_thread(__LINE__); } DLOG_DEBUG("Submitblock thread active"); while (1) { // Lock the mutex before waiting on the condition variable pthread_mutex_lock(&submitblock_mutex); // Wait for the event to be triggered while (!submit_block_triggered) { pthread_cond_wait(&submitblock_cond, &submitblock_mutex); } if (submitblock_ptr != NULL) { DLOG_DEBUG("SUBMITTING BLOCK TO OUR NODE!"); datum_submitblock_doit(tcurl,NULL,submitblock_ptr,submitblock_hash); if (datum_config.extra_block_submissions_count > 0) { for(i=0;i #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "datum_gateway.h" #include "datum_logger.h" #include "datum_utils.h" #include "thirdparty_base58.h" #include "thirdparty_segwit_addr.h" #include "datum_logger.h" unsigned int datum_test_failed = 0; volatile int panic_mode = 0; static uint64_t process_start_time = 0; bool datum_test_fail_(const char *expr, const char *file, unsigned int line, const char *func) { fprintf(stderr, "ERROR: TEST FAILED at %s:%u (%s): %s\n", file, line, func, expr); fflush(stderr); ++datum_test_failed; return false; } void get_target_from_diff(unsigned char *result, uint64_t diff) { uint64_t dividend_parts[4] = {0, 0, 0, 0x00000000FFFF0000}; uint64_t remainder = 0; uint64_t quotient; memset(result, 0, 32); for (int i = 3; i >= 0; i--) { __uint128_t temp = remainder; temp = (temp << 64) | dividend_parts[i]; quotient = temp / diff; remainder = temp % diff; for (int j = 0; j < 8; j++) { result[(i<<3) + j] = (quotient >> (j<<3)) & 0xFF; } } } uint64_t get_process_uptime_seconds() { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (uint64_t)ts.tv_sec - process_start_time; } void datum_utils_init(void) { build_hex_lookup(); process_start_time = monotonic_time_seconds(); } #ifdef __GNUC__ // faster, less portable uint64_t roundDownToPowerOfTwo_64(uint64_t x) { return 1ULL << (63 - __builtin_clzll(x)); } unsigned char floorPoT(uint64_t x) { if (x == 0) { return 0; } return (63 - __builtin_clzll(x)); } #else // More portable but slower uint64_t roundDownToPowerOfTwo_64(uint64_t x) { x |= x >> 1; x |= x >> 2; x |= x >> 4; x |= x >> 8; x |= x >> 16; x |= x >> 32; return x - (x >> 1); } unsigned char floorPoT(uint64_t x) { if (x == 0) { return 0; } unsigned char pos = 0; while (x >>= 1) { pos++; } return pos; } #endif uint64_t monotonic_time_seconds(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); // SAFE from system time changes (e.g., NTP adjustments, manual clock changes) return (uint64_t)ts.tv_sec; } uint64_t current_time_millis(void) { struct timeval te; gettimeofday(&te, NULL); // get current time uint64_t milliseconds = te.tv_sec * 1000LL + te.tv_usec / 1000; // calculate milliseconds return milliseconds; } uint64_t current_time_micros(void) { struct timeval te; gettimeofday(&te, NULL); // get current time uint64_t microseconds = te.tv_sec * 1000000LL + te.tv_usec; // calculate microseconds return microseconds; } unsigned char hex_lookup[65536]; unsigned short uchar_hex_lookup[256]; unsigned char hex2bin_uchar(const char *in) { unsigned short i; memcpy(&i, in, sizeof(i)); return hex_lookup[i]; } void uchar_to_hex(char *s, const unsigned char b) { // place the ASCII hexidecimal value for the unsigned char into the string at ptr s // this does NOT null terminate the string! unsigned short i = uchar_hex_lookup[b]; memcpy(s, &i, sizeof(i)); } void build_hex_lookup(void) { unsigned int i; char a[3]; unsigned short b; for(i = 0; i < 65535; ++i) hex_lookup[i] = 0; hex_lookup[65535] = 0; for(i=0;i<256;i++) { sprintf(a,"%2.2X",i); memcpy(&b, a, sizeof(b)); hex_lookup[b] = i; sprintf(a,"%2.2x",i); memcpy(&b, a, sizeof(b)); hex_lookup[b] = i; uchar_hex_lookup[i] = b; } } bool my_sha256(void *digest, const void *buffer, size_t length) { crypto_hash_sha256(digest, buffer, length); return 1; } bool double_sha256(void *out, const void *in, size_t length) { unsigned char dg1[32]; my_sha256(dg1, in, length); my_sha256(out, dg1, 32); return 1; } long double get_approx_achieved_diff(const unsigned char *bytes) { if (bytes == NULL) { // Handle null pointer return 0.0L; } // Check if all bytes are zero int allZero = 1; for (int i = 0; i < 32; i++) { if (bytes[i] != 0) { allZero = 0; break; } } if (allZero) { return 0.0L; } // bdiff 1 unsigned char dividendBytes[32] = { // Least significant byte at index 0 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00 // Most significant byte at index 31 }; long double dividend = 0.0L; for (int i = 0; i < 32; i++) { dividend *= 256.0L; // Shift left by 8 bits dividend += dividendBytes[31 - i]; // Add the byte } long double divisor = 0.0L; for (int i = 0; i < 32; i++) { divisor *= 256.0L; // Shift left by 8 bits divisor += bytes[31 - i]; // Add the byte } return dividend / divisor; } void nbits_to_target(uint32_t nbits, uint8_t *target) { uint32_t exponent = (nbits >> 24) & 0xff; uint32_t mantissa = nbits & 0xffffff; int i; memset(target, 0, 32); if (exponent <= 3) { mantissa >>= 8 * (3 - exponent); for (i = 0; i < 3; i++) { target[i] = (mantissa >> (8 * i)) & 0xff; } } else { for (i = 0; i < 3; i++) { target[i + exponent - 3] = (mantissa >> (8 * i)) & 0xff; } } } int compare_hashes(const uint8_t *share_hash, const uint8_t *target) { for (int i = 31; i >= 0; i--) { if (share_hash[i] < target[i]) return -1; // share_hash is smaller, valid block/share if (share_hash[i] > target[i]) return 1; // share_hash is larger, not valid } return 0; // hashes are equal } unsigned long long block_reward(unsigned int block_height) { unsigned long long reward = 5000000000; unsigned int halvings = block_height / 210000; if (halvings >= 64) return 0; reward >>= halvings; return reward; } int get_bitcoin_varint_len_bytes(uint64_t n) { if (n < 0xFD) { return 1; } else if (n <= 0xFFFF) { return 3; } else if (n <= 0xFFFFFFFF) { return 5; } else { return 9; } } int append_bitcoin_varint_hex(uint64_t n, char *s) { if (n < 0xFD) { // Single byte is sufficient uchar_to_hex(s, n); s[2] = 0; return 2; } else if (n <= 0xFFFF) { // Use 0xFD followed by the number, little-endian order sprintf(s, "fd%04" PRIx16, __builtin_bswap16((uint16_t)n)); return 6; } else if (n <= 0xFFFFFFFF) { // Use 0xFE followed by the number, little-endian order sprintf(s, "fe%08" PRIx32, __builtin_bswap32((uint32_t)n)); return 10; } else { // Use 0xFF followed by the number, little-endian order sprintf(s, "ff%016" PRIx64, __builtin_bswap64(n)); return 18; } } int append_UNum_hex(uint64_t n, char *s) { int count = 0; uint64_t temp = n; bool last_msb = false; do { count++; temp >>= 8; } while (temp != 0); int len = 2; uchar_to_hex(s, count); for (int i = 0; i < count; i++) { uchar_to_hex(s+len, (uint8_t)(n & 0xFF)); last_msb = (n >= 0x80); n >>= 8; len += 2; } // if the last byte is >= 0x80, then we need to inject a zero at the end if (last_msb) { count++; uchar_to_hex(s, count); uchar_to_hex(s+len, 0); len+=2; } s[len] = '\0'; return len; } void hex_to_bin_le(const char *hex, unsigned char *bin) { size_t len = strlen(hex); for (size_t i = 0; i < len>>1; i++) { bin[i] = hex2bin_uchar(&hex[len - ((i+1)<<1)]); } } void hex_to_bin(const char *hex, unsigned char *bin) { size_t len = strlen(hex); for (size_t i = 0; i < len>>1; i++) { bin[i] = hex2bin_uchar(&hex[(i<<1)]); } } void panic_from_thread(int a) { // set panic flag panic_mode = 1; printf("PANIC TRIGGERED - %d\n",a); fflush(stdout); DLOG_FATAL("***********************"); DLOG_FATAL("*** PANIC TRIGGERED ***"); DLOG_FATAL("***********************"); // the main thread needs to pickup on this failure and exit as gracefully as possible. while(1) sleep(1); } void hash2hex(unsigned char *bytes, char *hexString) { const char hexDigits[] = "0123456789abcdef"; for (int i = 0; i < 32; ++i) { hexString[i * 2] = hexDigits[(bytes[i] >> 4) & 0x0F]; hexString[i * 2 + 1] = hexDigits[bytes[i] & 0x0F]; } hexString[64] = '\0'; } int addr_2_output_script(const char *addr, unsigned char *script, int max_len) { // takes any valid bitcoin address, and converts it to an output script // returns length of script written, or 0 on failure // NOTE: This is agnostic to testnet vs mainnet addresses! be careful with your networks! int i; size_t al; uint8_t witprog[80]; size_t witprog_len; int witver; const char* hrp = "bc"; al = strlen(addr); if (al < 16) return 0; if (((addr[0] == 'b') && (addr[1] == 'c')) || ((addr[0] == 't') && (addr[1] == 'b'))) { // bitcoin mainnet and testnet BIP 0173 if (addr[0] == 't') { hrp = "tb"; } i = segwit_addr_decode(&witver, witprog, &witprog_len, hrp, addr); if (!i) { return 0; } if (!(((witver == 0) && ((witprog_len == 20) || (witprog_len == 32))) || ((witver == 1) && (witprog_len == 32)))) { // enforcing length restrictions and known witness versions // TODO: Add any new witness version/len combos that are valid return 0; } if (max_len < witprog_len+2) { return 0; } script[0] = (uint8_t)(witver ? (witver + 0x50) : 0); script[1] = (uint8_t)witprog_len; memcpy(script + 2, witprog, witprog_len); return witprog_len + 2; } else { // try P2PKH or P2SH const size_t sz = blkmk_address_to_script(script, max_len, addr); if (sz > INT_MAX) return 0; return sz; } // nothing worked? return 0; } int output_script_2_addr(const unsigned char *script, const int len, char *addr) { int i; int version = 0; int programLen; const unsigned char *ptr; addr[0] = 0; unsigned char temp[32]; unsigned char digest[32]; size_t sz; if (!len) { return 0; } if (script[0] == 0x6A) { i = sprintf(addr, "OP_RETURN"); return i; } if ((script[0] == 0xA9) || (script[0] == 0x76)) { // P2SH / P2PKH if (script[0] == 0xA9) { version = 5; ptr = &script[2]; if (len != 23) return 0; } else { version = 0; ptr = &script[3]; if (len != 25) return 0; } temp[0] = version; memcpy(&temp[1], ptr, 20); double_sha256(digest, temp, 21); memcpy(&temp[21], digest, 4); sz = 255; if (!b58enc(addr, &sz, temp, 25)) { i = sprintf(addr, "UNKNOWN"); return i; } return sz; } if ((script[0] == 0) || (script[0] == 0x51)) { // segwit/taproot if (len < 4 || len > 42) { i = sprintf(addr, "UNKNOWN"); return i; } if (script[0] != 0x00 && (script[0] < 0x51 || script[0] > 0x60)) { i = sprintf(addr, "UNKNOWN"); return i; } if (script[1] < 0x02 || script[1] > 0x28) { i = sprintf(addr, "UNKNOWN"); return i; } version = (script[0] == 0x00) ? 0 : script[0] - 0x50; programLen = script[1]; if (segwit_addr_encode(addr, "bc", version, &script[2], programLen) != 1) { i = sprintf(addr, "UNKNOWN"); return i; } return strlen(addr); } i = sprintf(addr, "UNKNOWN"); return i; } static const unsigned char b64d[] = { 66,66,66,66,66,66,66,66,66,66,64,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,62,66,66,66,63,52,53,54,55,56,57,58,59,60,61,66,66,66,65,66,66,66, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,66,66,66,66,66,66,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66, 66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66, 66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66 }; int base64_decode(const char *in_c, size_t inLen, unsigned char *out, size_t *outLen) { const unsigned char *in = (const unsigned char *)in_c; const unsigned char * const end = in + inLen; char iter = 0; uint32_t buf = 0; size_t len = 0; while (in < end) { unsigned char c = b64d[*in++]; switch (c) { case 64: continue; case 66: return 1; case 65: in = end; continue; default: buf = buf << 6 | c; iter++; if (iter == 4) { if ((len += 3) > *outLen) return 2; *(out++) = (buf >> 16) & 255; *(out++) = (buf >> 8) & 255; *(out++) = buf & 255; buf = 0; iter = 0; } } } if (iter == 3) { if ((len += 2) > *outLen) return 3; *(out++) = (buf >> 10) & 255; *(out++) = (buf >> 2) & 255; } else if (iter == 2) { if (++len > *outLen) return 4; *(out++) = (buf >> 4) & 255; } *outLen = len; return 0; } bool strncpy_workerchars(char *out, const char *in, size_t maxlen) { // copy a string from in to out, stripping out unwanted characters // copy a max of maxlen chars from in to out // in could technically be longer than maxlen if it has a bunch of unwanted chars int i=0; int j=0; char c; if (in == NULL || out == NULL || maxlen == 0) { return false; } out[0] = 0; while((in[i] != 0) && (maxlen > 1)) { c = in[i]; if ((isalnum(c)) || (c == '.') || (c == '-') || (c == '_') || (c == '=') || (c == '@') || (c == ',')) { out[j] = c; j++; maxlen--; } i++; } out[j] = 0; return true; } bool strncpy_uachars(char *out, const char *in, size_t maxlen) { // copy a string from in to out, stripping out unwanted characters // copy a max of maxlen chars from in to out // in could technically be longer than maxlen if it has a bunch of unwanted chars int i=0; int j=0; char c; if (in == NULL || out == NULL || maxlen == 0) { return false; } out[0] = 0; while((in[i] != 0) && (maxlen > 1)) { c = in[i]; if ((isalnum(c)) || (c == '.') || (c == '-') || (c == '_') || (c == '=') || (c == '@') || (c == ',') || (c == ' ') || (c == '|') || (c == '/') || (c == '|') || (c == ':') || (c == '<') || (c == '>') || (c == '\'') || (c == ';')) { out[j] = c; j++; maxlen--; } i++; } out[j] = 0; return true; } long double calc_network_difficulty(const char *bits_hex) { // given a share solution in hex, calculate the network difficulty // Postgres code for this (with hex_to_int added function) // (pow(10, ( (29-tpower_val)*2.4082399653118495617099111577959 ) + log( (65535 / tvalue_val) ) ) ) as network_difficulty char tpower[3]; char tvalue[7]; unsigned char tpower_val; unsigned long tvalue_val; char *ep; int i; long double d; signed short s; tpower[0] = bits_hex[0]; tpower[1] = bits_hex[1]; tpower[2] = 0; for(i=0;i<6;i++) tvalue[i] = bits_hex[2+i]; tvalue[6] = 0; tpower_val = (unsigned char)strtoul(tpower, &ep, 16); tvalue_val = strtoul(tvalue, &ep, 16); s = (signed short)29 - (signed short)tpower_val; d = powl(10.0,(double)s*(long double)2.4082399653118495617099111577959 + log10(65535.0 / (double)tvalue_val)); return d; } #define SIPHASH_ROTATE(a, b) ((uint64_t)(((a)<<(b))|((a)>>(64-(b))))) #define SIPHASH_HALF_ROUND(a,b,c,d,e,f) do { \ a += b; \ c += d; \ b = SIPHASH_ROTATE(b, e) ^ a; \ d = SIPHASH_ROTATE(d, f) ^ c; \ a = SIPHASH_ROTATE(a, 32); \ } while(0) #define SIPHASH_DOUBLE_ROUND(v0,v1,v2,v3) do { \ SIPHASH_HALF_ROUND(v0,v1,v2,v3,13,16); \ SIPHASH_HALF_ROUND(v2,v1,v0,v3,17,21); \ SIPHASH_HALF_ROUND(v0,v1,v2,v3,13,16); \ SIPHASH_HALF_ROUND(v2,v1,v0,v3,17,21); \ } while(0) uint64_t datum_siphash(const void *src, uint64_t sz, const unsigned char key[16]) { uint64_t k0 = (*(const uint64_t *)&key[0]); uint64_t k1 = (*(const uint64_t *)&key[8]); uint64_t b = sz<<56; const uint64_t *in = src; uint64_t v0 = k0 ^ 0x736f6d6570736575ULL; uint64_t v1 = k1 ^ 0x646f72616e646f6dULL; uint64_t v2 = k0 ^ 0x6c7967656e657261ULL; uint64_t v3 = k1 ^ 0x7465646279746573ULL; uint64_t mi,t; uint8_t *pt; const uint8_t *m; while (sz >= 8) { mi = *in; in += 1; sz -= 8; v3 ^= mi; SIPHASH_DOUBLE_ROUND(v0,v1,v2,v3); v0 ^= mi; } t = 0; pt = (uint8_t *)&t; m = (const uint8_t *)in; switch (sz) { case 7: pt[6] = m[6]; [[fallthrough]]; case 6: pt[5] = m[5]; [[fallthrough]]; case 5: pt[4] = m[4]; [[fallthrough]]; case 4: pt[3] = m[3]; [[fallthrough]]; case 3: pt[2] = m[2]; [[fallthrough]]; case 2: pt[1] = m[1]; [[fallthrough]]; case 1: pt[0] = m[0]; [[fallthrough]]; default: break; } b |= t; v3 ^= b; SIPHASH_DOUBLE_ROUND(v0,v1,v2,v3); v0 ^= b; v2 ^= 0xff; SIPHASH_DOUBLE_ROUND(v0,v1,v2,v3); SIPHASH_DOUBLE_ROUND(v0,v1,v2,v3); return (v0 ^ v1) ^ (v2 ^ v3); } uint64_t datum_siphash_mod8(const void *src, uint64_t sz, const unsigned char key[16]) { // use if size is guaranteed to be mod 8 = 0 // should save a few cycles uint64_t k0 = (*(const uint64_t *)&key[0]); uint64_t k1 = (*(const uint64_t *)&key[8]); uint64_t b = sz<<56; const uint64_t *in = src; uint64_t v0 = k0 ^ 0x736f6d6570736575ULL; uint64_t v1 = k1 ^ 0x646f72616e646f6dULL; uint64_t v2 = k0 ^ 0x6c7967656e657261ULL; uint64_t v3 = k1 ^ 0x7465646279746573ULL; uint64_t mi; while (sz >= 8) { mi = *in; in += 1; sz -= 8; v3 ^= mi; SIPHASH_DOUBLE_ROUND(v0,v1,v2,v3); v0 ^= mi; } v3 ^= b; SIPHASH_DOUBLE_ROUND(v0,v1,v2,v3); v0 ^= b; v2 ^= 0xff; SIPHASH_DOUBLE_ROUND(v0,v1,v2,v3); SIPHASH_DOUBLE_ROUND(v0,v1,v2,v3); return (v0 ^ v1) ^ (v2 ^ v3); } unsigned int datum_double_precision(double *inout_dbl) { if (*inout_dbl >= 0.9) { *inout_dbl = ceil(*inout_dbl); return 0; } else if (*inout_dbl >= 0.09) { *inout_dbl = ceil(*inout_dbl * 10) / 10; return 1; } else if (*inout_dbl >= 0.009) { *inout_dbl = ceil(*inout_dbl * 100) / 100; return 2; } else { *inout_dbl = ceil(*inout_dbl * 1000) / 1000; return 3; } } // Uses a fixed-size buffer; positive only; digits only // Returns UINT64_MAX on failure uint64_t datum_atoi_strict_u64(const char * const s, const size_t size) { if (!size) return UINT64_MAX; assert(s); uint64_t ret = 0; for (size_t i = 0; i < size; ++i) { if (s[i] < '0' || s[i] > '9') return UINT64_MAX; int digit = s[i] - '0'; if (ret > (UINT64_MAX - digit) / 10) return UINT64_MAX; ret = (ret * 10) + digit; } return ret; } // Uses a fixed-size buffer; positive only; digits only // Returns -1 on failure int datum_atoi_strict(const char * const s, const size_t size) { const uint64_t ret = datum_atoi_strict_u64(s, size); return (ret == UINT64_MAX || ret > INT_MAX) ? -1 : ret; } // Currently accepts 0 and 1 only, but may add more later // Returns true if valid, actual value in *out bool datum_str_to_bool_strict(const char * const s, bool * const out) { if (0 == strcmp(s, "0")) { *out = false; return true; } else if (0 == strcmp(s, "1")) { *out = true; return true; } return false; } char **datum_deepcopy_charpp(const char * const * const p) { size_t sz = sizeof(char*), n = 0; for (const char * const *p2 = p; *p2; ++p2) { sz += sizeof(char*) + strlen(*p2) + 1; ++n; } char **out = malloc(sz); char *p3 = (void*)(&out[n + 1]); out[n] = NULL; for (size_t i = 0; i < n; ++i) { const size_t item_sz = strlen(p[i]) + 1; memcpy(p3, p[i], item_sz); out[i] = p3; p3 += item_sz; } assert(p3 - (char*)out == sz); return out; } void datum_reexec() { // FIXME: kill other threads (except logging?) before closing fds DIR * const D = opendir("/proc/self/fd"); if (D) { for (struct dirent *ent; (ent = readdir(D)) != NULL; ) { const int fd = datum_atoi_strict(ent->d_name, strlen(ent->d_name)); if (fd < 3) continue; fcntl(fd, F_SETFD, FD_CLOEXEC); } closedir(D); } else { DLOG_ERROR("%s: Failed to close files, this could cause issues! (Is /proc mounted?)", __func__); } execv((void*)datum_argv[0], (void*)datum_argv); // execv shouldn't return! DLOG_FATAL("Failed to restart! We're too deep in to recover!"); abort(); } bool datum_secure_strequals(const char *secret, size_t secret_len, const char *guess) { if (!guess) return false; const size_t guess_len = strlen(guess); size_t acc = secret_len ^ guess_len; if (!secret_len) { secret = ""; // null byte avoids dereferencing out of bounds secret_len = 1; } for (size_t i = 0; i < guess_len; ++i) { acc |= ((size_t)guess[i]) ^ ((size_t)secret[i % secret_len]); } return !acc; } const char *dynamic_hash_unit(double * const inout_hashrate){ if (*inout_hashrate >= 1000000.0) { *inout_hashrate /= 1000000.0; return "Eh/s"; } else if (*inout_hashrate >= 1000.0) { *inout_hashrate /= 1000.0; return "Ph/s"; } else { return "Th/s"; } } datum_gateway-0.4.1beta/src/datum_utils.h000066400000000000000000000144011512710151500204730ustar00rootroot00000000000000/* * * DATUM Gateway * Decentralized Alternative Templates for Universal Mining * * This file is part of OCEAN's Bitcoin mining decentralization * project, DATUM. * * https://ocean.xyz * * --- * * Copyright (c) 2024-2025 Bitcoin Ocean, LLC & Jason Hughes * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ #ifndef _DATUM_UTILS_H_ #define _DATUM_UTILS_H_ #include #include #include #include "datum_logger.h" void datum_utils_init(void); extern unsigned int datum_test_failed; bool datum_test_fail_(const char *expr, const char *file, unsigned int line, const char *func); #define datum_test_(expr, fake_expr, line, fake_func) \ ((expr) ? true : datum_test_fail_(fake_expr, __FILE__, line, fake_func)) #define datum_test(expr) \ datum_test_(expr, #expr, __LINE__, __func__) uint64_t monotonic_time_seconds(void); uint64_t current_time_millis(void); uint64_t current_time_micros(void); uint64_t get_process_uptime_seconds(void); unsigned char hex2bin_uchar(const char *in); void build_hex_lookup(void); bool my_sha256(void *digest, const void *buffer, size_t length); void nbits_to_target(uint32_t nbits, uint8_t *target); int compare_hashes(const uint8_t *hash1, const uint8_t *hash2); unsigned long long block_reward(unsigned int block_height); int append_bitcoin_varint_hex(uint64_t n, char *s); int append_UNum_hex(uint64_t n, char *s); void panic_from_thread(int a); bool double_sha256(void *out, const void *in, size_t length); void hex_to_bin_le(const char *hex, unsigned char *bin); void hex_to_bin(const char *hex, unsigned char *bin); void hash2hex(unsigned char *bytes, char *hexString); void get_target_from_diff(unsigned char *result, uint64_t diff); uint64_t roundDownToPowerOfTwo_64(uint64_t x); int addr_2_output_script(const char *addr, unsigned char *script, int max_len); int output_script_2_addr(const unsigned char *script, const int len, char *addr); int base64_decode(const char *in, size_t inLen, unsigned char *out, size_t *outLen); void uchar_to_hex(char *s, const unsigned char b); int get_bitcoin_varint_len_bytes(uint64_t n); bool strncpy_uachars(char *out, const char *in, size_t maxlen); bool strncpy_workerchars(char *out, const char *in, size_t maxlen); long double calc_network_difficulty(const char *bits_hex); unsigned char floorPoT(uint64_t x); uint64_t datum_siphash(const void *src, uint64_t sz, const unsigned char key[16]); uint64_t datum_siphash_mod8(const void *src, uint64_t sz, const unsigned char key[16]); unsigned int datum_double_precision(double *inout_dbl); uint64_t datum_atoi_strict_u64(const char *s, size_t size); int datum_atoi_strict(const char *s, size_t size); bool datum_str_to_bool_strict(const char *s, bool *out); char **datum_deepcopy_charpp(const char * const *p); void datum_reexec(); bool datum_secure_strequals(const char *secret, const size_t secret_len, const char *guess); const char *dynamic_hash_unit(double *inout_hashrate); static inline size_t datum_align_sz(const size_t min_sz, const size_t alignment) { return (min_sz + alignment - 1) / alignment * alignment; } static inline uint8_t upk_u8(const void * const bufp, const int offset) { const uint8_t * const buf = bufp; return buf[offset]; } #define upk_u8le(buf, offset) upk_u8(buf, offset) static inline uint16_t upk_u16le(const void * const bufp, const int offset) { const uint8_t * const buf = bufp; return (((uint16_t)buf[offset+0]) << 0) | (((uint16_t)buf[offset+1]) << 8); } static inline uint32_t upk_u32le(const void * const bufp, const int offset) { const uint8_t * const buf = bufp; return (((uint32_t)buf[offset+0]) << 0) | (((uint32_t)buf[offset+1]) << 8) | (((uint32_t)buf[offset+2]) << 0x10) | (((uint32_t)buf[offset+3]) << 0x18); } static inline uint64_t upk_u64le(const void * const bufp, const int offset) { const uint8_t * const buf = bufp; return (((uint64_t)buf[offset+0]) << 0) | (((uint64_t)buf[offset+1]) << 8) | (((uint64_t)buf[offset+2]) << 0x10) | (((uint64_t)buf[offset+3]) << 0x18) | (((uint64_t)buf[offset+4]) << 0x20) | (((uint64_t)buf[offset+5]) << 0x28) | (((uint64_t)buf[offset+6]) << 0x30) | (((uint64_t)buf[offset+7]) << 0x38); } static inline void pk_u8(void * const bufp, const int offset, const uint8_t nv) { uint8_t * const buf = bufp; buf[offset] = nv; } #define pk_u8le(buf, offset, nv) pk_u8(buf, offset, nv) static inline void pk_u16le(void * const bufp, const int offset, const uint16_t nv) { uint8_t * const buf = bufp; buf[offset+0] = (nv >> 0) & 0xff; buf[offset+1] = (nv >> 8) & 0xff; } static inline void pk_u32le(void * const bufp, const int offset, const uint32_t nv) { uint8_t * const buf = bufp; buf[offset+0] = (nv >> 0) & 0xff; buf[offset+1] = (nv >> 8) & 0xff; buf[offset+2] = (nv >> 0x10) & 0xff; buf[offset+3] = (nv >> 0x18) & 0xff; } static inline void pk_u64le(void * const bufp, const int offset, const uint64_t nv) { uint8_t * const buf = bufp; buf[offset+0] = (nv >> 0) & 0xff; buf[offset+1] = (nv >> 8) & 0xff; buf[offset+2] = (nv >> 0x10) & 0xff; buf[offset+3] = (nv >> 0x18) & 0xff; buf[offset+4] = (nv >> 0x20) & 0xff; buf[offset+5] = (nv >> 0x28) & 0xff; buf[offset+6] = (nv >> 0x30) & 0xff; buf[offset+7] = (nv >> 0x38) & 0xff; } extern volatile int panic_mode; #endif datum_gateway-0.4.1beta/src/datum_utils_tests.c000066400000000000000000000070211512710151500217100ustar00rootroot00000000000000/* * * DATUM Gateway * Decentralized Alternative Templates for Universal Mining * * This file is part of OCEAN's Bitcoin mining decentralization * project, DATUM. * * https://ocean.xyz * * --- * * Copyright (c) 2025 Bitcoin Ocean, LLC & Luke Dashjr * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ #include #include #include #include #include "datum_utils.h" void datum_utils_tests_hex_to_bin(const uint8_t c, char * const x, const char * const fmt) { unsigned char b[3]; datum_test(2 == snprintf(&x[2], 3, fmt, c)); datum_test(c == hex2bin_uchar(&x[2])); memcpy(x, "00", 2); b[2] = 0x0e; hex_to_bin(x, b); datum_test(b[0] == 0); datum_test(b[1] == c); datum_test(b[2] == 0x0e); b[2] = 0x0e; hex_to_bin_le(x, b); datum_test(b[0] == c); datum_test(b[1] == 0); datum_test(b[2] == 0x0e); } void datum_utils_tests_hex(void) { char x[6], x2[6]; strcpy(&x[2], "00"); for (unsigned int c = 0; ; ++c) { datum_utils_tests_hex_to_bin(c, &x2[1], "%2.2X"); datum_test(strcasecmp(&x[2], &x2[3]) == 0); datum_utils_tests_hex_to_bin(c, x2, "%2.2X"); datum_utils_tests_hex_to_bin(c, &x[1], "%2.2x"); datum_test(strcasecmp(&x[3], &x2[2]) == 0); datum_utils_tests_hex_to_bin(c, x, "%2.2x"); datum_test(strcasecmp(&x[2], &x2[2]) == 0); x2[2] = 0x0e; uchar_to_hex(x2, c); datum_test(memcmp(&x[2], x2, 2) == 0); datum_test(x2[2] == 0x0e); x2[3] = 0x0e; uchar_to_hex(&x2[1], c); datum_test(memcmp(&x[2], &x2[1], 2) == 0); datum_test(x2[3] == 0x0e); if (c == 255) break; if (x[3] == 'f') { x[3] = '0'; if (x[2] == '9') { x[2] = 'a'; } else { ++x[2]; } } else if (x[3] == '9') { x[3] = 'a'; } else { ++x[3]; } } } void datum_utils_tests_secure_strequals(void) { const char * const secret = "abc"; const size_t secret_len = strlen(secret); /* equal strings */ datum_test(datum_secure_strequals(secret, secret_len, "abc")); /* guess longer than secret */ datum_test(!datum_secure_strequals(secret, secret_len, "abcd")); /* guess shorter than secret */ datum_test(!datum_secure_strequals(secret, secret_len, "ab")); /* guess repeats secret but is longer */ datum_test(!datum_secure_strequals(secret, secret_len, "abcabc")); /* zero-length secret matches only on empty guess, and doesn't dereference NULL */ datum_test(!datum_secure_strequals(NULL, 0, "anything")); datum_test(datum_secure_strequals(NULL, 0, "")); } void datum_utils_tests(void) { datum_utils_tests_hex(); datum_utils_tests_secure_strequals(); } datum_gateway-0.4.1beta/src/thirdparty_base58.c000066400000000000000000000161201512710151500214750ustar00rootroot00000000000000/* * Copyright 2012-2014 Luke Dashjr * * This program is free software; you can redistribute it and/or modify it * under the terms of the standard MIT license. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ /* Combined and slightly modified from Luke's libblkmaker and libbase58 */ #include #include #include #include #include "thirdparty_base58.h" #include "datum_utils.h" #define b58_sha256_impl my_sha256 bool _blkmk_b58tobin(void *bin, size_t binsz, const char *b58, size_t b58sz) { return b58tobin(bin, &binsz, b58, b58sz); } int _blkmk_b58check(void *bin, size_t binsz, const char *base58str) { return b58check(bin, binsz, base58str, 34); } size_t blkmk_address_to_script(void *out, size_t outsz, const char *addr) { unsigned char addrbin[25]; unsigned char *cout = out; const size_t b58sz = strlen(addr); int addrver; size_t rv; rv = sizeof(addrbin); if (!b58tobin(addrbin, &rv, addr, b58sz)) return 0; addrver = b58check(addrbin, sizeof(addrbin), addr, b58sz); switch (addrver) { case 0: // Bitcoin pubkey hash case 111: // Testnet pubkey hash if (outsz < (rv = 25)) return rv; cout[ 0] = 0x76; // OP_DUP cout[ 1] = 0xa9; // OP_HASH160 cout[ 2] = 0x14; // push 20 bytes memcpy(&cout[3], &addrbin[1], 20); cout[23] = 0x88; // OP_EQUALVERIFY cout[24] = 0xac; // OP_CHECKSIG return rv; case 5: // Bitcoin script hash case 196: // Testnet script hash if (outsz < (rv = 23)) return rv; cout[ 0] = 0xa9; // OP_HASH160 cout[ 1] = 0x14; // push 20 bytes memcpy(&cout[2], &addrbin[1], 20); cout[22] = 0x87; // OP_EQUAL return rv; default: return 0; } } static const int8_t b58digits_map[] = { -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8,-1,-1,-1,-1,-1,-1, -1, 9,10,11,12,13,14,15, 16,-1,17,18,19,20,21,-1, 22,23,24,25,26,27,28,29, 30,31,32,-1,-1,-1,-1,-1, -1,33,34,35,36,37,38,39, 40,41,42,43,-1,44,45,46, 47,48,49,50,51,52,53,54, 55,56,57,-1,-1,-1,-1,-1, }; typedef uint64_t b58_maxint_t; typedef uint32_t b58_almostmaxint_t; #define b58_almostmaxint_bits (sizeof(b58_almostmaxint_t) * 8) static const b58_almostmaxint_t b58_almostmaxint_mask = ((((b58_maxint_t)1) << b58_almostmaxint_bits) - 1); bool b58tobin(void *bin, size_t *binszp, const char *b58, size_t b58sz) { size_t binsz = *binszp; const unsigned char *b58u = (void*)b58; unsigned char *binu = bin; size_t outisz = (binsz + sizeof(b58_almostmaxint_t) - 1) / sizeof(b58_almostmaxint_t); b58_almostmaxint_t outi[outisz]; b58_maxint_t t; b58_almostmaxint_t c; size_t i, j; uint8_t bytesleft = binsz % sizeof(b58_almostmaxint_t); b58_almostmaxint_t zeromask = bytesleft ? (b58_almostmaxint_mask << (bytesleft * 8)) : 0; unsigned zerocount = 0; if (!b58sz) b58sz = strlen(b58); for (i = 0; i < outisz; ++i) { outi[i] = 0; } // Leading zeros, just count for (i = 0; i < b58sz && b58u[i] == '1'; ++i) ++zerocount; for ( ; i < b58sz; ++i) { if (b58u[i] & 0x80) // High-bit set on invalid digit return false; if (b58digits_map[b58u[i]] == -1) // Invalid base58 digit return false; c = (unsigned)b58digits_map[b58u[i]]; for (j = outisz; j--; ) { t = ((b58_maxint_t)outi[j]) * 58 + c; c = t >> b58_almostmaxint_bits; outi[j] = t & b58_almostmaxint_mask; } if (c) // Output number too big (carry to the next int32) return false; if (outi[0] & zeromask) // Output number too big (last int32 filled too far) return false; } j = 0; if (bytesleft) { for (i = bytesleft; i > 0; --i) { *(binu++) = (outi[0] >> (8 * (i - 1))) & 0xff; } ++j; } for (; j < outisz; ++j) { for (i = sizeof(*outi); i > 0; --i) { *(binu++) = (outi[j] >> (8 * (i - 1))) & 0xff; } } // Count canonical base58 byte count binu = bin; for (i = 0; i < binsz; ++i) { if (binu[i]) break; --*binszp; } *binszp += zerocount; return true; } static bool my_dblsha256(void *hash, const void *data, size_t datasz) { uint8_t buf[0x20]; return b58_sha256_impl(buf, data, datasz) && b58_sha256_impl(hash, buf, sizeof(buf)); } int b58check(const void *bin, size_t binsz, const char *base58str, size_t b58sz) { unsigned char buf[32]; const uint8_t *binc = bin; unsigned i; if (binsz < 4) return -4; if (!my_dblsha256(buf, bin, binsz - 4)) return -2; if (memcmp(&binc[binsz - 4], buf, 4)) return -1; // Check number of zeros is correct AFTER verifying checksum (to avoid possibility of accessing base58str beyond the end) for (i = 0; binc[i] == '\0' && base58str[i] == '1'; ++i) {} // Just finding the end of zeros, nothing to do in loop if (binc[i] == '\0' || base58str[i] == '1') return -3; return binc[0]; } static const char b58digits_ordered[] = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; bool b58enc(char *b58, size_t *b58sz, const void *data, size_t binsz) { const uint8_t *bin = data; int carry; size_t i, j, high, zcount = 0; size_t size; while (zcount < binsz && !bin[zcount]) ++zcount; size = (binsz - zcount) * 138 / 100 + 1; uint8_t buf[size]; memset(buf, 0, size); for (i = zcount, high = size - 1; i < binsz; ++i, high = j) { for (carry = bin[i], j = size - 1; (j > high) || carry; --j) { carry += 256 * buf[j]; buf[j] = carry % 58; carry /= 58; if (!j) { // Otherwise j wraps to maxint which is > high break; } } } for (j = 0; j < size && !buf[j]; ++j); if (*b58sz <= zcount + size - j) { *b58sz = zcount + size - j + 1; return false; } if (zcount) memset(b58, '1', zcount); for (i = zcount; j < size; ++i, ++j) b58[i] = b58digits_ordered[buf[j]]; b58[i] = '\0'; *b58sz = i + 1; return true; } bool b58check_enc(char *b58c, size_t *b58c_sz, uint8_t ver, const void *data, size_t datasz) { uint8_t buf[1 + datasz + 0x20]; uint8_t *hash = &buf[1 + datasz]; buf[0] = ver; memcpy(&buf[1], data, datasz); if (!my_dblsha256(hash, buf, datasz + 1)) { *b58c_sz = 0; return false; } return b58enc(b58c, b58c_sz, buf, 1 + datasz + 4); } datum_gateway-0.4.1beta/src/thirdparty_base58.h000066400000000000000000000035511512710151500215060ustar00rootroot00000000000000/* * Copyright 2012-2014 Luke Dashjr * * This program is free software; you can redistribute it and/or modify it * under the terms of the standard MIT license. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * */ #ifndef LIBBASE58_H #define LIBBASE58_H #include #include #ifdef __cplusplus extern "C" { #endif extern size_t blkmk_address_to_script(void *out, size_t outsz, const char *addr); extern bool (*b58_sha256_impl)(void *, const void *, size_t); extern bool b58tobin(void *bin, size_t *binsz, const char *b58, size_t b58sz); extern int b58check(const void *bin, size_t binsz, const char *b58, size_t b58sz); extern bool b58enc(char *b58, size_t *b58sz, const void *bin, size_t binsz); extern bool b58check_enc(char *b58c, size_t *b58c_sz, uint8_t ver, const void *data, size_t datasz); #ifdef __cplusplus } #endif #endif datum_gateway-0.4.1beta/src/thirdparty_segwit_addr.c000066400000000000000000000164751512710151500227170ustar00rootroot00000000000000/* Copyright (c) 2017, 2021 Pieter Wuille * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #include #include #include #include "thirdparty_segwit_addr.h" static uint32_t bech32_polymod_step(uint32_t pre) { uint8_t b = pre >> 25; return ((pre & 0x1FFFFFF) << 5) ^ (-((b >> 0) & 1) & 0x3b6a57b2UL) ^ (-((b >> 1) & 1) & 0x26508e6dUL) ^ (-((b >> 2) & 1) & 0x1ea119faUL) ^ (-((b >> 3) & 1) & 0x3d4233ddUL) ^ (-((b >> 4) & 1) & 0x2a1462b3UL); } static uint32_t bech32_final_constant(bech32_encoding enc) { if (enc == BECH32_ENCODING_BECH32) return 1; if (enc == BECH32_ENCODING_BECH32M) return 0x2bc830a3; abort(); } static const char* charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; static const int8_t charset_rev[128] = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1, -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1, -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1 }; int bech32_encode(char *output, const char *hrp, const uint8_t *data, size_t data_len, bech32_encoding enc) { uint32_t chk = 1; size_t i = 0; while (hrp[i] != 0) { int ch = hrp[i]; if (ch < 33 || ch > 126) { return 0; } if (ch >= 'A' && ch <= 'Z') return 0; chk = bech32_polymod_step(chk) ^ (ch >> 5); ++i; } if (i + 7 + data_len > 90) return 0; chk = bech32_polymod_step(chk); while (*hrp != 0) { chk = bech32_polymod_step(chk) ^ (*hrp & 0x1f); *(output++) = *(hrp++); } *(output++) = '1'; for (i = 0; i < data_len; ++i) { if (*data >> 5) return 0; chk = bech32_polymod_step(chk) ^ (*data); *(output++) = charset[*(data++)]; } for (i = 0; i < 6; ++i) { chk = bech32_polymod_step(chk); } chk ^= bech32_final_constant(enc); for (i = 0; i < 6; ++i) { *(output++) = charset[(chk >> ((5 - i) * 5)) & 0x1f]; } *output = 0; return 1; } bech32_encoding bech32_decode(char* hrp, uint8_t *data, size_t *data_len, const char *input) { uint32_t chk = 1; size_t i; size_t input_len = strlen(input); size_t hrp_len; int have_lower = 0, have_upper = 0; if (input_len < 8 || input_len > 90) { return BECH32_ENCODING_NONE; } *data_len = 0; while (*data_len < input_len && input[(input_len - 1) - *data_len] != '1') { ++(*data_len); } hrp_len = input_len - (1 + *data_len); if (1 + *data_len >= input_len || *data_len < 6) { return BECH32_ENCODING_NONE; } *(data_len) -= 6; for (i = 0; i < hrp_len; ++i) { int ch = input[i]; if (ch < 33 || ch > 126) { return BECH32_ENCODING_NONE; } if (ch >= 'a' && ch <= 'z') { have_lower = 1; } else if (ch >= 'A' && ch <= 'Z') { have_upper = 1; ch = (ch - 'A') + 'a'; } hrp[i] = ch; chk = bech32_polymod_step(chk) ^ (ch >> 5); } hrp[i] = 0; chk = bech32_polymod_step(chk); for (i = 0; i < hrp_len; ++i) { chk = bech32_polymod_step(chk) ^ (input[i] & 0x1f); } ++i; while (i < input_len) { int v = (input[i] & 0x80) ? -1 : charset_rev[(int)input[i]]; if (input[i] >= 'a' && input[i] <= 'z') have_lower = 1; if (input[i] >= 'A' && input[i] <= 'Z') have_upper = 1; if (v == -1) { return BECH32_ENCODING_NONE; } chk = bech32_polymod_step(chk) ^ v; if (i + 6 < input_len) { data[i - (1 + hrp_len)] = v; } ++i; } if (have_lower && have_upper) { return BECH32_ENCODING_NONE; } if (chk == bech32_final_constant(BECH32_ENCODING_BECH32)) { return BECH32_ENCODING_BECH32; } else if (chk == bech32_final_constant(BECH32_ENCODING_BECH32M)) { return BECH32_ENCODING_BECH32M; } else { return BECH32_ENCODING_NONE; } } static int convert_bits(uint8_t* out, size_t* outlen, int outbits, const uint8_t* in, size_t inlen, int inbits, int pad) { uint32_t val = 0; int bits = 0; uint32_t maxv = (((uint32_t)1) << outbits) - 1; while (inlen--) { val = (val << inbits) | *(in++); bits += inbits; while (bits >= outbits) { bits -= outbits; out[(*outlen)++] = (val >> bits) & maxv; } } if (pad) { if (bits) { out[(*outlen)++] = (val << (outbits - bits)) & maxv; } } else if (((val << (outbits - bits)) & maxv) || bits >= inbits) { return 0; } return 1; } int segwit_addr_encode(char *output, const char *hrp, int witver, const uint8_t *witprog, size_t witprog_len) { uint8_t data[65]; size_t datalen = 0; bech32_encoding enc = BECH32_ENCODING_BECH32; if (witver > 16) return 0; if (witver == 0 && witprog_len != 20 && witprog_len != 32) return 0; if (witprog_len < 2 || witprog_len > 40) return 0; if (witver > 0) enc = BECH32_ENCODING_BECH32M; data[0] = witver; convert_bits(data + 1, &datalen, 5, witprog, witprog_len, 8, 1); ++datalen; return bech32_encode(output, hrp, data, datalen, enc); } int segwit_addr_decode(int* witver, uint8_t* witdata, size_t* witdata_len, const char* hrp, const char* addr) { uint8_t data[84]; char hrp_actual[84]; size_t data_len; bech32_encoding enc = bech32_decode(hrp_actual, data, &data_len, addr); if (enc == BECH32_ENCODING_NONE) return 0; if (data_len == 0 || data_len > 65) return 0; if (strncmp(hrp, hrp_actual, 84) != 0) return 0; if (data[0] > 16) return 0; if (data[0] == 0 && enc != BECH32_ENCODING_BECH32) return 0; if (data[0] > 0 && enc != BECH32_ENCODING_BECH32M) return 0; *witdata_len = 0; if (!convert_bits(witdata, witdata_len, 8, data + 1, data_len - 1, 5, 0)) return 0; if (*witdata_len < 2 || *witdata_len > 40) return 0; if (data[0] == 0 && *witdata_len != 20 && *witdata_len != 32) return 0; *witver = data[0]; return 1; } datum_gateway-0.4.1beta/src/thirdparty_segwit_addr.h000066400000000000000000000101251512710151500227060ustar00rootroot00000000000000/* Copyright (c) 2017, 2021 Pieter Wuille * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef _SEGWIT_ADDR_H_ #define _SEGWIT_ADDR_H_ 1 #include /** Encode a SegWit address * * Out: output: Pointer to a buffer of size 73 + strlen(hrp) that will be * updated to contain the null-terminated address. * In: hrp: Pointer to the null-terminated human readable part to use * (chain/network specific). * ver: Version of the witness program (between 0 and 16 inclusive). * prog: Data bytes for the witness program (between 2 and 40 bytes). * prog_len: Number of data bytes in prog. * Returns 1 if successful. */ int segwit_addr_encode( char *output, const char *hrp, int ver, const uint8_t *prog, size_t prog_len ); /** Decode a SegWit address * * Out: ver: Pointer to an int that will be updated to contain the witness * program version (between 0 and 16 inclusive). * prog: Pointer to a buffer of size 40 that will be updated to * contain the witness program bytes. * prog_len: Pointer to a size_t that will be updated to contain the length * of bytes in prog. * hrp: Pointer to the null-terminated human readable part that is * expected (chain/network specific). * addr: Pointer to the null-terminated address. * Returns 1 if successful. */ int segwit_addr_decode( int* ver, uint8_t* prog, size_t* prog_len, const char* hrp, const char* addr ); /** Supported encodings. */ typedef enum { BECH32_ENCODING_NONE, BECH32_ENCODING_BECH32, BECH32_ENCODING_BECH32M } bech32_encoding; /** Encode a Bech32 or Bech32m string * * Out: output: Pointer to a buffer of size strlen(hrp) + data_len + 8 that * will be updated to contain the null-terminated Bech32 string. * In: hrp : Pointer to the null-terminated human readable part. * data : Pointer to an array of 5-bit values. * data_len: Length of the data array. * enc: Which encoding to use (BECH32_ENCODING_BECH32{,M}). * Returns 1 if successful. */ int bech32_encode( char *output, const char *hrp, const uint8_t *data, size_t data_len, bech32_encoding enc ); /** Decode a Bech32 or Bech32m string * * Out: hrp: Pointer to a buffer of size strlen(input) - 6. Will be * updated to contain the null-terminated human readable part. * data: Pointer to a buffer of size strlen(input) - 8 that will * hold the encoded 5-bit data values. * data_len: Pointer to a size_t that will be updated to be the number * of entries in data. * In: input: Pointer to a null-terminated Bech32 string. * Returns BECH32_ENCODING_BECH32{,M} to indicate decoding was successful * with the specified encoding standard. BECH32_ENCODING_NONE is returned if * decoding failed. */ bech32_encoding bech32_decode( char *hrp, uint8_t *data, size_t *data_len, const char *input ); #endif datum_gateway-0.4.1beta/www/000077500000000000000000000000001512710151500160255ustar00rootroot00000000000000datum_gateway-0.4.1beta/www/assets/000077500000000000000000000000001512710151500173275ustar00rootroot00000000000000datum_gateway-0.4.1beta/www/assets/icons/000077500000000000000000000000001512710151500204425ustar00rootroot00000000000000datum_gateway-0.4.1beta/www/assets/icons/datum_logo.svg000066400000000000000000000045051512710151500233210ustar00rootroot00000000000000 datum_gateway-0.4.1beta/www/assets/icons/favicon.ico000066400000000000000000001112621512710151500225660ustar00rootroot00000000000000Xg (X #.#.<<<<<<v!f<<<<<<<<<Y3!<<<<<<<<`!uU<<<<<9 777777K)[<777777777hK7777777kO7777777qV87767a8D: 7777777l;77777777}d7777777D!7777777sX777777D!88N077777777ǽ77777777<H&7777777=777777sX777777lP777787377777777O.V6777777777777777aC777777sX777777u77777773777777777t97777777Q09777777r777777sX777777dF777777R186UU7777777779y77777777ƻoT777777nR777777sX777777?7777777776U7777777777T4R27777777u[777777G$777777sX77777B777777<H&777779?7777777777}87777779M+777777<77777sX77777iM777777}d7777777E"%7777777779o7777777777777]>77777sX77777{z777777gJ7777777ol~777777777Y9M+777777K):77777m77777sX77777gJ77777X87777777tZ>7. \=~777777777ź7777777w^77777sX77777sX77777B77777v777777ChK77797]?s77777777:~f777777mQȾ77777K)77777sX7777@77777>D!777777777778)77dFp77777777]>I&777778U477777:7777sX7777eH77777m777777kOA7777775>777fIg77777777777777}77777źY97777sX7777x}77777_A77777@oT77777778oS7777lP|c7777777<x^77777F#=7777i7777sX7777kO7777]?77777777777777D77777oStZ7777777cEE"77777g7777w]7777sX7777E"7777l77777dFD!7777777Q13777777w]oT777777777777eH7777O-7777sX777>7777D ?7777>u[7777777T477+7777777x^pU777777?tZ77777]>77779777sX777bD7777x777777777777|Y9777777777777igJ777777kOB7777t7777ǽV6777sX777s7777W77777aCG%777777|[<777775C777777777lgJ7777777777AA777}d777sX777oS777hK7777<|c777777v`B77777776 Y9777777777u`B77777BkO7777p777y_777sX777G%777|c7777777777obD777777776977777777y_A77777qV@777\=777R2777sX77<777G$<777[E"77y_77sX77rW77pUź777V5R17777oTsY7777777V6v\777777aBjN7777777Q07777777z77~e77sX77K(77sY779t7777mQ{b7777778pȾK)7777827777><777778O.777G%_A77V587V6ǽ77sX7:77M+977y9777fI~e777777I&øy8777777777777qVZ:777778J'777g;77jN7977sX7[<Ĺ7777R1T4777fIi777777x^U477777777<7777777G%977779I&77777za77O-7sX7k7:L*78ɿ|777bDr77777B<7777777778n777777777mN,77779E"77M+Y97;J'7w]7sX7v\7w^77p977^@u77777gJcE7777777777F#wD!777777777R1|c7777;C77o977h7sX7O-7ɿlP7M+Y977[<{7777<@777777777T4!V5777777779yE"777<øA78ɿ|7O-9Z:Ĺ7sX97R28777V57777W7u[777777777_AK+_dG77777777`BjN777=ɿ?7R1T47øsX97sXW7Ⱦ7|7h;7T47778y÷H&7777777:{bs?76=ii:777777=?77>>7z9sXø7K(sXg<G%G%_A7O-777J'Ĺy8777777Ayi97777779sYD 777777mQ]?77?Ⱦ=9t9P/rWsXz`h77N,777y_U4777777K(ødF77777778 77777^@O-77777D 97Bƻ<V5R1msXR1dFtZ=I&87B;77777]?O.7777777777T7777777P/bD77777z`R17C:J'aCsY[i|cL*:X8tZ777H&ølP7777777777777K(z`|cL*77777777777hL÷H&77^@lPL*¶u[<zG%77Z;T477777777778X8pmV5777777777U4W77>U4źR2m78nRȾF#77777777:aC{oSA4'rWy_A9777777G$źlP7nRw]ɿkR2<ll<777777?jNĹeH<77775C777=gJǼǽhK>7777<mhJ'H&ȾnR87777E"tZv[<9777777778`77777779^@xrWD 778oTɿy}dZ;777M+~ejR177777777777777}777777777777K)z`iP/7[<ǼM+7R1iiN,7777777777777777777777777777777777K(zamiMxiMzoSA777777777777777777777777777777777777777777CqVǼĹeH<777777777777777777777<D!R1Y9^@pgx^lPcE[K)R1Y9fIsYz`iyøøyiz`sYfI_AY9K)D!7777777777777777777777777777777779E"dFq}dZ:C77777777777777777777777777777777777777777777777777D!_AiplPR1877777777777777777777777777777777777:R1nRt÷ɿǽlhKK(7777777777777777777777;T4pUwƻǼxqVU4<F#ou[<@]>y_njNN,87777777777zȾøuoSR2:7777D kƻźqV;7777>Z;v\}rlPP/9777rlPP/977777777A~f÷bD{bmQmQ977777777<W7sYzȾøznjNN,8777777777777?za`B8za÷_A;qViM9777777777777;T4pUwƻ7777777777777=w]dF87aBmQǽlPK)7<u[eH97777777777777777:R1cGd7777777777<sYhK977M+_AnRP/za?77>x^bD8777777777777777787777777:oTkO9777@x^7rzx^cE=g8777@|c^@7777777777777;77779fIsY;77778z`=8u[w^~f{bX8~f7F#gJ77777?z`øfI7777777777U79hKsX<777777gJG%7@¶]>\=hKk¶Ai87\=P/777777D!lȾV67777777w]=7777777Q0ȾZ:77L*K)F#utZlo`BK)8hK<77tZA7777777G$qR27777øi@77777777@z`777[<ƻA9ƻ9iMiMfIv>7R2G%77;py977777777F#iøS37p~fA7777777779pw;777u[97=~fƻ<<R1ȾaCȾAR1R1rW7E"U4777F#kO7777777777M+z`7777777777kOG%7777yu77w^M+;^@nRG$?9A{7lPW77:jN7777U4Y977777777777N,y77777777V5ø¶U47777;lP77]>fI7r7~eZ;77w^=Y97~A77l77777mQD!777777777776777777D!pU77777AźX877E"t7=W77F#nR7;w]7o78877w^977779i<77777777777777;py977777Q0H&779Ǽ777Z:7j7M+@99cE7>777]>?77777@z`7777777764777lPC777777cE>777>7CQ07~7m7aB7ǼbD7|c77O.sX777L*M+777777N,[<7777777W7źøR17777777z`8777~eM+7777V67qV7sY7u78lP77lPX8777@÷^@7777777bDI&7778eH77777778~e7777]?eH77H&H&7G%77^@7|q7O-77rW877}B7779qV77777779j~<79?p97777777<hK7777E"r77777i77K(7~f77O.77u[777¶97777p77777777?}d?77777777G%R17777:777O-C77kO7797kO77t77hK977?77777pU977777777M+777777777W7D 77777<77777<;7777W7÷77cEƻ777Ǽh777P/tZ77777Z;ƻB777777777bCy77777777lP;77777gK)777V5?77lP77B77D 779@77^@;777lPV677777J'O.7777777777777777n}777777^@dG7777s777h77U4ź777777v\777t7777|B77777>aC77777773 777779v\777777G%p7777]?;777F#77hL777977w^777V5?7777¶9777778z`777776u7777@`B7777779ǽ77777ƻg777X8777|c777G%77@87777777>7777777k7777I777K(O-7777777<7777fI9777777zu777[<777ȾaB777O-D!7777O.y_7777777eH<76U77Z;ǼB7777777~fL*77777u[7777Y9777w^777oS777w777777777kOY97777777R2G#7oT:7777777]>gJ77777rW8777E"7777dF777j777O.7777G%I&77777{D 7777777E"pv77777777F#p777778lP7777g7777P/777l7777M+7777777777ǽ97777777<tVrW77777777:777777|c77777mQ7777>777pU7777q7777CQ077777=77777777x]>777777777<777779cE7777;<77777777]>7777eHø77777777777O-y_777776H&777777777iJ'777777n77777kO7777<7777I&77779?7777?V5777777jNX87777777777777]>fI777777=\=77777j7777O.7777977777sY77777t7777777zD 77+77777777H&m7777777z777777G%7777cE7777777777y_77777;^@7777777Ĺ89 7777777;7777777AR177777V577777v\77777C7777B877777hǼ7777777>·UU7777777;7777777777777}77777s{77777øV677777ɿ_A777779hK7777777N,s777777jJ'7777777F#K(777777Z;77777}d77777jN77777x777777v\7777775>77777]?bD77777777777777C777777iM77777|c77777R17777777qV7777@ 7777H&n77777777K)E"777777}d777777ĹV577777sz777777L*777777hL978D777:7777777777777777oS777777B77777v\777777o7777777rWI777<77777777Q0A7777779=777777777777bD777777gJ¶7777777]>77hK)777777777x7777777hL7777779777777O-7777779?77777f. 7]?aC777777777Z;<7777777l777777I&777777<7777777qV774,I&m7777777777÷l77777777J'777777]>77777777777777w^qX7777777777dF97777777T47777777pU7777777>7778{7J6 ;7777777777z`77777777z7777777l7777776N->0?????????datum_gateway-0.4.1beta/www/assets/post.js000066400000000000000000000005151512710151500206530ustar00rootroot00000000000000async function sendPostRequest(url, data) { data.csrf = '%s'; var r = await fetch(url, { method: 'POST', headers: { 'Content-Type':'application/json' }, body: JSON.stringify(data) }); if (r.ok) return; if (r.status == 401) { alert('This action requires admin access.'); } else { alert('Error ' + r.status); } } datum_gateway-0.4.1beta/www/assets/style.css000066400000000000000000000037771512710151500212170ustar00rootroot00000000000000body { background-color: black; color: white; margin: 0; font-family: Arial, sans-serif; } a { color: lightblue; text-decoration: none; } a:hover { color: red; text-decoration: underline; } .container { max-width: 800px; margin: 0 auto; } .header { background-color: #222; color: white; text-align: center; padding: 1px; margin-bottom: 0; border-top-left-radius: 10px; border-top-right-radius: 10px; } .header-content img { vertical-align: text-top; } .menu-container { background-color: #222; padding: 1px; text-align: center; border-bottom-left-radius: 10px; border-bottom-right-radius: 10px; } .menu-container a { display: inline-block; background-color: #444; color: #3498db; padding: 10px 20px; margin: 5px; border-radius: 5px; text-decoration: none; transition: background-color 0.3s, color 0.3s; font-weight: bold; font-size: large; } .menu-container a:hover { background-color: #012740; color: red; text-decoration: underline; } .content { margin-bottom: 20px; } .tables-container { width: 100%; margin: 0 auto; } .table-wrapper { display: flex; flex-wrap: wrap; } .table-container { background-color: #333; padding: 20px; margin-bottom: 20px; border-radius: 10px; flex: 1 1 100%; box-sizing: border-box; margin: 10px; } .table-container h2 { color: white; text-align: center; margin-top: 0; } table { width: 100%; table-layout: auto; } tr { border-bottom: 1px solid #444; } tr:last-child { border-bottom: none; } td { padding: 10px; color: white; } td.label { background-color: #2a2a3b; width: 1%; white-space: nowrap; } td:not(.label) { background-color: black; } .table-footer { text-align: center; margin-top: 30px; } @media (min-width: 1100px) { .table-wrapper { flex-wrap: nowrap; } .table-container { flex: 1 1 800px; margin: 20px; } .table-wrapper + .table-container { flex: 1 1 800px; margin: 20px auto; } } .fixed-width { font-family: "Courier New", Courier, monospace; } .note { text-align: center; font-size: small; } datum_gateway-0.4.1beta/www/auth_failed.html000066400000000000000000000000431512710151500211550ustar00rootroot00000000000000This action requires admin access. datum_gateway-0.4.1beta/www/clients_top.html000066400000000000000000000016301512710151500212360ustar00rootroot00000000000000 DATUM Gateway Status

Stratum Client List

datum_gateway-0.4.1beta/www/coinbaser_top.html000066400000000000000000000021441512710151500215430ustar00rootroot00000000000000 DATUM Gateway Status

Current Coinbaser

datum_gateway-0.4.1beta/www/config.html000066400000000000000000000216231512710151500201640ustar00rootroot00000000000000 DATUM Gateway Configuration
Save ${msg:*ro}

Basic

Pool

Advanced

Save ${msg:*ro}
datum_gateway-0.4.1beta/www/config_errors.html000066400000000000000000000035411512710151500215570ustar00rootroot00000000000000 DATUM Gateway Configuration - ERROR

Errors Occurred

${*errors}
datum_gateway-0.4.1beta/www/config_restart.html000066400000000000000000000035621512710151500217320ustar00rootroot00000000000000 DATUM Gateway Configuration - Restarting

Changes Successful

DATUM Gateway is restarting... Please wait a few seconds before continuing.
datum_gateway-0.4.1beta/www/foot.html000066400000000000000000000001571512710151500176650ustar00rootroot00000000000000

Note: This page does not automatically refresh

datum_gateway-0.4.1beta/www/home.html000066400000000000000000000106211512710151500176430ustar00rootroot00000000000000 DATUM Gateway Status

Decentralized Client Stats

Shares Accepted: ${DATUM_SHARES_ACCEPTED}
Shares Rejected: ${DATUM_SHARES_REJECTED}
Status: ${DATUM_CONNECTION_STATUS}
Pool Host: ${DATUM_POOL_HOST}
Pool Tag: ${DATUM_POOL_TAG}
Secondary/Miner Tag: ${DATUM_MINER_TAG}
Pool Current MinDiff: ${DATUM_POOL_DIFF}
Pool Pubkey: ${DATUM_POOL_PUBKEY}
Process uptime: ${DATUM_PROCESS_UPTIME}

Stratum Server Info

Active Threads: ${STRATUM_ACTIVE_THREADS}
Total Connections: ${STRATUM_TOTAL_CONNECTIONS}
Total Work Subscriptions: ${STRATUM_TOTAL_SUBSCRIPTIONS}
Estimated Hashrate: ${STRATUM_HASHRATE_ESTIMATE}

Current Stratum Job

Job ID: ${STRATUM_JOB_INFO}
Block Height: ${STRATUM_JOB_BLOCK_HEIGHT}
Block Value: ${STRATUM_JOB_BLOCK_VALUE}
Previous Block: ${STRATUM_JOB_PREVBLOCK}
Block Target: ${STRATUM_JOB_TARGET}
Witness Commitment: ${STRATUM_JOB_WITNESS}
Block Difficulty: ${STRATUM_JOB_DIFF}
Version: ${STRATUM_JOB_VERSION}
Bits: ${STRATUM_JOB_BITS}
Time: ${STRATUM_JOB_TIMEINFO}
Limits: ${STRATUM_JOB_LIMITINFO}
Size: ${STRATUM_JOB_SIZE}
Weight: ${STRATUM_JOB_WEIGHT}
Sigops: ${STRATUM_JOB_SIGOPS}
Txn Count: ${STRATUM_JOB_TXNCOUNT}

Note: This page does not automatically refresh

datum_gateway-0.4.1beta/www/threads_top.html000066400000000000000000000021371512710151500212320ustar00rootroot00000000000000 DATUM Gateway Status

Thread Stats