From f6c5b813075cae4228d241a3567e70b210504749 Mon Sep 17 00:00:00 2001 From: mtg0001 <106211915+mtg0001@users.noreply.github.com> Date: Tue, 4 Aug 2026 10:18:01 -0300 Subject: Add release verification tooling (#108) Co-authored-by: Gilcimar Duarte --- scripts/make-release-checksums.sh | 78 ++++++++++++++++++++++++++ scripts/verify-release.sh | 115 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100755 scripts/make-release-checksums.sh create mode 100755 scripts/verify-release.sh (limited to 'scripts') diff --git a/scripts/make-release-checksums.sh b/scripts/make-release-checksums.sh new file mode 100755 index 0000000..5b29e2d --- /dev/null +++ b/scripts/make-release-checksums.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +set -euo pipefail + +sign=0 +key="" + +usage() { + cat <<'EOF' +Usage: scripts/make-release-checksums.sh [--sign] [--local-user KEYID] + +Creates SHA256SUMS for release assets in the current directory: + Bitflash-*-windows.zip + Bitflash-*-x86_64.AppImage + bitflash-node-*-x86_64 + +With --sign, also creates SHA256SUMS.asc as a detached ASCII-armored GPG +signature. Upload both files with the release assets. +EOF +} + +while [ "$#" -gt 0 ]; do + case "$1" in + --sign) sign=1 ;; + --local-user) + [ "$#" -ge 2 ] || { echo "--local-user needs a key id" >&2; exit 2; } + key="$2" + shift + ;; + -h|--help) usage; exit 0 ;; + *) echo "unknown option: $1" >&2; usage >&2; exit 2 ;; + esac + shift +done + +if command -v sha256sum >/dev/null 2>&1; then + sha_cmd=(sha256sum) +elif command -v shasum >/dev/null 2>&1; then + sha_cmd=(shasum -a 256) +else + echo "missing required command: sha256sum or shasum" >&2 + exit 1 +fi + +assets=() +for pattern in \ + "Bitflash-*-windows.zip" \ + "Bitflash-*-x86_64.AppImage" \ + "bitflash-node-*-x86_64" +do + for file in $pattern; do + [ -e "$file" ] || continue + assets+=("$file") + done +done + +if [ "${#assets[@]}" -eq 0 ]; then + echo "no release assets found in $(pwd)" >&2 + exit 1 +fi + +printf '%s\n' "${assets[@]}" | LC_ALL=C sort | while IFS= read -r file; do + "${sha_cmd[@]}" "$file" +done > SHA256SUMS + +echo "Wrote SHA256SUMS" + +if [ "$sign" -eq 1 ]; then + if ! command -v gpg >/dev/null 2>&1; then + echo "missing required command: gpg" >&2 + exit 1 + fi + gpg_args=(--armor --detach-sign --output SHA256SUMS.asc) + if [ -n "$key" ]; then + gpg_args+=(--local-user "$key") + fi + gpg "${gpg_args[@]}" SHA256SUMS + echo "Wrote SHA256SUMS.asc" +fi diff --git a/scripts/verify-release.sh b/scripts/verify-release.sh new file mode 100755 index 0000000..23e71d1 --- /dev/null +++ b/scripts/verify-release.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo="Bitflash-sh/bitflash" +tag="${1:-latest}" +require_signature=0 + +usage() { + cat <<'EOF' +Usage: scripts/verify-release.sh [tag|latest] [--require-signature] + +Downloads the release checksum file and every asset named in it, then verifies: + 1. SHA256SUMS.asc, if present, against SHA256SUMS with gpg. + 2. SHA256SUMS against the downloaded assets. + +Use --require-signature for release audits. Without it, older releases that +ship only SHA256SUMS are checked for integrity and reported as unsigned. +EOF +} + +for arg in "${@:2}"; do + case "$arg" in + --require-signature) require_signature=1 ;; + -h|--help) usage; exit 0 ;; + *) echo "unknown option: $arg" >&2; usage >&2; exit 2 ;; + esac +done + +if [ "$tag" = "-h" ] || [ "$tag" = "--help" ]; then + usage + exit 0 +fi + +need() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "missing required command: $1" >&2 + exit 1 + fi +} + +need sed +need awk + +if command -v sha256sum >/dev/null 2>&1; then + sha_check=(sha256sum -c SHA256SUMS) +elif command -v shasum >/dev/null 2>&1; then + sha_check=(shasum -a 256 -c SHA256SUMS) +else + echo "missing required command: sha256sum or shasum" >&2 + exit 1 +fi + +download() { + local url="$1" + local out="$2" + if command -v curl >/dev/null 2>&1; then + curl -fsSL "$url" -o "$out" + elif command -v wget >/dev/null 2>&1; then + wget -q "$url" -O "$out" + else + echo "missing required command: curl or wget" >&2 + exit 1 + fi +} + +if [ "$tag" = "latest" ]; then + tag="$(download "https://api.github.com/repos/$repo/releases/latest" - | + sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | + head -1)" + if [ -z "$tag" ]; then + echo "could not resolve latest release tag" >&2 + exit 1 + fi +fi + +workdir="$(mktemp -d "${TMPDIR:-/tmp}/bitflash-release-verify.XXXXXX")" +cleanup() { rm -rf "$workdir"; } +trap cleanup EXIT + +base_url="https://github.com/$repo/releases/download/$tag" + +echo "Verifying Bitflash $tag" +echo "Working directory: $workdir" +cd "$workdir" + +download "$base_url/SHA256SUMS" SHA256SUMS + +if download "$base_url/SHA256SUMS.asc" SHA256SUMS.asc 2>/dev/null; then + if ! command -v gpg >/dev/null 2>&1; then + echo "SHA256SUMS.asc exists, but gpg is not installed" >&2 + exit 1 + fi + gpg --verify SHA256SUMS.asc SHA256SUMS + echo "Signature OK" +else + if [ "$require_signature" -eq 1 ]; then + echo "release does not publish SHA256SUMS.asc" >&2 + exit 1 + fi + echo "WARNING: no SHA256SUMS.asc found; checking hashes only" +fi + +awk '{print $2}' SHA256SUMS | while IFS= read -r file; do + file="${file#\*}" + file="${file#./}" + [ -z "$file" ] && continue + case "$file" in + */*|..*) echo "refusing unexpected checksum path: $file" >&2; exit 1 ;; + esac + echo "Downloading $file" + download "$base_url/$file" "$file" +done + +"${sha_check[@]}" +echo "Release assets OK" -- cgit v1.2.3 From c3f6b68f907b03865d2c417ee39fe9526787196f Mon Sep 17 00:00:00 2001 From: Bitflash Date: Thu, 6 Aug 2026 10:46:26 -0300 Subject: release: sign SHA256SUMS with a pinned key, verify against it verify-release.sh embeds the Bitflash release signing key and now requires that the checksum signature validate against that exact key, not merely against some key in the caller's keyring (#138). It also fetches releases from releases.bitflash.network instead of the retired GitHub host, resolving 'latest' from latest.txt. Release signing key fingerprint: 910A 2B4C CA87 9E81 FB4B 2AEA 1D4A 53D3 B78A A4B8 --- docs/release-verification.md | 78 ++++++++++++++++++++----------- scripts/verify-release.sh | 108 +++++++++++++++++++++++-------------------- 2 files changed, 108 insertions(+), 78 deletions(-) (limited to 'scripts') diff --git a/docs/release-verification.md b/docs/release-verification.md index ecacd5f..32de74f 100644 --- a/docs/release-verification.md +++ b/docs/release-verification.md @@ -3,60 +3,82 @@ Bitflash releases publish binaries and a `SHA256SUMS` file. The checksum file proves that the file you downloaded matches the file the release page names. A detached signature, `SHA256SUMS.asc`, proves that the checksum file itself was -signed by a trusted release key. +signed by the Bitflash release key. That gives users two separate checks: -1. `gpg --verify SHA256SUMS.asc SHA256SUMS` checks who signed the checksums. -2. `sha256sum -c SHA256SUMS` checks the binaries against those checksums. +1. `gpg --verify SHA256SUMS.asc SHA256SUMS` — who signed the checksums. +2. `sha256sum -c SHA256SUMS` — the binaries against those checksums. -## User Check +But the first check is only worth something if you require a **specific** key. +A good signature by *some* key proves nothing; anyone can make one. -From a shell with `curl` or `wget` and `sha256sum`: +## The release signing key -```bash -scripts/verify-release.sh latest +```text +fingerprint 910A 2B4C CA87 9E81 FB4B 2AEA 1D4A 53D3 B78A A4B8 +uid Bitflash Releases (bitflash.network release signing) + ``` -For a specific release: +The public key is published at +`https://releases.bitflash.network/bitflash-release-key.asc` and embedded in +`scripts/verify-release.sh`, so the tool's trust anchor lives in this +repository rather than on the server it verifies. + +## User check + +From a shell with `curl` or `wget`, `sha256sum` and `gpg`: ```bash -scripts/verify-release.sh v1.2.13 +scripts/verify-release.sh latest ``` -For release audits, require the signature: +For a specific release: ```bash -scripts/verify-release.sh v1.2.13 --require-signature +scripts/verify-release.sh v1.2.15 ``` -Older releases may not have `SHA256SUMS.asc`. In that case the script warns and -still checks file integrity. New release audits should use `--require-signature`. - -## Maintainer Flow +The script downloads the release, imports the pinned key into a throwaway +keyring, and **refuses anything that is not a valid signature by the key above** +before checking the hashes. Releases from before signing was introduced ship +only `SHA256SUMS`; without `--require-signature` those are checked for integrity +and reported as unsigned. -After building release assets in the repository root: +By hand, if you would rather not run the script: ```bash -scripts/make-release-checksums.sh --sign --local-user RELEASE_KEY_ID +curl -O https://releases.bitflash.network/bitflash-release-key.asc +gpg --import bitflash-release-key.asc +gpg --verify SHA256SUMS.asc SHA256SUMS # must name the fingerprint above +sha256sum -c SHA256SUMS ``` -Upload all built assets plus: +## Maintainer flow -```text -SHA256SUMS -SHA256SUMS.asc +The signing key lives on a dedicated location, not in the build tree. After +building the release assets in the repository root, write and sign the +checksums: + +```bash +sha256sum Bitflash-*.zip Bitflash-*.AppImage bitflash-node-* > SHA256SUMS +gpg --armor --detach-sign --output SHA256SUMS.asc SHA256SUMS ``` -Keep the private signing key offline or on a dedicated release machine. Publish -the public key fingerprint in the release notes and keep using the same key for -future releases unless there is a clearly announced rotation. +Publish, alongside the assets, `SHA256SUMS` and `SHA256SUMS.asc`, and update +`latest.txt` on the releases host to the new tag. Keep using the same key; if it +is ever rotated, announce the new fingerprint clearly and update the embedded +key in `verify-release.sh`. -## Why This Matters +## Why this matters `SHA256SUMS` alone protects against a broken download, but not against someone -replacing both a binary and the checksum file. Signing `SHA256SUMS` means an -attacker must also have the release signing key to make the replacement verify. +replacing both a binary and the checksum file. Signing `SHA256SUMS`, and +requiring a pinned key, means an attacker must also hold the release signing key +to make the replacement verify. Serving from bitflash.network makes this the +guarantee that matters: without a pinned key, "trust the site" is the only +thing standing behind a download. This is not reproducible builds yet. It is the smaller, immediate step that -makes every release asset auditable by users before they run it. +makes every release asset auditable before anyone runs it. diff --git a/scripts/verify-release.sh b/scripts/verify-release.sh index 23e71d1..4fd4558 100755 --- a/scripts/verify-release.sh +++ b/scripts/verify-release.sh @@ -1,23 +1,50 @@ #!/usr/bin/env bash set -euo pipefail -repo="Bitflash-sh/bitflash" -tag="${1:-latest}" +# Verifies a Bitflash release: the checksum file's signature against a pinned +# key, then the assets against the checksums. +# +# The signing key is embedded below, not fetched. That is the whole point of a +# pinned key: the trust anchor ships with this script (in git), so it does not +# depend on the same server that serves the release. A server that is +# compromised can swap the binaries and the checksums, but it cannot forge a +# signature by this key. + +host="https://releases.bitflash.network" require_signature=0 +# The Bitflash release signing key. Verification requires this exact key -- +# gpg's own "good signature" is not enough, because a good signature by *some* +# key proves nothing. See issue #138. +RELEASE_KEY_FP="910A2B4CCA879E81FB4B2AEA1D4A53D3B78AA4B8" +read -r -d '' RELEASE_KEY <<'KEY' || true +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEanSOmRYJKwYBBAHaRw8BAQdAXj24i2BYUcAmOpoXwjE4ZTynHsifCZ+niwQK +HqO0nMa0UEJpdGZsYXNoIFJlbGVhc2VzIChiaXRmbGFzaC5uZXR3b3JrIHJlbGVh +c2Ugc2lnbmluZykgPHJlbGVhc2VzQGJpdGZsYXNoLm5ldHdvcms+iJAEExYKADgW +IQSRCitMyoeegftLKuodSlPTt4qkuAUCanSOmQIbAwULCQgHAgYVCgkICwIEFgID +AQIeAQIXgAAKCRAdSlPTt4qkuIi9AP96X3OcstiCTTgKOr16Vhds3LyuZ19q3Apq +4KAcNuOUMAD+NqgJ1CqkLGPXfZLCtYBCRHXNYWxMMEk+ViLcRtdLMg4= +=mxic +-----END PGP PUBLIC KEY BLOCK----- +KEY + usage() { cat <<'EOF' Usage: scripts/verify-release.sh [tag|latest] [--require-signature] Downloads the release checksum file and every asset named in it, then verifies: - 1. SHA256SUMS.asc, if present, against SHA256SUMS with gpg. + 1. SHA256SUMS.asc against SHA256SUMS, requiring the pinned Bitflash release + key (fingerprint 910A 2B4C CA87 9E81 FB4B 2AEA 1D4A 53D3 B78A A4B8). 2. SHA256SUMS against the downloaded assets. -Use --require-signature for release audits. Without it, older releases that -ship only SHA256SUMS are checked for integrity and reported as unsigned. +Releases from before signing was introduced ship only SHA256SUMS; without +--require-signature those are checked for integrity and reported as unsigned. EOF } +tag="${1:-latest}" for arg in "${@:2}"; do case "$arg" in --require-signature) require_signature=1 ;; @@ -25,19 +52,9 @@ for arg in "${@:2}"; do *) echo "unknown option: $arg" >&2; usage >&2; exit 2 ;; esac done +if [ "$tag" = "-h" ] || [ "$tag" = "--help" ]; then usage; exit 0; fi -if [ "$tag" = "-h" ] || [ "$tag" = "--help" ]; then - usage - exit 0 -fi - -need() { - if ! command -v "$1" >/dev/null 2>&1; then - echo "missing required command: $1" >&2 - exit 1 - fi -} - +need() { command -v "$1" >/dev/null 2>&1 || { echo "missing required command: $1" >&2; exit 1; }; } need sed need awk @@ -46,39 +63,25 @@ if command -v sha256sum >/dev/null 2>&1; then elif command -v shasum >/dev/null 2>&1; then sha_check=(shasum -a 256 -c SHA256SUMS) else - echo "missing required command: sha256sum or shasum" >&2 - exit 1 + echo "missing required command: sha256sum or shasum" >&2; exit 1 fi download() { - local url="$1" - local out="$2" - if command -v curl >/dev/null 2>&1; then - curl -fsSL "$url" -o "$out" - elif command -v wget >/dev/null 2>&1; then - wget -q "$url" -O "$out" - else - echo "missing required command: curl or wget" >&2 - exit 1 - fi + if command -v curl >/dev/null 2>&1; then curl -fsSL "$1" -o "$2" + elif command -v wget >/dev/null 2>&1; then wget -q "$1" -O "$2" + else echo "missing required command: curl or wget" >&2; exit 1; fi } if [ "$tag" = "latest" ]; then - tag="$(download "https://api.github.com/repos/$repo/releases/latest" - | - sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | - head -1)" - if [ -z "$tag" ]; then - echo "could not resolve latest release tag" >&2 - exit 1 - fi + tag="$(download "$host/latest.txt" - 2>/dev/null | tr -d '[:space:]' || true)" + [ -n "$tag" ] || { echo "could not resolve the latest release tag from $host/latest.txt" >&2; exit 1; } fi workdir="$(mktemp -d "${TMPDIR:-/tmp}/bitflash-release-verify.XXXXXX")" cleanup() { rm -rf "$workdir"; } trap cleanup EXIT -base_url="https://github.com/$repo/releases/download/$tag" - +base_url="$host/$tag" echo "Verifying Bitflash $tag" echo "Working directory: $workdir" cd "$workdir" @@ -86,27 +89,32 @@ cd "$workdir" download "$base_url/SHA256SUMS" SHA256SUMS if download "$base_url/SHA256SUMS.asc" SHA256SUMS.asc 2>/dev/null; then - if ! command -v gpg >/dev/null 2>&1; then - echo "SHA256SUMS.asc exists, but gpg is not installed" >&2 + need gpg + # Verify in a throwaway keyring holding ONLY the pinned key, and require that + # the signature validates against it with the pinned fingerprint. This is the + # difference between "signed" and "signed by Bitflash". + keyring="$workdir/keyring" + mkdir -p "$keyring"; chmod 700 "$keyring" + printf '%s\n' "$RELEASE_KEY" | GNUPGHOME="$keyring" gpg --batch --quiet --import 2>/dev/null + status="$(GNUPGHOME="$keyring" gpg --batch --status-fd=1 --verify SHA256SUMS.asc SHA256SUMS 2>/dev/null || true)" + if printf '%s' "$status" | grep -q "VALIDSIG.*$RELEASE_KEY_FP"; then + echo "Signature OK -- signed by the pinned Bitflash release key" + else + echo "SIGNATURE REJECTED: not a valid signature by the pinned release key" >&2 + echo " expected key: $RELEASE_KEY_FP" >&2 exit 1 fi - gpg --verify SHA256SUMS.asc SHA256SUMS - echo "Signature OK" else if [ "$require_signature" -eq 1 ]; then - echo "release does not publish SHA256SUMS.asc" >&2 - exit 1 + echo "release does not publish SHA256SUMS.asc" >&2; exit 1 fi - echo "WARNING: no SHA256SUMS.asc found; checking hashes only" + echo "WARNING: no SHA256SUMS.asc found; checking hashes only (unsigned release)" fi awk '{print $2}' SHA256SUMS | while IFS= read -r file; do - file="${file#\*}" - file="${file#./}" + file="${file#\*}"; file="${file#./}" [ -z "$file" ] && continue - case "$file" in - */*|..*) echo "refusing unexpected checksum path: $file" >&2; exit 1 ;; - esac + case "$file" in */*|..*) echo "refusing unexpected checksum path: $file" >&2; exit 1 ;; esac echo "Downloading $file" download "$base_url/$file" "$file" done -- cgit v1.2.3 From a0355492c3bae37ad57ef3e05302a3e5b1a80e3e Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Fri, 7 Aug 2026 11:42:30 -0300 Subject: tools: build a static pool directory --- docs/pool-directory.md | 35 ++++++++++ scripts/build-pool-index.py | 153 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 docs/pool-directory.md create mode 100644 scripts/build-pool-index.py (limited to 'scripts') diff --git a/docs/pool-directory.md b/docs/pool-directory.md new file mode 100644 index 0000000..6456084 --- /dev/null +++ b/docs/pool-directory.md @@ -0,0 +1,35 @@ +# Public Pool Directory + +`pool.bitflash.network` should be a public directory, not a network dependency. +Nodes discover pools through Nostr and `.btf`; the website only helps humans +compare operators. + +Operators can publish the `pool_status.json` file produced by: + +```bash +bitflash -nogui -gen -operator -poolstatusfile=/var/www/pool_status.json +``` + +Build a static directory from one or more status files: + +```bash +python3 scripts/build-pool-index.py --out-dir public/pool \ + https://operator-one.example/pool_status.json \ + https://operator-two.example/pool_status.json +``` + +The generated output contains: + +- `index.html`: human-readable ranking page. +- `pools.json`: machine-readable API for `api.bitflash.network/pools.json`. + +Ranking order is intentionally simple: + +1. online pools first; +2. higher reported hashrate; +3. more authorized miners; +4. lower fee; +5. name as a stable tie-breaker. + +An entry is considered online when the source says the pool is running and its +status file is no more than 120 seconds old. diff --git a/scripts/build-pool-index.py b/scripts/build-pool-index.py new file mode 100644 index 0000000..40eb8f4 --- /dev/null +++ b/scripts/build-pool-index.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +# +# Build a static public pool directory from one or more Bitflash pool_status.json +# files. Inputs may be local paths or http(s) URLs. + +import argparse +import html +import json +import os +import time +import urllib.request + + +def load_json(source): + if source.startswith("http://") or source.startswith("https://"): + with urllib.request.urlopen(source, timeout=10) as r: + return json.loads(r.read().decode("utf-8")) + with open(source, "r", encoding="utf-8-sig") as f: + return json.load(f) + + +def normalize_pool(source, obj, now): + operator = obj.get("operator", {}) + pool = obj.get("pool", {}) + node = obj.get("node", {}) + updated = int(obj.get("updatedAt") or 0) + age = max(0, now - updated) if updated else None + online = bool(node.get("poolRunning")) and age is not None and age <= 120 + return { + "source": source, + "name": operator.get("name") or "Bitflash Pool", + "btfAddress": operator.get("btfAddress") or "", + "feePercent": float(operator.get("feePercent") or 0.0), + "dashboardUrl": operator.get("dashboardUrl") or "", + "height": int(node.get("height") or 0), + "online": online, + "ageSeconds": age, + "authorizedMiners": int(pool.get("authorizedMiners") or 0), + "blocksFoundSession": int(pool.get("blocksFoundSession") or 0), + "roundShares": int(pool.get("roundShares") or 0), + "hashRate": float(pool.get("hashRate") or 0.0), + } + + +def rank_key(pool): + return ( + 1 if pool["online"] else 0, + pool["hashRate"], + pool["authorizedMiners"], + -pool["feePercent"], + pool["name"].lower(), + ) + + +def render_html(pools, generated_at): + rows = [] + for p in pools: + dash = p["dashboardUrl"] + dash_html = ( + 'dashboard'.format(html.escape(dash, quote=True)) + if dash else "-" + ) + rows.append( + "" + "{status}" + "{name}
{addr}" + "{fee:.2f}%" + "{miners}" + "{hashrate:.2f}" + "{blocks}" + "{height}" + "{age}" + "{dash}" + "".format( + status="online" if p["online"] else "stale", + name=html.escape(p["name"]), + addr=html.escape(p["btfAddress"]), + fee=p["feePercent"], + miners=p["authorizedMiners"], + hashrate=p["hashRate"], + blocks=p["blocksFoundSession"], + height=p["height"], + age="{}s".format(p["ageSeconds"]) if p["ageSeconds"] is not None else "-", + dash=dash_html, + ) + ) + + return """ + + + + + Bitflash Pools + + + +

Bitflash Pools

+

This directory is informational. Bitflash nodes discover pools through Nostr and .btf; this page only helps humans compare public operators.

+ + + + + + {rows} + +
StatusPoolFeeMinersH/sBlocksHeightAgeLink
+

Generated at {generated_at}. JSON: pools.json.

+ + +""".format(rows="\n ".join(rows), generated_at=generated_at) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--out-dir", default="pool-site") + ap.add_argument("sources", nargs="+", help="pool_status.json paths or URLs") + args = ap.parse_args() + + now = int(time.time()) + pools = [] + errors = [] + for source in args.sources: + try: + pools.append(normalize_pool(source, load_json(source), now)) + except Exception as e: + errors.append({"source": source, "error": str(e)}) + + pools.sort(key=rank_key, reverse=True) + os.makedirs(args.out_dir, exist_ok=True) + + generated_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(now)) + with open(os.path.join(args.out_dir, "pools.json"), "w", encoding="utf-8") as f: + json.dump({ + "schema": "bitflash-pool-directory-1", + "generatedAt": generated_at, + "pools": pools, + "errors": errors, + }, f, indent=2, sort_keys=True) + f.write("\n") + + with open(os.path.join(args.out_dir, "index.html"), "w", encoding="utf-8") as f: + f.write(render_html(pools, generated_at)) + + +if __name__ == "__main__": + main() -- cgit v1.2.3 From 78cf0c3cb6752bd36bbc8fe7017d5d60cca13da7 Mon Sep 17 00:00:00 2001 From: Bitflash Date: Fri, 7 Aug 2026 15:07:56 -0300 Subject: tools: reject non-http(s) dashboard URLs in the pool directory A pool's dashboard URL comes from its self-published status file. Escaping it for HTML is not enough: a javascript: or data: scheme would still render as a clickable script-injection on the public directory page. Only emit a link for http(s) schemes; anything else shows as no link. --- scripts/build-pool-index.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) (limited to 'scripts') diff --git a/scripts/build-pool-index.py b/scripts/build-pool-index.py index 40eb8f4..382b6c3 100644 --- a/scripts/build-pool-index.py +++ b/scripts/build-pool-index.py @@ -56,10 +56,15 @@ def render_html(pools, generated_at): rows = [] for p in pools: dash = p["dashboardUrl"] - dash_html = ( - 'dashboard'.format(html.escape(dash, quote=True)) - if dash else "-" - ) + # Only linkify http(s). The dashboard URL comes from an operator's + # self-published status file, so a "javascript:" or "data:" scheme + # would become a clickable script-injection on this public page. + dash_lower = dash.lower() + if dash_lower.startswith("http://") or dash_lower.startswith("https://"): + dash_html = 'dashboard'.format( + html.escape(dash, quote=True)) + else: + dash_html = "-" rows.append( "" "{status}" -- cgit v1.2.3 From b1eb8b66b8d7b47c7fb2e85d5d4db86a11823366 Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Fri, 7 Aug 2026 17:14:36 -0300 Subject: pool: publish round proof ledger --- README.md | 14 +- docs/pool-directory.md | 12 +- scripts/build-pool-index.py | 18 ++- src/main.cpp | 1 + src/main.h | 1 + src/main_gui.cpp | 5 + src/readme.txt | 4 + src/rpc.cpp | 316 +++++++++++++++++++++++++++++++++++++++++++- 8 files changed, 359 insertions(+), 12 deletions(-) (limited to 'scripts') diff --git a/README.md b/README.md index 7f4d9b0..ec8e9c4 100644 --- a/README.md +++ b/README.md @@ -219,10 +219,12 @@ Open **Options** from the menu bar. Under Mining Mode: Pool announcements are published on Nostr with live status fields, so external tools can query the latest pool state by `.btf` address. -Pool operators also write a local `pool_status.json` every ten seconds. By -default it is created in the data directory; use `-poolstatusfile=PATH` to write -it somewhere a dashboard or web sync can read. The file is informational only: -nodes still discover pools through Nostr and `.btf`, not through any web domain. +Pool operators also write a local `pool_status.json` every ten seconds and a +`pool_rounds.json` proof ledger whenever they find blocks or pay miners. By +default both are created in the data directory; use `-poolstatusfile=PATH` and +`-poolroundsfile=PATH` to write them somewhere a dashboard or web sync can read. +The files are informational only: nodes still discover pools through Nostr and +`.btf`, not through any web domain. **Participant** — mine to someone else's pool. Enter or select the pool's `.btf` address and enable Start Mining. @@ -243,7 +245,9 @@ xmrig -a rx/0 -o 127.0.0.1:3333 -u YOUR_BTF_ADDRESS -p x ./bitflash -nogui # node only ./bitflash -nogui -gen # node + solo mining ./bitflash -nogui -gen -operator # pool operator -./bitflash -nogui -gen -operator -poolstatusfile=/var/www/api/pools.json +./bitflash -nogui -gen -operator \ + -poolstatusfile=/var/www/pool_status.json \ + -poolroundsfile=/var/www/pool_rounds.json ./bitflash -nogui -gen -participant=POOL_BTF_ADDRESS # mine to pool ./bitflash -nogui -stratumbridge=POOL_BTF_ADDRESS # local bridge for XMRig/SRBMiner ``` diff --git a/docs/pool-directory.md b/docs/pool-directory.md index 6456084..76269b9 100644 --- a/docs/pool-directory.md +++ b/docs/pool-directory.md @@ -4,10 +4,13 @@ Nodes discover pools through Nostr and `.btf`; the website only helps humans compare operators. -Operators can publish the `pool_status.json` file produced by: +Operators can publish the `pool_status.json` and `pool_rounds.json` files +produced by: ```bash -bitflash -nogui -gen -operator -poolstatusfile=/var/www/pool_status.json +bitflash -nogui -gen -operator \ + -poolstatusfile=/var/www/pool_status.json \ + -poolroundsfile=/var/www/pool_rounds.json ``` Build a static directory from one or more status files: @@ -23,6 +26,11 @@ The generated output contains: - `index.html`: human-readable ranking page. - `pools.json`: machine-readable API for `api.bitflash.network/pools.json`. +When a source is an HTTP(S) `pool_status.json`, the directory links to a +`pool_rounds.json` proof file next to it. That file records the block height, +block hash, coinbase txid, coinbase value, pool fee, total shares, per-address +share counts, computed payouts, and payout txids once paid. + Ranking order is intentionally simple: 1. online pools first; diff --git a/scripts/build-pool-index.py b/scripts/build-pool-index.py index 382b6c3..d3eb386 100644 --- a/scripts/build-pool-index.py +++ b/scripts/build-pool-index.py @@ -8,6 +8,7 @@ import html import json import os import time +import urllib.parse import urllib.request @@ -23,6 +24,10 @@ def normalize_pool(source, obj, now): operator = obj.get("operator", {}) pool = obj.get("pool", {}) node = obj.get("node", {}) + rounds_file = operator.get("roundsFile") or "pool_rounds.json" + rounds_url = "" + if source.startswith("http://") or source.startswith("https://"): + rounds_url = urllib.parse.urljoin(source, rounds_file) updated = int(obj.get("updatedAt") or 0) age = max(0, now - updated) if updated else None online = bool(node.get("poolRunning")) and age is not None and age <= 120 @@ -39,6 +44,8 @@ def normalize_pool(source, obj, now): "blocksFoundSession": int(pool.get("blocksFoundSession") or 0), "roundShares": int(pool.get("roundShares") or 0), "hashRate": float(pool.get("hashRate") or 0.0), + "roundsUrl": rounds_url, + "recentRounds": obj.get("recentRounds", []), } @@ -56,6 +63,7 @@ def render_html(pools, generated_at): rows = [] for p in pools: dash = p["dashboardUrl"] + rounds = p.get("roundsUrl", "") # Only linkify http(s). The dashboard URL comes from an operator's # self-published status file, so a "javascript:" or "data:" scheme # would become a clickable script-injection on this public page. @@ -65,6 +73,12 @@ def render_html(pools, generated_at): html.escape(dash, quote=True)) else: dash_html = "-" + rounds_lower = rounds.lower() + if rounds_lower.startswith("http://") or rounds_lower.startswith("https://"): + rounds_html = 'proof'.format( + html.escape(rounds, quote=True)) + else: + rounds_html = "-" rows.append( "" "{status}" @@ -76,6 +90,7 @@ def render_html(pools, generated_at): "{height}" "{age}" "{dash}" + "{rounds}" "".format( status="online" if p["online"] else "stale", name=html.escape(p["name"]), @@ -87,6 +102,7 @@ def render_html(pools, generated_at): height=p["height"], age="{}s".format(p["ageSeconds"]) if p["ageSeconds"] is not None else "-", dash=dash_html, + rounds=rounds_html, ) ) @@ -110,7 +126,7 @@ def render_html(pools, generated_at):

This directory is informational. Bitflash nodes discover pools through Nostr and .btf; this page only helps humans compare public operators.

- + {rows} diff --git a/src/main.cpp b/src/main.cpp index 2be9c54..2eb0e3a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -105,6 +105,7 @@ string strParticipantPool; // participant mode: pool .btf address string strPoolName = "Bitflash Pool"; string strPoolDashboardUrl; string strPoolStatusFile; +string strPoolRoundsFile; double dPoolFeePercent = 0.0; bool fStratumBridge = false; int nStratumBridgePort = 3333; diff --git a/src/main.h b/src/main.h index 2786a4a..eafbf16 100644 --- a/src/main.h +++ b/src/main.h @@ -87,6 +87,7 @@ extern string strParticipantPool; // participant: pool .btf address extern string strPoolName; // operator: announced pool name extern string strPoolDashboardUrl; // operator: optional dashboard URL extern string strPoolStatusFile; // operator: optional JSON status output path +extern string strPoolRoundsFile; // operator: optional public round proof path extern double dPoolFeePercent; // operator: announced fee percent extern bool fStratumBridge; // participant helper: local Stratum bridge extern int nStratumBridgePort; // local bridge listen port, default 3333 diff --git a/src/main_gui.cpp b/src/main_gui.cpp index ce6d2b5..7ad2075 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -126,6 +126,7 @@ static void PrintUsage() printf(" /poolfee=PCT\n"); printf(" /pooldashboard=URL (alias: /pooldash=URL)\n"); printf(" /poolstatusfile=PATH (write pool status JSON for dashboards)\n"); + printf(" /poolroundsfile=PATH (write public pool round proofs JSON)\n"); printf("\n"); printf(".btf and rendezvous:\n"); printf(" /connectbtf=PEER_BTF_ADDRESS\n"); @@ -262,6 +263,10 @@ static void ParseStartupArguments(int argc, char* argv[]) if (!poolStatusFile.empty()) strPoolStatusFile = poolStatusFile; + string poolRoundsFile = argval2(argc, argv, "/poolroundsfile", "-poolroundsfile"); + if (!poolRoundsFile.empty()) + strPoolRoundsFile = poolRoundsFile; + string btfConnect = argval2(argc, argv, "/connectbtf", "-connectbtf"); if (!btfConnect.empty()) strBtfConnect = btfConnect; diff --git a/src/readme.txt b/src/readme.txt index fa58b6e..b8b89c1 100644 --- a/src/readme.txt +++ b/src/readme.txt @@ -55,6 +55,10 @@ Command-line options (in addition to the original ones) /poolfee=PCT operator announcement fee percent (example: 0.75) /pooldashboard=URL operator announcement dashboard URL + /poolstatusfile=PATH + write pool status JSON for dashboards + /poolroundsfile=PATH + write public pool round proofs JSON /connectbtf=ADDR keep an outbound .btf connection to ADDR /rvrelay=HOST:PORT diff --git a/src/rpc.cpp b/src/rpc.cpp index 1aa4f9b..813f1d1 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp @@ -263,11 +263,47 @@ struct PendingPayout { static std::vector gPendingPayouts; static std::mutex gPayoutLedgerMutex; +struct PoolRoundProof { + std::string id; + int blockHeight; + int matureAtHeight; + int64 createdAt; + std::string blockHash; + std::string coinbaseTxid; + std::string foundBy; + double feePercent; + uint64 totalShares; + int64 coinbaseValue; + int64 subsidy; + int64 minerReward; + int64 operatorReward; + int skippedDust; + int64 forfeitedBadAddress; + std::map shares; + std::map payouts; + std::map paidTxids; + + PoolRoundProof() + : blockHeight(0), matureAtHeight(0), createdAt(0), feePercent(0.0), + totalShares(0), coinbaseValue(0), subsidy(0), minerReward(0), operatorReward(0), + skippedDust(0), forfeitedBadAddress(0) {} +}; +static std::vector gPoolRounds; +static std::mutex gPoolRoundsMutex; +static const size_t MAX_POOL_ROUNDS = 200; + static std::string PayoutFilePath() { return GetAppDir() + "/pending_payouts.json"; } +static std::string PoolRoundsFilePath() +{ + if (!strPoolRoundsFile.empty()) + return strPoolRoundsFile; + return GetAppDir() + "/pool_rounds.json"; +} + static std::string PayoutIdForBlock(int blockHeight) { return strprintf("block-%d", blockHeight); @@ -362,6 +398,167 @@ static void LoadPendingPayouts() } } +static nlohmann::json PoolRoundToJson(const PoolRoundProof& r) +{ + nlohmann::json j; + j["id"] = r.id; + j["blockHeight"] = r.blockHeight; + j["matureAtHeight"] = r.matureAtHeight; + j["createdAt"] = r.createdAt; + j["blockHash"] = r.blockHash; + j["coinbaseTxid"] = r.coinbaseTxid; + j["foundBy"] = r.foundBy; + j["feePercent"] = r.feePercent; + j["totalShares"] = r.totalShares; + j["coinbaseValueSatoshis"] = r.coinbaseValue; + j["coinbaseValueBtf"] = FormatMoney(r.coinbaseValue); + j["subsidySatoshis"] = r.subsidy; + j["subsidyBtf"] = FormatMoney(r.subsidy); + j["minerRewardSatoshis"] = r.minerReward; + j["minerRewardBtf"] = FormatMoney(r.minerReward); + j["operatorRewardSatoshis"] = r.operatorReward; + j["operatorRewardBtf"] = FormatMoney(r.operatorReward); + j["skippedDust"] = r.skippedDust; + j["forfeitedBadAddressSatoshis"] = r.forfeitedBadAddress; + j["forfeitedBadAddressBtf"] = FormatMoney(r.forfeitedBadAddress); + + j["shares"] = nlohmann::json::object(); + for (const auto& kv : r.shares) + j["shares"][kv.first] = kv.second; + + j["payouts"] = nlohmann::json::object(); + for (const auto& kv : r.payouts) { + nlohmann::json p; + p["amountSatoshis"] = kv.second; + p["amountBtf"] = FormatMoney(kv.second); + std::map::const_iterator tx = r.paidTxids.find(kv.first); + if (tx != r.paidTxids.end()) + p["txid"] = tx->second; + else + p["txid"] = nullptr; + j["payouts"][kv.first] = p; + } + return j; +} + +static void SavePoolRoundsLocked() +{ + nlohmann::json root; + root["schema"] = "bitflash-pool-rounds-1"; + root["updatedAt"] = GetTime(); + root["rounds"] = nlohmann::json::array(); + for (const PoolRoundProof& r : gPoolRounds) + root["rounds"].push_back(PoolRoundToJson(r)); + + std::string path = PoolRoundsFilePath(); + std::string tmp = path + ".tmp"; + FILE* f = fopen(tmp.c_str(), "w"); + if (!f) { + LogPrint("payout", "[payout] WARNING: could not write %s\n", tmp.c_str()); + return; + } + std::string s = root.dump(2); + fwrite(s.c_str(), 1, s.size(), f); + if (fclose(f) != 0) { + LogPrint("payout", "[payout] WARNING: could not close %s\n", tmp.c_str()); + return; + } +#ifdef _WIN32 + remove(path.c_str()); +#endif + if (rename(tmp.c_str(), path.c_str()) != 0) + LogPrint("payout", "[payout] WARNING: could not replace %s\n", path.c_str()); +} + +static void LoadPoolRounds() +{ + std::string path = PoolRoundsFilePath(); + FILE* f = fopen(path.c_str(), "r"); + if (!f) return; + fseek(f, 0, SEEK_END); + long sz = ftell(f); + fseek(f, 0, SEEK_SET); + std::string s(sz, '\0'); + fread(&s[0], 1, sz, f); + fclose(f); + + try { + nlohmann::json root = nlohmann::json::parse(s); + nlohmann::json arr = root.is_array() ? root : root["rounds"]; + std::lock_guard lk(gPoolRoundsMutex); + for (const auto& obj : arr) { + PoolRoundProof r; + if (obj.contains("id") && obj["id"].is_string()) r.id = obj["id"].get(); + if (obj.contains("blockHeight")) r.blockHeight = obj["blockHeight"].get(); + if (obj.contains("matureAtHeight")) r.matureAtHeight = obj["matureAtHeight"].get(); + if (obj.contains("createdAt")) r.createdAt = obj["createdAt"].get(); + if (obj.contains("blockHash") && obj["blockHash"].is_string()) r.blockHash = obj["blockHash"].get(); + if (obj.contains("coinbaseTxid") && obj["coinbaseTxid"].is_string()) r.coinbaseTxid = obj["coinbaseTxid"].get(); + if (obj.contains("foundBy") && obj["foundBy"].is_string()) r.foundBy = obj["foundBy"].get(); + if (obj.contains("feePercent")) r.feePercent = obj["feePercent"].get(); + if (obj.contains("totalShares")) r.totalShares = obj["totalShares"].get(); + if (obj.contains("coinbaseValueSatoshis")) r.coinbaseValue = obj["coinbaseValueSatoshis"].get(); + if (obj.contains("subsidySatoshis")) r.subsidy = obj["subsidySatoshis"].get(); + if (obj.contains("minerRewardSatoshis")) r.minerReward = obj["minerRewardSatoshis"].get(); + if (obj.contains("operatorRewardSatoshis")) r.operatorReward = obj["operatorRewardSatoshis"].get(); + if (obj.contains("skippedDust")) r.skippedDust = obj["skippedDust"].get(); + if (obj.contains("forfeitedBadAddressSatoshis")) r.forfeitedBadAddress = obj["forfeitedBadAddressSatoshis"].get(); + if (obj.contains("shares") && obj["shares"].is_object()) + for (auto it = obj["shares"].begin(); it != obj["shares"].end(); ++it) + r.shares[it.key()] = it.value().get(); + if (obj.contains("payouts") && obj["payouts"].is_object()) + for (auto it = obj["payouts"].begin(); it != obj["payouts"].end(); ++it) { + if (it.value().is_object()) { + r.payouts[it.key()] = it.value().value("amountSatoshis", (int64)0); + if (it.value().contains("txid") && it.value()["txid"].is_string()) + r.paidTxids[it.key()] = it.value()["txid"].get(); + } else if (it.value().is_number_integer()) { + r.payouts[it.key()] = it.value().get(); + } + } + if (!r.id.empty()) + gPoolRounds.push_back(r); + } + while (gPoolRounds.size() > MAX_POOL_ROUNDS) + gPoolRounds.erase(gPoolRounds.begin()); + LogPrint("payout", "[payout] loaded %zu pool round proof(s) from disk\n", + gPoolRounds.size()); + } catch (const std::exception& e) { + LogPrint("payout", "[payout] WARNING: failed to parse %s: %s\n", + path.c_str(), e.what()); + } +} + +static void RecordPoolRound(const PoolRoundProof& round) +{ + std::lock_guard lk(gPoolRoundsMutex); + for (PoolRoundProof& existing : gPoolRounds) { + if (existing.id == round.id) { + existing = round; + SavePoolRoundsLocked(); + return; + } + } + gPoolRounds.push_back(round); + while (gPoolRounds.size() > MAX_POOL_ROUNDS) + gPoolRounds.erase(gPoolRounds.begin()); + SavePoolRoundsLocked(); +} + +static void MarkPoolRoundPaid(const std::string& id, + const std::string& address, + const std::string& txid) +{ + std::lock_guard lk(gPoolRoundsMutex); + for (PoolRoundProof& r : gPoolRounds) { + if (r.id != id) + continue; + r.paidTxids[address] = txid; + SavePoolRoundsLocked(); + return; + } +} + // Payouts ready to execute are moved here under gPayoutExecMutex. // A dedicated thread drains this list so SendMoney (which takes cs_main) // never runs on the event loop thread and can't stall miner I/O. @@ -399,10 +596,28 @@ static bool FindRecordedPoolPayout(const std::string& id, } static void QueuePayouts(int blockHeight, + const std::string& blockHash, + const std::string& coinbaseTxid, + int64 coinbaseValue, + const std::string& foundBy, const std::map& shareCount, uint64 total) { + PoolRoundProof round; + round.id = PayoutIdForBlock(blockHeight); + round.blockHeight = blockHeight; + round.matureAtHeight = blockHeight + COINBASE_MATURITY; + round.createdAt = GetTime(); + round.blockHash = blockHash; + round.coinbaseTxid = coinbaseTxid; + round.coinbaseValue = coinbaseValue; + round.foundBy = foundBy; + round.feePercent = dPoolFeePercent; + round.totalShares = total; + round.shares = shareCount; + if (total == 0) { + RecordPoolRound(round); LogPrint("payout", "[payout] block %d: zero shares recorded -- nothing to pay out\n", blockHeight); return; @@ -418,9 +633,13 @@ static void QueuePayouts(int blockHeight, int64 nSubsidy = 50 * COIN; nSubsidy >>= (blockHeight / 210000); // halving schedule int64 minerReward = (int64)((double)nSubsidy * (1.0 - feeFrac)); + int64 operatorCut = nSubsidy - minerReward; + round.subsidy = nSubsidy; + round.minerReward = minerReward; + round.operatorReward = operatorCut; PendingPayout pp; - pp.id = PayoutIdForBlock(blockHeight); + pp.id = round.id; pp.blockHeight = blockHeight; pp.matureAtHeight = blockHeight + COINBASE_MATURITY; @@ -443,15 +662,18 @@ static void QueuePayouts(int blockHeight, continue; } pp.amounts[kv.first] = amount; + round.payouts[kv.first] = amount; } + round.skippedDust = skipped; + round.forfeitedBadAddress = burned; { std::lock_guard lk(gPayoutLedgerMutex); gPendingPayouts.push_back(pp); SavePendingPayoutsLocked(); } + RecordPoolRound(round); - int64 operatorCut = nSubsidy - minerReward; LogPrint("payout", "[payout] block %d: subsidy %s BTF, miners get %s" " (fee %.2f%% = %s to operator)," @@ -548,12 +770,14 @@ static void PayoutThreadFn(void*) LogPrint("payout", "[payout] recovered recorded payout %s to %s" " from wallet tx %s\n", FormatMoney(kv.second).c_str(), kv.first.c_str(), txid.c_str()); + MarkPoolRoundPaid(ppQueued.id, kv.first, txid); } else if (SendMoney(sc, kv.second, wtx)) { txid = wtx.GetHash().GetHex(); LogPrint("payout", "[payout] paid %s to %s\n", FormatMoney(kv.second).c_str(), kv.first.c_str()); + MarkPoolRoundPaid(ppQueued.id, kv.first, txid); } else { @@ -664,7 +888,8 @@ static void WritePoolStatusJsonLocked() {"name", strPoolName}, {"btfAddress", BtfLocalAddress()}, {"feePercent", dPoolFeePercent}, - {"dashboardUrl", strPoolDashboardUrl} + {"dashboardUrl", strPoolDashboardUrl}, + {"roundsFile", "pool_rounds.json"} }; j["node"] = { @@ -702,6 +927,30 @@ static void WritePoolStatusJsonLocked() }); } + j["recentRounds"] = nlohmann::json::array(); + { + std::lock_guard lk(gPoolRoundsMutex); + size_t start = gPoolRounds.size() > 10 ? gPoolRounds.size() - 10 : 0; + for (size_t i = start; i < gPoolRounds.size(); i++) { + const PoolRoundProof& r = gPoolRounds[i]; + int unpaid = 0; + for (const auto& kv : r.payouts) + if (r.paidTxids.count(kv.first) == 0) + unpaid++; + j["recentRounds"].push_back({ + {"id", r.id}, + {"blockHeight", r.blockHeight}, + {"blockHash", r.blockHash}, + {"coinbaseTxid", r.coinbaseTxid}, + {"totalShares", r.totalShares}, + {"payouts", (int)r.payouts.size()}, + {"unpaidPayouts", unpaid}, + {"coinbaseValueBtf", FormatMoney(r.coinbaseValue)}, + {"minerRewardBtf", FormatMoney(r.minerReward)} + }); + } + } + std::string path = PoolStatusFilePath(); std::string tmp = path + ".tmp"; FILE* f = fopen(tmp.c_str(), "w"); @@ -1231,7 +1480,9 @@ static bool HandleLine(Miner* m, const std::string& rawLine, if (accepted) { // Snapshot height before RebuildJob() (which increments it) int foundHeight = gCurrentJob.height; - QueuePayouts(foundHeight, roundShareCount, roundShareTotal); + QueuePayouts(foundHeight, b.GetHash().GetHex(), b.vtx[0].GetHash().GetHex(), + b.vtx[0].vout[0].nValue, m->address, + roundShareCount, roundShareTotal); // Reset round shares roundShareCount.clear(); roundShareTotal = 0; @@ -1333,6 +1584,62 @@ int RunPoolStratumSelfTest() HandleLine(&miner, stale.dump(), roundShareCount, roundShareTotal, blocksFound); check(roundShareTotal == 1, "stale submit does not increment shares"); + std::vector savedRounds = gPoolRounds; + std::string savedRoundsFile = strPoolRoundsFile; + std::string proofPath = "pool_rounds_selftest.json"; + remove(proofPath.c_str()); + remove((proofPath + ".tmp").c_str()); + gPoolRounds.clear(); + strPoolRoundsFile = proofPath; + + PoolRoundProof proof; + proof.id = "selftest-round"; + proof.blockHeight = 42; + proof.matureAtHeight = 42 + COINBASE_MATURITY; + proof.createdAt = GetTime(); + proof.blockHash = block.GetHash().GetHex(); + proof.coinbaseTxid = coinbase.GetHash().GetHex(); + proof.foundBy = miner.address; + proof.feePercent = 1.25; + proof.totalShares = 3; + proof.coinbaseValue = 50 * COIN; + proof.subsidy = 50 * COIN; + proof.minerReward = 49 * COIN; + proof.operatorReward = 1 * COIN; + proof.shares[miner.address] = 3; + proof.payouts[miner.address] = 49 * COIN; + RecordPoolRound(proof); + MarkPoolRoundPaid(proof.id, miner.address, "selftest-txid"); + + bool proofOk = false; + FILE* pf = fopen(proofPath.c_str(), "r"); + if (pf) { + fseek(pf, 0, SEEK_END); + long sz = ftell(pf); + fseek(pf, 0, SEEK_SET); + std::string s(sz, '\0'); + fread(&s[0], 1, sz, pf); + fclose(pf); + try { + json root = json::parse(s); + proofOk = + root.value("schema", "") == "bitflash-pool-rounds-1" && + root["rounds"].is_array() && + root["rounds"].size() == 1 && + root["rounds"][0].value("id", "") == proof.id && + root["rounds"][0]["shares"].value(miner.address, (uint64)0) == 3 && + root["rounds"][0]["payouts"][miner.address].value("txid", "") == "selftest-txid"; + } catch (...) { + proofOk = false; + } + } + check(proofOk, "pool round proof ledger is written and paid txid is recorded"); + + remove(proofPath.c_str()); + remove((proofPath + ".tmp").c_str()); + strPoolRoundsFile = savedRoundsFile; + gPoolRounds = savedRounds; + gCurrentJob = savedJob; gHaveJob = savedHaveJob; @@ -1754,6 +2061,7 @@ void ThreadRPCServer(void*) } LoadPendingPayouts(); + LoadPoolRounds(); _beginthread(AcceptLoopFn, 0, NULL); _beginthread(ThreadBtfPoolAnnouncer, 0, NULL); -- cgit v1.2.3 From d2679e2c09c17e4af50535c13324ba2ddb2eb4ad Mon Sep 17 00:00:00 2001 From: Bitflash Date: Mon, 10 Aug 2026 11:50:10 -0300 Subject: tools: static block explorer generator (v0) --- scripts/build-explorer.py | 243 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100755 scripts/build-explorer.py (limited to 'scripts') diff --git a/scripts/build-explorer.py b/scripts/build-explorer.py new file mode 100755 index 0000000..7849e8a --- /dev/null +++ b/scripts/build-explorer.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 +# +# Build a static Bitflash block explorer (v0) from a node's blk0001.dat. +# Emits into : +# blocks.json summary of every main-chain block (newest first) +# block/.json full detail per block (header + all txs) +# index.html self-contained viewer (vanilla JS, same-origin fetch) +# +# Scope (v0, deliberately): block list, block detail, and coinbase. No mempool, +# no per-address history. On this chain the coinbase payout key rotates every +# block, so an address index would be nearly useless; block-level transparency is +# what lets miners and users confirm the chain is honest. +# +# Usage: build-explorer.py + +import sys, os, json, hashlib, struct + +MAGIC = bytes([0xbf, 0x20, 0x5c, 0xfd]) +ADDRESS_VERSION = 25 # Bitflash addresses start with "B" + +def sha256d(b): return hashlib.sha256(hashlib.sha256(b).digest()).digest() + +B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" +def b58check(payload): + v = bytes([ADDRESS_VERSION]) + payload + v += sha256d(v)[:4] + n = int.from_bytes(v, 'big') + s = "" + while n > 0: + n, r = divmod(n, 58); s = B58[r] + s + for byte in v: + if byte == 0: s = "1" + s + else: break + return s + +def hash160(b): + return hashlib.new('ripemd160', hashlib.sha256(b).digest()).digest() + +class R: + def __init__(s, b, o=0): s.b = b; s.o = o + def u(s, n): v = s.b[s.o:s.o+n]; s.o += n; return v + def i32(s): return struct.unpack(' OP_EQUALVERIFY OP_CHECKSIG + if len(spk) == 25 and spk[0] == 0x76 and spk[1] == 0xa9 and spk[2] == 0x14 and spk[23] == 0x88 and spk[24] == 0xac: + return {"type": "p2pkh", "address": b58check(spk[3:23])} + # P2PK: OP_CHECKSIG (65- or 33-byte key) + if len(spk) >= 35 and spk[-1] == 0xac: + if spk[0] == 0x41 and len(spk) == 67: + pk = spk[1:66]; return {"type": "p2pk", "pubkey": pk.hex(), "address": b58check(hash160(pk))} + if spk[0] == 0x21 and len(spk) == 35: + pk = spk[1:34]; return {"type": "p2pk", "pubkey": pk.hex(), "address": b58check(hash160(pk))} + return {"type": "other", "hex": spk.hex()[:80]} + +def parse_tx(r): + start = r.o + r.i32() + nin = r.cs(); vin = [] + for _ in range(nin): + ph = r.u(32); idx = r.u32(); r.script(); r.u32() + coinbase = (ph == b'\x00' * 32 and idx == 0xffffffff) + vin.append({"coinbase": coinbase, "prev": (None if coinbase else ph[::-1].hex()), "vout": (None if coinbase else idx)}) + nout = r.cs(); vout = [] + for _ in range(nout): + val = r.i64(); spk = r.script(); vout.append(dict({"value": val}, **spk_info(spk))) + r.u32() + raw = r.b[start:r.o] + return {"txid": sha256d(raw)[::-1].hex(), "vin": vin, "vout": vout, "size": len(raw)} + +def main(): + if len(sys.argv) != 3: + print("usage: build-explorer.py ", file=sys.stderr); sys.exit(2) + blkpath, out = sys.argv[1], sys.argv[2] + data = open(blkpath, 'rb').read() + blocks = {}; o = 0; n = len(data) + while o + 8 <= n: + if data[o:o+4] != MAGIC: o += 1; continue + o += 4; size = struct.unpack(' + + + + +Bitflash Block Explorer + + + +
+

Bitflash Block Explorer

+ + + +
+
+

Read-only view of the main chain: blocks, their transactions, and the coinbase payout. There is no mempool and no per-address history here. On Bitflash the coinbase payout key rotates every block, so an address index would say almost nothing; what matters is that every block and every coinbase is here to check.

+
+ +
StatusPoolFeeMinersH/sBlocksHeightAgeLink
StatusPoolFeeMinersH/sBlocksHeightAgeLinkProof
+ + +
HeightTime (UTC)TxsSizeCoinbaseTo
+

+ + + + + + +""" + +if __name__ == "__main__": + main() -- cgit v1.2.3 From 5550481af68add9616a3410bb4db35abfdb70d9b Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Mon, 10 Aug 2026 12:31:38 -0300 Subject: tools: add fair launch verifier --- README.md | 11 + docs/fair-launch.md | 69 +++++++ scripts/verify-fair-launch.py | 452 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 532 insertions(+) create mode 100644 docs/fair-launch.md create mode 100644 scripts/verify-fair-launch.py (limited to 'scripts') diff --git a/README.md b/README.md index ec8e9c4..b4c3b8b 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,17 @@ scripts/verify-release.sh latest See [release verification](docs/release-verification.md) for the full release audit flow and the maintainer signing step. +Bitflash also ships a deterministic fair-launch verifier: + +```bash +python3 scripts/verify-fair-launch.py --max-blocks 1000 --out bitflash-fair-launch-report.json --json +``` + +The report reads the local chain directly, checks the genesis launch baseline, +and produces canonical JSON whose hash can be compared by independent auditors +or timestamped externally with OpenTimestamps. See +[fair-launch verification](docs/fair-launch.md). + **Keep your node current.** Consensus rules have changed since the first releases — 1.2.1 fixed a bug that let anyone spend anyone's coins, and 1.2.2 added a per-block signature-operation cap. A node on an older build will accept diff --git a/docs/fair-launch.md b/docs/fair-launch.md new file mode 100644 index 0000000..79eca8e --- /dev/null +++ b/docs/fair-launch.md @@ -0,0 +1,69 @@ +# Bitflash fair-launch verifier + +Bitflash ships a deterministic fair-launch audit script: + +```bash +python3 scripts/verify-fair-launch.py --max-blocks 1000 --out bitflash-fair-launch-report.json --json +``` + +The verifier reads `blk*.dat` directly from the local Bitflash data directory. +It does not use Berkeley DB, does not call the node binary, and does not need +network access. + +The report proves the launch baseline that can be checked from the chain: + +- the genesis block hash matches the source constant; +- the genesis merkle root matches the source constant; +- the genesis transaction is a single coinbase transaction; +- the genesis output total is exactly `50.00000000 BTF`; +- the scanned block range and mined coinbase total are reproducible. + +For machines that do not have a synced data directory yet, the source constants +can be checked without disk blocks: + +```bash +python3 scripts/verify-fair-launch.py --reconstruct-genesis +``` + +## Deterministic output + +Use `--json` or `--out` for canonical JSON. The JSON is written with sorted +keys, fixed separators, ASCII escaping, and one trailing newline. It deliberately +does not include local paths or wall-clock generation time, so two auditors +scanning the same block range produce the same bytes. + +That makes the report shareable: + +```bash +python3 scripts/verify-fair-launch.py --max-blocks 1000 --out report.json --json +sha256sum report.json +``` + +If another auditor runs the same command against the same chain prefix, the +`sha256sum` should match. + +## OpenTimestamps + +OpenTimestamps is optional and external. It anchors the report in Bitcoin +without adding any bridge or dependency to Bitflash consensus. + +After generating a canonical JSON report: + +```bash +ots stamp report.json +ots verify report.json.ots +``` + +The `.ots` file proves that the exact report bytes existed no later than the +Bitcoin block that confirms the timestamp. + +## What this does not claim + +This is not a retroactive genesis commitment. The Bitflash genesis block already +exists, so adding a new on-chain commitment to genesis would create a different +chain. This verifier instead audits the launch state that is already on-chain +and makes the result reproducible and timestampable. + +It also does not prove real-world miner identity. It proves the genesis state, +the absence of non-coinbase premine in genesis, and the deterministic coinbase +totals over the scanned prefix. diff --git a/scripts/verify-fair-launch.py b/scripts/verify-fair-launch.py new file mode 100644 index 0000000..ce396fb --- /dev/null +++ b/scripts/verify-fair-launch.py @@ -0,0 +1,452 @@ +#!/usr/bin/env python3 +"""Produce a deterministic Bitflash fair-launch audit report. + +This verifier is intentionally self-contained. It parses Bitflash block files +directly, without Berkeley DB, without the node binary, and without network +access. The canonical JSON output is stable for the same inputs and can be +timestamped later with OpenTimestamps or any other external notary. +""" + +import argparse +import datetime as _dt +import hashlib +import json +import os +import struct +import sys +from pathlib import Path + + +MAGIC = bytes([0xBF, 0x20, 0x5C, 0xFD]) +COIN = 100_000_000 +GENESIS_HASH = "5bd7cb255d814e48cebcdfb72da4dc87b34bd774227f8ceb546c5640f4bdc169" +GENESIS_MERKLE = "1a45b4482532abb29b10e234d3f13132230525a339ecea91658ffa675a5b1325" +GENESIS_TIME = 1_753_315_200 +GENESIS_BITS = 0x1F0FFFFF +GENESIS_NONCE = 3141 +GENESIS_TEXT = "Bitflash 24/Jul/2026 Fair launch: one CPU one vote, no premine" + + +class ParseError(Exception): + pass + + +class Reader: + def __init__(self, data): + self.data = data + self.pos = 0 + + def take(self, n): + if self.pos + n > len(self.data): + raise ParseError("unexpected end of data") + out = self.data[self.pos:self.pos + n] + self.pos += n + return out + + def u8(self): + return self.take(1)[0] + + def u32(self): + return struct.unpack(" 1: + nxt = [] + for i in range(0, len(layer), 2): + left = layer[i] + right = layer[i + 1] if i + 1 < len(layer) else left + nxt.append(sha256d(left + right)) + layer = nxt + return layer[0][::-1].hex() + + +def parse_block(payload, file_name="", offset=0): + r = Reader(payload) + header = r.take(80) + version, = struct.unpack(" len(data): + break + size = struct.unpack(" len(data): + break + blocks.append(parse_block(data[start:end], path.name, start)) + if limit and len(blocks) >= limit: + return blocks + pos = end + return blocks + + +def genesis_transaction(): + script_sig = ( + bytes.fromhex("04ffff001d") + + b"\x01\x04" + + bytes([len(GENESIS_TEXT)]) + + GENESIS_TEXT.encode("ascii") + ) + pubkey_num = bytes.fromhex( + "5f1df16b2b704c8a578d0bbaf74d385cde12c11ee50455f3c438ef4c3fbcf649" + "b6de611feae06279a60939e028a8d65c10b73071a6f16719274855feb0fd8a6704" + ) + # The genesis source uses CScript << CBigNum("0x..."), and CBigNum::getvch() + # returns the minimal little-endian script number representation. + script_pub_key = bytes([65]) + pubkey_num[::-1] + b"\xac" + tx = { + "version": 1, + "vin": [{ + "prevout_hash": "0" * 64, + "prevout_n": 0xFFFFFFFF, + "script_sig_hex": script_sig.hex(), + "sequence": 0xFFFFFFFF, + }], + "vout": [{ + "value_satoshis": 50 * COIN, + "value_btf": format_money(50 * COIN), + "script_pub_key_hex": script_pub_key.hex(), + }], + "lock_time": 0, + } + tx["txid"] = hash_hex(ser_tx(tx)) + return tx + + +def reconstructed_genesis(): + tx = genesis_transaction() + header = ( + struct.pack("") + + +def main(argv): + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--datadir", default=default_datadir(), help="Bitflash data directory") + ap.add_argument("--max-blocks", type=int, default=100, help="number of blocks to scan from blk*.dat") + ap.add_argument("--json", action="store_true", help="print canonical JSON") + ap.add_argument("--out", help="write canonical JSON to this path") + ap.add_argument("--reconstruct-genesis", action="store_true", + help="do not read disk; reconstruct only the genesis block from source constants") + args = ap.parse_args(argv) + + if args.reconstruct_genesis: + blocks = [reconstructed_genesis()] + source_mode = "reconstructed-genesis" + datadir = "" + else: + datadir = Path(args.datadir) + blocks = read_blocks(datadir, args.max_blocks) + if not blocks: + raise SystemExit("no Bitflash blocks found in %s; use --reconstruct-genesis for a source-only check" % datadir) + source_mode = "local-block-files" + + report = build_report(blocks, source_mode) + data = canonical_bytes(report) + + if args.out: + Path(args.out).write_bytes(data) + if args.json: + sys.stdout.buffer.write(data) + else: + print_text(report) + return 0 if report["verdict"] == "FAIR_LAUNCH_BASELINE_VERIFIED" else 1 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) -- cgit v1.2.3 From ccf4489dbdf9e9d691e02e867562518c92168007 Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Mon, 10 Aug 2026 14:49:21 -0300 Subject: tools: add deterministic UTXO commitment verifier --- README.md | 11 ++ docs/utxo-commitment.md | 93 +++++++++ scripts/verify-utxo-set.py | 464 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 568 insertions(+) create mode 100644 docs/utxo-commitment.md create mode 100644 scripts/verify-utxo-set.py (limited to 'scripts') diff --git a/README.md b/README.md index ec8e9c4..0827ac1 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,17 @@ scripts/verify-release.sh latest See [release verification](docs/release-verification.md) for the full release audit flow and the maintainer signing step. +Bitflash also ships a deterministic UTXO-set commitment tool: + +```bash +python3 scripts/verify-utxo-set.py --out utxo-report.json --json +``` + +It reconstructs the best chain from local block files, computes the current +UTXO root and supply, and writes canonical JSON that independent auditors can +compare or timestamp externally with OpenTimestamps. See +[UTXO-set commitments](docs/utxo-commitment.md). + **Keep your node current.** Consensus rules have changed since the first releases — 1.2.1 fixed a bug that let anyone spend anyone's coins, and 1.2.2 added a per-block signature-operation cap. A node on an older build will accept diff --git a/docs/utxo-commitment.md b/docs/utxo-commitment.md new file mode 100644 index 0000000..ddf5450 --- /dev/null +++ b/docs/utxo-commitment.md @@ -0,0 +1,93 @@ +# Bitflash UTXO-set commitment + +`scripts/verify-utxo-set.py` reconstructs a deterministic commitment to the +current UTXO set from local `blk*.dat` files. + +This is intentionally not a consensus rule yet. It is an audit tool: independent +operators can run it against the same chain prefix and compare the resulting +root and canonical JSON hash. + +```bash +python3 scripts/verify-utxo-set.py --max-blocks 1000 --out utxo-report.json --json +``` + +Use `--max-blocks 0` or omit the option to scan every available block file: + +```bash +python3 scripts/verify-utxo-set.py --out utxo-report.json --json +``` + +## What It Verifies + +The verifier: + +- parses Bitflash block files directly; +- reconstructs the best chain rooted at the Bitflash genesis block; +- ignores side blocks that may also be present in `blk*.dat`; +- verifies every scanned block merkle root; +- applies spends and creates outputs in deterministic order; +- reports current UTXO count, current supply, mined coinbase total, fees, and + historical duplicate-output overwrites; +- computes a deterministic UTXO root. + +## Commitment Algorithm + +The UTXO map is keyed by: + +```text +txid:vout +``` + +Keys are sorted lexicographically. Each leaf is: + +```text +SHA256("BTFUTXO1|" || outpoint || "|" || value_satoshis || "|" || + script_pub_key_hex || "|" || created_height || "|" || created_txid) +``` + +The tree is then reduced pairwise. If a level has an odd number of hashes, the +last hash is duplicated. Each parent is: + +```text +SHA256("BTFNODE1|" || left_hash || right_hash) +``` + +The final hash is the UTXO-set root. + +## Deterministic Output + +`--json` and `--out` write canonical JSON with sorted keys, fixed separators, +ASCII escaping, and one trailing newline. It contains no local path and no +generation timestamp, so two auditors scanning the same chain prefix should +produce byte-identical reports. + +```bash +python3 scripts/verify-utxo-set.py --out utxo-report.json --json +sha256sum utxo-report.json +``` + +## OpenTimestamps + +The canonical JSON can be timestamped in Bitcoin without adding any dependency +to Bitflash consensus: + +```bash +ots stamp utxo-report.json +ots verify utxo-report.json.ots +``` + +## Historical Duplicates + +Bitflash inherited early Bitcoin behavior around duplicate transaction IDs. +The verifier reports duplicate-output overwrites as warnings and uses +last-write-wins semantics for the UTXO map, matching the legacy index model. +Those warnings are part of the report so auditors can see the historical debt +instead of relying on a polished summary. + +## Future Consensus Path + +This tool is the low-risk first step toward consensus commitments. A future +hard-fork design can commit this root, or a Utreexo-style accumulator root, in +coinbase or block headers after a fixed activation height. Until then this +script provides reproducible supply and state commitments without changing what +nodes accept. diff --git a/scripts/verify-utxo-set.py b/scripts/verify-utxo-set.py new file mode 100644 index 0000000..12b024f --- /dev/null +++ b/scripts/verify-utxo-set.py @@ -0,0 +1,464 @@ +#!/usr/bin/env python3 +"""Build a deterministic Bitflash UTXO-set commitment from local blk*.dat. + +This is deliberately outside consensus. It lets auditors reconstruct the chain +state from disk blocks and compare the same canonical UTXO root independently. +""" + +import argparse +import datetime as _dt +import hashlib +import json +import os +import struct +import sys +from pathlib import Path + + +MAGIC = bytes([0xBF, 0x20, 0x5C, 0xFD]) +COIN = 100_000_000 +GENESIS_HASH = "5bd7cb255d814e48cebcdfb72da4dc87b34bd774227f8ceb546c5640f4bdc169" +GENESIS_MERKLE = "1a45b4482532abb29b10e234d3f13132230525a339ecea91658ffa675a5b1325" + + +class ParseError(Exception): + pass + + +class Reader: + def __init__(self, data): + self.data = data + self.pos = 0 + + def take(self, n): + if self.pos + n > len(self.data): + raise ParseError("unexpected end of data") + out = self.data[self.pos:self.pos + n] + self.pos += n + return out + + def u8(self): + return self.take(1)[0] + + def u32(self): + return struct.unpack(" 1: + nxt = [] + for i in range(0, len(layer), 2): + left = layer[i] + right = layer[i + 1] if i + 1 < len(layer) else left + nxt.append(sha256d(left + right)) + layer = nxt + return layer[0][::-1].hex() + + +def parse_block(payload, file_name="", offset=0): + r = Reader(payload) + header = r.take(80) + version, = struct.unpack(" len(data): + break + size = struct.unpack(" len(data): + break + blocks.append(parse_block(data[start:end], path.name, start)) + if max_blocks and len(blocks) >= max_blocks: + return blocks + pos = end + return blocks + + +def select_main_chain(blocks): + by_hash = {} + for block in blocks: + by_hash.setdefault(block["hash"], block) + if GENESIS_HASH not in by_hash: + raise ParseError("genesis block not found in block files") + + children = {} + for block in by_hash.values(): + children.setdefault(block["previous_hash"], []).append(block) + for v in children.values(): + v.sort(key=lambda b: (b["hash"], b["source_file"], b["source_offset"])) + + best = [] + stack = [(by_hash[GENESIS_HASH], [by_hash[GENESIS_HASH]])] + while stack: + block, path = stack.pop() + if len(path) > len(best) or (len(path) == len(best) and block["hash"] < best[-1]["hash"]): + best = path + for child in children.get(block["hash"], []): + stack.append((child, path + [child])) + return best + + +def is_coinbase(tx): + return ( + len(tx["vin"]) == 1 + and tx["vin"][0]["prevout_hash"] == "0" * 64 + and tx["vin"][0]["prevout_n"] == 0xFFFFFFFF + ) + + +def utxo_leaf_hash(outpoint, utxo): + # Text fields are length-delimited by separators and all variable data is + # hex or decimal ASCII. That keeps the commitment language-neutral. + payload = "|".join([ + "BTFUTXO1", + outpoint, + str(utxo["value_satoshis"]), + utxo["script_pub_key_hex"], + str(utxo["created_height"]), + utxo["created_txid"], + ]).encode("ascii") + return sha256(payload) + + +def utxo_root(utxos): + leaves = [utxo_leaf_hash(k, utxos[k]) for k in sorted(utxos)] + if not leaves: + return sha256(b"BTFUTXO1|empty").hex() + layer = leaves + while len(layer) > 1: + nxt = [] + for i in range(0, len(layer), 2): + left = layer[i] + right = layer[i + 1] if i + 1 < len(layer) else left + nxt.append(sha256(b"BTFNODE1|" + left + right)) + layer = nxt + return layer[0].hex() + + +def apply_blocks(blocks): + utxos = {} + spent = 0 + missing_spends = [] + duplicate_outputs = [] + total_coinbase = 0 + total_fees = 0 + normal_txs = 0 + + for height, block in enumerate(blocks): + if block["computed_merkle_root"] != block["merkle_root"]: + raise ParseError("bad merkle root at height %d" % height) + for tx_index, tx in enumerate(block["transactions"]): + coinbase = is_coinbase(tx) + input_value = 0 + output_value = sum(out["value_satoshis"] for out in tx["vout"]) + if coinbase: + total_coinbase += output_value + else: + normal_txs += 1 + for txin in tx["vin"]: + prev = "%s:%d" % (txin["prevout_hash"], txin["prevout_n"]) + prev_utxo = utxos.pop(prev, None) + if prev_utxo is None: + missing_spends.append({ + "height": height, + "txid": tx["txid"], + "missing_outpoint": prev, + }) + else: + spent += prev_utxo["value_satoshis"] + input_value += prev_utxo["value_satoshis"] + if input_value >= output_value: + total_fees += input_value - output_value + for n, out in enumerate(tx["vout"]): + key = "%s:%d" % (tx["txid"], n) + if key in utxos: + duplicate_outputs.append({"height": height, "outpoint": key}) + utxos[key] = { + "txid": tx["txid"], + "vout": n, + "value_satoshis": out["value_satoshis"], + "value_btf": out["value_btf"], + "script_pub_key_hex": out["script_pub_key_hex"], + "created_height": height, + "created_block_hash": block["hash"], + "created_txid": tx["txid"], + "coinbase": coinbase, + } + + total_unspent = sum(u["value_satoshis"] for u in utxos.values()) + return { + "utxos": utxos, + "stats": { + "utxo_count": len(utxos), + "spent_outputs": spent, + "total_unspent_satoshis": total_unspent, + "total_unspent_btf": format_money(total_unspent), + "total_coinbase_satoshis": total_coinbase, + "total_coinbase_btf": format_money(total_coinbase), + "coinbase_minus_unspent_satoshis": total_coinbase - total_unspent, + "coinbase_minus_unspent_btf": format_money(total_coinbase - total_unspent), + "total_fees_satoshis": total_fees, + "total_fees_btf": format_money(total_fees), + "normal_transactions": normal_txs, + "missing_spends": missing_spends, + "duplicate_outputs": duplicate_outputs, + }, + } + + +def format_money(value): + sign = "-" if value < 0 else "" + value = abs(value) + return "%s%d.%08d" % (sign, value // COIN, value % COIN) + + +def utc(ts): + return _dt.datetime.fromtimestamp(ts, _dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + +def default_datadir(): + if os.name == "nt" and os.environ.get("APPDATA"): + return os.path.join(os.environ["APPDATA"], "Bitflash") + if os.environ.get("HOME"): + return os.path.join(os.environ["HOME"], ".bitflash") + return "." + + +def canonical_bytes(report): + return (json.dumps(report, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + "\n").encode("ascii") + + +def build_report(raw_blocks, include_utxos=False): + blocks = select_main_chain(raw_blocks) + state = apply_blocks(blocks) + utxos = state["utxos"] + stats = state["stats"] + tip = blocks[-1] + root = utxo_root(utxos) + checks = { + "genesis_hash_matches_source": blocks[0]["hash"] == GENESIS_HASH, + "genesis_merkle_matches_source": blocks[0]["merkle_root"] == GENESIS_MERKLE, + "all_scanned_merkle_roots_match": all(b["computed_merkle_root"] == b["merkle_root"] for b in blocks), + "no_missing_spends": len(stats["missing_spends"]) == 0, + } + warnings = { + "legacy_duplicate_output_overwrites": len(stats["duplicate_outputs"]), + } + report = { + "schema": "bitflash-utxo-set-commitment-1", + "deterministic": True, + "chain": { + "name": "Bitflash", + "ticker": "BTF", + "message_start_hex": MAGIC.hex(), + "coin": COIN, + }, + "commitment": { + "algorithm": "sorted-outpoint-merkle-sha256", + "leaf_domain": "BTFUTXO1", + "node_domain": "BTFNODE1", + "root": root, + }, + "scanned": { + "raw_block_count": len(raw_blocks), + "side_blocks_ignored": len(raw_blocks) - len(blocks), + "main_chain_block_count": len(blocks), + "first_height": 0, + "last_height": len(blocks) - 1, + "tip_hash": tip["hash"], + "tip_time": tip["time"], + "tip_time_utc": tip["time_utc"], + }, + "stats": stats, + "checks": checks, + "warnings": warnings, + "verdict": "UTXO_COMMITMENT_VERIFIED" if all(checks.values()) else "CHECKS_FAILED", + } + if include_utxos: + report["utxos"] = {k: utxos[k] for k in sorted(utxos)} + return report + + +def print_text(report): + data_hash = hashlib.sha256(canonical_bytes(report)).hexdigest() + print("Bitflash UTXO-set commitment verifier") + print("schema: %s" % report["schema"]) + print("canonical report sha256: %s" % data_hash) + print() + print("Commitment") + print(" root: %s" % report["commitment"]["root"]) + print(" algo: %s" % report["commitment"]["algorithm"]) + print() + print("Scanned") + print(" main chain blocks: %d" % report["scanned"]["main_chain_block_count"]) + print(" raw disk blocks: %d" % report["scanned"]["raw_block_count"]) + print(" side ignored: %d" % report["scanned"]["side_blocks_ignored"]) + print(" height: %d..%d" % (report["scanned"]["first_height"], report["scanned"]["last_height"])) + print(" tip: %s" % report["scanned"]["tip_hash"]) + print() + print("Supply") + print(" utxos: %d" % report["stats"]["utxo_count"]) + print(" supply: %s BTF" % report["stats"]["total_unspent_btf"]) + print(" mined: %s BTF" % report["stats"]["total_coinbase_btf"]) + print(" delta: %s BTF" % report["stats"]["coinbase_minus_unspent_btf"]) + print(" fees: %s BTF" % report["stats"]["total_fees_btf"]) + if report["warnings"]["legacy_duplicate_output_overwrites"]: + print(" legacy duplicate overwrites: %d" % report["warnings"]["legacy_duplicate_output_overwrites"]) + print() + print("Checks") + for key in sorted(report["checks"]): + print(" %s %s" % ("OK " if report["checks"][key] else "FAIL", key)) + print() + print("Verdict: %s" % report["verdict"]) + print() + print("OpenTimestamps") + print(" canonical JSON can be stamped later with: ots stamp ") + + +def main(argv): + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--datadir", default=default_datadir(), help="Bitflash data directory") + ap.add_argument("--max-blocks", type=int, default=0, + help="number of blocks to scan; 0 means all block files") + ap.add_argument("--include-utxos", action="store_true", + help="include the full sorted UTXO map in JSON output") + ap.add_argument("--json", action="store_true", help="print canonical JSON") + ap.add_argument("--out", help="write canonical JSON to this path") + args = ap.parse_args(argv) + + blocks = read_blocks(args.datadir, args.max_blocks) + if not blocks: + raise SystemExit("no Bitflash blocks found in %s" % args.datadir) + report = build_report(blocks, include_utxos=args.include_utxos) + data = canonical_bytes(report) + if args.out: + Path(args.out).write_bytes(data) + if args.json: + sys.stdout.buffer.write(data) + else: + print_text(report) + return 0 if report["verdict"] == "UTXO_COMMITMENT_VERIFIED" else 1 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) -- cgit v1.2.3 From 7265a381405125aad5d91c3a11e9b619c9518c10 Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Mon, 10 Aug 2026 16:47:44 -0300 Subject: tools: add Bitflash mining calculator --- README.md | 13 +++ docs/mining-calculator.md | 86 ++++++++++++++ scripts/bitflash-mining-calculator.py | 206 ++++++++++++++++++++++++++++++++++ 3 files changed, 305 insertions(+) create mode 100644 docs/mining-calculator.md create mode 100644 scripts/bitflash-mining-calculator.py (limited to 'scripts') diff --git a/README.md b/README.md index ec8e9c4..d2e969a 100644 --- a/README.md +++ b/README.md @@ -237,6 +237,19 @@ SRBMiner-MULTI --algorithm randomx --pool 127.0.0.1:3333 --wallet YOUR_BTF_ADDRE xmrig -a rx/0 -o 127.0.0.1:3333 -u YOUR_BTF_ADDRESS -p x ``` +Estimate rewards and electricity cost locally: + +```bash +python3 scripts/bitflash-mining-calculator.py \ + --hashrate 21 --hashrate-unit kh/s \ + --network-hashrate 1.56 --network-hashrate-unit mh/s \ + --watts 140 --kwh-cost 0.10 \ + --price 0.001 --pool-fee 1 +``` + +See [mining calculator](docs/mining-calculator.md). Price is manual until +Bitflash has a reliable public market. + --- ## Headless / server mode diff --git a/docs/mining-calculator.md b/docs/mining-calculator.md new file mode 100644 index 0000000..352eea1 --- /dev/null +++ b/docs/mining-calculator.md @@ -0,0 +1,86 @@ +# Bitflash mining calculator + +`scripts/bitflash-mining-calculator.py` is a local WhatToMine-style calculator +for Bitflash. + +It does not fetch market prices, does not talk to exchanges, and does not +change consensus. Price is a manual input until Bitflash has a reliable public +market. + +## Examples + +Estimate from network hashrate: + +```bash +python3 scripts/bitflash-mining-calculator.py \ + --hashrate 21 --hashrate-unit kh/s \ + --network-hashrate 1.56 --network-hashrate-unit mh/s \ + --watts 140 --kwh-cost 0.10 \ + --price 0.001 --pool-fee 1 +``` + +Estimate from Bitflash difficulty: + +```bash +python3 scripts/bitflash-mining-calculator.py \ + --hashrate 21 --hashrate-unit kh/s \ + --difficulty 45600 \ + --watts 140 --kwh-cost 0.10 +``` + +Machine-readable output: + +```bash +python3 scripts/bitflash-mining-calculator.py \ + --hashrate 21 --hashrate-unit kh/s \ + --network-hashrate 1.56 --network-hashrate-unit mh/s \ + --json +``` + +## Bitflash Difficulty Scale + +Bitflash does not use Bitcoin's SHA256 difficulty-1 constant for pool shares. +The chain's easiest RandomX target is `bnProofOfWorkLimit = ~uint256(0) >> 12`, +so difficulty 1 expects about `4096` hashes. + +The calculator uses: + +```text +network_hashrate = difficulty * 4096 / block_time_seconds +``` + +At the default two-minute block target: + +```text +network_hashrate = difficulty * 4096 / 120 +``` + +If you already know network hashrate, pass `--network-hashrate` directly and +the script derives the equivalent Bitflash difficulty for display. + +## Reward Estimate + +The expected daily reward is: + +```text +blocks_per_day = 86400 / block_time_seconds +miner_share = miner_hashrate / network_hashrate +gross_btf_per_day = blocks_per_day * block_reward * miner_share +net_btf_per_day = gross_btf_per_day * (1 - pool_fee_percent / 100) +``` + +Electricity is: + +```text +electricity_per_day = watts / 1000 * 24 * kwh_cost +``` + +This is an expectation, not a promise. Solo mining variance can be very large, +especially when the miner has a small share of the network. + +## Future Website Use + +The script is intentionally standalone so the same math can later back a +website widget or status dashboard. Until a public price exists, a website +calculator should keep price as a manual field and label fiat profit as +speculative. diff --git a/scripts/bitflash-mining-calculator.py b/scripts/bitflash-mining-calculator.py new file mode 100644 index 0000000..1fcc1a9 --- /dev/null +++ b/scripts/bitflash-mining-calculator.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +"""Estimate Bitflash mining rewards and electricity cost. + +This is a local WhatToMine-style calculator for Bitflash. It does not fetch +prices, does not talk to the node, and does not affect consensus. +""" + +import argparse +import json +import math +import sys + + +HASHES_AT_DIFF_1 = 4096.0 +DEFAULT_BLOCK_TIME = 120.0 +DEFAULT_BLOCK_REWARD = 50.0 + + +UNITS = { + "h": 1.0, + "h/s": 1.0, + "hs": 1.0, + "kh": 1_000.0, + "kh/s": 1_000.0, + "khs": 1_000.0, + "mh": 1_000_000.0, + "mh/s": 1_000_000.0, + "mhs": 1_000_000.0, + "gh": 1_000_000_000.0, + "gh/s": 1_000_000_000.0, + "ghs": 1_000_000_000.0, +} + + +def parse_hashrate(value, unit): + try: + n = float(value) + except ValueError: + raise SystemExit("invalid hashrate: %s" % value) + mul = UNITS.get(unit.lower()) + if mul is None: + raise SystemExit("unknown hashrate unit: %s" % unit) + if n < 0: + raise SystemExit("hashrate must not be negative") + return n * mul + + +def fmt_hashrate(hps): + units = [("GH/s", 1_000_000_000.0), ("MH/s", 1_000_000.0), ("kH/s", 1_000.0), ("H/s", 1.0)] + for name, div in units: + if hps >= div: + return "%.4g %s" % (hps / div, name) + return "%.4g H/s" % hps + + +def fmt_money(x): + return "%.8f" % x + + +def estimate(args): + miner_hps = parse_hashrate(args.hashrate, args.hashrate_unit) + if args.network_hashrate is not None: + network_hps = parse_hashrate(args.network_hashrate, args.network_hashrate_unit) + difficulty = network_hps * args.block_time / HASHES_AT_DIFF_1 + network_source = "network_hashrate" + else: + if args.difficulty is None: + raise SystemExit("provide either --network-hashrate or --difficulty") + difficulty = float(args.difficulty) + if difficulty <= 0: + raise SystemExit("difficulty must be positive") + network_hps = difficulty * HASHES_AT_DIFF_1 / args.block_time + network_source = "difficulty" + + if network_hps <= 0: + raise SystemExit("network hashrate must be positive") + + block_reward = float(args.block_reward) + block_time = float(args.block_time) + pool_fee_percent = float(args.pool_fee) + price = float(args.price) + watts = float(args.watts) + kwh_cost = float(args.kwh_cost) + if block_reward < 0 or block_time <= 0 or pool_fee_percent < 0 or price < 0 or watts < 0 or kwh_cost < 0: + raise SystemExit("numeric inputs must be non-negative; block time must be positive") + + share = miner_hps / network_hps + blocks_per_day = 86400.0 / block_time + gross_btf_day = blocks_per_day * block_reward * share + net_btf_day = gross_btf_day * max(0.0, 1.0 - pool_fee_percent / 100.0) + revenue_day = net_btf_day * price + cost_day = watts / 1000.0 * 24.0 * kwh_cost + profit_day = revenue_day - cost_day + expected_seconds_per_block = math.inf if miner_hps <= 0 else difficulty * HASHES_AT_DIFF_1 / miner_hps + + periods = { + "hour": 1.0 / 24.0, + "day": 1.0, + "week": 7.0, + "month_30d": 30.0, + } + rows = {} + for name, days in periods.items(): + rows[name] = { + "btf": net_btf_day * days, + "revenue": revenue_day * days, + "electricity_cost": cost_day * days, + "profit": profit_day * days, + } + + return { + "schema": "bitflash-mining-calculator-1", + "network_source": network_source, + "inputs": { + "miner_hashrate_hps": miner_hps, + "miner_hashrate": fmt_hashrate(miner_hps), + "network_hashrate_hps": network_hps, + "network_hashrate": fmt_hashrate(network_hps), + "difficulty": difficulty, + "block_time_seconds": block_time, + "block_reward_btf": block_reward, + "pool_fee_percent": pool_fee_percent, + "price_per_btf": price, + "power_watts": watts, + "electricity_per_kwh": kwh_cost, + }, + "estimates": { + "miner_network_share": share, + "blocks_per_day": blocks_per_day, + "expected_time_to_solo_block_seconds": expected_seconds_per_block, + "expected_time_to_solo_block_hours": expected_seconds_per_block / 3600.0, + "gross_btf_per_day": gross_btf_day, + "net_btf_per_day": net_btf_day, + "periods": rows, + }, + } + + +def print_text(result): + i = result["inputs"] + e = result["estimates"] + print("Bitflash mining calculator") + print("schema: %s" % result["schema"]) + print() + print("Network") + print(" source: %s" % result["network_source"]) + print(" hashrate: %s" % i["network_hashrate"]) + print(" difficulty: %.8f" % i["difficulty"]) + print(" block time: %.0fs" % i["block_time_seconds"]) + print(" reward: %s BTF" % fmt_money(i["block_reward_btf"])) + print() + print("Miner") + print(" hashrate: %s" % i["miner_hashrate"]) + print(" share: %.8f%%" % (e["miner_network_share"] * 100.0)) + if math.isinf(e["expected_time_to_solo_block_seconds"]): + print(" solo block: never") + else: + print(" solo block: %.2f hours expected average" % e["expected_time_to_solo_block_hours"]) + print() + print("Economics") + print(" price: $%.8f / BTF" % i["price_per_btf"]) + print(" power: %.2f W" % i["power_watts"]) + print(" kWh cost: $%.4f" % i["electricity_per_kwh"]) + print(" pool fee: %.2f%%" % i["pool_fee_percent"]) + print() + print("Estimated rewards") + print(" %-9s %16s %12s %12s %12s" % ("period", "BTF", "revenue", "power", "profit")) + for name in ["hour", "day", "week", "month_30d"]: + row = e["periods"][name] + print(" %-9s %16s $%11.4f $%11.4f $%11.4f" % ( + name, + fmt_money(row["btf"]), + row["revenue"], + row["electricity_cost"], + row["profit"], + )) + print() + print("Note: this is an expectation, not a promise. Solo mining variance can be huge.") + + +def main(argv): + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--hashrate", required=True, help="miner hashrate number") + ap.add_argument("--hashrate-unit", default="kh/s", choices=sorted(UNITS), help="miner hashrate unit") + ap.add_argument("--network-hashrate", help="network hashrate number") + ap.add_argument("--network-hashrate-unit", default="kh/s", choices=sorted(UNITS), help="network hashrate unit") + ap.add_argument("--difficulty", type=float, help="Bitflash difficulty; diff 1 expects 4096 hashes") + ap.add_argument("--block-time", type=float, default=DEFAULT_BLOCK_TIME, help="target block time in seconds") + ap.add_argument("--block-reward", type=float, default=DEFAULT_BLOCK_REWARD, help="block reward in BTF") + ap.add_argument("--pool-fee", type=float, default=0.0, help="pool fee percent") + ap.add_argument("--price", type=float, default=0.0, help="price per BTF in USD or your chosen fiat unit") + ap.add_argument("--watts", type=float, default=0.0, help="miner or whole-system power in watts") + ap.add_argument("--kwh-cost", type=float, default=0.0, help="electricity cost per kWh") + ap.add_argument("--json", action="store_true", help="print JSON") + args = ap.parse_args(argv) + + result = estimate(args) + if args.json: + print(json.dumps(result, sort_keys=True, indent=2)) + else: + print_text(result) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) -- cgit v1.2.3 From 034bc9a9497a336239aadb2e52d620661e90e536 Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Tue, 11 Aug 2026 09:31:58 -0300 Subject: tools: add UTXO inclusion proofs --- README.md | 10 +++ docs/utxo-commitment.md | 14 +++ docs/utxo-proofs.md | 61 +++++++++++++ scripts/prove-utxo.py | 202 +++++++++++++++++++++++++++++++++++++++++++ scripts/verify-utxo-proof.py | 163 ++++++++++++++++++++++++++++++++++ 5 files changed, 450 insertions(+) create mode 100644 docs/utxo-proofs.md create mode 100644 scripts/prove-utxo.py create mode 100644 scripts/verify-utxo-proof.py (limited to 'scripts') diff --git a/README.md b/README.md index d549337..c28b0f1 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,16 @@ It reconstructs the best chain from local block files, computes the current UTXO root and supply, and writes canonical JSON that independent auditors can compare or timestamp externally with OpenTimestamps. See [UTXO-set commitments](docs/utxo-commitment.md). + +The same root can prove one unspent output without sharing the full UTXO set: + +```bash +python3 scripts/prove-utxo.py TXID:VOUT --out utxo-proof.json --json +python3 scripts/verify-utxo-proof.py utxo-proof.json +``` + +See [UTXO inclusion proofs](docs/utxo-proofs.md). + Bitflash also ships a deterministic fair-launch verifier: ```bash diff --git a/docs/utxo-commitment.md b/docs/utxo-commitment.md index ddf5450..991e5d9 100644 --- a/docs/utxo-commitment.md +++ b/docs/utxo-commitment.md @@ -54,6 +54,20 @@ SHA256("BTFNODE1|" || left_hash || right_hash) The final hash is the UTXO-set root. +## Inclusion Proofs + +After a root has been computed, `scripts/prove-utxo.py` can create a compact +inclusion proof for one unspent output: + +```bash +python3 scripts/prove-utxo.py TXID:VOUT --out utxo-proof.json --json +python3 scripts/verify-utxo-proof.py utxo-proof.json +``` + +The verifier does not need local block files. It recomputes the target leaf, +walks the sibling path, and compares the result with the expected root. See +[UTXO inclusion proofs](utxo-proofs.md). + ## Deterministic Output `--json` and `--out` write canonical JSON with sorted keys, fixed separators, diff --git a/docs/utxo-proofs.md b/docs/utxo-proofs.md new file mode 100644 index 0000000..4947c3c --- /dev/null +++ b/docs/utxo-proofs.md @@ -0,0 +1,61 @@ +# Bitflash UTXO inclusion proofs + +`scripts/prove-utxo.py` creates a deterministic inclusion proof for one +currently unspent output. `scripts/verify-utxo-proof.py` verifies that proof +against the UTXO root produced by `scripts/verify-utxo-set.py`. + +This is intentionally outside consensus. It turns the audit root into something +useful for a specific coin without changing what nodes accept. + +## Create A Proof + +Use an unspent output in `txid:vout` form: + +```bash +python3 scripts/prove-utxo.py TXID:VOUT --out utxo-proof.json --json +``` + +The proof contains: + +- the target UTXO; +- its sorted leaf index; +- the leaf hash; +- the Merkle sibling path; +- the UTXO root and chain tip used to create it. + +It does not contain the full UTXO set. + +## Verify A Proof + +Verify against the root embedded in the proof: + +```bash +python3 scripts/verify-utxo-proof.py utxo-proof.json +``` + +Or verify against a root copied from another machine, dashboard, block explorer, +or future node export: + +```bash +python3 scripts/verify-utxo-proof.py utxo-proof.json \ + --root ROOT_FROM_REPORT_OR_EXPORT +``` + +The verifier does not need local `blk*.dat` files. It only needs the proof and +the expected root. + +## Scope + +This first proof format supports inclusion only. Absence and range proofs are +left for a later design because they require a more careful commitment scheme +and are easier to get subtly wrong. + +The proof uses the exact same leaf and node domains as the UTXO commitment: + +```text +BTFUTXO1 +BTFNODE1 +``` + +That keeps the verifier language-neutral and gives future implementations a +small, stable target. diff --git a/scripts/prove-utxo.py b/scripts/prove-utxo.py new file mode 100644 index 0000000..7a10d2a --- /dev/null +++ b/scripts/prove-utxo.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +"""Create a Bitflash UTXO inclusion proof from local blk*.dat files. + +The proof is deliberately outside consensus. It proves that one txid:vout is +included in the deterministic UTXO root produced by verify-utxo-set.py. +""" + +import argparse +import hashlib +import importlib.util +import json +import os +import sys +from pathlib import Path + + +SCHEMA = "bitflash-utxo-inclusion-proof-1" +NODE_DOMAIN = b"BTFNODE1|" + + +def load_utxo_tool(): + path = Path(__file__).with_name("verify-utxo-set.py") + spec = importlib.util.spec_from_file_location("bitflash_verify_utxo_set", path) + if spec is None or spec.loader is None: + raise SystemExit("could not load %s" % path) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +def canonical_bytes(obj): + return (json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + "\n").encode("ascii") + + +def split_outpoint(outpoint): + parts = outpoint.split(":") + if len(parts) != 2 or len(parts[0]) != 64: + raise SystemExit("outpoint must be txid:vout") + try: + vout = int(parts[1], 10) + except ValueError: + raise SystemExit("vout must be a decimal integer") + if vout < 0: + raise SystemExit("vout must not be negative") + return parts[0].lower(), vout + + +def build_merkle_proof(keys, leaf_hashes, index): + current_index = index + layer = list(leaf_hashes) + proof = [] + level = 0 + while len(layer) > 1: + if current_index % 2 == 0: + sibling_index = current_index + 1 + duplicated = sibling_index >= len(layer) + sibling = layer[current_index] if duplicated else layer[sibling_index] + side = "right" + else: + sibling_index = current_index - 1 + duplicated = False + sibling = layer[sibling_index] + side = "left" + proof.append({ + "level": level, + "side": side, + "hash": sibling.hex(), + "duplicated": duplicated, + }) + + next_layer = [] + for i in range(0, len(layer), 2): + left = layer[i] + right = layer[i + 1] if i + 1 < len(layer) else left + next_layer.append(hashlib.sha256(NODE_DOMAIN + left + right).digest()) + layer = next_layer + current_index //= 2 + level += 1 + return proof, layer[0].hex() + + +def build_proof(args): + tool = load_utxo_tool() + txid, vout = split_outpoint(args.outpoint) + outpoint = "%s:%d" % (txid, vout) + + raw_blocks = tool.read_blocks(args.datadir, args.max_blocks) + if not raw_blocks: + raise SystemExit("no Bitflash blocks found in %s" % args.datadir) + blocks = tool.select_main_chain(raw_blocks) + state = tool.apply_blocks(blocks) + utxos = state["utxos"] + stats = state["stats"] + if outpoint not in utxos: + raise SystemExit("outpoint is not unspent in the scanned chain: %s" % outpoint) + + keys = sorted(utxos) + leaf_hashes = [tool.utxo_leaf_hash(k, utxos[k]) for k in keys] + index = keys.index(outpoint) + proof_path, root = build_merkle_proof(keys, leaf_hashes, index) + direct_root = tool.utxo_root(utxos) + if root != direct_root: + raise SystemExit("internal proof root mismatch") + + tip = blocks[-1] + utxo = utxos[outpoint] + report = { + "schema": SCHEMA, + "deterministic": True, + "chain": { + "name": "Bitflash", + "ticker": "BTF", + "message_start_hex": tool.MAGIC.hex(), + "coin": tool.COIN, + }, + "commitment": { + "algorithm": "sorted-outpoint-merkle-sha256", + "leaf_domain": "BTFUTXO1", + "node_domain": "BTFNODE1", + "root": root, + "leaf_count": len(keys), + }, + "target": { + "outpoint": outpoint, + "txid": txid, + "vout": vout, + "leaf_index": index, + "leaf_hash": leaf_hashes[index].hex(), + "utxo": utxo, + }, + "proof": proof_path, + "scanned": { + "raw_block_count": len(raw_blocks), + "side_blocks_ignored": len(raw_blocks) - len(blocks), + "main_chain_block_count": len(blocks), + "first_height": 0, + "last_height": len(blocks) - 1, + "tip_hash": tip["hash"], + "tip_time": tip["time"], + "tip_time_utc": tip["time_utc"], + }, + "warnings": { + "legacy_duplicate_output_overwrites": len(stats["duplicate_outputs"]), + }, + "verdict": "UTXO_INCLUSION_PROOF_CREATED", + } + return report + + +def print_text(proof): + data_hash = hashlib.sha256(canonical_bytes(proof)).hexdigest() + target = proof["target"] + utxo = target["utxo"] + print("Bitflash UTXO inclusion proof") + print("schema: %s" % proof["schema"]) + print("canonical proof sha256: %s" % data_hash) + print() + print("Target") + print(" outpoint: %s" % target["outpoint"]) + print(" value: %s BTF" % utxo["value_btf"]) + print(" height: %d" % utxo["created_height"]) + print(" index: %d of %d" % (target["leaf_index"], proof["commitment"]["leaf_count"])) + print() + print("Commitment") + print(" root: %s" % proof["commitment"]["root"]) + print(" path: %d sibling hashes" % len(proof["proof"])) + print(" tip: %s" % proof["scanned"]["tip_hash"]) + print() + print("Verdict: %s" % proof["verdict"]) + + +def default_datadir(): + if os.name == "nt" and os.environ.get("APPDATA"): + return os.path.join(os.environ["APPDATA"], "Bitflash") + if os.environ.get("HOME"): + return os.path.join(os.environ["HOME"], ".bitflash") + return "." + + +def main(argv): + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("outpoint", help="unspent output to prove, as txid:vout") + ap.add_argument("--datadir", default=default_datadir(), help="Bitflash data directory") + ap.add_argument("--max-blocks", type=int, default=0, + help="number of blocks to scan; 0 means all block files") + ap.add_argument("--json", action="store_true", help="print canonical JSON") + ap.add_argument("--out", help="write canonical JSON proof to this path") + args = ap.parse_args(argv) + + proof = build_proof(args) + data = canonical_bytes(proof) + if args.out: + Path(args.out).write_bytes(data) + if args.json: + sys.stdout.buffer.write(data) + else: + print_text(proof) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/scripts/verify-utxo-proof.py b/scripts/verify-utxo-proof.py new file mode 100644 index 0000000..31099f7 --- /dev/null +++ b/scripts/verify-utxo-proof.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +"""Verify a Bitflash UTXO inclusion proof against a UTXO root.""" + +import argparse +import hashlib +import json +import sys +from pathlib import Path + + +SCHEMA = "bitflash-utxo-inclusion-proof-1" +LEAF_DOMAIN = "BTFUTXO1" +NODE_DOMAIN = b"BTFNODE1|" + + +def canonical_bytes(obj): + return (json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + "\n").encode("ascii") + + +def sha256(data): + return hashlib.sha256(data).digest() + + +def utxo_leaf_hash(outpoint, utxo): + payload = "|".join([ + LEAF_DOMAIN, + outpoint, + str(utxo["value_satoshis"]), + utxo["script_pub_key_hex"], + str(utxo["created_height"]), + utxo["created_txid"], + ]).encode("ascii") + return sha256(payload) + + +def load_proof(path): + try: + return json.loads(Path(path).read_text(encoding="ascii")) + except Exception as e: + raise SystemExit("could not read proof: %s" % e) + + +def verify(proof, expected_root=None): + failures = [] + if proof.get("schema") != SCHEMA: + failures.append("unexpected schema") + commitment = proof.get("commitment", {}) + if commitment.get("algorithm") != "sorted-outpoint-merkle-sha256": + failures.append("unexpected commitment algorithm") + if commitment.get("leaf_domain") != LEAF_DOMAIN: + failures.append("unexpected leaf domain") + if commitment.get("node_domain") != "BTFNODE1": + failures.append("unexpected node domain") + + target = proof.get("target", {}) + outpoint = target.get("outpoint", "") + utxo = target.get("utxo", {}) + if outpoint != "%s:%s" % (target.get("txid"), target.get("vout")): + failures.append("target outpoint does not match txid:vout") + + try: + current = utxo_leaf_hash(outpoint, utxo) + leaf_ok = target.get("leaf_hash") == current.hex() + except Exception as e: + failures.append("could not hash target leaf: %s" % e) + current = b"\x00" * 32 + leaf_ok = False + + if not leaf_ok: + failures.append("target leaf hash mismatch") + + for i, step in enumerate(proof.get("proof", [])): + try: + sibling = bytes.fromhex(step["hash"]) + except Exception: + failures.append("bad sibling hash at proof level %d" % i) + sibling = b"\x00" * 32 + if len(sibling) != 32: + failures.append("bad sibling hash length at proof level %d" % i) + side = step.get("side") + if side == "right": + current = sha256(NODE_DOMAIN + current + sibling) + elif side == "left": + current = sha256(NODE_DOMAIN + sibling + current) + else: + failures.append("bad sibling side at proof level %d" % i) + + computed_root = current.hex() + root = expected_root or commitment.get("root") + if computed_root != root: + failures.append("computed root does not match expected root") + + return { + "schema": "bitflash-utxo-proof-verification-1", + "deterministic": True, + "target": { + "outpoint": outpoint, + "value_satoshis": utxo.get("value_satoshis"), + "value_btf": utxo.get("value_btf"), + "created_height": utxo.get("created_height"), + }, + "commitment": { + "expected_root": root, + "computed_root": computed_root, + "leaf_hash": target.get("leaf_hash"), + "path_length": len(proof.get("proof", [])), + }, + "checks": { + "schema_is_supported": proof.get("schema") == SCHEMA, + "leaf_hash_matches_target": leaf_ok, + "root_matches": computed_root == root, + }, + "failures": failures, + "verdict": "UTXO_PROOF_VERIFIED" if not failures else "UTXO_PROOF_FAILED", + } + + +def print_text(result): + data_hash = hashlib.sha256(canonical_bytes(result)).hexdigest() + print("Bitflash UTXO proof verifier") + print("schema: %s" % result["schema"]) + print("canonical verification sha256: %s" % data_hash) + print() + print("Target") + print(" outpoint: %s" % result["target"]["outpoint"]) + print(" value: %s BTF" % result["target"]["value_btf"]) + print(" height: %s" % result["target"]["created_height"]) + print() + print("Commitment") + print(" expected: %s" % result["commitment"]["expected_root"]) + print(" computed: %s" % result["commitment"]["computed_root"]) + print(" path: %d sibling hashes" % result["commitment"]["path_length"]) + print() + print("Checks") + for key in sorted(result["checks"]): + print(" %s %s" % ("OK " if result["checks"][key] else "FAIL", key)) + if result["failures"]: + print() + print("Failures") + for failure in result["failures"]: + print(" - %s" % failure) + print() + print("Verdict: %s" % result["verdict"]) + + +def main(argv): + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("proof", help="canonical JSON proof from prove-utxo.py") + ap.add_argument("--root", help="expected UTXO root; defaults to root embedded in proof") + ap.add_argument("--json", action="store_true", help="print canonical JSON") + args = ap.parse_args(argv) + + result = verify(load_proof(args.proof), args.root) + data = canonical_bytes(result) + if args.json: + sys.stdout.buffer.write(data) + else: + print_text(result) + return 0 if result["verdict"] == "UTXO_PROOF_VERIFIED" else 1 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) -- cgit v1.2.3 From 1839347459759fb3b23cc747ec4368b724fee008 Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Tue, 11 Aug 2026 14:13:26 -0300 Subject: tools: harden chain audit scripts --- .github/workflows/ci.yml | 3 + README.md | 8 + docs/fair-launch.md | 3 +- docs/utxo-commitment.md | 20 +- scripts/bitflash_chain.py | 453 +++++++++++++++++++++++++++++++++++ scripts/build-explorer.py | 382 ++++++++++++++++++----------- scripts/prove-utxo.py | 43 ++-- scripts/verify-fair-launch.py | 346 +++----------------------- scripts/verify-utxo-proof.py | 23 +- scripts/verify-utxo-set.py | 375 ++--------------------------- tests/scripts/test_bitflash_tools.py | 107 +++++++++ 11 files changed, 897 insertions(+), 866 deletions(-) create mode 100644 scripts/bitflash_chain.py create mode 100644 tests/scripts/test_bitflash_tools.py (limited to 'scripts') diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba8074c..f4045f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,9 @@ jobs: - name: Run Linux selftests run: make tests + - name: Run Python tool regressions + run: python3 tests/scripts/test_bitflash_tools.py + - name: Run P2P message fuzz smoke run: make fuzz-net-message-smoke diff --git a/README.md b/README.md index c28b0f1..9c6db0e 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,14 @@ and produces canonical JSON whose hash can be compared by independent auditors or timestamped externally with OpenTimestamps. See [fair-launch verification](docs/fair-launch.md). +For a static local block explorer, generate HTML and JSON from either a data +directory or explicit block files: + +```bash +python3 scripts/build-explorer.py --datadir ~/.bitflash ./explorer-out +python3 scripts/build-explorer.py ~/.bitflash/blk0001.dat ~/.bitflash/blk0002.dat ./explorer-out +``` + **Keep your node current.** Consensus rules have changed since the first releases — 1.2.1 fixed a bug that let anyone spend anyone's coins, and 1.2.2 added a per-block signature-operation cap. A node on an older build will accept diff --git a/docs/fair-launch.md b/docs/fair-launch.md index 79eca8e..9603910 100644 --- a/docs/fair-launch.md +++ b/docs/fair-launch.md @@ -8,7 +8,8 @@ python3 scripts/verify-fair-launch.py --max-blocks 1000 --out bitflash-fair-laun The verifier reads `blk*.dat` directly from the local Bitflash data directory. It does not use Berkeley DB, does not call the node binary, and does not need -network access. +network access. It uses the same shared Python chain parser as the UTXO +commitment and proof tools, so block serialization fixes land in one place. The report proves the launch baseline that can be checked from the chain: diff --git a/docs/utxo-commitment.md b/docs/utxo-commitment.md index 991e5d9..ab43deb 100644 --- a/docs/utxo-commitment.md +++ b/docs/utxo-commitment.md @@ -21,7 +21,8 @@ python3 scripts/verify-utxo-set.py --out utxo-report.json --json The verifier: -- parses Bitflash block files directly; +- parses Bitflash block files directly with the shared Python chain parser; +- streams `blk*.dat` records instead of loading full block files into memory; - reconstructs the best chain rooted at the Bitflash genesis block; - ignores side blocks that may also be present in `blk*.dat`; - verifies every scanned block merkle root; @@ -98,6 +99,23 @@ last-write-wins semantics for the UTXO map, matching the legacy index model. Those warnings are part of the report so auditors can see the historical debt instead of relying on a polished summary. +For synthetic chains or future clean consensus ranges, auditors can make +duplicates a hard failure: + +```bash +python3 scripts/verify-utxo-set.py --strict-duplicates +``` + +The default remains compatible with the current historical chain because it has +known legacy duplicate-output overwrites. + +## Shared Parser + +The audit scripts share `scripts/bitflash_chain.py` for block parsing, +main-chain selection, UTXO application, canonical JSON, and genesis +reconstruction. That keeps `verify-utxo-set.py`, `verify-fair-launch.py`, +`prove-utxo.py`, and the explorer on the same serialization rules. + ## Future Consensus Path This tool is the low-risk first step toward consensus commitments. A future diff --git a/scripts/bitflash_chain.py b/scripts/bitflash_chain.py new file mode 100644 index 0000000..f3688f1 --- /dev/null +++ b/scripts/bitflash_chain.py @@ -0,0 +1,453 @@ +#!/usr/bin/env python3 +"""Shared Bitflash block/chain parser for deterministic audit tools.""" + +import datetime as _dt +import hashlib +import json +import os +import struct +from pathlib import Path + + +MAGIC = bytes([0xBF, 0x20, 0x5C, 0xFD]) +MAX_BLOCK_PAYLOAD = 32 * 1024 * 1024 +COIN = 100_000_000 +GENESIS_HASH = "5bd7cb255d814e48cebcdfb72da4dc87b34bd774227f8ceb546c5640f4bdc169" +GENESIS_MERKLE = "1a45b4482532abb29b10e234d3f13132230525a339ecea91658ffa675a5b1325" +GENESIS_TIME = 1_753_315_200 +GENESIS_BITS = 0x1F0FFFFF +GENESIS_NONCE = 3141 +GENESIS_TEXT = "Bitflash 24/Jul/2026 Fair launch: one CPU one vote, no premine" + + +class ParseError(Exception): + pass + + +class Reader: + def __init__(self, data): + self.data = data + self.pos = 0 + + def take(self, n): + if self.pos + n > len(self.data): + raise ParseError("unexpected end of data") + out = self.data[self.pos:self.pos + n] + self.pos += n + return out + + def u8(self): + return self.take(1)[0] + + def u32(self): + return struct.unpack(" 1: + nxt = [] + for i in range(0, len(layer), 2): + left = layer[i] + right = layer[i + 1] if i + 1 < len(layer) else left + nxt.append(sha256d(left + right)) + layer = nxt + return layer[0][::-1].hex() + + +def parse_block(payload, file_name="", offset=0): + r = Reader(payload) + header = r.take(80) + version, = struct.unpack(" MAX_BLOCK_PAYLOAD: + offset += 1 + f.seek(offset) + window = f.read(4) + continue + payload_offset = offset + 8 + payload = f.read(size) + if len(payload) != size: + break + try: + yield parse_block(payload, path.name, payload_offset) + except ParseError: + offset += 1 + f.seek(offset) + window = f.read(4) + continue + + offset = f.tell() + window = f.read(4) + + +def read_blocks_from_files(paths, max_blocks=0): + blocks = [] + for path in paths: + for block in iter_blocks_from_file(path): + blocks.append(block) + if max_blocks and len(blocks) >= max_blocks: + return blocks + return blocks + + +def read_blocks(datadir, max_blocks=0): + return read_blocks_from_files(iter_block_files(datadir), max_blocks) + + +def select_main_chain(blocks): + by_hash = {} + for block in blocks: + by_hash.setdefault(block["hash"], block) + if GENESIS_HASH not in by_hash: + raise ParseError("genesis block not found in block files") + + children = {} + for block in by_hash.values(): + children.setdefault(block["previous_hash"], []).append(block) + for v in children.values(): + v.sort(key=lambda b: (b["hash"], b["source_file"], b["source_offset"])) + + best = [] + stack = [(by_hash[GENESIS_HASH], [by_hash[GENESIS_HASH]])] + while stack: + block, path = stack.pop() + if len(path) > len(best) or (len(path) == len(best) and block["hash"] < best[-1]["hash"]): + best = path + for child in children.get(block["hash"], []): + stack.append((child, path + [child])) + return best + + +def is_coinbase(tx): + return ( + len(tx["vin"]) == 1 + and tx["vin"][0]["prevout_hash"] == "0" * 64 + and tx["vin"][0]["prevout_n"] == 0xFFFFFFFF + ) + + +def utxo_leaf_hash(outpoint, utxo): + payload = "|".join([ + "BTFUTXO1", + outpoint, + str(utxo["value_satoshis"]), + utxo["script_pub_key_hex"], + str(utxo["created_height"]), + utxo["created_txid"], + ]).encode("ascii") + return sha256(payload) + + +def utxo_root(utxos): + leaves = [utxo_leaf_hash(k, utxos[k]) for k in sorted(utxos)] + if not leaves: + return sha256(b"BTFUTXO1|empty").hex() + layer = leaves + while len(layer) > 1: + nxt = [] + for i in range(0, len(layer), 2): + left = layer[i] + right = layer[i + 1] if i + 1 < len(layer) else left + nxt.append(sha256(b"BTFNODE1|" + left + right)) + layer = nxt + return layer[0].hex() + + +def apply_blocks(blocks, strict_duplicates=False): + utxos = {} + spent = 0 + missing_spends = [] + duplicate_outputs = [] + total_coinbase = 0 + total_fees = 0 + normal_txs = 0 + + for height, block in enumerate(blocks): + if block["computed_merkle_root"] != block["merkle_root"]: + raise ParseError("bad merkle root at height %d" % height) + for tx in block["transactions"]: + coinbase = is_coinbase(tx) + input_value = 0 + output_value = sum(out["value_satoshis"] for out in tx["vout"]) + if coinbase: + total_coinbase += output_value + else: + normal_txs += 1 + for txin in tx["vin"]: + prev = "%s:%d" % (txin["prevout_hash"], txin["prevout_n"]) + prev_utxo = utxos.pop(prev, None) + if prev_utxo is None: + missing_spends.append({ + "height": height, + "txid": tx["txid"], + "missing_outpoint": prev, + }) + else: + spent += prev_utxo["value_satoshis"] + input_value += prev_utxo["value_satoshis"] + if input_value >= output_value: + total_fees += input_value - output_value + for n, out in enumerate(tx["vout"]): + key = "%s:%d" % (tx["txid"], n) + if key in utxos: + duplicate_outputs.append({"height": height, "outpoint": key}) + if strict_duplicates: + raise ParseError("duplicate output at height %d: %s" % (height, key)) + utxos[key] = { + "txid": tx["txid"], + "vout": n, + "value_satoshis": out["value_satoshis"], + "value_btf": out["value_btf"], + "script_pub_key_hex": out["script_pub_key_hex"], + "created_height": height, + "created_block_hash": block["hash"], + "created_txid": tx["txid"], + "coinbase": coinbase, + } + + total_unspent = sum(u["value_satoshis"] for u in utxos.values()) + return { + "utxos": utxos, + "stats": { + "utxo_count": len(utxos), + "spent_outputs": spent, + "total_unspent_satoshis": total_unspent, + "total_unspent_btf": format_money(total_unspent), + "total_coinbase_satoshis": total_coinbase, + "total_coinbase_btf": format_money(total_coinbase), + "coinbase_minus_unspent_satoshis": total_coinbase - total_unspent, + "coinbase_minus_unspent_btf": format_money(total_coinbase - total_unspent), + "total_fees_satoshis": total_fees, + "total_fees_btf": format_money(total_fees), + "normal_transactions": normal_txs, + "missing_spends": missing_spends, + "duplicate_outputs": duplicate_outputs, + }, + } + + +def genesis_transaction(): + script_sig = ( + bytes.fromhex("04ffff001d") + + b"\x01\x04" + + bytes([len(GENESIS_TEXT)]) + + GENESIS_TEXT.encode("ascii") + ) + pubkey_num = bytes.fromhex( + "5f1df16b2b704c8a578d0bbaf74d385cde12c11ee50455f3c438ef4c3fbcf649" + "b6de611feae06279a60939e028a8d65c10b73071a6f16719274855feb0fd8a6704" + ) + script_pub_key = bytes([65]) + pubkey_num[::-1] + b"\xac" + tx = { + "version": 1, + "vin": [{ + "prevout_hash": "0" * 64, + "prevout_n": 0xFFFFFFFF, + "script_sig_hex": script_sig.hex(), + "sequence": 0xFFFFFFFF, + }], + "vout": [{ + "value_satoshis": 50 * COIN, + "value_btf": format_money(50 * COIN), + "script_pub_key_hex": script_pub_key.hex(), + }], + "lock_time": 0, + } + tx["txid"] = hash_hex(ser_tx(tx)) + return tx + + +def reconstructed_genesis(): + tx = genesis_transaction() + header = ( + struct.pack(": -# blocks.json summary of every main-chain block (newest first) -# block/.json full detail per block (header + all txs) -# index.html self-contained viewer (vanilla JS, same-origin fetch) -# -# Scope (v0, deliberately): block list, block detail, and coinbase. No mempool, -# no per-address history. On this chain the coinbase payout key rotates every -# block, so an address index would be nearly useless; block-level transparency is -# what lets miners and users confirm the chain is honest. -# -# Usage: build-explorer.py - -import sys, os, json, hashlib, struct - -MAGIC = bytes([0xbf, 0x20, 0x5c, 0xfd]) -ADDRESS_VERSION = 25 # Bitflash addresses start with "B" - -def sha256d(b): return hashlib.sha256(hashlib.sha256(b).digest()).digest() +"""Build a static Bitflash block explorer from local blk*.dat files.""" +import argparse +import hashlib +import json +import sys +from pathlib import Path + +import bitflash_chain as chain + + +ADDRESS_VERSION = 25 B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + + def b58check(payload): v = bytes([ADDRESS_VERSION]) + payload - v += sha256d(v)[:4] - n = int.from_bytes(v, 'big') + v += chain.sha256d(v)[:4] + n = int.from_bytes(v, "big") s = "" while n > 0: - n, r = divmod(n, 58); s = B58[r] + s + n, r = divmod(n, 58) + s = B58[r] + s for byte in v: - if byte == 0: s = "1" + s - else: break + if byte == 0: + s = "1" + s + else: + break return s + def hash160(b): - return hashlib.new('ripemd160', hashlib.sha256(b).digest()).digest() - -class R: - def __init__(s, b, o=0): s.b = b; s.o = o - def u(s, n): v = s.b[s.o:s.o+n]; s.o += n; return v - def i32(s): return struct.unpack(' OP_EQUALVERIFY OP_CHECKSIG - if len(spk) == 25 and spk[0] == 0x76 and spk[1] == 0xa9 and spk[2] == 0x14 and spk[23] == 0x88 and spk[24] == 0xac: + return hashlib.new("ripemd160", hashlib.sha256(b).digest()).digest() + + +def spk_info(script_hex): + spk = bytes.fromhex(script_hex) + if ( + len(spk) == 25 and spk[0] == 0x76 and spk[1] == 0xA9 + and spk[2] == 0x14 and spk[23] == 0x88 and spk[24] == 0xAC + ): return {"type": "p2pkh", "address": b58check(spk[3:23])} - # P2PK: OP_CHECKSIG (65- or 33-byte key) - if len(spk) >= 35 and spk[-1] == 0xac: + if len(spk) >= 35 and spk[-1] == 0xAC: if spk[0] == 0x41 and len(spk) == 67: - pk = spk[1:66]; return {"type": "p2pk", "pubkey": pk.hex(), "address": b58check(hash160(pk))} + pk = spk[1:66] + return {"type": "p2pk", "pubkey": pk.hex(), "address": b58check(hash160(pk))} if spk[0] == 0x21 and len(spk) == 35: - pk = spk[1:34]; return {"type": "p2pk", "pubkey": pk.hex(), "address": b58check(hash160(pk))} - return {"type": "other", "hex": spk.hex()[:80]} - -def parse_tx(r): - start = r.o - r.i32() - nin = r.cs(); vin = [] - for _ in range(nin): - ph = r.u(32); idx = r.u32(); r.script(); r.u32() - coinbase = (ph == b'\x00' * 32 and idx == 0xffffffff) - vin.append({"coinbase": coinbase, "prev": (None if coinbase else ph[::-1].hex()), "vout": (None if coinbase else idx)}) - nout = r.cs(); vout = [] - for _ in range(nout): - val = r.i64(); spk = r.script(); vout.append(dict({"value": val}, **spk_info(spk))) - r.u32() - raw = r.b[start:r.o] - return {"txid": sha256d(raw)[::-1].hex(), "vin": vin, "vout": vout, "size": len(raw)} - -def main(): - if len(sys.argv) != 3: - print("usage: build-explorer.py ", file=sys.stderr); sys.exit(2) - blkpath, out = sys.argv[1], sys.argv[2] - data = open(blkpath, 'rb').read() - blocks = {}; o = 0; n = len(data) - while o + 8 <= n: - if data[o:o+4] != MAGIC: o += 1; continue - o += 4; size = struct.unpack('") + args.block_files = None + args.out_dir = args.paths[0] + else: + if len(args.paths) < 2: + raise SystemExit("usage: build-explorer.py ... ") + args.block_files = args.paths[:-1] + args.out_dir = args.paths[-1] + return args + + +def load_blocks(args): + if args.datadir: + return chain.read_blocks(args.datadir, args.max_blocks) + return chain.read_blocks_from_files(args.block_files, args.max_blocks) + + +def build_explorer(raw_blocks, out_dir): + try: + blocks = chain.select_main_chain(raw_blocks) + except chain.ParseError as e: + raise SystemExit(str(e)) + out = Path(out_dir) + (out / "block").mkdir(parents=True, exist_ok=True) + summaries = [] - for h in chain: - b = blocks[h]; H = hc[h] - cb = b["txs"][0]["vout"][0] if b["txs"] and b["txs"][0]["vout"] else {} - summaries.append({"height": H, "hash": h, "time": b["time"], "nTx": len(b["txs"]), - "size": b["size"], "cbValue": cb.get("value", 0), "cbAddr": cb.get("address", "")}) - json.dump(dict({"height": H}, **b), open(os.path.join(out, "block", "%d.json" % H), "w"), separators=(',', ':')) + for height, block in enumerate(blocks): + txs = [explorer_tx(tx) for tx in block["transactions"]] + detail = { + "height": height, + "hash": block["hash"], + "prev": block["previous_hash"], + "merkleRoot": block["merkle_root"], + "version": block["version"], + "time": block["time"], + "bits": int(block["bits"], 16), + "nonce": block["nonce"], + "size": block["size"], + "txs": txs, + } + cb = txs[0]["vout"][0] if txs and txs[0]["vout"] else {} + summaries.append({ + "height": height, + "hash": block["hash"], + "time": block["time"], + "nTx": len(txs), + "size": block["size"], + "cbValue": cb.get("value", 0), + "cbAddr": cb.get("address", ""), + }) + (out / "block" / ("%d.json" % height)).write_text( + json.dumps(detail, sort_keys=True, separators=(",", ":")) + "\n", + encoding="ascii", + ) + summaries.sort(key=lambda x: -x["height"]) - json.dump({"network": "bitflash", "tipHeight": hc[tip], "count": len(chain), "blocks": summaries}, - open(os.path.join(out, "blocks.json"), "w"), separators=(',', ':')) - open(os.path.join(out, "index.html"), "w").write(INDEX_HTML) - print("explorer built: %d main-chain blocks, tip %d, into %s" % (len(chain), hc[tip], out)) + (out / "blocks.json").write_text( + json.dumps({ + "network": "bitflash", + "tipHeight": len(blocks) - 1, + "count": len(blocks), + "blocks": summaries, + }, sort_keys=True, separators=(",", ":")) + "\n", + encoding="ascii", + ) + (out / "index.html").write_text(INDEX_HTML, encoding="utf-8") + return len(blocks), len(blocks) - 1 + + +def main(argv): + args = parse_args(argv) + raw_blocks = load_blocks(args) + if not raw_blocks: + raise SystemExit("no Bitflash blocks parsed") + count, tip = build_explorer(raw_blocks, args.out_dir) + print("explorer built: %d main-chain blocks, tip %d, into %s" % (count, tip, args.out_dir)) + return 0 INDEX_HTML = r""" @@ -144,6 +171,7 @@ INDEX_HTML = r""" body { font: 15px/1.5 system-ui, sans-serif; margin:0; background:var(--bg); color:var(--fg); } header { padding:20px 24px; border-bottom:1px solid var(--line); display:flex; gap:16px; align-items:baseline; flex-wrap:wrap; } h1 { font-size:18px; margin:0; } + h2 { font-size:16px; } .mut { color:var(--mut); } .wrap { padding:16px 24px; max-width:1100px; margin:0 auto; } input { width:100%; max-width:420px; padding:9px 12px; border:1px solid var(--line); border-radius:8px; background:var(--card); color:var(--fg); font-size:14px; } @@ -154,7 +182,6 @@ INDEX_HTML = r""" tr.blk:hover td { background:var(--card); } code { font-size:12px; word-break:break-all; } .scroll { overflow-x:auto; } - a { color:var(--acc); text-decoration:none; } .card { border:1px solid var(--line); border-radius:10px; background:var(--card); padding:14px 16px; margin-top:12px; } .kv { display:grid; grid-template-columns:150px 1fr; gap:4px 12px; font-size:13px; } .kv div:nth-child(odd){ color:var(--mut); } @@ -174,7 +201,7 @@ INDEX_HTML = r"""
-

Read-only view of the main chain: blocks, their transactions, and the coinbase payout. There is no mempool and no per-address history here. On Bitflash the coinbase payout key rotates every block, so an address index would say almost nothing; what matters is that every block and every coinbase is here to check.

+

Read-only view of the main chain: blocks, their transactions, and the coinbase payout. There is no mempool and no per-address history here.

@@ -186,52 +213,114 @@ INDEX_HTML = r""" @@ -239,5 +328,6 @@ fetch('blocks.json').then(function(r){return r.json();}).then(function(d){ """ + if __name__ == "__main__": - main() + raise SystemExit(main(sys.argv[1:])) diff --git a/scripts/prove-utxo.py b/scripts/prove-utxo.py index 7a10d2a..9f2704b 100644 --- a/scripts/prove-utxo.py +++ b/scripts/prove-utxo.py @@ -7,27 +7,16 @@ included in the deterministic UTXO root produced by verify-utxo-set.py. import argparse import hashlib -import importlib.util import json -import os import sys from pathlib import Path +import bitflash_chain as chain SCHEMA = "bitflash-utxo-inclusion-proof-1" NODE_DOMAIN = b"BTFNODE1|" -def load_utxo_tool(): - path = Path(__file__).with_name("verify-utxo-set.py") - spec = importlib.util.spec_from_file_location("bitflash_verify_utxo_set", path) - if spec is None or spec.loader is None: - raise SystemExit("could not load %s" % path) - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - return mod - - def canonical_bytes(obj): return (json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + "\n").encode("ascii") @@ -80,25 +69,27 @@ def build_merkle_proof(keys, leaf_hashes, index): def build_proof(args): - tool = load_utxo_tool() txid, vout = split_outpoint(args.outpoint) outpoint = "%s:%d" % (txid, vout) - raw_blocks = tool.read_blocks(args.datadir, args.max_blocks) + raw_blocks = chain.read_blocks(args.datadir, args.max_blocks) if not raw_blocks: raise SystemExit("no Bitflash blocks found in %s" % args.datadir) - blocks = tool.select_main_chain(raw_blocks) - state = tool.apply_blocks(blocks) + try: + blocks = chain.select_main_chain(raw_blocks) + state = chain.apply_blocks(blocks, strict_duplicates=args.strict_duplicates) + except chain.ParseError as e: + raise SystemExit(str(e)) utxos = state["utxos"] stats = state["stats"] if outpoint not in utxos: raise SystemExit("outpoint is not unspent in the scanned chain: %s" % outpoint) keys = sorted(utxos) - leaf_hashes = [tool.utxo_leaf_hash(k, utxos[k]) for k in keys] + leaf_hashes = [chain.utxo_leaf_hash(k, utxos[k]) for k in keys] index = keys.index(outpoint) proof_path, root = build_merkle_proof(keys, leaf_hashes, index) - direct_root = tool.utxo_root(utxos) + direct_root = chain.utxo_root(utxos) if root != direct_root: raise SystemExit("internal proof root mismatch") @@ -110,8 +101,8 @@ def build_proof(args): "chain": { "name": "Bitflash", "ticker": "BTF", - "message_start_hex": tool.MAGIC.hex(), - "coin": tool.COIN, + "message_start_hex": chain.MAGIC.hex(), + "coin": chain.COIN, }, "commitment": { "algorithm": "sorted-outpoint-merkle-sha256", @@ -169,20 +160,14 @@ def print_text(proof): print("Verdict: %s" % proof["verdict"]) -def default_datadir(): - if os.name == "nt" and os.environ.get("APPDATA"): - return os.path.join(os.environ["APPDATA"], "Bitflash") - if os.environ.get("HOME"): - return os.path.join(os.environ["HOME"], ".bitflash") - return "." - - def main(argv): ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("outpoint", help="unspent output to prove, as txid:vout") - ap.add_argument("--datadir", default=default_datadir(), help="Bitflash data directory") + ap.add_argument("--datadir", default=chain.default_datadir(), help="Bitflash data directory") ap.add_argument("--max-blocks", type=int, default=0, help="number of blocks to scan; 0 means all block files") + ap.add_argument("--strict-duplicates", action="store_true", + help="fail instead of using legacy last-write-wins duplicate output handling") ap.add_argument("--json", action="store_true", help="print canonical JSON") ap.add_argument("--out", help="write canonical JSON proof to this path") args = ap.parse_args(argv) diff --git a/scripts/verify-fair-launch.py b/scripts/verify-fair-launch.py index ce396fb..c33c8be 100644 --- a/scripts/verify-fair-launch.py +++ b/scripts/verify-fair-launch.py @@ -1,299 +1,12 @@ #!/usr/bin/env python3 -"""Produce a deterministic Bitflash fair-launch audit report. - -This verifier is intentionally self-contained. It parses Bitflash block files -directly, without Berkeley DB, without the node binary, and without network -access. The canonical JSON output is stable for the same inputs and can be -timestamped later with OpenTimestamps or any other external notary. -""" +"""Produce a deterministic Bitflash fair-launch audit report.""" import argparse -import datetime as _dt import hashlib -import json -import os -import struct import sys from pathlib import Path - -MAGIC = bytes([0xBF, 0x20, 0x5C, 0xFD]) -COIN = 100_000_000 -GENESIS_HASH = "5bd7cb255d814e48cebcdfb72da4dc87b34bd774227f8ceb546c5640f4bdc169" -GENESIS_MERKLE = "1a45b4482532abb29b10e234d3f13132230525a339ecea91658ffa675a5b1325" -GENESIS_TIME = 1_753_315_200 -GENESIS_BITS = 0x1F0FFFFF -GENESIS_NONCE = 3141 -GENESIS_TEXT = "Bitflash 24/Jul/2026 Fair launch: one CPU one vote, no premine" - - -class ParseError(Exception): - pass - - -class Reader: - def __init__(self, data): - self.data = data - self.pos = 0 - - def take(self, n): - if self.pos + n > len(self.data): - raise ParseError("unexpected end of data") - out = self.data[self.pos:self.pos + n] - self.pos += n - return out - - def u8(self): - return self.take(1)[0] - - def u32(self): - return struct.unpack(" 1: - nxt = [] - for i in range(0, len(layer), 2): - left = layer[i] - right = layer[i + 1] if i + 1 < len(layer) else left - nxt.append(sha256d(left + right)) - layer = nxt - return layer[0][::-1].hex() - - -def parse_block(payload, file_name="", offset=0): - r = Reader(payload) - header = r.take(80) - version, = struct.unpack(" len(data): - break - size = struct.unpack(" len(data): - break - blocks.append(parse_block(data[start:end], path.name, start)) - if limit and len(blocks) >= limit: - return blocks - pos = end - return blocks - - -def genesis_transaction(): - script_sig = ( - bytes.fromhex("04ffff001d") - + b"\x01\x04" - + bytes([len(GENESIS_TEXT)]) - + GENESIS_TEXT.encode("ascii") - ) - pubkey_num = bytes.fromhex( - "5f1df16b2b704c8a578d0bbaf74d385cde12c11ee50455f3c438ef4c3fbcf649" - "b6de611feae06279a60939e028a8d65c10b73071a6f16719274855feb0fd8a6704" - ) - # The genesis source uses CScript << CBigNum("0x..."), and CBigNum::getvch() - # returns the minimal little-endian script number representation. - script_pub_key = bytes([65]) + pubkey_num[::-1] + b"\xac" - tx = { - "version": 1, - "vin": [{ - "prevout_hash": "0" * 64, - "prevout_n": 0xFFFFFFFF, - "script_sig_hex": script_sig.hex(), - "sequence": 0xFFFFFFFF, - }], - "vout": [{ - "value_satoshis": 50 * COIN, - "value_btf": format_money(50 * COIN), - "script_pub_key_hex": script_pub_key.hex(), - }], - "lock_time": 0, - } - tx["txid"] = hash_hex(ser_tx(tx)) - return tx - - -def reconstructed_genesis(): - tx = genesis_transaction() - header = ( - struct.pack(" len(self.data): - raise ParseError("unexpected end of data") - out = self.data[self.pos:self.pos + n] - self.pos += n - return out - - def u8(self): - return self.take(1)[0] - - def u32(self): - return struct.unpack(" 1: - nxt = [] - for i in range(0, len(layer), 2): - left = layer[i] - right = layer[i + 1] if i + 1 < len(layer) else left - nxt.append(sha256d(left + right)) - layer = nxt - return layer[0][::-1].hex() - - -def parse_block(payload, file_name="", offset=0): - r = Reader(payload) - header = r.take(80) - version, = struct.unpack(" len(data): - break - size = struct.unpack(" len(data): - break - blocks.append(parse_block(data[start:end], path.name, start)) - if max_blocks and len(blocks) >= max_blocks: - return blocks - pos = end - return blocks - - -def select_main_chain(blocks): - by_hash = {} - for block in blocks: - by_hash.setdefault(block["hash"], block) - if GENESIS_HASH not in by_hash: - raise ParseError("genesis block not found in block files") - - children = {} - for block in by_hash.values(): - children.setdefault(block["previous_hash"], []).append(block) - for v in children.values(): - v.sort(key=lambda b: (b["hash"], b["source_file"], b["source_offset"])) - - best = [] - stack = [(by_hash[GENESIS_HASH], [by_hash[GENESIS_HASH]])] - while stack: - block, path = stack.pop() - if len(path) > len(best) or (len(path) == len(best) and block["hash"] < best[-1]["hash"]): - best = path - for child in children.get(block["hash"], []): - stack.append((child, path + [child])) - return best - - -def is_coinbase(tx): - return ( - len(tx["vin"]) == 1 - and tx["vin"][0]["prevout_hash"] == "0" * 64 - and tx["vin"][0]["prevout_n"] == 0xFFFFFFFF - ) - - -def utxo_leaf_hash(outpoint, utxo): - # Text fields are length-delimited by separators and all variable data is - # hex or decimal ASCII. That keeps the commitment language-neutral. - payload = "|".join([ - "BTFUTXO1", - outpoint, - str(utxo["value_satoshis"]), - utxo["script_pub_key_hex"], - str(utxo["created_height"]), - utxo["created_txid"], - ]).encode("ascii") - return sha256(payload) - - -def utxo_root(utxos): - leaves = [utxo_leaf_hash(k, utxos[k]) for k in sorted(utxos)] - if not leaves: - return sha256(b"BTFUTXO1|empty").hex() - layer = leaves - while len(layer) > 1: - nxt = [] - for i in range(0, len(layer), 2): - left = layer[i] - right = layer[i + 1] if i + 1 < len(layer) else left - nxt.append(sha256(b"BTFNODE1|" + left + right)) - layer = nxt - return layer[0].hex() - - -def apply_blocks(blocks): - utxos = {} - spent = 0 - missing_spends = [] - duplicate_outputs = [] - total_coinbase = 0 - total_fees = 0 - normal_txs = 0 - - for height, block in enumerate(blocks): - if block["computed_merkle_root"] != block["merkle_root"]: - raise ParseError("bad merkle root at height %d" % height) - for tx_index, tx in enumerate(block["transactions"]): - coinbase = is_coinbase(tx) - input_value = 0 - output_value = sum(out["value_satoshis"] for out in tx["vout"]) - if coinbase: - total_coinbase += output_value - else: - normal_txs += 1 - for txin in tx["vin"]: - prev = "%s:%d" % (txin["prevout_hash"], txin["prevout_n"]) - prev_utxo = utxos.pop(prev, None) - if prev_utxo is None: - missing_spends.append({ - "height": height, - "txid": tx["txid"], - "missing_outpoint": prev, - }) - else: - spent += prev_utxo["value_satoshis"] - input_value += prev_utxo["value_satoshis"] - if input_value >= output_value: - total_fees += input_value - output_value - for n, out in enumerate(tx["vout"]): - key = "%s:%d" % (tx["txid"], n) - if key in utxos: - duplicate_outputs.append({"height": height, "outpoint": key}) - utxos[key] = { - "txid": tx["txid"], - "vout": n, - "value_satoshis": out["value_satoshis"], - "value_btf": out["value_btf"], - "script_pub_key_hex": out["script_pub_key_hex"], - "created_height": height, - "created_block_hash": block["hash"], - "created_txid": tx["txid"], - "coinbase": coinbase, - } - - total_unspent = sum(u["value_satoshis"] for u in utxos.values()) - return { - "utxos": utxos, - "stats": { - "utxo_count": len(utxos), - "spent_outputs": spent, - "total_unspent_satoshis": total_unspent, - "total_unspent_btf": format_money(total_unspent), - "total_coinbase_satoshis": total_coinbase, - "total_coinbase_btf": format_money(total_coinbase), - "coinbase_minus_unspent_satoshis": total_coinbase - total_unspent, - "coinbase_minus_unspent_btf": format_money(total_coinbase - total_unspent), - "total_fees_satoshis": total_fees, - "total_fees_btf": format_money(total_fees), - "normal_transactions": normal_txs, - "missing_spends": missing_spends, - "duplicate_outputs": duplicate_outputs, - }, - } - - -def format_money(value): - sign = "-" if value < 0 else "" - value = abs(value) - return "%s%d.%08d" % (sign, value // COIN, value % COIN) - - -def utc(ts): - return _dt.datetime.fromtimestamp(ts, _dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") - - -def default_datadir(): - if os.name == "nt" and os.environ.get("APPDATA"): - return os.path.join(os.environ["APPDATA"], "Bitflash") - if os.environ.get("HOME"): - return os.path.join(os.environ["HOME"], ".bitflash") - return "." - - -def canonical_bytes(report): - return (json.dumps(report, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + "\n").encode("ascii") +import bitflash_chain as chain -def build_report(raw_blocks, include_utxos=False): - blocks = select_main_chain(raw_blocks) - state = apply_blocks(blocks) +def build_report(raw_blocks, include_utxos=False, strict_duplicates=False): + blocks = chain.select_main_chain(raw_blocks) + state = chain.apply_blocks(blocks, strict_duplicates=strict_duplicates) utxos = state["utxos"] stats = state["stats"] tip = blocks[-1] - root = utxo_root(utxos) + root = chain.utxo_root(utxos) checks = { - "genesis_hash_matches_source": blocks[0]["hash"] == GENESIS_HASH, - "genesis_merkle_matches_source": blocks[0]["merkle_root"] == GENESIS_MERKLE, + "genesis_hash_matches_source": blocks[0]["hash"] == chain.GENESIS_HASH, + "genesis_merkle_matches_source": blocks[0]["merkle_root"] == chain.GENESIS_MERKLE, "all_scanned_merkle_roots_match": all(b["computed_merkle_root"] == b["merkle_root"] for b in blocks), "no_missing_spends": len(stats["missing_spends"]) == 0, } @@ -370,8 +31,8 @@ def build_report(raw_blocks, include_utxos=False): "chain": { "name": "Bitflash", "ticker": "BTF", - "message_start_hex": MAGIC.hex(), - "coin": COIN, + "message_start_hex": chain.MAGIC.hex(), + "coin": chain.COIN, }, "commitment": { "algorithm": "sorted-outpoint-merkle-sha256", @@ -400,7 +61,7 @@ def build_report(raw_blocks, include_utxos=False): def print_text(report): - data_hash = hashlib.sha256(canonical_bytes(report)).hexdigest() + data_hash = hashlib.sha256(chain.canonical_bytes(report)).hexdigest() print("Bitflash UTXO-set commitment verifier") print("schema: %s" % report["schema"]) print("canonical report sha256: %s" % data_hash) @@ -437,20 +98,26 @@ def print_text(report): def main(argv): ap = argparse.ArgumentParser(description=__doc__) - ap.add_argument("--datadir", default=default_datadir(), help="Bitflash data directory") + ap.add_argument("--datadir", default=chain.default_datadir(), help="Bitflash data directory") ap.add_argument("--max-blocks", type=int, default=0, help="number of blocks to scan; 0 means all block files") ap.add_argument("--include-utxos", action="store_true", help="include the full sorted UTXO map in JSON output") + ap.add_argument("--strict-duplicates", action="store_true", + help="fail instead of using legacy last-write-wins duplicate output handling") ap.add_argument("--json", action="store_true", help="print canonical JSON") ap.add_argument("--out", help="write canonical JSON to this path") args = ap.parse_args(argv) - blocks = read_blocks(args.datadir, args.max_blocks) + blocks = chain.read_blocks(args.datadir, args.max_blocks) if not blocks: raise SystemExit("no Bitflash blocks found in %s" % args.datadir) - report = build_report(blocks, include_utxos=args.include_utxos) - data = canonical_bytes(report) + try: + report = build_report(blocks, include_utxos=args.include_utxos, + strict_duplicates=args.strict_duplicates) + except chain.ParseError as e: + raise SystemExit(str(e)) + data = chain.canonical_bytes(report) if args.out: Path(args.out).write_bytes(data) if args.json: diff --git a/tests/scripts/test_bitflash_tools.py b/tests/scripts/test_bitflash_tools.py new file mode 100644 index 0000000..ff569e1 --- /dev/null +++ b/tests/scripts/test_bitflash_tools.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +"""Regression tests for deterministic Bitflash Python tooling.""" + +import json +import os +import struct +import subprocess +import sys +import tempfile +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] +SCRIPTS = ROOT / "scripts" +sys.path.insert(0, str(SCRIPTS)) + +import bitflash_chain as chain # noqa: E402 + + +def run(args, **kwargs): + env = dict(os.environ) + env["PYTHONIOENCODING"] = "utf-8" + return subprocess.run( + [sys.executable] + [str(a) for a in args], + cwd=str(ROOT), + env=env, + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + **kwargs + ) + + +def write_block_file(path, blocks): + with Path(path).open("wb") as f: + f.write(b"noise") + f.write(chain.MAGIC) + f.write(struct.pack(" Date: Tue, 11 Aug 2026 14:47:35 -0300 Subject: tools: address audit follow-ups --- scripts/bitflash_chain.py | 69 +++++++++++++++++++++----------------------- scripts/build-explorer.py | 5 +++- scripts/prove-utxo.py | 7 ++--- scripts/verify-utxo-proof.py | 10 +++---- 4 files changed, 44 insertions(+), 47 deletions(-) (limited to 'scripts') diff --git a/scripts/bitflash_chain.py b/scripts/bitflash_chain.py index f3688f1..4a3231c 100644 --- a/scripts/bitflash_chain.py +++ b/scripts/bitflash_chain.py @@ -4,6 +4,7 @@ import datetime as _dt import hashlib import json +import mmap import os import struct from pathlib import Path @@ -11,6 +12,9 @@ from pathlib import Path MAGIC = bytes([0xBF, 0x20, 0x5C, 0xFD]) MAX_BLOCK_PAYLOAD = 32 * 1024 * 1024 +UTXO_LEAF_DOMAIN = "BTFUTXO1" +UTXO_NODE_DOMAIN_TAG = "BTFNODE1" +UTXO_NODE_DOMAIN = (UTXO_NODE_DOMAIN_TAG + "|").encode("ascii") COIN = 100_000_000 GENESIS_HASH = "5bd7cb255d814e48cebcdfb72da4dc87b34bd774227f8ceb546c5640f4bdc169" GENESIS_MERKLE = "1a45b4482532abb29b10e234d3f13132230525a339ecea91658ffa675a5b1325" @@ -181,40 +185,33 @@ def iter_block_files(datadir): def iter_blocks_from_file(path): path = Path(path) with path.open("rb") as f: - window = f.read(4) - offset = 0 - while len(window) == 4: - if window != MAGIC: - nxt = f.read(1) - if not nxt: + f.seek(0, os.SEEK_END) + file_size = f.tell() + if file_size == 0: + return + f.seek(0) + mm = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) + try: + pos = 0 + while True: + offset = mm.find(MAGIC, pos) + if offset < 0 or offset + 8 > file_size: break - window = window[1:] + nxt - offset += 1 - continue - - size_bytes = f.read(4) - if len(size_bytes) != 4: - break - size = struct.unpack(" MAX_BLOCK_PAYLOAD: - offset += 1 - f.seek(offset) - window = f.read(4) - continue - payload_offset = offset + 8 - payload = f.read(size) - if len(payload) != size: - break - try: - yield parse_block(payload, path.name, payload_offset) - except ParseError: - offset += 1 - f.seek(offset) - window = f.read(4) - continue - - offset = f.tell() - window = f.read(4) + size = struct.unpack(" MAX_BLOCK_PAYLOAD or end > file_size: + pos = offset + 1 + continue + payload = mm[payload_offset:end] + try: + yield parse_block(payload, path.name, payload_offset) + except ParseError: + pos = offset + 1 + continue + pos = end + finally: + mm.close() def read_blocks_from_files(paths, max_blocks=0): @@ -265,7 +262,7 @@ def is_coinbase(tx): def utxo_leaf_hash(outpoint, utxo): payload = "|".join([ - "BTFUTXO1", + UTXO_LEAF_DOMAIN, outpoint, str(utxo["value_satoshis"]), utxo["script_pub_key_hex"], @@ -285,7 +282,7 @@ def utxo_root(utxos): for i in range(0, len(layer), 2): left = layer[i] right = layer[i + 1] if i + 1 < len(layer) else left - nxt.append(sha256(b"BTFNODE1|" + left + right)) + nxt.append(sha256(UTXO_NODE_DOMAIN + left + right)) layer = nxt return layer[0].hex() @@ -327,9 +324,9 @@ def apply_blocks(blocks, strict_duplicates=False): for n, out in enumerate(tx["vout"]): key = "%s:%d" % (tx["txid"], n) if key in utxos: - duplicate_outputs.append({"height": height, "outpoint": key}) if strict_duplicates: raise ParseError("duplicate output at height %d: %s" % (height, key)) + duplicate_outputs.append({"height": height, "outpoint": key}) utxos[key] = { "txid": tx["txid"], "vout": n, diff --git a/scripts/build-explorer.py b/scripts/build-explorer.py index 0d86b3c..97a7f4e 100755 --- a/scripts/build-explorer.py +++ b/scripts/build-explorer.py @@ -35,7 +35,10 @@ def hash160(b): def spk_info(script_hex): - spk = bytes.fromhex(script_hex) + try: + spk = bytes.fromhex(script_hex) + except ValueError: + return {"type": "malformed", "hex": script_hex[:80]} if ( len(spk) == 25 and spk[0] == 0x76 and spk[1] == 0xA9 and spk[2] == 0x14 and spk[23] == 0x88 and spk[24] == 0xAC diff --git a/scripts/prove-utxo.py b/scripts/prove-utxo.py index 9f2704b..105e677 100644 --- a/scripts/prove-utxo.py +++ b/scripts/prove-utxo.py @@ -14,7 +14,6 @@ from pathlib import Path import bitflash_chain as chain SCHEMA = "bitflash-utxo-inclusion-proof-1" -NODE_DOMAIN = b"BTFNODE1|" def canonical_bytes(obj): @@ -61,7 +60,7 @@ def build_merkle_proof(keys, leaf_hashes, index): for i in range(0, len(layer), 2): left = layer[i] right = layer[i + 1] if i + 1 < len(layer) else left - next_layer.append(hashlib.sha256(NODE_DOMAIN + left + right).digest()) + next_layer.append(hashlib.sha256(chain.UTXO_NODE_DOMAIN + left + right).digest()) layer = next_layer current_index //= 2 level += 1 @@ -106,8 +105,8 @@ def build_proof(args): }, "commitment": { "algorithm": "sorted-outpoint-merkle-sha256", - "leaf_domain": "BTFUTXO1", - "node_domain": "BTFNODE1", + "leaf_domain": chain.UTXO_LEAF_DOMAIN, + "node_domain": chain.UTXO_NODE_DOMAIN_TAG, "root": root, "leaf_count": len(keys), }, diff --git a/scripts/verify-utxo-proof.py b/scripts/verify-utxo-proof.py index c29caa9..50d9356 100644 --- a/scripts/verify-utxo-proof.py +++ b/scripts/verify-utxo-proof.py @@ -10,8 +10,6 @@ from pathlib import Path import bitflash_chain as chain SCHEMA = "bitflash-utxo-inclusion-proof-1" -LEAF_DOMAIN = "BTFUTXO1" -NODE_DOMAIN = b"BTFNODE1|" def canonical_bytes(obj): @@ -32,9 +30,9 @@ def verify(proof, expected_root=None): commitment = proof.get("commitment", {}) if commitment.get("algorithm") != "sorted-outpoint-merkle-sha256": failures.append("unexpected commitment algorithm") - if commitment.get("leaf_domain") != LEAF_DOMAIN: + if commitment.get("leaf_domain") != chain.UTXO_LEAF_DOMAIN: failures.append("unexpected leaf domain") - if commitment.get("node_domain") != "BTFNODE1": + if commitment.get("node_domain") != chain.UTXO_NODE_DOMAIN_TAG: failures.append("unexpected node domain") target = proof.get("target", {}) @@ -64,9 +62,9 @@ def verify(proof, expected_root=None): failures.append("bad sibling hash length at proof level %d" % i) side = step.get("side") if side == "right": - current = chain.sha256(NODE_DOMAIN + current + sibling) + current = chain.sha256(chain.UTXO_NODE_DOMAIN + current + sibling) elif side == "left": - current = chain.sha256(NODE_DOMAIN + sibling + current) + current = chain.sha256(chain.UTXO_NODE_DOMAIN + sibling + current) else: failures.append("bad sibling side at proof level %d" % i) -- cgit v1.2.3 From b0ed2ce585d2a6fd7c6d8d4db7422734aedccb58 Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Wed, 12 Aug 2026 11:58:03 -0300 Subject: site: document public deployment contract --- README.md | 17 +++++ docs/mining-calculator.md | 22 ++++++ docs/pool-directory.md | 18 +++++ docs/release-verification.md | 10 +++ docs/site.md | 115 +++++++++++++++++++++++++++++++ docs/tor.md | 29 ++++++++ scripts/build-explorer.py | 128 +++++++++++++++++++++++------------ tests/scripts/test_bitflash_tools.py | 7 +- 8 files changed, 300 insertions(+), 46 deletions(-) create mode 100644 docs/site.md (limited to 'scripts') diff --git a/README.md b/README.md index ef4f39b..e671302 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ Worth being precise, because the difference matters if you are relying on it. This is unlinkability between peers, not anonymity against a network observer. For Tor routing, start the node with `-tor`; see [Tor mode](docs/tor.md). +For a plain SOCKS5 proxy without Tor-specific defaults, use +`-socks=HOST:PORT`. --- @@ -74,6 +76,14 @@ scripts/verify-release.sh latest See [release verification](docs/release-verification.md) for the full release audit flow and the maintainer signing step. +Public mirrors should make the latest checksum files available both under the +versioned release directory and at the release root: + +```text +https://releases.bitflash.network/v1.2.18/SHA256SUMS +https://releases.bitflash.network/SHA256SUMS # alias to latest +``` + Bitflash also ships a deterministic UTXO-set commitment tool: ```bash @@ -113,6 +123,12 @@ python3 scripts/build-explorer.py --datadir ~/.bitflash ./explorer-out python3 scripts/build-explorer.py ~/.bitflash/blk0001.dat ~/.bitflash/blk0002.dat ./explorer-out ``` +The generated explorer is static and self-contained: `index.html`, +`style.css`, `explorer.js`, `blocks.json`, `block/*.json`, and `logo.png`. It is +safe to host behind a strict policy such as `default-src 'none'; img-src 'self'; +style-src 'self'; script-src 'self'; connect-src 'self'; base-uri 'none'; +form-action 'none'`. + **Keep your node current.** Consensus rules have changed since the first releases — 1.2.1 fixed a bug that let anyone spend anyone's coins, and 1.2.2 added a per-block signature-operation cap. A node on an older build will accept @@ -265,6 +281,7 @@ default both are created in the data directory; use `-poolstatusfile=PATH` and `-poolroundsfile=PATH` to write them somewhere a dashboard or web sync can read. The files are informational only: nodes still discover pools through Nostr and `.btf`, not through any web domain. +See [public pool directory](docs/pool-directory.md) for the website/API shape. **Participant** — mine to someone else's pool. Enter or select the pool's `.btf` address and enable Start Mining. diff --git a/docs/mining-calculator.md b/docs/mining-calculator.md index 352eea1..e6cb61b 100644 --- a/docs/mining-calculator.md +++ b/docs/mining-calculator.md @@ -84,3 +84,25 @@ The script is intentionally standalone so the same math can later back a website widget or status dashboard. Until a public price exists, a website calculator should keep price as a manual field and label fiat profit as speculative. + +## Website Fields + +A public calculator should ask for: + +- miner hashrate and unit; +- power draw in watts; +- electricity price per kWh; +- pool fee percent; +- optional BTF price; +- either current Bitflash difficulty or current network hashrate. + +The output should separate: + +- estimated BTF per day; +- estimated gross fiat revenue, if a manual price was provided; +- electricity cost; +- net profit/loss; +- the difficulty/hashrate assumptions used. + +Never present the fiat number as a market quote unless it comes from a real +public market. Before that exists, label it as a manual scenario. diff --git a/docs/pool-directory.md b/docs/pool-directory.md index 76269b9..ff0640a 100644 --- a/docs/pool-directory.md +++ b/docs/pool-directory.md @@ -26,6 +26,10 @@ The generated output contains: - `index.html`: human-readable ranking page. - `pools.json`: machine-readable API for `api.bitflash.network/pools.json`. +The website is a convenience layer, not a consensus or discovery dependency. +Nodes discover pools through Nostr descriptors and `.btf`; a missing website +must not stop mining or pool operation. + When a source is an HTTP(S) `pool_status.json`, the directory links to a `pool_rounds.json` proof file next to it. That file records the block height, block hash, coinbase txid, coinbase value, pool fee, total shares, per-address @@ -41,3 +45,17 @@ Ranking order is intentionally simple: An entry is considered online when the source says the pool is running and its status file is no more than 120 seconds old. + +## Recommended Hostnames + +Use: + +```text +pool.bitflash.network human-readable pool directory +api.bitflash.network/pools.json +status.bitflash.network relay/node health, not pool ranking +docs.bitflash.network operator setup guide +``` + +The GUI may use the API as a shortcut for humans, but it should keep the +existing decentralized pool discovery path as the source of truth. diff --git a/docs/release-verification.md b/docs/release-verification.md index 32de74f..408bab9 100644 --- a/docs/release-verification.md +++ b/docs/release-verification.md @@ -34,6 +34,16 @@ From a shell with `curl` or `wget`, `sha256sum` and `gpg`: scripts/verify-release.sh latest ``` +The public release host should expose latest-file aliases for convenience: + +```text +https://releases.bitflash.network/SHA256SUMS +https://releases.bitflash.network/SHA256SUMS.asc +``` + +Those aliases must point at the current versioned directory. They do not replace +signature verification; they only make the common manual path shorter. + For a specific release: ```bash diff --git a/docs/site.md b/docs/site.md new file mode 100644 index 0000000..522da10 --- /dev/null +++ b/docs/site.md @@ -0,0 +1,115 @@ +# Public Site Contract + +The public Bitflash sites are generated from this repository. If a deployed +page and the source disagree, the deployed page is stale. + +## Canonical Navigation + +Every public page should use the same top navigation: + +| label | target | +|---|---| +| home | `https://bitflash.network/` | +| code | `https://git.bitflash.network/bitflash/bitflash` | +| docs | `https://docs.bitflash.network/` | +| downloads | `https://releases.bitflash.network/` | +| explorer | `https://explorer.bitflash.network/` | +| status | `https://status.bitflash.network/` | + +The `code` link intentionally points at the repository, not the Forgejo +instance root. A visitor choosing "code" wants the source tree. + +## Documentation Pages To Publish + +The docs index should expose these source files: + +| page | source | why it matters | +|---|---|---| +| Manual | `README.md` | run, mine, recover, operate | +| Key derivation and address format | `docs/derivation.md` | independent wallet recovery | +| Release verification | `docs/release-verification.md` | pinned signing-key flow | +| Dependencies | `docs/dependencies.md` | pinned secp256k1/RandomX supply chain | +| Rendezvous and .btf addressing | `docs/rendezvous.md` | network design and caveats | +| Tor mode | `docs/tor.md` | `-tor`, `-socks`, onion relay operation | +| Fair launch verifier | `docs/fair-launch.md` | no-premine launch audit | +| UTXO set commitment | `docs/utxo-commitment.md` | deterministic supply/root audit | +| UTXO inclusion proofs | `docs/utxo-proofs.md` | proof for one unspent output | +| Mining calculator | `docs/mining-calculator.md` | local WhatToMine-style estimates | +| Public pool directory | `docs/pool-directory.md` | `pool_status.json`, `pools.json`, ranking | + +## Release Mirror Aliases + +Each release remains under its versioned directory: + +```text +https://releases.bitflash.network/v1.2.18/SHA256SUMS +https://releases.bitflash.network/v1.2.18/SHA256SUMS.asc +``` + +The release root should also alias the latest files: + +```text +https://releases.bitflash.network/SHA256SUMS +https://releases.bitflash.network/SHA256SUMS.asc +https://releases.bitflash.network/Bitflash-latest-windows.zip +https://releases.bitflash.network/Bitflash-latest-x86_64.AppImage +https://releases.bitflash.network/bitflash-node-latest-x86_64 +``` + +The aliases are convenience only. Verification must still require the pinned +release key documented in `docs/release-verification.md`. + +## Static Explorer + +`scripts/build-explorer.py` writes a static explorer with no backend: + +```text +index.html +style.css +explorer.js +logo.png +blocks.json +block/.json +``` + +Serve it with a strict content policy: + +```text +default-src 'none'; +img-src 'self'; +style-src 'self'; +script-src 'self'; +connect-src 'self'; +base-uri 'none'; +form-action 'none'; +frame-ancestors 'none'; +``` + +The viewer must never use `innerHTML` for chain data. A malicious transaction +must be rendered as text, not interpreted as markup. + +## Cache Policy + +Recommended defaults: + +| path | cache | +|---|---| +| HTML pages | `public, max-age=0, must-revalidate` | +| `status.json` | `no-store` | +| `blocks.json`, `block/*.json` | `public, max-age=0, must-revalidate` | +| release assets | `public, max-age=14400, must-revalidate` | +| `SHA256SUMS`, signatures, keys | `public, max-age=0, must-revalidate` | + +## Security Headers + +Recommended for all static pages: + +```text +Strict-Transport-Security: max-age=31536000; includeSubDomains +X-Content-Type-Options: nosniff +Referrer-Policy: no-referrer +Permissions-Policy: interest-cohort=(), geolocation=(), camera=(), microphone=() +``` + +Forgejo has its own application headers and should not blindly inherit the +static-site CSP. diff --git a/docs/tor.md b/docs/tor.md index 087c822..69b29f1 100644 --- a/docs/tor.md +++ b/docs/tor.md @@ -25,6 +25,17 @@ bitflash -tor=127.0.0.1:9150 bitflash -tor=[::1]:9050 ``` +For a SOCKS5 proxy that is not Tor, use `-socks` directly: + +```bash +bitflash -socks=127.0.0.1:9050 +bitflash -socks=[::1]:9050 +``` + +If both `-tor` and `-socks` are present, Tor mode wins and the node prints a +warning. That keeps a command line with `-tor` from silently becoming a custom +proxy route. + When Tor mode is enabled: - outbound Nostr relay dials use Tor; @@ -34,6 +45,24 @@ When Tor mode is enabled: - direct `.onion` dials are refused unless a SOCKS5/Tor proxy is enabled; - plain HTTP external-IP probes are skipped. +Tor mode affects outbound discovery and relay dials. It does not encrypt +`wallet.dat`, hide mining rewards on-chain, or change the `.btf` identity. A +relay still sees the TCP client that connected to it; with Tor mode that client +is the Tor exit or onion circuit endpoint instead of the user's direct IP. + +## What to test + +Use the public status page only as a rough health check. To prove the local node +is using Tor/SOCKS, test from the node machine: + +```bash +bitflash -tor -debug +``` + +Then confirm in `debug.log` that Nostr and rendezvous dials report the proxy +path. A DNS leak test should show no local resolver lookup for relay hostnames: +the SOCKS5 request uses the domain-name form and asks the proxy to resolve it. + ## Onion rendezvous relay An operator can expose a Bitflash rendezvous relay as a Tor hidden service. The diff --git a/scripts/build-explorer.py b/scripts/build-explorer.py index 97a7f4e..dbb8e17 100755 --- a/scripts/build-explorer.py +++ b/scripts/build-explorer.py @@ -4,6 +4,7 @@ import argparse import hashlib import json +import shutil import sys from pathlib import Path @@ -148,6 +149,11 @@ def build_explorer(raw_blocks, out_dir): encoding="ascii", ) (out / "index.html").write_text(INDEX_HTML, encoding="utf-8") + (out / "style.css").write_text(STYLE_CSS, encoding="utf-8") + (out / "explorer.js").write_text(EXPLORER_JS, encoding="utf-8") + logo = Path(__file__).resolve().parents[1] / "docs" / "logo.png" + if logo.exists(): + shutil.copyfile(logo, out / "logo.png") return len(blocks), len(blocks) - 1 @@ -167,43 +173,26 @@ INDEX_HTML = r""" Bitflash Block Explorer - + + + + -
-

Bitflash Block Explorer

- - - +
+ Bitflash + +
+

Block explorer

Read-only view of the main chain: blocks, their transactions, and the coinbase payout. There is no mempool and no per-address history here.

@@ -213,9 +202,63 @@ INDEX_HTML = r"""

- +
- - - """ diff --git a/tests/scripts/test_bitflash_tools.py b/tests/scripts/test_bitflash_tools.py index 4b18d1e..3b4c846 100644 --- a/tests/scripts/test_bitflash_tools.py +++ b/tests/scripts/test_bitflash_tools.py @@ -159,8 +159,11 @@ def compare_golden(name, actual, update): path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(actual) return - expected = path.read_bytes() - if actual != expected: + # The tools promise canonical LF-terminated JSON. On Windows checkouts a + # fixture may still appear as CRLF in the working tree; normalize only line + # endings so content, field order and compact serialization still drift-test. + expected = path.read_bytes().replace(b"\r\n", b"\n") + if actual.replace(b"\r\n", b"\n") != expected: raise AssertionError("golden mismatch: %s" % name) -- cgit v1.2.3 From 44fbf6895bd3acd11f552514e746a2e3876cad2d Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Thu, 13 Aug 2026 14:29:14 -0300 Subject: release: package Windows builds with managed Tor --- Makefile | 6 +- README.md | 13 ++++ docs/release-verification.md | 6 ++ docs/tor.md | 16 +++++ scripts/make-release-checksums.sh | 2 + scripts/package-windows-tor.sh | 132 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 174 insertions(+), 1 deletion(-) create mode 100755 scripts/package-windows-tor.sh (limited to 'scripts') diff --git a/Makefile b/Makefile index b4d0cf7..d1277a4 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ # # make linux build Bitflash-*-x86_64.AppImage # make windows build Bitflash-*-windows.zip (from MSYS2 UCRT64) +# make windows-tor build Bitflash-*-windows-with-tor.zip # make tests build and run standalone unit tests # make checksums create SHA256SUMS for release assets # make verify-release TAG=v1.2.13 @@ -180,6 +181,9 @@ windows: deps-windows @zip -r Bitflash-$(VERSION)-windows.zip Bitflash-$(VERSION)-windows/ @echo "Built: Bitflash-$(VERSION)-windows.zip" +windows-tor: windows + ./scripts/package-windows-tor.sh + # ---- Clean ---------------------------------------------------------------- clean: @@ -206,6 +210,6 @@ sign-checksums: verify-release: ./scripts/verify-release.sh $(if $(TAG),$(TAG),latest) -.PHONY: linux windows clean appimage \ +.PHONY: linux windows windows-tor clean appimage \ tests fuzz-net-message-smoke fuzz-script-smoke checksums sign-checksums verify-release \ deps-linux deps-windows deps-apt deps-secp256k1 deps-randomx diff --git a/README.md b/README.md index a7ce2b2..c30f171 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,8 @@ configuration — it connects automatically and starts syncing. **Linux:** make the `.AppImage` executable and run it. **Windows:** extract the `-windows.zip` and run `Bitflash.exe`. +If you want managed Tor without a separate Tor install, use the +`-windows-with-tor.zip` package and start Bitflash with `-managedtor`. Every release ships a `SHA256SUMS` covering the assets. Verifying takes a second and is worth doing: @@ -452,6 +454,17 @@ make windows ``` Installs deps via pacman, produces `Bitflash-*-windows.zip`. +To build the Windows package that also carries the Tor Expert Bundle: + +```bash +make windows-tor +``` + +That produces `Bitflash-*-windows-with-tor.zip`. The release script downloads +the Tor Expert Bundle from the official Tor archive, verifies its pinned SHA256, +and places `tor/tor.exe` beside `Bitflash.exe` so `-managedtor` works without a +separate Tor install. + --- ## At a glance diff --git a/docs/release-verification.md b/docs/release-verification.md index 408bab9..04d18a3 100644 --- a/docs/release-verification.md +++ b/docs/release-verification.md @@ -81,6 +81,12 @@ Publish, alongside the assets, `SHA256SUMS` and `SHA256SUMS.asc`, and update is ever rotated, announce the new fingerprint clearly and update the embedded key in `verify-release.sh`. +For Windows releases with managed Tor built in, run `make windows-tor`. It +creates `Bitflash-*-windows-with-tor.zip` by downloading the Tor Expert Bundle, +checking the pinned SHA256 in `scripts/package-windows-tor.sh`, and placing +`tor/tor.exe` next to `Bitflash.exe`. Include that zip in `SHA256SUMS` like any +other release asset. + ## Why this matters `SHA256SUMS` alone protects against a broken download, but not against someone diff --git a/docs/tor.md b/docs/tor.md index 1c0f589..be6a145 100644 --- a/docs/tor.md +++ b/docs/tor.md @@ -53,6 +53,22 @@ Without a value, Bitflash looks for Tor in this order: 2. `tor.exe` or `tor` beside the Bitflash binary; 3. `tor.exe` or `tor` on `PATH`. +Windows releases can also be built as `Bitflash-*-windows-with-tor.zip`. That +package includes the official Tor Expert Bundle under `tor/`, so the first path +above exists immediately after extraction and `bitflash -managedtor` needs no +separate Tor install. + +The bundled-Tor package is created by: + +```bash +make windows-tor +``` + +The packaging script verifies a pinned SHA256 for the downloaded Tor Expert +Bundle before copying it into the release directory. The Tor binary is bundled; +the runtime data directory, hidden-service key, and control cookie are still +created under the user's Bitflash data directory. + To point at a specific Tor executable: ```bash diff --git a/scripts/make-release-checksums.sh b/scripts/make-release-checksums.sh index 5b29e2d..9c4f45a 100755 --- a/scripts/make-release-checksums.sh +++ b/scripts/make-release-checksums.sh @@ -10,6 +10,7 @@ Usage: scripts/make-release-checksums.sh [--sign] [--local-user KEYID] Creates SHA256SUMS for release assets in the current directory: Bitflash-*-windows.zip + Bitflash-*-windows-with-tor.zip Bitflash-*-x86_64.AppImage bitflash-node-*-x86_64 @@ -44,6 +45,7 @@ fi assets=() for pattern in \ "Bitflash-*-windows.zip" \ + "Bitflash-*-windows-with-tor.zip" \ "Bitflash-*-x86_64.AppImage" \ "bitflash-node-*-x86_64" do diff --git a/scripts/package-windows-tor.sh b/scripts/package-windows-tor.sh new file mode 100755 index 0000000..084fd1a --- /dev/null +++ b/scripts/package-windows-tor.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +VERSION="${VERSION:-$(sed -n 's/^VERSION[[:space:]]*:= *//p' "$ROOT/Makefile" | head -1)}" + +TOR_VERSION="${TOR_VERSION:-15.0.19}" +TOR_ARCHIVE_NAME="tor-expert-bundle-windows-x86_64-${TOR_VERSION}.tar.gz" +TOR_URL="${TOR_URL:-https://archive.torproject.org/tor-package-archive/torbrowser/${TOR_VERSION}/${TOR_ARCHIVE_NAME}}" +TOR_SHA256="${TOR_SHA256:-6AC067402C7B4A3DC37887ED3754B3914B67FDC220C966190683E9CCF91ABF0F}" + +BUNDLE_DIR="${BUNDLE_DIR:-$ROOT/Bitflash-${VERSION}-windows}" +OUT_ZIP="${OUT_ZIP:-$ROOT/Bitflash-${VERSION}-windows-with-tor.zip}" +CACHE_DIR="${CACHE_DIR:-$ROOT/.cache}" +ARCHIVE="${TOR_ARCHIVE:-$CACHE_DIR/$TOR_ARCHIVE_NAME}" + +usage() { + cat </dev/null 2>&1 || { + echo "missing required command: $1" >&2 + exit 1 + } +} + +need sha256sum +need tar +need zip + +if [ ! -d "$BUNDLE_DIR" ]; then + echo "missing Windows release directory: $BUNDLE_DIR" >&2 + echo "run 'make windows' first, or set BUNDLE_DIR" >&2 + exit 1 +fi +if [ ! -f "$BUNDLE_DIR/Bitflash.exe" ]; then + echo "missing Bitflash.exe in $BUNDLE_DIR" >&2 + exit 1 +fi +BUNDLE_PARENT="$(cd "$(dirname "$BUNDLE_DIR")" && pwd)" +BUNDLE_NAME="$(basename "$BUNDLE_DIR")" +OUT_PARENT="$(mkdir -p "$(dirname "$OUT_ZIP")" && cd "$(dirname "$OUT_ZIP")" && pwd)" +OUT_NAME="$(basename "$OUT_ZIP")" +OUT_ZIP_ABS="$OUT_PARENT/$OUT_NAME" + +mkdir -p "$CACHE_DIR" +if [ ! -f "$ARCHIVE" ]; then + if command -v curl >/dev/null 2>&1; then + curl -L --fail --output "$ARCHIVE" "$TOR_URL" + elif command -v wget >/dev/null 2>&1; then + wget -O "$ARCHIVE" "$TOR_URL" + else + echo "missing required command: curl or wget" >&2 + exit 1 + fi +fi + +got="$(sha256sum "$ARCHIVE" | awk '{print tolower($1)}')" +want="$(printf '%s' "$TOR_SHA256" | tr 'A-F' 'a-f')" +if [ "$got" != "$want" ]; then + echo "Tor archive SHA256 mismatch" >&2 + echo " expected: $want" >&2 + echo " got: $got" >&2 + exit 1 +fi + +tmp="$(mktemp -d "${TMPDIR:-/tmp}/bitflash-tor-package.XXXXXX")" +trap 'rm -rf "$tmp"' EXIT + +mkdir -p "$tmp/extract" +tar -xzf "$ARCHIVE" -C "$tmp/extract" + +if [ ! -f "$tmp/extract/tor/tor.exe" ]; then + echo "Tor archive did not contain tor/tor.exe" >&2 + exit 1 +fi + +rm -rf "$BUNDLE_DIR/tor" +mkdir -p "$BUNDLE_DIR/tor" +cp -R "$tmp/extract/tor/." "$BUNDLE_DIR/tor/" +if [ -d "$tmp/extract/docs" ]; then + mkdir -p "$BUNDLE_DIR/tor/docs" + cp -R "$tmp/extract/docs/." "$BUNDLE_DIR/tor/docs/" +fi +if [ -d "$tmp/extract/data" ]; then + mkdir -p "$BUNDLE_DIR/tor/data-defaults" + cp -R "$tmp/extract/data/." "$BUNDLE_DIR/tor/data-defaults/" +fi + +cat > "$BUNDLE_DIR/TOR-EXPERT-BUNDLE.txt" < Date: Thu, 13 Aug 2026 14:35:49 -0300 Subject: release: verify bundled Tor signatures --- README.md | 6 ++-- docs/release-verification.md | 8 +++-- docs/tor.md | 9 ++++-- scripts/package-windows-tor.sh | 71 +++++++++++++++++++++++++++++++++++++----- 4 files changed, 78 insertions(+), 16 deletions(-) (limited to 'scripts') diff --git a/README.md b/README.md index c30f171..bb6a4d4 100644 --- a/README.md +++ b/README.md @@ -462,8 +462,10 @@ make windows-tor That produces `Bitflash-*-windows-with-tor.zip`. The release script downloads the Tor Expert Bundle from the official Tor archive, verifies its pinned SHA256, -and places `tor/tor.exe` beside `Bitflash.exe` so `-managedtor` works without a -separate Tor install. +optionally verifies the Tor Project GPG signature when `gpg` is available, and +places `tor/tor.exe` beside `Bitflash.exe` so `-managedtor` works without a +separate Tor install. For release builds, use `TOR_VERIFY_GPG=required make +windows-tor` to require the extra signature check. --- diff --git a/docs/release-verification.md b/docs/release-verification.md index 04d18a3..b8eaf5e 100644 --- a/docs/release-verification.md +++ b/docs/release-verification.md @@ -83,9 +83,11 @@ key in `verify-release.sh`. For Windows releases with managed Tor built in, run `make windows-tor`. It creates `Bitflash-*-windows-with-tor.zip` by downloading the Tor Expert Bundle, -checking the pinned SHA256 in `scripts/package-windows-tor.sh`, and placing -`tor/tor.exe` next to `Bitflash.exe`. Include that zip in `SHA256SUMS` like any -other release asset. +checking the pinned SHA256 in `scripts/package-windows-tor.sh`, optionally +verifying the Tor Project `.asc` signature, and placing `tor/tor.exe` next to +`Bitflash.exe`. For release builds, run `TOR_VERIFY_GPG=required make +windows-tor` so the signature check is mandatory. Include that zip in +`SHA256SUMS` like any other release asset. ## Why this matters diff --git a/docs/tor.md b/docs/tor.md index be6a145..a408704 100644 --- a/docs/tor.md +++ b/docs/tor.md @@ -65,9 +65,12 @@ make windows-tor ``` The packaging script verifies a pinned SHA256 for the downloaded Tor Expert -Bundle before copying it into the release directory. The Tor binary is bundled; -the runtime data directory, hidden-service key, and control cookie are still -created under the user's Bitflash data directory. +Bundle before copying it into the release directory. When `gpg` is available it +also verifies the matching `.asc` signature against the pinned Tor Browser +Developers signing-key fingerprint. Release builds should use +`TOR_VERIFY_GPG=required make windows-tor` to make that second check mandatory. +The Tor binary is bundled; the runtime data directory, hidden-service key, and +control cookie are still created under the user's Bitflash data directory. To point at a specific Tor executable: diff --git a/scripts/package-windows-tor.sh b/scripts/package-windows-tor.sh index 084fd1a..c8cca3b 100755 --- a/scripts/package-windows-tor.sh +++ b/scripts/package-windows-tor.sh @@ -8,11 +8,15 @@ TOR_VERSION="${TOR_VERSION:-15.0.19}" TOR_ARCHIVE_NAME="tor-expert-bundle-windows-x86_64-${TOR_VERSION}.tar.gz" TOR_URL="${TOR_URL:-https://archive.torproject.org/tor-package-archive/torbrowser/${TOR_VERSION}/${TOR_ARCHIVE_NAME}}" TOR_SHA256="${TOR_SHA256:-6AC067402C7B4A3DC37887ED3754B3914B67FDC220C966190683E9CCF91ABF0F}" +TOR_ASC_URL="${TOR_ASC_URL:-${TOR_URL}.asc}" +TOR_SIGNING_KEY_FINGERPRINT="${TOR_SIGNING_KEY_FINGERPRINT:-EF6E286DDA85EA2A4BA7DE684E2C6E8793298290}" +TOR_VERIFY_GPG="${TOR_VERIFY_GPG:-auto}" BUNDLE_DIR="${BUNDLE_DIR:-$ROOT/Bitflash-${VERSION}-windows}" OUT_ZIP="${OUT_ZIP:-$ROOT/Bitflash-${VERSION}-windows-with-tor.zip}" CACHE_DIR="${CACHE_DIR:-$ROOT/.cache}" ARCHIVE="${TOR_ARCHIVE:-$CACHE_DIR/$TOR_ARCHIVE_NAME}" +ASC_FILE="${TOR_ASC:-$ARCHIVE.asc}" usage() { cat </dev/null 2>&1; then + curl -L --fail --output "$out" "$url" + elif command -v wget >/dev/null 2>&1; then + wget -O "$out" "$url" + else + echo "missing required command: curl or wget" >&2 + exit 1 + fi +} + if [ ! -d "$BUNDLE_DIR" ]; then echo "missing Windows release directory: $BUNDLE_DIR" >&2 echo "run 'make windows' first, or set BUNDLE_DIR" >&2 @@ -65,14 +85,7 @@ OUT_ZIP_ABS="$OUT_PARENT/$OUT_NAME" mkdir -p "$CACHE_DIR" if [ ! -f "$ARCHIVE" ]; then - if command -v curl >/dev/null 2>&1; then - curl -L --fail --output "$ARCHIVE" "$TOR_URL" - elif command -v wget >/dev/null 2>&1; then - wget -O "$ARCHIVE" "$TOR_URL" - else - echo "missing required command: curl or wget" >&2 - exit 1 - fi + download "$TOR_URL" "$ARCHIVE" fi got="$(sha256sum "$ARCHIVE" | awk '{print tolower($1)}')" @@ -87,6 +100,48 @@ fi tmp="$(mktemp -d "${TMPDIR:-/tmp}/bitflash-tor-package.XXXXXX")" trap 'rm -rf "$tmp"' EXIT +case "$TOR_VERIFY_GPG" in + auto|required|off) ;; + *) echo "TOR_VERIFY_GPG must be auto, required, or off" >&2; exit 2 ;; +esac + +if [ "$TOR_VERIFY_GPG" != "off" ]; then + if command -v gpg >/dev/null 2>&1; then + if [ ! -f "$ASC_FILE" ]; then + download "$TOR_ASC_URL" "$ASC_FILE" + fi + gnupg_home="$tmp/gnupg" + mkdir -p "$gnupg_home" + chmod 0700 "$gnupg_home" + gpg --homedir "$gnupg_home" --batch --auto-key-locate nodefault,wkd,keyserver \ + --locate-keys torbrowser@torproject.org >/dev/null 2>&1 || { + if [ "$TOR_VERIFY_GPG" = "required" ]; then + echo "could not import Tor Browser Developers signing key" >&2 + exit 1 + fi + echo "WARNING: skipped Tor GPG signature verification; signing key import failed" >&2 + } + if gpg --homedir "$gnupg_home" --batch --with-colons --fingerprint "$TOR_SIGNING_KEY_FINGERPRINT" \ + 2>/dev/null | grep -q "^fpr:::::::::${TOR_SIGNING_KEY_FINGERPRINT}:"; then + gpg --homedir "$gnupg_home" --batch --verify "$ASC_FILE" "$ARCHIVE" >/dev/null 2>&1 || { + echo "Tor archive GPG signature verification failed" >&2 + exit 1 + } + echo "Tor archive GPG signature verified against ${TOR_SIGNING_KEY_FINGERPRINT}" + elif [ "$TOR_VERIFY_GPG" = "required" ]; then + echo "Tor signing key fingerprint mismatch" >&2 + exit 1 + else + echo "WARNING: skipped Tor GPG signature verification; signing key fingerprint was not available" >&2 + fi + elif [ "$TOR_VERIFY_GPG" = "required" ]; then + echo "missing required command for TOR_VERIFY_GPG=required: gpg" >&2 + exit 1 + else + echo "WARNING: gpg not found; relying on pinned SHA256 only" >&2 + fi +fi + mkdir -p "$tmp/extract" tar -xzf "$ARCHIVE" -C "$tmp/extract" -- cgit v1.2.3 From 5309170e567f4a8655b3b622e7b26b6d8b382942 Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Thu, 13 Aug 2026 15:24:10 -0300 Subject: net: add onion-only btf dial mode --- docs/tor.md | 28 ++++++ scripts/smoke-managed-tor-onion.ps1 | 174 ++++++++++++++++++++++++++++++++++++ src/main_gui.cpp | 4 + src/net.cpp | 25 +++++- src/net.h | 1 + src/nostr.cpp | 11 +-- src/proxy.cpp | 51 ++++++++++- 7 files changed, 283 insertions(+), 11 deletions(-) create mode 100644 scripts/smoke-managed-tor-onion.ps1 (limited to 'scripts') diff --git a/docs/tor.md b/docs/tor.md index a408704..0016f8b 100644 --- a/docs/tor.md +++ b/docs/tor.md @@ -201,3 +201,31 @@ but an already-discovered onion-capable peer can be reached directly over Tor. Tor owns the private key under `HiddenServiceDir`; back it up if you want the onion name to stay stable. + +## Release smoke test + +After building a Windows binary with managed Tor support, run the two-node smoke +test before cutting a release: + +```powershell +powershell -ExecutionPolicy Bypass -File scripts\smoke-managed-tor-onion.ps1 ` + -Bitflash .\src\bitflash.exe ` + -Tor C:\path\to\tor.exe +``` + +The script creates two temporary datadirs, starts node A with `-managedtor`, +waits for it to publish its `.btf` identity and generated `.onion` endpoint, +then starts node B with its own `-managedtor`, `-oniononly`, and +`-connectbtf=`. It passes only when node B logs that it connected to +node A `via direct onion`; rendezvous fallback cannot satisfy the test. + +By default the script waits 120 seconds after node A's descriptor self-resolves +before it starts node B. Tor can create the local `hostname` before the hidden +service descriptor is visible enough for client circuits, and early dials often +fail with SOCKS5 reply `0x01`. Override this with `-OnionWarmupSec N` when +testing a slow or already-warmed Tor environment. + +This test still uses Nostr for descriptor discovery, because that is how nodes +learn each other's signed `.btf` descriptors. It does not require a Bitflash +rendezvous relay to carry the P2P connection: once the descriptor includes the +signed onion endpoint, the actual node connection goes directly through Tor. diff --git a/scripts/smoke-managed-tor-onion.ps1 b/scripts/smoke-managed-tor-onion.ps1 new file mode 100644 index 0000000..70658c7 --- /dev/null +++ b/scripts/smoke-managed-tor-onion.ps1 @@ -0,0 +1,174 @@ +param( + [string]$Bitflash, + [string]$Tor, + [string]$WorkDir, + [int]$TimeoutSec = 600, + [int]$OnionWarmupSec = 120 +) + +$ErrorActionPreference = "Stop" + +function Resolve-RepoRoot { + $scriptDir = Split-Path -Parent $PSCommandPath + return (Resolve-Path (Join-Path $scriptDir "..")).Path +} + +function Get-FreeTcpPort { + $listener = [Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback, 0) + $listener.Start() + try { + return ([Net.IPEndPoint]$listener.LocalEndpoint).Port + } + finally { + $listener.Stop() + } +} + +function Wait-RegexInFile { + param( + [string]$Path, + [string]$Pattern, + [datetime]$Deadline, + [string]$Label + ) + + while ((Get-Date) -lt $Deadline) { + if (Test-Path $Path) { + $text = Get-Content -Raw -ErrorAction SilentlyContinue $Path + if ($null -ne $text) { + $m = [regex]::Match($text, $Pattern) + if ($m.Success) { + return $m + } + } + } + Start-Sleep -Milliseconds 500 + } + throw "timed out waiting for $Label" +} + +function Stop-TestProcesses { + param( + [System.Diagnostics.Process[]]$Processes, + [string[]]$DataDirKeys + ) + + foreach ($p in $Processes) { + try { + $p.Refresh() + if (!$p.HasExited) { + Stop-Process -Id $p.Id -Force + } + } + catch {} + } + + foreach ($key in $DataDirKeys) { + if ([string]::IsNullOrWhiteSpace($key)) { + continue + } + try { + Get-CimInstance Win32_Process -Filter "name = 'tor.exe'" | + Where-Object { $_.CommandLine -like "*$key*" } | + ForEach-Object { Stop-Process -Id $_.ProcessId -Force } + } + catch {} + } +} + +$repo = Resolve-RepoRoot +if ([string]::IsNullOrWhiteSpace($Bitflash)) { + $Bitflash = Join-Path $repo "src\bitflash.exe" +} +if ([string]::IsNullOrWhiteSpace($Tor)) { + $candidates = @( + (Join-Path $repo "src\tor\tor.exe"), + (Join-Path $repo "Bitflash-$(Select-String -Path (Join-Path $repo 'Makefile') -Pattern '^VERSION[ ]*:=' | ForEach-Object { ($_.Line -split ':=')[1].Trim() })-windows\tor\tor.exe") + ) + foreach ($candidate in $candidates) { + if (Test-Path $candidate) { + $Tor = $candidate + break + } + } +} +if ([string]::IsNullOrWhiteSpace($WorkDir)) { + $WorkDir = Join-Path $env:TEMP ("bitflash-managed-tor-two-node-" + (Get-Date -Format "yyyyMMddHHmmss")) +} + +if (!(Test-Path $Bitflash)) { + throw "Bitflash binary not found: $Bitflash" +} +if (!(Test-Path $Tor)) { + throw "Tor binary not found. Pass -Tor or build the Windows-with-Tor package." +} + +New-Item -ItemType Directory -Force -Path $WorkDir | Out-Null +$dataA = Join-Path $WorkDir "node-a" +$dataB = Join-Path $WorkDir "node-b" +New-Item -ItemType Directory -Force -Path $dataA | Out-Null +New-Item -ItemType Directory -Force -Path $dataB | Out-Null + +$portA = Get-FreeTcpPort +$portB = Get-FreeTcpPort +$deadline = (Get-Date).AddSeconds($TimeoutSec) +$oldPath = $env:PATH +$env:PATH = "C:\msys64\ucrt64\bin;C:\msys64\usr\bin;" + $env:PATH + +$procA = $null +$procB = $null +$dataKeys = @((Split-Path $WorkDir -Leaf)) + +try { + $argsA = @( + "-datadir=$dataA", + "-port=$portA", + "-managedtor=$Tor", + "-nogui", + "-debug", + "-checkblocks=0" + ) + $procA = Start-Process -FilePath $Bitflash -ArgumentList $argsA -WorkingDirectory (Split-Path $Bitflash -Parent) -PassThru -WindowStyle Hidden + $logA = Join-Path $dataA "debug.log" + + $btfMatch = Wait-RegexInFile -Path $logA -Pattern "Nostr: node \.btf address = ([a-z2-7]+\.btf)" -Deadline $deadline -Label "node A .btf address" + $btfA = $btfMatch.Groups[1].Value + + $onionMatch = Wait-RegexInFile -Path $logA -Pattern "Managed Tor hidden service ready: ([a-z2-7]{56}\.onion:\d+)" -Deadline $deadline -Label "node A managed onion" + $onionA = $onionMatch.Groups[1].Value + + Wait-RegexInFile -Path $logA -Pattern "Nostr: \.btf self-resolve OK via .*" -Deadline $deadline -Label "node A descriptor self-resolve" | Out-Null + if ($OnionWarmupSec -gt 0) { + Write-Host "Waiting $OnionWarmupSec second(s) for Tor hidden-service descriptor propagation" + Start-Sleep -Seconds $OnionWarmupSec + } + + $argsB = @( + "-datadir=$dataB", + "-port=$portB", + "-managedtor=$Tor", + "-oniononly", + "-connectbtf=$btfA", + "-nogui", + "-debug", + "-checkblocks=0" + ) + $procB = Start-Process -FilePath $Bitflash -ArgumentList $argsB -WorkingDirectory (Split-Path $Bitflash -Parent) -PassThru -WindowStyle Hidden + $logB = Join-Path $dataB "debug.log" + + Wait-RegexInFile -Path $logB -Pattern ("connected " + [regex]::Escape($btfA) + " via direct onion ") -Deadline $deadline -Label "node B direct onion connection to node A" | Out-Null + + Write-Host "Managed Tor two-node onion smoke test passed" + Write-Host " node A .btf: $btfA" + Write-Host " node A onion: $onionA" + Write-Host " node A port: $portA" + Write-Host " node B port: $portB" + Write-Host " work dir: $WorkDir" +} +finally { + $env:PATH = $oldPath + $procs = @() + if ($null -ne $procA) { $procs += $procA } + if ($null -ne $procB) { $procs += $procB } + Stop-TestProcesses -Processes $procs -DataDirKeys $dataKeys +} diff --git a/src/main_gui.cpp b/src/main_gui.cpp index deed471..f0fe903 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -143,6 +143,7 @@ static void PrintUsage() printf(" /socks=HOST:PORT (SOCKS5 proxy for Nostr, .btf relay, and onion dials)\n"); printf(" /tor[=HOST:PORT] (Tor mode; default SOCKS5 proxy is 127.0.0.1:9050)\n"); printf(" /managedtor[=PATH] (start Tor, create a hidden service, advertise its onion)\n"); + printf(" /oniononly (do not fall back to rendezvous when a .btf onion dial fails)\n"); printf(" /btfseed=ADDRESS:ENCHEX (extra bootstrap peer, repeatable)\n"); printf("\n"); printf("Wallet:\n"); @@ -278,6 +279,7 @@ static void ParseStartupArguments(int argc, char* argv[]) string btfConnect = argval2(argc, argv, "/connectbtf", "-connectbtf"); if (!btfConnect.empty()) strBtfConnect = btfConnect; + fBtfOnionOnly = arg(argc, argv, "/oniononly") || arg(argc, argv, "-oniononly"); string rvRelay = argval2(argc, argv, "/rvrelay", "-rvrelay"); if (!rvRelay.empty()) @@ -360,6 +362,8 @@ static void ParseStartupArguments(int argc, char* argv[]) BtfSocks5ProxyName().c_str()); } } + if (fBtfOnionOnly && !BtfSocks5ProxyEnabled()) + fprintf(stderr, "Warning: /oniononly without /tor, /managedtor, or /socks cannot dial .onion peers\n"); // /btfseed=ADDRESS:ENCHEX -- extra bootstrap peers, repeatable. Useful for // testing the seed path and for private networks that ship no compiled list. diff --git a/src/net.cpp b/src/net.cpp index 6bc3c1c..997a2bd 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -65,6 +65,8 @@ deque > vRelayExpiration; CCriticalSection cs_mapRelay; map mapAlreadyAskedFor; string strBtfConnect; // .btf peer to keep connected to (from /connectbtf) +bool fBtfOnionOnly = false; // require direct onion transport for .btf peers +static const int BTF_ONION_CONNECT_TIMEOUT_SECS = 120; int nPeersWatched = 0; int64 nBlocksReceived = 0; @@ -1022,7 +1024,8 @@ static CNode* ConnectNodeBtfTail(const string& strBtfAddr, const unsigned char p if (SplitHostPort(strOnionNorm, strOnionHost, nOnionPort)) { BtfChurnNoteDialAttempt(strBtfAddr, strOnionNorm); - SOCKET hOnionSocket = BtfConnectSocket(strOnionHost, nOnionPort, SOCK_ONION_PEER, 20); + SOCKET hOnionSocket = BtfConnectSocket(strOnionHost, nOnionPort, SOCK_ONION_PEER, + BTF_ONION_CONNECT_TIMEOUT_SECS); if (hOnionSocket != INVALID_SOCKET) { if (fDebug) @@ -1040,12 +1043,23 @@ static CNode* ConnectNodeBtfTail(const string& strBtfAddr, const unsigned char p return pnode; } if (fDebug) - LogPrint("net", "ConnectNodeBtf: direct onion to %s at %s failed, falling back to rendezvous\n", - strBtfAddr.c_str(), strOnionNorm.c_str()); + LogPrint("net", "ConnectNodeBtf: direct onion to %s at %s failed%s\n", + strBtfAddr.c_str(), strOnionNorm.c_str(), + fBtfOnionOnly ? "" : ", falling back to rendezvous"); BtfChurnNoteDialResult(strBtfAddr, strOnionNorm, false); + if (fBtfOnionOnly) + return NULL; } } + if (fBtfOnionOnly) + { + if (fDebug) + LogPrint("net", "ConnectNodeBtf: onion-only mode refused rendezvous fallback for %s\n", + strBtfAddr.c_str()); + return NULL; + } + BtfChurnNoteDialAttempt(strBtfAddr, strMeeting); string strHost; unsigned short nPort = 0; @@ -2012,6 +2026,11 @@ bool StartNode(string& strError) // IP address, and port for the socket that is being bound int nRetryLimit = 15; struct sockaddr_in sockaddr = addrLocalHost.GetSockAddr(); + // Keep addrLocalHost as this node's advertised address, but listen on all + // local interfaces. Managed Tor forwards hidden-service traffic to + // 127.0.0.1:, and binding only to the LAN address made that onion + // endpoint unreachable even though the P2P listener was running. + sockaddr.sin_addr.s_addr = INADDR_ANY; if (bind(hListenSocket, (struct sockaddr*)&sockaddr, sizeof(sockaddr)) == SOCKET_ERROR) { int nErr = WSAGetLastError(); diff --git a/src/net.h b/src/net.h index 9300169..984588b 100644 --- a/src/net.h +++ b/src/net.h @@ -33,6 +33,7 @@ CNode* ConnectNodeBtfResolved(const string& strBtfAddr, const string& strMeeting void ThreadBtfAccept(void* parg); void ThreadBtfConnect(void* parg); extern string strBtfConnect; +extern bool fBtfOnionOnly; // Bootstrap seeds added at runtime with /btfseed=ADDRESS:ENCHEX (repeatable). // The compiled-in list lives in net.cpp; these are appended to it. extern std::vector > vBtfExtraSeeds; diff --git a/src/nostr.cpp b/src/nostr.cpp index 0c1b4b6..35fde43 100644 --- a/src/nostr.cpp +++ b/src/nostr.cpp @@ -1764,11 +1764,12 @@ void ThreadNostrSeed(void* parg) LogPrint("nostr", "Nostr: node pubkey = %s\n", key.PubKeyHex().c_str()); LogPrint("nostr", "Nostr: node .btf address = %s\n", key.BtfAddress().c_str()); - // The self-test below publishes a real descriptor, and PublishDescriptor now - // refuses to publish before ThreadBtfAccept has registered a rendezvous. Give - // registration a bounded head start so the test measures the publish/resolve - // chain instead of losing the race against it. - for (int i = 0; i < 60 && !fShutdown && BtfActiveRelay().empty(); i++) + // The self-test below publishes a real descriptor. Rendezvous-only nodes + // need ThreadBtfAccept to register first; Tor nodes can publish as soon as + // their signed onion endpoint exists, with rendezvous marked pending. + for (int i = 0; i < 60 && !fShutdown && + BtfActiveRelay().empty() && + BtfLocalOnionEndpoint().empty(); i++) Sleep(1000); // One-time self-test of the rendezvous: publish our descriptor to a relay, diff --git a/src/proxy.cpp b/src/proxy.cpp index 7d30911..eed054c 100644 --- a/src/proxy.cpp +++ b/src/proxy.cpp @@ -13,6 +13,7 @@ static bool g_fTorProxy = false; static std::string g_socks5Host; static unsigned short g_socks5Port = 0; static const char* DEFAULT_TOR_SOCKS5_PROXY = "127.0.0.1:9050"; +static const int TOR_CONNECT_TIMEOUT_SECS = 120; static void SetSocketTimeout(SOCKET s, int nTimeoutSecs) { @@ -297,10 +298,25 @@ static bool Socks5Connect(SOCKET s, const std::string& destHost, unsigned char hello[3] = { 0x05, 0x01, 0x00 }; // SOCKS5, one method, no auth if (!WriteN(s, hello, sizeof(hello))) + { + error("SOCKS5 hello write failed for %s:%u\n", + destHost.c_str(), (unsigned)destPort); return false; + } unsigned char choice[2] = { 0, 0 }; - if (!ReadN(s, choice, sizeof(choice)) || choice[0] != 0x05 || choice[1] != 0x00) + if (!ReadN(s, choice, sizeof(choice))) + { + error("SOCKS5 method selection read failed for %s:%u\n", + destHost.c_str(), (unsigned)destPort); + return false; + } + if (choice[0] != 0x05 || choice[1] != 0x00) + { + error("SOCKS5 proxy rejected no-auth method for %s:%u (version=0x%02x method=0x%02x)\n", + destHost.c_str(), (unsigned)destPort, + (unsigned)choice[0], (unsigned)choice[1]); return false; + } std::vector req; req.reserve(7 + destHost.size()); @@ -313,11 +329,25 @@ static bool Socks5Connect(SOCKET s, const std::string& destHost, req.push_back((unsigned char)(destPort >> 8)); req.push_back((unsigned char)(destPort & 0xff)); if (!WriteN(s, &req[0], (int)req.size())) + { + error("SOCKS5 CONNECT request write failed for %s:%u\n", + destHost.c_str(), (unsigned)destPort); return false; + } unsigned char rep[4] = {0,0,0,0}; - if (!ReadN(s, rep, sizeof(rep)) || rep[0] != 0x05) + if (!ReadN(s, rep, sizeof(rep))) + { + error("SOCKS5 CONNECT reply read failed for %s:%u\n", + destHost.c_str(), (unsigned)destPort); return false; + } + if (rep[0] != 0x05) + { + error("SOCKS5 CONNECT reply has invalid version 0x%02x for %s:%u\n", + (unsigned)rep[0], destHost.c_str(), (unsigned)destPort); + return false; + } if (rep[1] != 0x00) { error("SOCKS5 CONNECT to %s:%u failed with reply 0x%02x\n", @@ -335,10 +365,20 @@ static bool Socks5Connect(SOCKET s, const std::string& destHost, nAddr = len; } else + { + error("SOCKS5 CONNECT reply has invalid address type 0x%02x for %s:%u\n", + (unsigned)rep[3], destHost.c_str(), (unsigned)destPort); return false; + } std::vector discard(nAddr + 2); - return ReadN(s, &discard[0], (int)discard.size()); + if (!ReadN(s, &discard[0], (int)discard.size())) + { + error("SOCKS5 CONNECT bind address read failed for %s:%u\n", + destHost.c_str(), (unsigned)destPort); + return false; + } + return true; } SOCKET BtfConnectSocket(const std::string& destHost, unsigned short destPort, @@ -347,9 +387,11 @@ SOCKET BtfConnectSocket(const std::string& destHost, unsigned short destPort, std::string proxyHost; unsigned short proxyPort = 0; bool fProxy = false; + bool fTorProxy = false; CRITICAL_BLOCK(cs_socks5) { fProxy = g_fSocks5Proxy; + fTorProxy = g_fTorProxy; proxyHost = g_socks5Host; proxyPort = g_socks5Port; } @@ -365,6 +407,9 @@ SOCKET BtfConnectSocket(const std::string& destHost, unsigned short destPort, return ConnectDirect(destHost, destPort, nSockSite, nTimeoutSecs); } + if (fTorProxy && nTimeoutSecs > 0 && nTimeoutSecs < TOR_CONNECT_TIMEOUT_SECS) + nTimeoutSecs = TOR_CONNECT_TIMEOUT_SECS; + SOCKET s = ConnectDirect(proxyHost, proxyPort, nSockSite, nTimeoutSecs); if (s == INVALID_SOCKET) return INVALID_SOCKET; -- cgit v1.2.3 From 9e39a50d1f110020b0841d31e4b931aa877cc448 Mon Sep 17 00:00:00 2001 From: bitflash Date: Thu, 13 Aug 2026 16:56:01 -0300 Subject: release: v1.2.19 (nHDNext restore fix, Tor onion transport: managed Tor, direct onion peers, onion-only mode) --- Makefile | 2 +- README.md | 2 +- build-1219.log | 381 ++++++++++++++++++++ build-17.log | 78 +++++ build-appimage.log | 5 + build-node.log | 387 +++++++++++++++++++++ docs/site.md | 4 +- scripts/__pycache__/bitflash_chain.cpython-313.pyc | Bin 0 -> 22668 bytes 8 files changed, 855 insertions(+), 4 deletions(-) create mode 100644 build-1219.log create mode 100644 build-17.log create mode 100644 build-appimage.log create mode 100644 build-node.log create mode 100644 scripts/__pycache__/bitflash_chain.cpython-313.pyc (limited to 'scripts') diff --git a/Makefile b/Makefile index d1277a4..60d1227 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ ROOT := $(shell pwd) NPROC := $(shell nproc 2>/dev/null || echo 2) SUDO := $(shell [ "$$(id -u)" = "0" ] && echo "" || echo "sudo") -VERSION := 1.2.18 +VERSION := 1.2.19 SECP256K1_REPO := https://github.com/bitcoin-core/secp256k1 SECP256K1_COMMIT := 7fecac74aed8e1fd9078380d67dd04663705c989 diff --git a/README.md b/README.md index bb6a4d4..1782bc3 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ Public mirrors should make the latest checksum files available both under the versioned release directory and at the release root: ```text -https://releases.bitflash.network/v1.2.18/SHA256SUMS +https://releases.bitflash.network/v1.2.19/SHA256SUMS https://releases.bitflash.network/SHA256SUMS # alias to latest ``` diff --git a/build-1219.log b/build-1219.log new file mode 100644 index 0000000..9aa1b25 --- /dev/null +++ b/build-1219.log @@ -0,0 +1,381 @@ +Lendo listas de pacotes... +Construindo árvore de dependências... +Lendo informação de estado... +build-essential já é a versão mais nova (12.12). +cmake já é a versão mais nova (3.31.6-2). +git já é a versão mais nova (1:2.47.3-0+deb13u1). +pkg-config já é a versão mais nova (1.8.1-4). +autoconf já é a versão mais nova (2.72-3.1). +libtool já é a versão mais nova (2.5.4-4). +libssl-dev já é a versão mais nova (3.5.6-1~deb13u2). +libdb5.3++-dev já é a versão mais nova (5.3.28+dfsg2-9). +libsodium-dev já é a versão mais nova (1.0.18-1+deb13u1). +nlohmann-json3-dev já é a versão mais nova (3.11.3-2.1). +libboost-system-dev já é a versão mais nova (1.83.0.2+b2). +libglfw3-dev já é a versão mais nova (3.4-3). +libgl-dev já é a versão mais nova (1.7.0-1+b2). +python3-pil já é a versão mais nova (11.1.0-5+deb13u4). +clang já é a versão mais nova (1:19.0-63). +0 pacotes atualizados, 0 pacotes novos instalados, 0 a serem removidos e 44 não atualizados. +==> building libsecp256k1 +==> libsecp256k1 pinned at 7fecac74aed8e1fd9078380d67dd04663705c989 +libtoolize: putting auxiliary files in AC_CONFIG_AUX_DIR, 'autotools-aux'. +libtoolize: copying file 'autotools-aux/ltmain.sh' +libtoolize: putting macros in AC_CONFIG_MACRO_DIRS, 'autotools-aux/m4'. +libtoolize: copying file 'autotools-aux/m4/libtool.m4' +libtoolize: copying file 'autotools-aux/m4/ltoptions.m4' +libtoolize: copying file 'autotools-aux/m4/ltsugar.m4' +libtoolize: copying file 'autotools-aux/m4/ltversion.m4' +libtoolize: copying file 'autotools-aux/m4/lt~obsolete.m4' +configure.ac:39: installing 'autotools-aux/ar-lib' +configure.ac:37: installing 'autotools-aux/compile' +configure.ac:24: installing 'autotools-aux/config.guess' +configure.ac:24: installing 'autotools-aux/config.sub' +configure.ac:27: installing 'autotools-aux/install-sh' +configure.ac:27: installing 'autotools-aux/missing' +Makefile.am: installing 'autotools-aux/depcomp' +parallel-tests: installing 'autotools-aux/test-driver' +checking build system type... x86_64-pc-linux-gnu +checking host system type... x86_64-pc-linux-gnu +checking for a BSD-compatible install... /usr/bin/install -c +checking whether sleep supports fractional seconds... yes +checking filesystem timestamp resolution... 0.01 +checking whether build environment is sane... yes +checking for a race-free mkdir -p... /usr/bin/mkdir -p +checking for gawk... no +checking for mawk... mawk +checking whether make sets $(MAKE)... yes +checking whether make supports nested variables... yes +checking xargs -n works... yes +checking for gcc... gcc +checking whether the C compiler works... yes +checking for C compiler default output file name... a.out +checking for suffix of executables... +checking whether we are cross compiling... no +checking for suffix of object files... o +checking whether the compiler supports GNU C... yes +checking whether gcc accepts -g... yes +checking for gcc option to enable C11 features... none needed +checking whether gcc understands -c and -o together... yes +checking whether make supports the include directive... yes (GNU style) +checking dependency style of gcc... gcc3 +checking dependency style of gcc... gcc3 +checking for ar... ar +checking the archiver (ar) interface... ar +checking how to print strings... printf +checking for a sed that does not truncate output... /usr/bin/sed +checking for grep that handles long lines and -e... /usr/bin/grep +checking for egrep... /usr/bin/grep -E +checking for fgrep... /usr/bin/grep -F +checking for ld used by gcc... /usr/bin/ld +checking if the linker (/usr/bin/ld) is GNU ld... yes +checking for BSD- or MS-compatible name lister (nm)... /usr/bin/nm -B +checking the name lister (/usr/bin/nm -B) interface... BSD nm +checking whether ln -s works... yes +checking the maximum length of command line arguments... 1572864 +checking how to convert x86_64-pc-linux-gnu file names to x86_64-pc-linux-gnu format... func_convert_file_noop +checking how to convert x86_64-pc-linux-gnu file names to toolchain format... func_convert_file_noop +checking for /usr/bin/ld option to reload object files... -r +checking for file... file +checking for objdump... objdump +checking how to recognize dependent libraries... pass_all +checking for dlltool... no +checking how to associate runtime and link libraries... printf %s\n +checking for ranlib... ranlib +checking for archiver @FILE support... @ +checking for strip... strip +checking command to parse /usr/bin/nm -B output from gcc object... ok +checking for sysroot... no +checking for a working dd... /usr/bin/dd +checking how to truncate binary pipes... /usr/bin/dd bs=4096 count=1 +checking for mt... mt +checking if mt is a manifest tool... no +checking for stdio.h... yes +checking for stdlib.h... yes +checking for string.h... yes +checking for inttypes.h... yes +checking for stdint.h... yes +checking for strings.h... yes +checking for sys/stat.h... yes +checking for sys/types.h... yes +checking for unistd.h... yes +checking for dlfcn.h... yes +checking for objdir... .libs +checking if gcc supports -fno-rtti -fno-exceptions... no +checking for gcc option to produce PIC... -fPIC -DPIC +checking if gcc PIC flag -fPIC -DPIC works... yes +checking if gcc static flag -static works... yes +checking if gcc supports -c -o file.o... yes +checking if gcc supports -c -o file.o... (cached) yes +checking whether the gcc linker (/usr/bin/ld -m elf_x86_64) supports shared libraries... yes +checking dynamic linker characteristics... GNU/Linux ld.so +checking how to hardcode library paths into programs... immediate +checking whether stripping libraries is possible... yes +checking if libtool supports shared libraries... yes +checking whether to build shared libraries... no +checking whether to build static libraries... yes +checking if gcc supports -Werror... yes +checking if gcc supports -std=c89 -pedantic -Wno-long-long -Wnested-externs -Wshadow -Wstrict-prototypes -Wundef... yes +checking if gcc supports -Wno-overlength-strings... yes +checking if gcc supports -Wall... yes +checking if gcc supports -Wno-unused-function... yes +checking if gcc supports -Wextra... yes +checking if gcc supports -Wcast-align... yes +checking if gcc supports -Wcast-align=strict... yes +checking if gcc supports -Wconditional-uninitialized... no +checking if gcc supports -Wreserved-identifier... no +checking if gcc supports -Wtrailing-whitespace=any... no +checking if gcc supports -Wleading-whitespace=spaces... no +checking for valgrind support... +checking for x86_64 assembly availability... yes +checking that generated files are newer than configure... done +configure: creating ./config.status +config.status: creating Makefile +config.status: creating libsecp256k1.pc +config.status: executing depfiles commands +config.status: executing libtool commands + +Build Options: + with external callbacks = no + with benchmarks = no + with tests = no + with exhaustive tests = yes + with ctime tests = no + with coverage = no + with examples = no + module ecdh = yes + module recovery = no + module extrakeys = yes + module schnorrsig = yes + module musig = yes + module ellswift = yes + module silentpayments = yes + + asm = x86_64 + ecmult window size = 15 + ecmult gen table size = 86 KiB + + valgrind = no + CC = gcc + CPPFLAGS = + SECP_CFLAGS = -O2 -std=c89 -pedantic -Wno-long-long -Wnested-externs -Wshadow -Wstrict-prototypes -Wundef -Wno-overlength-strings -Wall -Wno-unused-function -Wextra -Wcast-align -Wcast-align=strict + CFLAGS = -g -O2 + LDFLAGS = +make[1]: Entrando no diretório '/root/rel16/secp256k1-build' + +make all-am +make[2]: Entrando no diretório '/root/rel16/secp256k1-build' + + CC src/exhaustive_tests-tests_exhaustive.o + CCLD exhaustive_tests + CC src/libsecp256k1_la-secp256k1.lo + CC src/libsecp256k1_precomputed_la-precomputed_ecmult.lo + CC src/libsecp256k1_precomputed_la-precomputed_ecmult_gen.lo + CCLD libsecp256k1_precomputed.la + CCLD libsecp256k1.la +make[2]: Saindo do diretório '/root/rel16/secp256k1-build' + +make[1]: Saindo do diretório '/root/rel16/secp256k1-build' + +make[1]: Entrando no diretório '/root/rel16/secp256k1-build' + +make install-am +make[2]: Entrando no diretório '/root/rel16/secp256k1-build' + +make[3]: Entrando no diretório '/root/rel16/secp256k1-build' + + /usr/bin/mkdir -p '/usr/local/lib' + /bin/bash ./libtool --mode=install /usr/bin/install -c libsecp256k1.la '/usr/local/lib' +libtool: install: /usr/bin/install -c .libs/libsecp256k1.lai /usr/local/lib/libsecp256k1.la +libtool: install: /usr/bin/install -c .libs/libsecp256k1.a /usr/local/lib/libsecp256k1.a +libtool: install: chmod 644 /usr/local/lib/libsecp256k1.a +libtool: install: ranlib /usr/local/lib/libsecp256k1.a +libtool: finish: PATH="/root/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/sbin" ldconfig -n /usr/local/lib +---------------------------------------------------------------------- +Libraries have been installed in: + /usr/local/lib + +If you ever happen to want to link against installed libraries +in a given directory, LIBDIR, you must either use libtool, and +specify the full pathname of the library, or use the '-LLIBDIR' +flag during linking and do at least one of the following: + - add LIBDIR to the 'LD_LIBRARY_PATH' environment variable + during execution + - add LIBDIR to the 'LD_RUN_PATH' environment variable + during linking + - use the '-Wl,-rpath -Wl,LIBDIR' linker flag + - have your system administrator add LIBDIR to '/etc/ld.so.conf' + +See any operating system documentation about shared libraries for +more information, such as the ld(1) and ld.so(8) manual pages. +---------------------------------------------------------------------- + /usr/bin/mkdir -p '/usr/local/include' + /usr/bin/install -c -m 644 include/secp256k1.h include/secp256k1_preallocated.h include/secp256k1_ecdh.h include/secp256k1_extrakeys.h include/secp256k1_schnorrsig.h include/secp256k1_musig.h include/secp256k1_ellswift.h include/secp256k1_silentpayments.h '/usr/local/include' + /usr/bin/mkdir -p '/usr/local/lib/pkgconfig' + /usr/bin/install -c -m 644 libsecp256k1.pc '/usr/local/lib/pkgconfig' +make[3]: Saindo do diretório '/root/rel16/secp256k1-build' + +make[2]: Saindo do diretório '/root/rel16/secp256k1-build' + +make[1]: Saindo do diretório '/root/rel16/secp256k1-build' + +==> libsecp256k1 done +==> building RandomX +==> RandomX pinned at 1e9d4b2df63fa6edf46b06789486e23c0fcaa55a +-- The C compiler identification is GNU 14.2.0 +-- The CXX compiler identification is GNU 14.2.0 +-- Detecting C compiler ABI info +-- Detecting C compiler ABI info - done +-- Check for working C compiler: /usr/bin/cc - skipped +-- Detecting C compile features +-- Detecting C compile features - done +-- Detecting CXX compiler ABI info +-- Detecting CXX compiler ABI info - done +-- Check for working CXX compiler: /usr/bin/c++ - skipped +-- Detecting CXX compile features +-- Detecting CXX compile features - done +-- Performing Test _maes_cxx +-- Performing Test _maes_cxx - Success +-- Setting CXX flag -maes +-- Performing Test _maes_c +-- Performing Test _maes_c - Success +-- Setting C flag -maes +-- Performing Test HAVE_SSSE3 +-- Performing Test HAVE_SSSE3 - Success +-- Performing Test HAVE_AVX2 +-- Performing Test HAVE_AVX2 - Success +-- Performing Test CMAKE_HAVE_LIBC_PTHREAD +-- Performing Test CMAKE_HAVE_LIBC_PTHREAD - Success +-- Found Threads: TRUE +-- Performing Test HAVE_CXX_ATOMICS +-- Performing Test HAVE_CXX_ATOMICS - Success +-- Performing Test HAVE_POSIX_MEMALIGN +-- Performing Test HAVE_POSIX_MEMALIGN - Success +-- Configuring done (0.9s) +-- Generating done (0.0s) +-- Build files have been written to: /root/RandomX/build +make[1]: Entrando no diretório '/root/RandomX/build' + +[ 3%] Building CXX object CMakeFiles/randomx.dir/src/aes_hash.cpp.o +[ 7%] Building C object CMakeFiles/randomx.dir/src/argon2_ref.c.o +[ 11%] Building C object CMakeFiles/randomx.dir/src/argon2_ssse3.c.o +[ 14%] Building C object CMakeFiles/randomx.dir/src/argon2_avx2.c.o +[ 18%] Building CXX object CMakeFiles/randomx.dir/src/bytecode_machine.cpp.o +[ 22%] Building CXX object CMakeFiles/randomx.dir/src/cpu.cpp.o +[ 25%] Building CXX object CMakeFiles/randomx.dir/src/dataset.cpp.o +[ 29%] Building CXX object CMakeFiles/randomx.dir/src/soft_aes.cpp.o +[ 33%] Building C object CMakeFiles/randomx.dir/src/virtual_memory.c.o +[ 37%] Building CXX object CMakeFiles/randomx.dir/src/vm_interpreted.cpp.o +[ 40%] Building CXX object CMakeFiles/randomx.dir/src/allocator.cpp.o +[ 44%] Building CXX object CMakeFiles/randomx.dir/src/assembly_generator_x86.cpp.o +[ 48%] Building CXX object CMakeFiles/randomx.dir/src/instruction.cpp.o +[ 51%] Building CXX object CMakeFiles/randomx.dir/src/randomx.cpp.o +[ 55%] Building CXX object CMakeFiles/randomx.dir/src/superscalar.cpp.o +[ 59%] Building CXX object CMakeFiles/randomx.dir/src/vm_compiled.cpp.o +[ 62%] Building CXX object CMakeFiles/randomx.dir/src/vm_interpreted_light.cpp.o +[ 66%] Building C object CMakeFiles/randomx.dir/src/argon2_core.c.o +[ 70%] Building CXX object CMakeFiles/randomx.dir/src/blake2_generator.cpp.o +[ 74%] Building CXX object CMakeFiles/randomx.dir/src/instructions_portable.cpp.o +[ 77%] Building C object CMakeFiles/randomx.dir/src/reciprocal.c.o +[ 81%] Building CXX object CMakeFiles/randomx.dir/src/virtual_machine.cpp.o +[ 85%] Building CXX object CMakeFiles/randomx.dir/src/vm_compiled_light.cpp.o +[ 88%] Building C object CMakeFiles/randomx.dir/src/blake2/blake2b.c.o +[ 92%] Building CXX object CMakeFiles/randomx.dir/src/jit_compiler_x86.cpp.o +[ 96%] Building C object CMakeFiles/randomx.dir/src/jit_compiler_x86_static.S.o +[100%] Linking CXX static library librandomx.a +[100%] Built target randomx +make[1]: Saindo do diretório '/root/RandomX/build' + +==> RandomX done +make -C src -f Makefile -j1 +make[1]: Entrando no diretório '/root/rel16/src' + +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/util.o util.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/script.o script.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/db.o db.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/net.o net.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/main.o main.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/market.o market.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/nostr.o nostr.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/randomx_pow.o randomx_pow.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/btfaddr.o btfaddr.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/btfchan.o btfchan.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/btfrv.o btfrv.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/btftunnel.o btftunnel.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/rpc.o rpc.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/selftest.o selftest.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/bip32.o bip32.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/walletcmd.o walletcmd.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/proxy.o proxy.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/gui.o gui.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/main_gui.o main_gui.cpp +g++ -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o bitflash obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o obj/gui.o obj/main_gui.o -L/usr/local/lib -lsecp256k1 -L/root/RandomX/build -lrandomx -lsodium -lssl -lcrypto -ldb_cxx -lpthread -lglfw -lGL +Built: bitflash +make[1]: Saindo do diretório '/root/rel16/src' + +strip src/bitflash +make -f Makefile appimage +make[1]: Entrando no diretório '/root/rel16' + +==> building AppImage + bundled libsodium.so.23 + bundled libdb_cxx-5.3.so + bundled libglfw.so.3 +Built: Bitflash-1.2.19-x86_64.AppImage +make[1]: Saindo do diretório '/root/rel16' + +make -f Makefile linux-node +make[1]: Entrando no diretório '/root/rel16' + +Lendo listas de pacotes... +Construindo árvore de dependências... +Lendo informação de estado... +build-essential já é a versão mais nova (12.12). +cmake já é a versão mais nova (3.31.6-2). +git já é a versão mais nova (1:2.47.3-0+deb13u1). +pkg-config já é a versão mais nova (1.8.1-4). +autoconf já é a versão mais nova (2.72-3.1). +libtool já é a versão mais nova (2.5.4-4). +libssl-dev já é a versão mais nova (3.5.6-1~deb13u2). +libdb5.3++-dev já é a versão mais nova (5.3.28+dfsg2-9). +libsodium-dev já é a versão mais nova (1.0.18-1+deb13u1). +nlohmann-json3-dev já é a versão mais nova (3.11.3-2.1). +libboost-system-dev já é a versão mais nova (1.83.0.2+b2). +libglfw3-dev já é a versão mais nova (3.4-3). +libgl-dev já é a versão mais nova (1.7.0-1+b2). +python3-pil já é a versão mais nova (11.1.0-5+deb13u4). +clang já é a versão mais nova (1:19.0-63). +0 pacotes atualizados, 0 pacotes novos instalados, 0 a serem removidos e 44 não atualizados. +==> libsecp256k1 already installed +==> RandomX already built +make -C src -f Makefile bitflash-node -j1 +make[2]: Entrando no diretório '/root/rel16/src' + +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/util.o util.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/script.o script.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/db.o db.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/net.o net.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/main.o main.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/market.o market.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/nostr.o nostr.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/randomx_pow.o randomx_pow.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/btfaddr.o btfaddr.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/btfchan.o btfchan.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/btfrv.o btfrv.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/btftunnel.o btftunnel.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/rpc.o rpc.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/selftest.o selftest.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/bip32.o bip32.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/walletcmd.o walletcmd.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/proxy.o proxy.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/main_gui.o main_gui.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/headless.o headless.cpp +g++ -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o bitflash-node obj-node/util.o obj-node/script.o obj-node/db.o obj-node/net.o obj-node/main.o obj-node/market.o obj-node/sha.o obj-node/nostr.o obj-node/randomx_pow.o obj-node/btfaddr.o obj-node/btfchan.o obj-node/btfrv.o obj-node/btftunnel.o obj-node/rpc.o obj-node/selftest.o obj-node/bip32.o obj-node/walletcmd.o obj-node/proxy.o obj-node/main_gui.o obj-node/headless.o -L/usr/local/lib -lsecp256k1 -L/root/RandomX/build -lrandomx -lsodium -lssl -lcrypto -ldb_cxx -lpthread +Built: bitflash-node (headless, no libGL needed) +make[2]: Saindo do diretório '/root/rel16/src' + +strip src/bitflash-node +cp src/bitflash-node bitflash-node-1.2.19-x86_64 +Built: bitflash-node-1.2.19-x86_64 +make[1]: Saindo do diretório '/root/rel16' + diff --git a/build-17.log b/build-17.log new file mode 100644 index 0000000..6c8c841 --- /dev/null +++ b/build-17.log @@ -0,0 +1,78 @@ +Lendo listas de pacotes... +Construindo árvore de dependências... +Lendo informação de estado... +build-essential já é a versão mais nova (12.12). +cmake já é a versão mais nova (3.31.6-2). +git já é a versão mais nova (1:2.47.3-0+deb13u1). +pkg-config já é a versão mais nova (1.8.1-4). +autoconf já é a versão mais nova (2.72-3.1). +libtool já é a versão mais nova (2.5.4-4). +libssl-dev já é a versão mais nova (3.5.6-1~deb13u2). +libdb5.3++-dev já é a versão mais nova (5.3.28+dfsg2-9). +libsodium-dev já é a versão mais nova (1.0.18-1+deb13u1). +nlohmann-json3-dev já é a versão mais nova (3.11.3-2.1). +libboost-system-dev já é a versão mais nova (1.83.0.2+b2). +libglfw3-dev já é a versão mais nova (3.4-3). +libgl-dev já é a versão mais nova (1.7.0-1+b2). +python3-pil já é a versão mais nova (11.1.0-5+deb13u4). +clang já é a versão mais nova (1:19.0-63). +0 pacotes atualizados, 0 pacotes novos instalados, 0 a serem removidos e 49 não atualizados. +==> libsecp256k1 already installed +==> RandomX already built +make -C src -f Makefile -j1 +make[1]: Entrando no diretório '/root/rel16/src' + +make[1]: Nada a ser feito para 'all'. +make[1]: Saindo do diretório '/root/rel16/src' + +strip src/bitflash +make -f Makefile appimage +make[1]: Entrando no diretório '/root/rel16' + +==> building AppImage + bundled libsodium.so.23 + bundled libdb_cxx-5.3.so + bundled libglfw.so.3 +Built: Bitflash-1.2.17-x86_64.AppImage +make[1]: Saindo do diretório '/root/rel16' + +make -f Makefile linux-node +make[1]: Entrando no diretório '/root/rel16' + +Lendo listas de pacotes... +Construindo árvore de dependências... +Lendo informação de estado... +build-essential já é a versão mais nova (12.12). +cmake já é a versão mais nova (3.31.6-2). +git já é a versão mais nova (1:2.47.3-0+deb13u1). +pkg-config já é a versão mais nova (1.8.1-4). +autoconf já é a versão mais nova (2.72-3.1). +libtool já é a versão mais nova (2.5.4-4). +libssl-dev já é a versão mais nova (3.5.6-1~deb13u2). +libdb5.3++-dev já é a versão mais nova (5.3.28+dfsg2-9). +libsodium-dev já é a versão mais nova (1.0.18-1+deb13u1). +nlohmann-json3-dev já é a versão mais nova (3.11.3-2.1). +libboost-system-dev já é a versão mais nova (1.83.0.2+b2). +libglfw3-dev já é a versão mais nova (3.4-3). +libgl-dev já é a versão mais nova (1.7.0-1+b2). +python3-pil já é a versão mais nova (11.1.0-5+deb13u4). +clang já é a versão mais nova (1:19.0-63). +0 pacotes atualizados, 0 pacotes novos instalados, 0 a serem removidos e 49 não atualizados. +==> libsecp256k1 already installed +==> RandomX already built +make -C src -f Makefile bitflash-node -j1 +make[2]: Entrando no diretório '/root/rel16/src' + +make[2]: 'bitflash-node' está atualizado. +make[2]: Saindo do diretório '/root/rel16/src' + +strip src/bitflash-node +cp src/bitflash-node bitflash-node-1.2.17-x86_64 +Built: bitflash-node-1.2.17-x86_64 +make[1]: Saindo do diretório '/root/rel16' + +==> building AppImage + bundled libsodium.so.23 + bundled libdb_cxx-5.3.so + bundled libglfw.so.3 +Built: Bitflash-1.2.17-x86_64.AppImage diff --git a/build-appimage.log b/build-appimage.log new file mode 100644 index 0000000..d6b95ed --- /dev/null +++ b/build-appimage.log @@ -0,0 +1,5 @@ +==> building AppImage + bundled libsodium.so.23 + bundled libdb_cxx-5.3.so + bundled libglfw.so.3 +Built: Bitflash-1.2.16-x86_64.AppImage diff --git a/build-node.log b/build-node.log new file mode 100644 index 0000000..951c2a0 --- /dev/null +++ b/build-node.log @@ -0,0 +1,387 @@ +Lendo listas de pacotes... +Construindo árvore de dependências... +Lendo informação de estado... +build-essential já é a versão mais nova (12.12). +cmake já é a versão mais nova (3.31.6-2). +git já é a versão mais nova (1:2.47.3-0+deb13u1). +pkg-config já é a versão mais nova (1.8.1-4). +autoconf já é a versão mais nova (2.72-3.1). +libtool já é a versão mais nova (2.5.4-4). +libssl-dev já é a versão mais nova (3.5.6-1~deb13u2). +libdb5.3++-dev já é a versão mais nova (5.3.28+dfsg2-9). +libsodium-dev já é a versão mais nova (1.0.18-1+deb13u1). +nlohmann-json3-dev já é a versão mais nova (3.11.3-2.1). +libboost-system-dev já é a versão mais nova (1.83.0.2+b2). +libglfw3-dev já é a versão mais nova (3.4-3). +libgl-dev já é a versão mais nova (1.7.0-1+b2). +python3-pil já é a versão mais nova (11.1.0-5+deb13u4). +clang já é a versão mais nova (1:19.0-63). +0 pacotes atualizados, 0 pacotes novos instalados, 0 a serem removidos e 49 não atualizados. +==> building libsecp256k1 +==> libsecp256k1 pinned at 7fecac74aed8e1fd9078380d67dd04663705c989 +libtoolize: putting auxiliary files in AC_CONFIG_AUX_DIR, 'autotools-aux'. +libtoolize: copying file 'autotools-aux/ltmain.sh' +libtoolize: putting macros in AC_CONFIG_MACRO_DIRS, 'autotools-aux/m4'. +libtoolize: copying file 'autotools-aux/m4/libtool.m4' +libtoolize: copying file 'autotools-aux/m4/ltoptions.m4' +libtoolize: copying file 'autotools-aux/m4/ltsugar.m4' +libtoolize: copying file 'autotools-aux/m4/ltversion.m4' +libtoolize: copying file 'autotools-aux/m4/lt~obsolete.m4' +configure.ac:39: installing 'autotools-aux/ar-lib' +configure.ac:37: installing 'autotools-aux/compile' +configure.ac:24: installing 'autotools-aux/config.guess' +configure.ac:24: installing 'autotools-aux/config.sub' +configure.ac:27: installing 'autotools-aux/install-sh' +configure.ac:27: installing 'autotools-aux/missing' +Makefile.am: installing 'autotools-aux/depcomp' +parallel-tests: installing 'autotools-aux/test-driver' +checking build system type... x86_64-pc-linux-gnu +checking host system type... x86_64-pc-linux-gnu +checking for a BSD-compatible install... /usr/bin/install -c +checking whether sleep supports fractional seconds... yes +checking filesystem timestamp resolution... 0.01 +checking whether build environment is sane... yes +checking for a race-free mkdir -p... /usr/bin/mkdir -p +checking for gawk... no +checking for mawk... mawk +checking whether make sets $(MAKE)... yes +checking whether make supports nested variables... yes +checking xargs -n works... yes +checking for gcc... gcc +checking whether the C compiler works... yes +checking for C compiler default output file name... a.out +checking for suffix of executables... +checking whether we are cross compiling... no +checking for suffix of object files... o +checking whether the compiler supports GNU C... yes +checking whether gcc accepts -g... yes +checking for gcc option to enable C11 features... none needed +checking whether gcc understands -c and -o together... yes +checking whether make supports the include directive... yes (GNU style) +checking dependency style of gcc... gcc3 +checking dependency style of gcc... gcc3 +checking for ar... ar +checking the archiver (ar) interface... ar +checking how to print strings... printf +checking for a sed that does not truncate output... /usr/bin/sed +checking for grep that handles long lines and -e... /usr/bin/grep +checking for egrep... /usr/bin/grep -E +checking for fgrep... /usr/bin/grep -F +checking for ld used by gcc... /usr/bin/ld +checking if the linker (/usr/bin/ld) is GNU ld... yes +checking for BSD- or MS-compatible name lister (nm)... /usr/bin/nm -B +checking the name lister (/usr/bin/nm -B) interface... BSD nm +checking whether ln -s works... yes +checking the maximum length of command line arguments... 1572864 +checking how to convert x86_64-pc-linux-gnu file names to x86_64-pc-linux-gnu format... func_convert_file_noop +checking how to convert x86_64-pc-linux-gnu file names to toolchain format... func_convert_file_noop +checking for /usr/bin/ld option to reload object files... -r +checking for file... file +checking for objdump... objdump +checking how to recognize dependent libraries... pass_all +checking for dlltool... no +checking how to associate runtime and link libraries... printf %s\n +checking for ranlib... ranlib +checking for archiver @FILE support... @ +checking for strip... strip +checking command to parse /usr/bin/nm -B output from gcc object... ok +checking for sysroot... no +checking for a working dd... /usr/bin/dd +checking how to truncate binary pipes... /usr/bin/dd bs=4096 count=1 +checking for mt... mt +checking if mt is a manifest tool... no +checking for stdio.h... yes +checking for stdlib.h... yes +checking for string.h... yes +checking for inttypes.h... yes +checking for stdint.h... yes +checking for strings.h... yes +checking for sys/stat.h... yes +checking for sys/types.h... yes +checking for unistd.h... yes +checking for dlfcn.h... yes +checking for objdir... .libs +checking if gcc supports -fno-rtti -fno-exceptions... no +checking for gcc option to produce PIC... -fPIC -DPIC +checking if gcc PIC flag -fPIC -DPIC works... yes +checking if gcc static flag -static works... yes +checking if gcc supports -c -o file.o... yes +checking if gcc supports -c -o file.o... (cached) yes +checking whether the gcc linker (/usr/bin/ld -m elf_x86_64) supports shared libraries... yes +checking dynamic linker characteristics... GNU/Linux ld.so +checking how to hardcode library paths into programs... immediate +checking whether stripping libraries is possible... yes +checking if libtool supports shared libraries... yes +checking whether to build shared libraries... no +checking whether to build static libraries... yes +checking if gcc supports -Werror... yes +checking if gcc supports -std=c89 -pedantic -Wno-long-long -Wnested-externs -Wshadow -Wstrict-prototypes -Wundef... yes +checking if gcc supports -Wno-overlength-strings... yes +checking if gcc supports -Wall... yes +checking if gcc supports -Wno-unused-function... yes +checking if gcc supports -Wextra... yes +checking if gcc supports -Wcast-align... yes +checking if gcc supports -Wcast-align=strict... yes +checking if gcc supports -Wconditional-uninitialized... no +checking if gcc supports -Wreserved-identifier... no +checking if gcc supports -Wtrailing-whitespace=any... no +checking if gcc supports -Wleading-whitespace=spaces... no +checking for valgrind support... +checking for x86_64 assembly availability... yes +checking that generated files are newer than configure... done +configure: creating ./config.status +config.status: creating Makefile +config.status: creating libsecp256k1.pc +config.status: executing depfiles commands +config.status: executing libtool commands + +Build Options: + with external callbacks = no + with benchmarks = no + with tests = no + with exhaustive tests = yes + with ctime tests = no + with coverage = no + with examples = no + module ecdh = yes + module recovery = no + module extrakeys = yes + module schnorrsig = yes + module musig = yes + module ellswift = yes + module silentpayments = yes + + asm = x86_64 + ecmult window size = 15 + ecmult gen table size = 86 KiB + + valgrind = no + CC = gcc + CPPFLAGS = + SECP_CFLAGS = -O2 -std=c89 -pedantic -Wno-long-long -Wnested-externs -Wshadow -Wstrict-prototypes -Wundef -Wno-overlength-strings -Wall -Wno-unused-function -Wextra -Wcast-align -Wcast-align=strict + CFLAGS = -g -O2 + LDFLAGS = +make[1]: Entrando no diretório '/root/rel16/secp256k1-build' + +make all-am +make[2]: Entrando no diretório '/root/rel16/secp256k1-build' + + CC src/exhaustive_tests-tests_exhaustive.o + CCLD exhaustive_tests + CC src/libsecp256k1_la-secp256k1.lo + CC src/libsecp256k1_precomputed_la-precomputed_ecmult.lo + CC src/libsecp256k1_precomputed_la-precomputed_ecmult_gen.lo + CCLD libsecp256k1_precomputed.la + CCLD libsecp256k1.la +make[2]: Saindo do diretório '/root/rel16/secp256k1-build' + +make[1]: Saindo do diretório '/root/rel16/secp256k1-build' + +make[1]: Entrando no diretório '/root/rel16/secp256k1-build' + +make install-am +make[2]: Entrando no diretório '/root/rel16/secp256k1-build' + +make[3]: Entrando no diretório '/root/rel16/secp256k1-build' + + /usr/bin/mkdir -p '/usr/local/lib' + /bin/bash ./libtool --mode=install /usr/bin/install -c libsecp256k1.la '/usr/local/lib' +libtool: install: /usr/bin/install -c .libs/libsecp256k1.lai /usr/local/lib/libsecp256k1.la +libtool: install: /usr/bin/install -c .libs/libsecp256k1.a /usr/local/lib/libsecp256k1.a +libtool: install: chmod 644 /usr/local/lib/libsecp256k1.a +libtool: install: ranlib /usr/local/lib/libsecp256k1.a +libtool: finish: PATH="/root/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/sbin" ldconfig -n /usr/local/lib +---------------------------------------------------------------------- +Libraries have been installed in: + /usr/local/lib + +If you ever happen to want to link against installed libraries +in a given directory, LIBDIR, you must either use libtool, and +specify the full pathname of the library, or use the '-LLIBDIR' +flag during linking and do at least one of the following: + - add LIBDIR to the 'LD_LIBRARY_PATH' environment variable + during execution + - add LIBDIR to the 'LD_RUN_PATH' environment variable + during linking + - use the '-Wl,-rpath -Wl,LIBDIR' linker flag + - have your system administrator add LIBDIR to '/etc/ld.so.conf' + +See any operating system documentation about shared libraries for +more information, such as the ld(1) and ld.so(8) manual pages. +---------------------------------------------------------------------- + /usr/bin/mkdir -p '/usr/local/include' + /usr/bin/install -c -m 644 include/secp256k1.h include/secp256k1_preallocated.h include/secp256k1_ecdh.h include/secp256k1_extrakeys.h include/secp256k1_schnorrsig.h include/secp256k1_musig.h include/secp256k1_ellswift.h include/secp256k1_silentpayments.h '/usr/local/include' + /usr/bin/mkdir -p '/usr/local/lib/pkgconfig' + /usr/bin/install -c -m 644 libsecp256k1.pc '/usr/local/lib/pkgconfig' +make[3]: Saindo do diretório '/root/rel16/secp256k1-build' + +make[2]: Saindo do diretório '/root/rel16/secp256k1-build' + +make[1]: Saindo do diretório '/root/rel16/secp256k1-build' + +==> libsecp256k1 done +==> building RandomX +==> RandomX pinned at 1e9d4b2df63fa6edf46b06789486e23c0fcaa55a +-- The C compiler identification is GNU 14.2.0 +-- The CXX compiler identification is GNU 14.2.0 +-- Detecting C compiler ABI info +-- Detecting C compiler ABI info - done +-- Check for working C compiler: /usr/bin/cc - skipped +-- Detecting C compile features +-- Detecting C compile features - done +-- Detecting CXX compiler ABI info +-- Detecting CXX compiler ABI info - done +-- Check for working CXX compiler: /usr/bin/c++ - skipped +-- Detecting CXX compile features +-- Detecting CXX compile features - done +-- Performing Test _maes_cxx +-- Performing Test _maes_cxx - Success +-- Setting CXX flag -maes +-- Performing Test _maes_c +-- Performing Test _maes_c - Success +-- Setting C flag -maes +-- Performing Test HAVE_SSSE3 +-- Performing Test HAVE_SSSE3 - Success +-- Performing Test HAVE_AVX2 +-- Performing Test HAVE_AVX2 - Success +-- Performing Test CMAKE_HAVE_LIBC_PTHREAD +-- Performing Test CMAKE_HAVE_LIBC_PTHREAD - Success +-- Found Threads: TRUE +-- Performing Test HAVE_CXX_ATOMICS +-- Performing Test HAVE_CXX_ATOMICS - Success +-- Performing Test HAVE_POSIX_MEMALIGN +-- Performing Test HAVE_POSIX_MEMALIGN - Success +-- Configuring done (1.0s) +-- Generating done (0.0s) +-- Build files have been written to: /root/RandomX/build +make[1]: Entrando no diretório '/root/RandomX/build' + +[ 3%] Building CXX object CMakeFiles/randomx.dir/src/aes_hash.cpp.o +[ 7%] Building C object CMakeFiles/randomx.dir/src/argon2_ref.c.o +[ 11%] Building C object CMakeFiles/randomx.dir/src/argon2_ssse3.c.o +[ 14%] Building C object CMakeFiles/randomx.dir/src/argon2_avx2.c.o +[ 18%] Building CXX object CMakeFiles/randomx.dir/src/bytecode_machine.cpp.o +[ 22%] Building CXX object CMakeFiles/randomx.dir/src/cpu.cpp.o +[ 25%] Building CXX object CMakeFiles/randomx.dir/src/dataset.cpp.o +[ 29%] Building CXX object CMakeFiles/randomx.dir/src/soft_aes.cpp.o +[ 33%] Building C object CMakeFiles/randomx.dir/src/virtual_memory.c.o +[ 37%] Building CXX object CMakeFiles/randomx.dir/src/vm_interpreted.cpp.o +[ 40%] Building CXX object CMakeFiles/randomx.dir/src/allocator.cpp.o +[ 44%] Building CXX object CMakeFiles/randomx.dir/src/assembly_generator_x86.cpp.o +[ 48%] Building CXX object CMakeFiles/randomx.dir/src/instruction.cpp.o +[ 51%] Building CXX object CMakeFiles/randomx.dir/src/randomx.cpp.o +[ 55%] Building CXX object CMakeFiles/randomx.dir/src/superscalar.cpp.o +[ 59%] Building CXX object CMakeFiles/randomx.dir/src/vm_compiled.cpp.o +[ 62%] Building CXX object CMakeFiles/randomx.dir/src/vm_interpreted_light.cpp.o +[ 66%] Building C object CMakeFiles/randomx.dir/src/argon2_core.c.o +[ 70%] Building CXX object CMakeFiles/randomx.dir/src/blake2_generator.cpp.o +[ 74%] Building CXX object CMakeFiles/randomx.dir/src/instructions_portable.cpp.o +[ 77%] Building C object CMakeFiles/randomx.dir/src/reciprocal.c.o +[ 81%] Building CXX object CMakeFiles/randomx.dir/src/virtual_machine.cpp.o +[ 85%] Building CXX object CMakeFiles/randomx.dir/src/vm_compiled_light.cpp.o +[ 88%] Building C object CMakeFiles/randomx.dir/src/blake2/blake2b.c.o +[ 92%] Building CXX object CMakeFiles/randomx.dir/src/jit_compiler_x86.cpp.o +[ 96%] Building C object CMakeFiles/randomx.dir/src/jit_compiler_x86_static.S.o +[100%] Linking CXX static library librandomx.a +[100%] Built target randomx +make[1]: Saindo do diretório '/root/RandomX/build' + +==> RandomX done +make -C src -f Makefile -j1 +make[1]: Entrando no diretório '/root/rel16/src' + +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/imgui.o imgui/imgui.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/imgui_draw.o imgui/imgui_draw.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/imgui_tables.o imgui/imgui_tables.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/imgui_widgets.o imgui/imgui_widgets.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/imgui_impl_glfw.o imgui/imgui_impl_glfw.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/imgui_impl_opengl3.o imgui/imgui_impl_opengl3.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/util.o util.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/script.o script.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/db.o db.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/net.o net.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/main.o main.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/market.o market.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -O3 -o obj/sha.o sha.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/nostr.o nostr.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/randomx_pow.o randomx_pow.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/btfaddr.o btfaddr.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/btfchan.o btfchan.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/btfrv.o btfrv.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/btftunnel.o btftunnel.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/rpc.o rpc.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/selftest.o selftest.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/bip32.o bip32.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/walletcmd.o walletcmd.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/gui.o gui.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o obj/main_gui.o main_gui.cpp +g++ -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o bitflash obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o obj/selftest.o obj/bip32.o obj/walletcmd.o obj/gui.o obj/main_gui.o -L/usr/local/lib -lsecp256k1 -L/root/RandomX/build -lrandomx -lsodium -lssl -lcrypto -ldb_cxx -lpthread -lglfw -lGL +Built: bitflash +make[1]: Saindo do diretório '/root/rel16/src' + +strip src/bitflash +make -f Makefile appimage +make[1]: Entrando no diretório '/root/rel16' + +==> building AppImage + bundled libsodium.so.23 + bundled libdb_cxx-5.3.so + bundled libglfw.so.3 +Built: Bitflash-1.2.16-x86_64.AppImage +make[1]: Saindo do diretório '/root/rel16' + +make -f Makefile linux-node +make[1]: Entrando no diretório '/root/rel16' + +Lendo listas de pacotes... +Construindo árvore de dependências... +Lendo informação de estado... +build-essential já é a versão mais nova (12.12). +cmake já é a versão mais nova (3.31.6-2). +git já é a versão mais nova (1:2.47.3-0+deb13u1). +pkg-config já é a versão mais nova (1.8.1-4). +autoconf já é a versão mais nova (2.72-3.1). +libtool já é a versão mais nova (2.5.4-4). +libssl-dev já é a versão mais nova (3.5.6-1~deb13u2). +libdb5.3++-dev já é a versão mais nova (5.3.28+dfsg2-9). +libsodium-dev já é a versão mais nova (1.0.18-1+deb13u1). +nlohmann-json3-dev já é a versão mais nova (3.11.3-2.1). +libboost-system-dev já é a versão mais nova (1.83.0.2+b2). +libglfw3-dev já é a versão mais nova (3.4-3). +libgl-dev já é a versão mais nova (1.7.0-1+b2). +python3-pil já é a versão mais nova (11.1.0-5+deb13u4). +clang já é a versão mais nova (1:19.0-63). +0 pacotes atualizados, 0 pacotes novos instalados, 0 a serem removidos e 49 não atualizados. +==> libsecp256k1 already installed +==> RandomX already built +make -C src -f Makefile bitflash-node -j1 +make[2]: Entrando no diretório '/root/rel16/src' + +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/util.o util.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/script.o script.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/db.o db.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/net.o net.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/main.o main.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/market.o market.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -O3 -o obj-node/sha.o sha.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/nostr.o nostr.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/randomx_pow.o randomx_pow.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/btfaddr.o btfaddr.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/btfchan.o btfchan.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/btfrv.o btfrv.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/btftunnel.o btftunnel.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/rpc.o rpc.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/selftest.o selftest.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/bip32.o bip32.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/walletcmd.o walletcmd.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/main_gui.o main_gui.cpp +g++ -c -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -DBITFLASH_NO_GUI -o obj-node/headless.o headless.cpp +g++ -std=gnu++14 -O2 -Wformat -Werror=format -Wno-deprecated-declarations -Wno-write-strings -Wno-narrowing -Wno-unused-result -fpermissive -pthread -DSECP256K1_STATIC -I/usr/local/include -I/root/RandomX/src -I/usr/include/nlohmann -Iimgui -o bitflash-node obj-node/util.o obj-node/script.o obj-node/db.o obj-node/net.o obj-node/main.o obj-node/market.o obj-node/sha.o obj-node/nostr.o obj-node/randomx_pow.o obj-node/btfaddr.o obj-node/btfchan.o obj-node/btfrv.o obj-node/btftunnel.o obj-node/rpc.o obj-node/selftest.o obj-node/bip32.o obj-node/walletcmd.o obj-node/main_gui.o obj-node/headless.o -L/usr/local/lib -lsecp256k1 -L/root/RandomX/build -lrandomx -lsodium -lssl -lcrypto -ldb_cxx -lpthread +Built: bitflash-node (headless, no libGL needed) +make[2]: Saindo do diretório '/root/rel16/src' + +strip src/bitflash-node +cp src/bitflash-node bitflash-node-1.2.16-x86_64 +Built: bitflash-node-1.2.16-x86_64 +make[1]: Saindo do diretório '/root/rel16' + diff --git a/docs/site.md b/docs/site.md index 522da10..9526a61 100644 --- a/docs/site.md +++ b/docs/site.md @@ -42,8 +42,8 @@ The docs index should expose these source files: Each release remains under its versioned directory: ```text -https://releases.bitflash.network/v1.2.18/SHA256SUMS -https://releases.bitflash.network/v1.2.18/SHA256SUMS.asc +https://releases.bitflash.network/v1.2.19/SHA256SUMS +https://releases.bitflash.network/v1.2.19/SHA256SUMS.asc ``` The release root should also alias the latest files: diff --git a/scripts/__pycache__/bitflash_chain.cpython-313.pyc b/scripts/__pycache__/bitflash_chain.cpython-313.pyc new file mode 100644 index 0000000..47a7995 Binary files /dev/null and b/scripts/__pycache__/bitflash_chain.cpython-313.pyc differ -- cgit v1.2.3 From e63fcfff1ae24dfff8aca6581badb51a06ddae28 Mon Sep 17 00:00:00 2001 From: bitflash-maintainer Date: Thu, 3 Sep 2026 18:31:32 +0000 Subject: payout: fail review when a money line lands on collapsing LogPrint scripts/check-money-log-lines.sh flattens each LogPrint call in src/rpc.cpp and fails if a money-moving phrase appears in a plain LogPrint call. A new line that reports money moving needs LogPrintAlways and one more phrase in the list. --- scripts/check-money-log-lines.sh | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100755 scripts/check-money-log-lines.sh (limited to 'scripts') diff --git a/scripts/check-money-log-lines.sh b/scripts/check-money-log-lines.sh new file mode 100755 index 0000000..c9f348b --- /dev/null +++ b/scripts/check-money-log-lines.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Money-moving payout lines must print every time they fire: they are the +# audit trail for obligations created, moved and destroyed, and LogPrint's +# burst collapse can stand one line in for any number of events inside one +# 3-second window (issue #3: 100 voids totalling 4,950 BTF read as one +# 49.49 BTF line). Every line carrying one of the phrases below must use +# LogPrintAlways, which keeps the category gate but skips the collapse. +# +# Adding a payout line that reports money moving? Use LogPrintAlways and +# add its phrase here. A phrase found in a plain LogPrint call fails this +# check; anything else passes, so a non-money line is never held up by it. +set -euo pipefail +cd "$(dirname "$0")/.." + +phrases='was orphaned -- voiding|orphaned while queued|\[payout\] paid %s to %s|FAILED to pay|writing off|SKIPPED bad address' + +# Call sites span several lines; flatten each call to a single line first so +# the phrase and the macro it belongs to are matched together. Only a line +# whose first token is the macro starts a call, so comments mentioning +# LogPrint are never mistaken for one. +bad=$(awk ' + /^[ \t]*LogPrint(Always)?[(]/ { + if (collecting) print buf + buf = $0; collecting = 1; next + } + collecting { + buf = buf $0 + if (/;[ \t]*$/) { print buf; collecting = 0 } + } + END { if (collecting) print buf } +' src/rpc.cpp | grep -E '^[[:space:]]*LogPrint[(]' | grep -E "$phrases" || true) + +if [ -n "$bad" ]; then + echo "error: money-moving payout line(s) on collapsing LogPrint:" >&2 + echo "$bad" >&2 + exit 1 +fi +echo "ok: every money-moving payout line in src/rpc.cpp uses LogPrintAlways" -- cgit v1.2.3