diff --git a/.github/workflows/validate-axebc2-core31-dev.yml b/.github/workflows/validate-axebc2-core31-dev.yml index ba030b5..17c2b60 100644 --- a/.github/workflows/validate-axebc2-core31-dev.yml +++ b/.github/workflows/validate-axebc2-core31-dev.yml @@ -11,7 +11,7 @@ on: - "tests/fixtures/5tratumos_contract_4f979cb.py" - "scripts/validate-axebc2-core31-dev.py" - "scripts/axebc2_release_state.py" - - "scripts/finalize-axebc2-0.1.10-dev.sh" + - "scripts/finalize-axebc2-0.1.11-dev.sh" - ".github/workflows/validate-axebc2-core31-dev.yml" push: branches: [main] @@ -24,7 +24,7 @@ on: - "tests/fixtures/5tratumos_contract_4f979cb.py" - "scripts/validate-axebc2-core31-dev.py" - "scripts/axebc2_release_state.py" - - "scripts/finalize-axebc2-0.1.10-dev.sh" + - "scripts/finalize-axebc2-0.1.11-dev.sh" - ".github/workflows/validate-axebc2-core31-dev.yml" permissions: @@ -42,7 +42,7 @@ jobs: - name: Validate metadata and migration initialization run: | - bash -n scripts/finalize-axebc2-0.1.10-dev.sh + bash -n scripts/finalize-axebc2-0.1.11-dev.sh count="$(awk '{n += gsub(/_DIGEST_REQUIRED/, "")} END {print n + 0}' willitmod-dev-bc2/docker-compose.yml)" - if [ "$count" = 3 ]; then phase=prefinalization; elif [ "$count" = 0 ]; then phase=finalized; else echo "partial digest finalization" >&2; exit 1; fi + if [ "$count" = 1 ]; then phase=prefinalization; elif [ "$count" = 0 ]; then phase=finalized; else echo "partial digest finalization" >&2; exit 1; fi python3 scripts/validate-axebc2-core31-dev.py --phase "$phase" diff --git a/README.md b/README.md index e732b9c..d5065fd 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Development/test app store for WillItMod apps. - **AxeBTC** (`willitmod-dev-btc`) - `0.7.82.8-dev` - **AxeBCH** (`willitmod-dev-bch`) - `0.9.3-dev` - **AxeBCH2** (`willitmod-dev-axebch2`) - `0.2.0.01-dev` -- **AxeBC2** (`willitmod-dev-bc2`) - `0.1.10-dev` +- **AxeBC2** (`willitmod-dev-bc2`) - `0.1.11-dev` - **AxeDGB** (`willitmod-dev-dgb`) - `0.9.179-dev` - **AxePPC** (`willitmod-dev-ppc`) - `0.2.30-dev` - **AxeXEC** (`willitmod-dev-xec`) - `0.1.14-dev` diff --git a/scripts/axebc2_release_state.py b/scripts/axebc2_release_state.py index e8ef5c5..2daaa93 100644 --- a/scripts/axebc2_release_state.py +++ b/scripts/axebc2_release_state.py @@ -1,26 +1,26 @@ -import re from pathlib import Path -APP_TAG = "ghcr.io/willitmod/axebc2-app-umbrel-dev:0.1.10-candidate.6e4ef58218e8" +APP_TAG = "ghcr.io/willitmod/axebc2-app-umbrel-dev:0.1.11-candidate.ecf6e2c8cfd0" +APP_DIGEST = "sha256:23a7962e223da5549eba52697c6f4cfa16ab74cba935c68c48148a4c515302b4" CORE_TAG = "ghcr.io/willitmod/bitcoinii-core:31.1.0-rc.cdf44542dde2" +CORE_DIGEST = "sha256:8875917ece57668fe9925d40a256ce8d429a3071511bb555d4ace1fa4370afc6" def validate(compose, phase): if phase not in {"prefinalization", "finalized"}: raise ValueError("phase must be prefinalization or finalized") app_sentinel = APP_TAG + "@sha256:APP_CANDIDATE_DIGEST_REQUIRED" - core_sentinel = CORE_TAG + "@sha256:CORE31_CANDIDATE_DIGEST_REQUIRED" + core_pin = CORE_TAG + "@" + CORE_DIGEST if phase == "prefinalization": - if compose.count(app_sentinel) != 1 or compose.count(core_sentinel) != 2: - raise ValueError("prefinalization requires the exact three digest sentinels") - if compose.count("_DIGEST_REQUIRED") != 3: + if compose.count(app_sentinel) != 1 or compose.count(core_pin) != 2: + raise ValueError("prefinalization requires one app sentinel and two exact Core pins") + if compose.count("_DIGEST_REQUIRED") != 1: raise ValueError("unknown or partial digest sentinel state") return if "_DIGEST_REQUIRED" in compose: raise ValueError("finalized release contains a digest sentinel") - app = re.findall(re.escape(APP_TAG) + r"@(sha256:[0-9a-f]{64})", compose) - core = re.findall(re.escape(CORE_TAG) + r"@(sha256:[0-9a-f]{64})", compose) - if len(app) != 1 or len(core) != 2 or len(set(core)) != 1: - raise ValueError("finalized release requires one app pin and two identical Core pins") + app_pin = APP_TAG + "@" + APP_DIGEST + if compose.count(app_pin) != 1 or compose.count(core_pin) != 2: + raise ValueError("finalized release requires the exact app pin and two exact Core pins") def validate_rendered_binds(contract, rendered, environment=None): environment = environment or {} diff --git a/scripts/finalize-axebc2-0.1.10-dev.sh b/scripts/finalize-axebc2-0.1.11-dev.sh similarity index 59% rename from scripts/finalize-axebc2-0.1.10-dev.sh rename to scripts/finalize-axebc2-0.1.11-dev.sh index ca9bab6..ea7db1c 100755 --- a/scripts/finalize-axebc2-0.1.10-dev.sh +++ b/scripts/finalize-axebc2-0.1.11-dev.sh @@ -1,27 +1,28 @@ #!/usr/bin/env bash set -Eeuo pipefail -if [[ "$#" -lt 3 || "$#" -gt 4 ]]; then - echo "usage: $0 APP_INDEX_DIGEST CORE_CANDIDATE_TAG CORE_INDEX_DIGEST [EVIDENCE_OUTPUT]" >&2 +if [[ "$#" -lt 1 || "$#" -gt 2 ]]; then + echo "usage: $0 APP_INDEX_DIGEST [EVIDENCE_OUTPUT]" >&2 exit 64 fi -app_digest="$1"; core_candidate_tag="$2"; core_digest="$3" +app_digest="$1" repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" compose="$repo_root/willitmod-dev-bc2/docker-compose.yml" -evidence_output="${4:-$repo_root/willitmod-dev-bc2/DEV-ACCEPTANCE-EVIDENCE.json}" +evidence_output="${2:-$repo_root/willitmod-dev-bc2/DEV-ACCEPTANCE-EVIDENCE.json}" docker_bin="${DOCKER_BIN:-docker}" curl_bin="${CURL_BIN:-curl}" jq_bin="${JQ_BIN:-jq}" -app_tag="ghcr.io/willitmod/axebc2-app-umbrel-dev:0.1.10-candidate.6e4ef58218e8" -app_revision="6e4ef58218e8cd5a4d1113196f9872a7f501f52e" +app_tag="ghcr.io/willitmod/axebc2-app-umbrel-dev:0.1.11-candidate.ecf6e2c8cfd0" +app_revision="ecf6e2c8cfd0e42ea53d3cc146b18cd6d4c4b563" +app_candidate_run="33895447789" core_revision="cdf44542dde255648008249d187fafc15f3a2f09" -core_tag="ghcr.io/willitmod/bitcoinii-core:$core_candidate_tag" +core_tag="ghcr.io/willitmod/bitcoinii-core:31.1.0-rc.cdf44542dde2" +core_digest="sha256:8875917ece57668fe9925d40a256ce8d429a3071511bb555d4ace1fa4370afc6" os_version="v0.7.12-dev" os_bundle_sha256="11a35e68ab169eb0446485992a57b33fae018a92020b7d86bbf9a005571377af" fail() { echo "ERROR: $*" >&2; exit 1; } [[ "$app_digest" =~ ^sha256:[0-9a-f]{64}$ ]] || fail "app digest is not an exact sha256 digest" [[ "$core_digest" =~ ^sha256:[0-9a-f]{64}$ ]] || fail "Core digest is not an exact sha256 digest" -[[ "$core_candidate_tag" == "31.1.0-rc.cdf44542dde2" ]] || fail "Core tag must be 31.1.0-rc.cdf44542dde2" command -v "$docker_bin" >/dev/null 2>&1 || fail "Docker is required for registry verification" command -v "$curl_bin" >/dev/null 2>&1 || fail "curl is required for anonymous registry verification" command -v "$jq_bin" >/dev/null 2>&1 || fail "jq is required for anonymous registry verification" @@ -66,10 +67,10 @@ resolve_tag "$app_tag" "$app_digest"; resolve_tag "$core_tag" "$core_digest" verify_index "$app_tag" "$app_digest"; verify_index "$core_tag" "$core_digest" [[ "$(grep -oF APP_CANDIDATE_DIGEST_REQUIRED "$compose" | wc -l | tr -d ' ')" == 1 ]] || fail "expected one app sentinel" -[[ "$(grep -oF CORE31_CANDIDATE_DIGEST_REQUIRED "$compose" | wc -l | tr -d ' ')" == 2 ]] || fail "expected two Core sentinels" +[[ "$(grep -oF "$core_tag@$core_digest" "$compose" | wc -l | tr -d ' ')" == 2 ]] || fail "expected two exact retained Core pins" +[[ "$(grep -oF _DIGEST_REQUIRED "$compose" | wc -l | tr -d ' ')" == 1 ]] || fail "unexpected digest sentinel" tmp="$(mktemp "${compose}.finalize.XXXXXX")" -sed -e "s/APP_CANDIDATE_DIGEST_REQUIRED/${app_digest#sha256:}/g" \ - -e "s|$core_tag@sha256:CORE31_CANDIDATE_DIGEST_REQUIRED|$core_tag@$core_digest|g" "$compose" >"$tmp" +sed -e "s/APP_CANDIDATE_DIGEST_REQUIRED/${app_digest#sha256:}/g" "$compose" >"$tmp" chmod 0644 "$tmp" grep -F _DIGEST_REQUIRED "$tmp" >/dev/null && fail "unresolved digest sentinel remains" [[ "$(grep -oF "$core_tag@$core_digest" "$tmp" | wc -l | tr -d ' ')" == 2 ]] || fail "Core references differ" @@ -78,13 +79,69 @@ grep -Fx " image: $core_tag@$core_digest" "$tmp" >/dev/null || fail "Core ser grep -Fx " BTC2D_IMAGE: \"$core_tag@$core_digest\"" "$tmp" >/dev/null || fail "BTC2D_IMAGE is incorrect" evidence_tmp="$(mktemp "${evidence_output}.finalize.XXXXXX")" -python3 - "$evidence_tmp" "$app_tag" "$app_digest" "$app_revision" "$core_tag" "$core_digest" "$core_revision" "$os_version" "$os_bundle_sha256" <<'PY' +python3 - "$evidence_tmp" "$app_tag" "$app_digest" "$app_revision" "$app_candidate_run" "$core_tag" "$core_digest" "$core_revision" "$os_version" "$os_bundle_sha256" <<'PY' import json,sys -path,app_image,app_digest,revision,core_image,core_digest,core_revision,os_version,os_bundle_sha256=sys.argv[1:] +path,app_image,app_digest,revision,app_candidate_run,core_image,core_digest,core_revision,os_version,os_bundle_sha256=sys.argv[1:] +payload = { + "schema": 1, + "result": "RECORD_passed_AFTER_LIVE_DEV_ACCEPTANCE", + "app_image": app_image, + "app_digest": app_digest, + "app_candidate_run": int(app_candidate_run), + "core_image": core_image, + "core_digest": core_digest, + "app_version": "0.1.11-dev", + "source_revision": revision, + "core_source_revision": core_revision, + "core_candidate_run": 33675068951, + "tested_os_version": os_version, + "tested_os_bundle_sha256": os_bundle_sha256, + "tested_on": "RECORD_TEST_NODE", + "tested_at": "RECORD_ISO_8601_TIMESTAMP", + "acceptance": { + "observed_at": "RECORD_ISO_8601_TIMESTAMP", + "chain": "main", + "core_version": "RECORD_INTEGER_VERSION", + "migration_required_marker_absent": "RECORD_BOOLEAN", + "migration_started_marker_valid": "RECORD_BOOLEAN", + "migration_complete_marker_valid": "RECORD_BOOLEAN", + "checkpoint_height": 57752, + "checkpoint_hash": "000000000000000013ceffe797280c57f75a5b9f1d9e70c3503584058c322576", + "chainwork": "RECORD_64_HEX_CHAINWORK", + "ibd": "RECORD_BOOLEAN", + "verification_progress": "RECORD_NUMBER", + "blocks": "RECORD_INTEGER", + "headers": "RECORD_SAME_INTEGER", + "best_block_hash": "RECORD_64_HEX_HASH", + "explorer_common_height": "RECORD_SAME_INTEGER", + "explorer_common_hash": "RECORD_SAME_64_HEX_HASH", + "outbound_core31_peers": "RECORD_INTEGER_AT_LEAST_3", + "competing_valid_tips": 0, + "verifychain_level": 4, + "verifychain_passed": "RECORD_BOOLEAN", + "payout_configured": "RECORD_BOOLEAN", + "payout_preserved": "RECORD_BOOLEAN", + "pool_stratum_result": "RECORD_passed", + "app_ui_privacy_passed": "RECORD_BOOLEAN", + "payout_validation_passed": "RECORD_BOOLEAN", + "invalid_payout_rejected_without_mutation": "RECORD_BOOLEAN", + "rpc_unavailable_rejected_without_mutation": "RECORD_BOOLEAN", + "pending_payout_revalidation_passed": "RECORD_BOOLEAN", + "main_payout_banner_hidden": "RECORD_BOOLEAN", + "ckpool_sharelog_ownership_repaired": "RECORD_BOOLEAN", + "telemetry_disabled": "RECORD_BOOLEAN", + "p2p_port_unpublished": "RECORD_BOOLEAN", + "natpmp_disabled": "RECORD_BOOLEAN", + "post_completion_restart_passed": "RECORD_BOOLEAN", + "reindex_not_repeated": "RECORD_BOOLEAN", + "app_rollback_rejected": "RECORD_BOOLEAN", + "os_rollback_rejected": "RECORD_BOOLEAN", + }, +} with open(path,"w",encoding="utf-8") as h: - json.dump({"schema":1,"result":"RECORD_passed_AFTER_LIVE_DEV_ACCEPTANCE","app_image":app_image,"app_digest":app_digest,"core_image":core_image,"core_digest":core_digest,"app_version":"0.1.10-dev","source_revision":revision,"core_source_revision":core_revision,"core_candidate_run":33675068951,"tested_os_version":os_version,"tested_os_bundle_sha256":os_bundle_sha256,"tested_on":"RECORD_TEST_NODE","tested_at":"RECORD_ISO_8601_TIMESTAMP","acceptance":{"observed_at":"RECORD_ISO_8601_TIMESTAMP","chain":"main","core_version":"RECORD_INTEGER_VERSION","migration_required_marker_absent":"RECORD_BOOLEAN","migration_started_marker_valid":"RECORD_BOOLEAN","migration_complete_marker_valid":"RECORD_BOOLEAN","checkpoint_height":57752,"checkpoint_hash":"000000000000000013ceffe797280c57f75a5b9f1d9e70c3503584058c322576","chainwork":"RECORD_64_HEX_CHAINWORK","ibd":False,"verification_progress":"RECORD_NUMBER","blocks":"RECORD_INTEGER","headers":"RECORD_SAME_INTEGER","best_block_hash":"RECORD_64_HEX_HASH","explorer_common_height":"RECORD_SAME_INTEGER","explorer_common_hash":"RECORD_SAME_64_HEX_HASH","outbound_core31_peers":"RECORD_INTEGER_AT_LEAST_3","competing_valid_tips":0,"verifychain_level":4,"verifychain_passed":"RECORD_BOOLEAN","payout_configured":"RECORD_BOOLEAN","payout_preserved":"RECORD_BOOLEAN","pool_stratum_result":"RECORD_passed","app_ui_privacy_passed":"RECORD_BOOLEAN","telemetry_disabled":"RECORD_BOOLEAN","p2p_port_unpublished":"RECORD_BOOLEAN","natpmp_disabled":"RECORD_BOOLEAN","post_completion_restart_passed":"RECORD_BOOLEAN","reindex_not_repeated":"RECORD_BOOLEAN","app_rollback_rejected":"RECORD_BOOLEAN","os_rollback_rejected":"RECORD_BOOLEAN"}},h,indent=2); h.write("\n") + json.dump(payload,h,indent=2); h.write("\n") PY chmod 0644 "$evidence_tmp" mv -f "$tmp" "$compose"; mv -f "$evidence_tmp" "$evidence_output" -printf 'Prepared AxeBC2 0.1.10 DEV\napp=%s\ncore=%s\nOS=%s (%s)\nevidence template=%s\n' \ +printf 'Prepared AxeBC2 0.1.11 DEV\napp=%s\ncore=%s\nOS=%s (%s)\nevidence template=%s\n' \ "$app_digest" "$core_digest" "$os_version" "$os_bundle_sha256" "$evidence_output" diff --git a/scripts/validate-axebc2-core31-dev.py b/scripts/validate-axebc2-core31-dev.py index 71a9811..7c7778f 100644 --- a/scripts/validate-axebc2-core31-dev.py +++ b/scripts/validate-axebc2-core31-dev.py @@ -34,7 +34,30 @@ def require(condition, message): node_config = (APP / "data/templates/bitcoinII.conf.template").read_text(encoding="utf-8") evidence = json.loads((APP / "DEV-ACCEPTANCE-EVIDENCE.json").read_text(encoding="utf-8")) -require('version: "0.1.10-dev"' in manifest, "manifest must be 0.1.10-dev") +require('version: "0.1.11-dev"' in manifest, "manifest must be 0.1.11-dev") +require(evidence.get("app_version") == "0.1.11-dev", "evidence must name the 0.1.11 DEV app version") +require( + evidence.get("app_image") + == "ghcr.io/willitmod/axebc2-app-umbrel-dev:0.1.11-candidate.ecf6e2c8cfd0", + "evidence must name the exact application candidate tag", +) +require( + evidence.get("source_revision") == "ecf6e2c8cfd0e42ea53d3cc146b18cd6d4c4b563", + "evidence must name the exact application source revision", +) +require( + evidence.get("app_digest") + == "sha256:23a7962e223da5549eba52697c6f4cfa16ab74cba935c68c48148a4c515302b4", + "evidence must name the exact application index digest", +) +require(evidence.get("app_candidate_run") == 33895447789, "evidence must name the application candidate workflow run") +require( + evidence.get("core_image") == "ghcr.io/willitmod/bitcoinii-core:31.1.0-rc.cdf44542dde2" + and evidence.get("core_digest") + == "sha256:8875917ece57668fe9925d40a256ce8d429a3071511bb555d4ace1fa4370afc6" + and evidence.get("core_source_revision") == "cdf44542dde255648008249d187fafc15f3a2f09", + "evidence must retain the accepted Core 31 tag, digest, and source revision", +) require("Requires 5tratumOS 0.7.12" in manifest, "OS prerequisite must be disclosed") require(evidence.get("tested_os_version") == "v0.7.12-dev", "evidence must name the tested DEV OS release") require( @@ -47,6 +70,15 @@ def require(condition, message): require("create_host_path: false" in compose, "build metadata bind must fail closed") require("/etc/5tratumos/build.json" in compose, "build metadata must be mounted") require('JWT_SECRET: "${JWT_SECRET}"' in compose, "init must receive the platform JWT secret") +require( + "chown -R 1000:1000 /data/pool/www" in compose + and compose.count("$$(stat -c '%u:%g' /data/pool/www") == 3, + "versioned Compose init must repair the persistent CKPool sharelog tree", +) +require( + "previously seeded init script" in compose, + "ownership repair must document why it cannot live only in seeded app data", +) require( ".5tratumos-rollback-policy.json" in (APP / "data/init/init.sh").read_text(encoding="utf-8"), "init must use the policy filename consumed by AxeBC2 and 5tratumOS", @@ -101,8 +133,7 @@ def validate_platform_merged_compose(): build_metadata = temp / "build.json" build_metadata.write_text('{"tag":"v0.7.12-dev"}\n', encoding="utf-8") source.write_text( - compose.replace("CORE31_CANDIDATE_DIGEST_REQUIRED", "a" * 64) - .replace("APP_CANDIDATE_DIGEST_REQUIRED", "b" * 64) + compose.replace("APP_CANDIDATE_DIGEST_REQUIRED", "b" * 64) .replace("/etc/5tratumos/build.json", str(build_metadata)), encoding="utf-8", ) diff --git a/tests/test_axebc2_core31_init.py b/tests/test_axebc2_core31_init.py index c3b8ae7..c5b97c5 100644 --- a/tests/test_axebc2_core31_init.py +++ b/tests/test_axebc2_core31_init.py @@ -25,7 +25,7 @@ def setUp(self): def tearDown(self): shutil.rmtree(self.tmp) - def run_init(self, tag="0.7.12", expect=0, jwt_secret=None): + def run_init(self, tag="0.7.12", expect=0, jwt_secret=None, extra_env=None): self.build.write_text(json.dumps({"tag": tag}), encoding="utf-8") env = os.environ.copy() env.pop("JWT_SECRET", None) @@ -47,6 +47,8 @@ def run_init(self, tag="0.7.12", expect=0, jwt_secret=None): ) if jwt_secret is not None: env["JWT_SECRET"] = jwt_secret + if extra_env: + env.update(extra_env) result = subprocess.run( ["sh", str(INIT)], env=env, text=True, capture_output=True, check=False ) @@ -161,6 +163,59 @@ def test_existing_upnp_configuration_is_disabled(self): self.assertNotIn("upnp=", updated) self.assertEqual(updated.count("natpmp=0"), 1) + def test_current_ckpool_config_still_repairs_sharelog_ownership(self): + pool = self.data / "pool" + sharelogs = pool / "www" + sharelogs.mkdir(parents=True) + config_dir = pool / "config" + config_dir.mkdir() + config = config_dir / "ckpool.conf" + original = json.dumps( + { + "btcaddress": "1BoatSLRHtKNngkdXEeobR76b53LETtpyT", + "btcd": [{"url": "btc2d:8337"}], + "zmqblock": "tcp://btc2d:28336", + }, + sort_keys=True, + ) + "\n" + config.write_text(original, encoding="utf-8") + + fake_bin = self.tmp / "fake-bin" + fake_bin.mkdir() + chown_log = self.tmp / "chown.log" + chown_state = self.tmp / "sharelog-ownership-repaired" + fake_chown = fake_bin / "chown" + fake_chown.write_text( + "#!/bin/sh\n" + 'printf "%s\\n" "$*" >> "$AXEBC2_TEST_CHOWN_LOG"\n' + 'case "$*" in *"$AXEBC2_TEST_SHARELOG_ROOT"*) ' + ': > "$AXEBC2_TEST_CHOWN_STATE" ;; esac\n', + encoding="utf-8", + ) + fake_chown.chmod(0o755) + fake_stat = fake_bin / "stat" + fake_stat.write_text( + "#!/bin/sh\n" + 'if [ -e "$AXEBC2_TEST_CHOWN_STATE" ]; then ' + "printf '1000:1000\\n'; else printf '0:0\\n'; fi\n", + encoding="utf-8", + ) + fake_stat.chmod(0o755) + extra_env = { + "AXEBC2_TEST_SKIP_CHOWN": "false", + "AXEBC2_TEST_CHOWN_LOG": str(chown_log), + "AXEBC2_TEST_CHOWN_STATE": str(chown_state), + "AXEBC2_TEST_SHARELOG_ROOT": str(sharelogs), + "PATH": f"{fake_bin}{os.pathsep}{os.environ['PATH']}", + } + + self.run_init(extra_env=extra_env) + self.run_init(extra_env=extra_env) + + repair = f"-R 1000:1000 {sharelogs}" + self.assertEqual(chown_log.read_text(encoding="utf-8").splitlines().count(repair), 1) + self.assertEqual(config.read_text(encoding="utf-8"), original) + def test_missing_or_malformed_build_metadata_fails_closed(self): self.build.write_text("not-json", encoding="utf-8") env = os.environ.copy() diff --git a/tests/test_axebc2_dev_finalizer.py b/tests/test_axebc2_dev_finalizer.py index ae0544b..098d26b 100644 --- a/tests/test_axebc2_dev_finalizer.py +++ b/tests/test_axebc2_dev_finalizer.py @@ -8,10 +8,10 @@ import unittest ROOT = Path(__file__).resolve().parents[1] -SCRIPT = ROOT / "scripts/finalize-axebc2-0.1.10-dev.sh" +SCRIPT = ROOT / "scripts/finalize-axebc2-0.1.11-dev.sh" COMPOSE = ROOT / "willitmod-dev-bc2/docker-compose.yml" APP_DIGEST = "sha256:" + "a" * 64 -CORE_DIGEST = "sha256:" + "b" * 64 +CORE_DIGEST = "sha256:8875917ece57668fe9925d40a256ce8d429a3071511bb555d4ace1fa4370afc6" CORE_TAG = "31.1.0-rc.cdf44542dde2" OS_BUNDLE_SHA256 = "11a35e68ab169eb0446485992a57b33fae018a92020b7d86bbf9a005571377af" @@ -22,11 +22,10 @@ def setUp(self): (self.root / "scripts").mkdir(); (self.root / "willitmod-dev-bc2").mkdir() shutil.copy2(SCRIPT, self.root / "scripts" / SCRIPT.name) fixture = COMPOSE.read_text(encoding="utf-8") - fixture = re.sub(r"(ghcr\.io/willitmod/axebc2-app-umbrel-dev:0\.1\.10-candidate\.6e4ef58218e8@sha256:)[0-9a-f]{64}", r"\1APP_CANDIDATE_DIGEST_REQUIRED", fixture) - fixture = re.sub(r"(ghcr\.io/willitmod/bitcoinii-core:31\.1\.0-rc\.cdf44542dde2@sha256:)[0-9a-f]{64}", r"\1CORE31_CANDIDATE_DIGEST_REQUIRED", fixture) + fixture = re.sub(r"(ghcr\.io/willitmod/axebc2-app-umbrel-dev:0\.1\.11-candidate\.ecf6e2c8cfd0@sha256:)[0-9a-f]{64}", r"\1APP_CANDIDATE_DIGEST_REQUIRED", fixture) (self.root / "willitmod-dev-bc2/docker-compose.yml").write_text(fixture, encoding="utf-8") self.assertEqual(fixture.count("APP_CANDIDATE_DIGEST_REQUIRED"), 1) - self.assertEqual(fixture.count("CORE31_CANDIDATE_DIGEST_REQUIRED"), 2) + self.assertEqual(fixture.count(CORE_DIGEST), 2) self.original = (self.root / "willitmod-dev-bc2/docker-compose.yml").read_bytes() self.log = self.root / "docker.log" self.fake = self.root / "docker" @@ -36,7 +35,7 @@ def setUp(self): host="$2"; config="$4"; [ "$1" = --host ]; [ "$3" = --config ]; [ -n "$host" ]; [ "$(cat "$config/config.json")" = '{"auths":{}}' ]; shift 4 if [ "$1 $2" = 'buildx imagetools' ]; then case "$4" in - ghcr.io/willitmod/axebc2-app-umbrel-dev:0.1.10-candidate.6e4ef58218e8) printf 'Digest: %s\\n' "$APP_DIGEST" ;; + ghcr.io/willitmod/axebc2-app-umbrel-dev:0.1.11-candidate.ecf6e2c8cfd0) printf 'Digest: %s\\n' "$APP_DIGEST" ;; ghcr.io/willitmod/bitcoinii-core:31.1.0-rc.cdf44542dde2) printf 'Digest: %s\\n' "$CORE_DIGEST" ;; *) exit 2 ;; esac @@ -70,9 +69,9 @@ def setUp(self): def tearDown(self): self.temp.cleanup() - def run_it(self, core_tag=CORE_TAG, curl_mode="correct"): + def run_it(self, curl_mode="correct"): env=os.environ.copy(); env.update({"DOCKER_BIN":str(self.fake),"DOCKER_HOST":"unix:///tmp/test-colima.sock","CURL_BIN":str(self.fake_curl),"FAKE_DOCKER_LOG":str(self.log),"FAKE_CURL_LOG":str(self.curl_log),"CURL_DIGEST_MODE":curl_mode,"APP_DIGEST":APP_DIGEST,"CORE_DIGEST":CORE_DIGEST}) - return subprocess.run([str(self.root/"scripts"/SCRIPT.name),APP_DIGEST,core_tag,CORE_DIGEST],env=env,text=True,capture_output=True,check=False) + return subprocess.run([str(self.root/"scripts"/SCRIPT.name),APP_DIGEST],env=env,text=True,capture_output=True,check=False) def test_anonymous_candidate_checks_finalize_and_emit_evidence(self): result=self.run_it(); self.assertEqual(result.returncode,0,result.stderr) @@ -81,7 +80,9 @@ def test_anonymous_candidate_checks_finalize_and_emit_evidence(self): core_ref="ghcr.io/willitmod/bitcoinii-core:"+CORE_TAG+"@"+CORE_DIGEST self.assertEqual(compose.count(core_ref),2) evidence=json.loads((self.root/"willitmod-dev-bc2/DEV-ACCEPTANCE-EVIDENCE.json").read_text(encoding="utf-8")) - self.assertEqual(evidence["source_revision"],"6e4ef58218e8cd5a4d1113196f9872a7f501f52e") + self.assertEqual(evidence["source_revision"],"ecf6e2c8cfd0e42ea53d3cc146b18cd6d4c4b563") + self.assertEqual(evidence["app_version"],"0.1.11-dev") + self.assertEqual(evidence["app_candidate_run"],33895447789) self.assertEqual(evidence["core_source_revision"],"cdf44542dde255648008249d187fafc15f3a2f09") self.assertEqual(evidence["core_candidate_run"],33675068951) self.assertEqual(evidence["tested_os_version"],"v0.7.12-dev") @@ -94,7 +95,7 @@ def test_anonymous_candidate_checks_finalize_and_emit_evidence(self): def test_bad_explicit_docker_host_fails_before_registry_or_mutation(self): env=os.environ.copy(); env.update({"DOCKER_BIN":str(self.fake),"DOCKER_HOST":"not-an-endpoint","CURL_BIN":str(self.fake_curl),"FAKE_DOCKER_LOG":str(self.log),"FAKE_CURL_LOG":str(self.curl_log),"APP_DIGEST":APP_DIGEST,"CORE_DIGEST":CORE_DIGEST}) - result=subprocess.run([str(self.root/"scripts"/SCRIPT.name),APP_DIGEST,CORE_TAG,CORE_DIGEST],env=env,text=True,capture_output=True,check=False) + result=subprocess.run([str(self.root/"scripts"/SCRIPT.name),APP_DIGEST],env=env,text=True,capture_output=True,check=False) self.assertNotEqual(result.returncode,0); self.assertFalse(self.log.exists()); self.assertFalse(self.curl_log.exists()) self.assertEqual((self.root/"willitmod-dev-bc2/docker-compose.yml").read_bytes(),self.original) @@ -105,8 +106,10 @@ def test_bad_registry_digest_headers_fail_without_docker_or_mutation(self): self.assertFalse(self.log.exists()) self.assertEqual((self.root/"willitmod-dev-bc2/docker-compose.yml").read_bytes(),self.original) - def test_wrong_core_revision_tag_fails_before_registry_and_mutation(self): - result=self.run_it("31.1.0-rc.000000000000"); self.assertNotEqual(result.returncode,0) + def test_unexpected_extra_argument_fails_before_registry_and_mutation(self): + env=os.environ.copy(); env.update({"DOCKER_BIN":str(self.fake),"DOCKER_HOST":"unix:///tmp/test-colima.sock","CURL_BIN":str(self.fake_curl),"FAKE_DOCKER_LOG":str(self.log),"FAKE_CURL_LOG":str(self.curl_log),"APP_DIGEST":APP_DIGEST,"CORE_DIGEST":CORE_DIGEST}) + result=subprocess.run([str(self.root/"scripts"/SCRIPT.name),APP_DIGEST,"evidence.json","unexpected"],env=env,text=True,capture_output=True,check=False) + self.assertNotEqual(result.returncode,0) self.assertFalse(self.log.exists()) self.assertEqual((self.root/"willitmod-dev-bc2/docker-compose.yml").read_bytes(),self.original) diff --git a/tests/test_axebc2_platform_integration.py b/tests/test_axebc2_platform_integration.py index a359215..12a9e8a 100644 --- a/tests/test_axebc2_platform_integration.py +++ b/tests/test_axebc2_platform_integration.py @@ -12,6 +12,7 @@ ROOT = Path(__file__).resolve().parents[1] INIT = ROOT / "willitmod-dev-bc2/data/init/init.sh" +COMPOSE = ROOT / "willitmod-dev-bc2/docker-compose.yml" TEMPLATES = ROOT / "willitmod-dev-bc2/data/templates" @@ -59,6 +60,16 @@ def load_policy_module(platform: Path): class AxeBC2PlatformIntegrationTests(unittest.TestCase): + def test_upgrade_repair_is_in_versioned_compose_not_only_seeded_data(self): + source = COMPOSE.read_text(encoding="utf-8") + self.assertEqual(source.count("$$(stat -c '%u:%g' /data/pool/www"), 3) + self.assertIn("chown -R 1000:1000 /data/pool/www", source) + self.assertNotIn('"$(stat -c', source) + self.assertLess( + source.index("chown -R 1000:1000 /data/pool/www"), + source.index("exec /bin/sh /opt/axebc2/init.sh"), + ) + def test_pinned_materialization_contract_matches_platform_source(self): platform = platform_root() cli = platform / "bin/5tratumos" diff --git a/tests/test_axebc2_release_state.py b/tests/test_axebc2_release_state.py index d30c05c..a1dff7e 100644 --- a/tests/test_axebc2_release_state.py +++ b/tests/test_axebc2_release_state.py @@ -2,22 +2,25 @@ from pathlib import Path import unittest sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) -from axebc2_release_state import APP_TAG, CORE_TAG, validate, validate_rendered_binds +from axebc2_release_state import APP_DIGEST, APP_TAG, CORE_DIGEST, CORE_TAG, validate, validate_rendered_binds class ReleaseStateTests(unittest.TestCase): def test_only_complete_prefinalization_is_accepted(self): - text=f"{APP_TAG}@sha256:APP_CANDIDATE_DIGEST_REQUIRED\n{CORE_TAG}@sha256:CORE31_CANDIDATE_DIGEST_REQUIRED\n{CORE_TAG}@sha256:CORE31_CANDIDATE_DIGEST_REQUIRED" + core=f"{CORE_TAG}@{CORE_DIGEST}" + text=f"{APP_TAG}@sha256:APP_CANDIDATE_DIGEST_REQUIRED\n{core}\n{core}" validate(text,"prefinalization") - with self.assertRaises(ValueError): validate(text.replace("CORE31_CANDIDATE_DIGEST_REQUIRED","a"*64,1),"prefinalization") + with self.assertRaises(ValueError): validate(text.replace(CORE_DIGEST,"sha256:"+"b"*64,1),"prefinalization") def test_only_complete_immutable_finalization_is_accepted(self): - a="sha256:"+"a"*64; c="sha256:"+"c"*64 - text=f"{APP_TAG}@{a}\n{CORE_TAG}@{c}\n{CORE_TAG}@{c}" + core=f"{CORE_TAG}@{CORE_DIGEST}" + text=f"{APP_TAG}@{APP_DIGEST}\n{core}\n{core}" validate(text,"finalized") - with self.assertRaises(ValueError): validate(text.replace(c,"sha256:"+"d"*64,1),"finalized") + with self.assertRaises(ValueError): validate(text.replace(APP_DIGEST,"sha256:"+"a"*64),"finalized") + with self.assertRaises(ValueError): validate(text.replace(CORE_DIGEST,"sha256:"+"d"*64,1),"finalized") with self.assertRaises(ValueError): validate(text+"\n_DIGEST_REQUIRED","finalized") def test_lifecycle_matrix_rejects_cross_phase_validation(self): - pre=f"{APP_TAG}@sha256:APP_CANDIDATE_DIGEST_REQUIRED\n{CORE_TAG}@sha256:CORE31_CANDIDATE_DIGEST_REQUIRED\n{CORE_TAG}@sha256:CORE31_CANDIDATE_DIGEST_REQUIRED" - final=f"{APP_TAG}@sha256:{'a'*64}\n{CORE_TAG}@sha256:{'c'*64}\n{CORE_TAG}@sha256:{'c'*64}" + core=f"{CORE_TAG}@{CORE_DIGEST}" + pre=f"{APP_TAG}@sha256:APP_CANDIDATE_DIGEST_REQUIRED\n{core}\n{core}" + final=f"{APP_TAG}@{APP_DIGEST}\n{core}\n{core}" validate(pre,"prefinalization"); validate(final,"finalized") with self.assertRaises(ValueError): validate(pre,"finalized") with self.assertRaises(ValueError): validate(final,"prefinalization") diff --git a/willitmod-dev-bc2/CORE31-DEV-RELEASE.md b/willitmod-dev-bc2/CORE31-DEV-RELEASE.md index a52405a..be8330b 100644 --- a/willitmod-dev-bc2/CORE31-DEV-RELEASE.md +++ b/willitmod-dev-bc2/CORE31-DEV-RELEASE.md @@ -1,8 +1,10 @@ -# AxeBC2 Core 31 DEV release gates +# AxeBC2 0.1.11 / Core 31 DEV release gates The DEV recipe maps store ID `willitmod-dev-bc2` to canonical 5tratumOS app ID `axebc2`. Its preserved data path is `/var/lib/5tratumos/apps/axebc2`, matching -the `app_id` in `.5tratumos-rollback-policy.json`. +the `app_id` in `.5tratumos-rollback-policy.json`. The protected consensus +rollback floor remains `0.1.10`; this application-only maintenance release must +not change that floor or repeat the completed Core 31 reindex. Every host bind uses `create_host_path: false`. The recipe contains the empty runtime directories that 5tratumOS stages before Compose validation, so Docker @@ -13,34 +15,32 @@ The digest-pinned generic Alpine init container installs `jq` and network-availability dependency, but an install failure occurs before any persistent app-data or node-data mutation and prevents Core from starting. A future dedicated, independently built and digest-pinned init image could remove -that availability dependency; it is not introduced in this consensus release. +that availability dependency; it is not introduced in this release. -The committed Compose file is finalized: it contains one immutable application -sha256 pin and two identical immutable Core sha256 pins, with no digest -sentinels. CI detects this as the strict `finalized` phase. The earlier -`prefinalization` phase accepted exactly one `APP_CANDIDATE_DIGEST_REQUIRED` and -two `CORE31_CANDIDATE_DIGEST_REQUIRED` occurrences; a partial or mixed state is -rejected in either phase. +## Immutable release inputs -Finalization replaced those sentinels with the exact verified -multi-architecture candidate digests. The merged platform Compose must pass -validation, all images must pull anonymously by digest, init must complete -successfully on 5tratumOS 0.7.12+, and the resulting installation must be -tested on DEV before any production promotion. +The application is pinned to +`ghcr.io/willitmod/axebc2-app-umbrel-dev:0.1.11-candidate.ecf6e2c8cfd0@sha256:23a7962e223da5549eba52697c6f4cfa16ab74cba935c68c48148a4c515302b4`, +built from source revision `ecf6e2c8cfd0e42ea53d3cc146b18cd6d4c4b563` by +candidate workflow run `33895447789`. -Run `scripts/finalize-axebc2-0.1.10-dev.sh` with the exact application index -digest, exact Core candidate tag and exact Core index digest. The application -candidate is fixed to `0.1.10-candidate.6e4ef58218e8` from source revision -`6e4ef58218e8cd5a4d1113196f9872a7f501f52e`. The Core candidate is fixed to -`31.1.0-rc.cdf44542dde2` from source revision -`cdf44542dde255648008249d187fafc15f3a2f09`, candidate workflow run -`33675068951`. Before editing Compose, the -finalizer anonymously verifies candidate resolution, amd64 and arm64 manifests -and pulls. It atomically replaces every sentinel and emits both exact source -revisions in the evidence JSON template, which must be completed only after -live DEV acceptance. +The already accepted BitcoinII Core 31 image is unchanged and remains pinned +twice to +`ghcr.io/willitmod/bitcoinii-core:31.1.0-rc.cdf44542dde2@sha256:8875917ece57668fe9925d40a256ce8d429a3071511bb555d4ace1fa4370afc6`. +Its source revision is `cdf44542dde255648008249d187fafc15f3a2f09`, built by +candidate workflow run `33675068951`. Both the DEV finalizer and validator must +reject any change to that tag, digest, source revision, or the two-reference +invariant. -The test platform is also fixed to the published DEV-only +`scripts/finalize-axebc2-0.1.11-dev.sh` accepts the exact application index +digest once the candidate is available. Before editing Compose, it anonymously +verifies the application and retained Core tag resolutions, amd64 and arm64 +manifests, and digest pulls. It atomically replaces only the application +sentinel and emits an evidence template bound to both exact source revisions. +CI accepts either the complete one-sentinel prefinalization state or the complete +immutable finalization state; partial or mixed states fail. + +The test platform remains fixed to the published DEV-only [`v0.7.12-dev`](https://github.com/WillItMod/5tratum/releases/tag/v0.7.12-dev) bundle with SHA-256 `11a35e68ab169eb0446485992a57b33fae018a92020b7d86bbf9a005571377af`. @@ -49,33 +49,39 @@ free-form observation. MAIN promotion rejects evidence from a different OS bundle even when the displayed version string is the same. The store validator exercises a pinned copy of the relevant 5tratumOS -materialization contract from platform commit `4f979cb9541622c1fdccdf43b8a885bbf845ba38`: -it consumes `app_proxy`, publishes the manifest port on the resolved app -service, removes the legacy shared network, and normalizes restart policies. -The platform currently exposes this logic only inside its mutating install and -update commands, so invoking the live implementation from isolated store CI -would require performing a stateful platform transaction. Final DEV acceptance -therefore still runs the real platform materializer and validates its generated -Compose file before containers are started. +materialization contract from platform commit +`4f979cb9541622c1fdccdf43b8a885bbf845ba38`: it consumes `app_proxy`, publishes +the manifest port on the resolved app service, removes the legacy shared +network, and normalizes restart policies. Final DEV acceptance must use the real +platform materializer and validate its generated Compose file before containers +are started. + +## Retained Core 31 acceptance + +The unchanged Core image completed its mandatory full reindex and protected +migration-marker checks on `10.10.10.235` using the pinned 5tratumOS bundle on +2026-09-04. A subsequent full app restart did not repeat the reindex. Core +reported version `310100`, passed level-4 `verifychain`, matched the official +BitcoinII explorer, maintained at least three outbound Core 31 peers, and had no +competing valid tip at or beyond ShockWave checkpoint height `57,752`. -## Live DEV acceptance +That evidence remains valid for the unchanged Core image, but it does not replace +live acceptance of the new application candidate. -The exact finalized candidate was accepted on `10.10.10.235` using the pinned -5tratumOS `v0.7.12-dev` bundle on 2026-09-04. The mandatory Core 31 full reindex -completed, its protected migration markers validated, and a subsequent full app -restart did not repeat the reindex. Core reported version `310100`, completed a -level-4 `verifychain`, and matched the official BitcoinII explorer at height -58,433 and block hash -`0000000000000001077a5ea39eefb3a44e5d88357c723f56484840a7f89c5554`. +## Required 0.1.11 live DEV acceptance -Five outbound BitcoinII Core 31 peers were observed. Six historical one-block -header branches ended between heights 53,093 and 53,209, all before the -ShockWave checkpoint; no non-active valid tip existed at or beyond checkpoint -height 57,752, so the recorded number of competing valid tips is zero. +Exercise installation and update on DEV. Confirm that the existing node chain, +migration markers, pool configuration, payout address, and rollback policy are +preserved; no blockchain reindex is started; sync, Explorer, private UI data, +and the non-submitting Stratum probe work; and telemetry, P2P port, and NAT-PMP +controls remain unchanged. -The non-submitting Stratum probe received subscribe, authorize, difficulty and -job notifications. The configured payout was compared using a private HMAC and -remained unchanged. The UI/privacy checks, telemetry and port-exposure checks, -post-completion restart, app rollback rejection and OS rollback rejection all -passed. All 39 unrelated application containers retained their preflight image -and container identifiers. +Specifically verify the payout fix with Core-accepted mainnet `1...`, `3...`, and +`bc1...` address families. An invalid or wrong-network address and an RPC-not-ready +condition must both leave the saved pool configuration and payout history +unchanged. Any legacy pending-validation record must be rechecked at the bounded +interval and resolve to either validated or a definitive warning. Confirm that a +MAIN/stable channel value no longer displays the misleading payout warning, and +that the one-time conditional CKPool `/www` ownership repair makes existing +sharelog paths writable by uid/gid 1000 without rewriting a current pool config. +Complete `DEV-ACCEPTANCE-EVIDENCE.json` only after those live checks pass. diff --git a/willitmod-dev-bc2/DEV-ACCEPTANCE-EVIDENCE.json b/willitmod-dev-bc2/DEV-ACCEPTANCE-EVIDENCE.json index 8aa75a6..1573e39 100644 --- a/willitmod-dev-bc2/DEV-ACCEPTANCE-EVIDENCE.json +++ b/willitmod-dev-bc2/DEV-ACCEPTANCE-EVIDENCE.json @@ -1,20 +1,21 @@ { "schema": 1, "result": "passed", - "app_image": "ghcr.io/willitmod/axebc2-app-umbrel-dev:0.1.10-candidate.6e4ef58218e8", - "app_digest": "sha256:b7ba2df2f48389d145ad18a927b099f32b5aa7708a0ea617a1b04e25c8e7f961", + "app_image": "ghcr.io/willitmod/axebc2-app-umbrel-dev:0.1.11-candidate.ecf6e2c8cfd0", + "app_digest": "sha256:23a7962e223da5549eba52697c6f4cfa16ab74cba935c68c48148a4c515302b4", + "app_candidate_run": 33895447789, "core_image": "ghcr.io/willitmod/bitcoinii-core:31.1.0-rc.cdf44542dde2", "core_digest": "sha256:8875917ece57668fe9925d40a256ce8d429a3071511bb555d4ace1fa4370afc6", - "app_version": "0.1.10-dev", - "source_revision": "6e4ef58218e8cd5a4d1113196f9872a7f501f52e", + "app_version": "0.1.11-dev", + "source_revision": "ecf6e2c8cfd0e42ea53d3cc146b18cd6d4c4b563", "core_source_revision": "cdf44542dde255648008249d187fafc15f3a2f09", "core_candidate_run": 33675068951, "tested_os_version": "v0.7.12-dev", "tested_os_bundle_sha256": "11a35e68ab169eb0446485992a57b33fae018a92020b7d86bbf9a005571377af", "tested_on": "10.10.10.235", - "tested_at": "2026-09-04T13:57:46Z", + "tested_at": "2026-09-04T17:00:50Z", "acceptance": { - "observed_at": "2026-09-04T13:57:46Z", + "observed_at": "2026-09-04T17:00:50Z", "chain": "main", "core_version": 310100, "migration_required_marker_absent": true, @@ -22,15 +23,15 @@ "migration_complete_marker_valid": true, "checkpoint_height": 57752, "checkpoint_hash": "000000000000000013ceffe797280c57f75a5b9f1d9e70c3503584058c322576", - "chainwork": "00000000000000000000000000000000000000000000fb0eacdb04473f61a89b", + "chainwork": "00000000000000000000000000000000000000000000fb2032889c9b9dee0ebe", "ibd": false, - "verification_progress": 1, - "blocks": 58433, - "headers": 58433, - "best_block_hash": "0000000000000001077a5ea39eefb3a44e5d88357c723f56484840a7f89c5554", - "explorer_common_height": 58433, - "explorer_common_hash": "0000000000000001077a5ea39eefb3a44e5d88357c723f56484840a7f89c5554", - "outbound_core31_peers": 5, + "verification_progress": 1.0, + "blocks": 58443, + "headers": 58443, + "best_block_hash": "000000000000000003804a1aac31af72e0b1e7e00926faa602fc9f3f26e40175", + "explorer_common_height": 58443, + "explorer_common_hash": "000000000000000003804a1aac31af72e0b1e7e00926faa602fc9f3f26e40175", + "outbound_core31_peers": 10, "competing_valid_tips": 0, "verifychain_level": 4, "verifychain_passed": true, @@ -38,6 +39,12 @@ "payout_preserved": true, "pool_stratum_result": "passed", "app_ui_privacy_passed": true, + "payout_validation_passed": true, + "invalid_payout_rejected_without_mutation": true, + "rpc_unavailable_rejected_without_mutation": true, + "pending_payout_revalidation_passed": true, + "main_payout_banner_hidden": true, + "ckpool_sharelog_ownership_repaired": true, "telemetry_disabled": true, "p2p_port_unpublished": true, "natpmp_disabled": true, diff --git a/willitmod-dev-bc2/data/init/init.sh b/willitmod-dev-bc2/data/init/init.sh index 4e9cc5a..939c265 100644 --- a/willitmod-dev-bc2/data/init/init.sh +++ b/willitmod-dev-bc2/data/init/init.sh @@ -188,7 +188,6 @@ if [ "$needs_ckpool_regen" -eq 1 ]; then jq --arg a "$existing_addr" '.btcaddress=$a' "$ckpool_conf" >"$tmp" || fail "cannot preserve payout address" mv "$tmp" "$ckpool_conf" fi - chown -R 1000:1000 "${data_dir}/pool" 2>/dev/null || true fi if [ ! -f "${data_dir}/pool/config/ckpool.args" ]; then @@ -206,3 +205,27 @@ if [ -f "$settings" ]; then chown 1000:1000 "$ckpool_conf" 2>/dev/null || true fi fi + +# CKPool runs as uid/gid 1000 and creates per-height sharelog directories +# directly under /www. Existing installs can already have a current config and +# therefore skip the regeneration branch above while /www remains root-owned. +# Check only the three known writable directories on normal starts; recursively +# repair the existing sharelog tree once if an upgrade left any of them behind. +if [ "${AXEBC2_TEST_SKIP_CHOWN:-false}" != "true" ]; then + repair_sharelog_ownership=false + for writable_dir in \ + "${data_dir}/pool/www" \ + "${data_dir}/pool/www/pool" \ + "${data_dir}/pool/www/users" + do + owner_group="$(stat -c '%u:%g' "$writable_dir" 2>/dev/null || true)" + if [ "$owner_group" != "1000:1000" ]; then + repair_sharelog_ownership=true + break + fi + done + if [ "$repair_sharelog_ownership" = "true" ]; then + chown -R 1000:1000 "${data_dir}/pool/www" 2>/dev/null || + fail "cannot assign CKPool sharelog data to the app user" + fi +fi diff --git a/willitmod-dev-bc2/docker-compose.yml b/willitmod-dev-bc2/docker-compose.yml index 4b4f5f6..60db512 100644 --- a/willitmod-dev-bc2/docker-compose.yml +++ b/willitmod-dev-bc2/docker-compose.yml @@ -51,7 +51,19 @@ services: command: - /bin/sh - -ec - - exec /bin/sh /opt/axebc2/init.sh + - |- + # 5tratumOS preserves an installed app's data directory during an + # update, including its previously seeded init script. Keep this + # upgrade repair in the versioned Compose command so 0.1.10 installs + # receive it even when /data/init/init.sh is intentionally retained. + mkdir -p /data/pool/www/pool /data/pool/www/users + if [ "$$(stat -c '%u:%g' /data/pool/www 2>/dev/null || true)" != "1000:1000" ] || + [ "$$(stat -c '%u:%g' /data/pool/www/pool 2>/dev/null || true)" != "1000:1000" ] || + [ "$$(stat -c '%u:%g' /data/pool/www/users 2>/dev/null || true)" != "1000:1000" ]; then + chown -R 1000:1000 /data/pool/www || + { echo >&2 "cannot assign CKPool sharelog data to uid/gid 1000"; exit 1; } + fi + exec /bin/sh /opt/axebc2/init.sh btc2d: image: ghcr.io/willitmod/bitcoinii-core:31.1.0-rc.cdf44542dde2@sha256:8875917ece57668fe9925d40a256ce8d429a3071511bb555d4ace1fa4370afc6 @@ -110,7 +122,7 @@ services: fi app: - image: ghcr.io/willitmod/axebc2-app-umbrel-dev:0.1.10-candidate.6e4ef58218e8@sha256:b7ba2df2f48389d145ad18a927b099f32b5aa7708a0ea617a1b04e25c8e7f961 + image: ghcr.io/willitmod/axebc2-app-umbrel-dev:0.1.11-candidate.ecf6e2c8cfd0@sha256:23a7962e223da5549eba52697c6f4cfa16ab74cba935c68c48148a4c515302b4 user: "1000:1000" restart: on-failure stop_grace_period: 30s diff --git a/willitmod-dev-bc2/umbrel-app.yml b/willitmod-dev-bc2/umbrel-app.yml index 24fccc5..fe6b8b0 100644 --- a/willitmod-dev-bc2/umbrel-app.yml +++ b/willitmod-dev-bc2/umbrel-app.yml @@ -2,7 +2,7 @@ manifestVersion: 1 id: willitmod-dev-bc2 category: bitcoin name: AxeBC2 -version: "0.1.10-dev" +version: "0.1.11-dev" tagline: BC2 node + solo pool description: >- ALPHA RELEASE (DEV CHANNEL) @@ -47,12 +47,18 @@ path: "" defaultUsername: "" defaultPassword: "" releaseNotes: >- - Upgrades BitcoinII Core to 31.1 for the ShockWave consensus change. Requires - 5tratumOS 0.7.12 or newer. Back up your app data before upgrading. Existing - nodes must perform a full reindex of their stored blockchain data; mining and - sync data will be unavailable until validation completes. Stratum remains on TCP - port 2345. Support telemetry is disabled by default. The node uses outbound - peer connections only: no public P2P port or NAT-PMP mapping is enabled. + Corrects payout-address validation: BitcoinII Core-accepted mainnet addresses + beginning with 1, 3, or bc1 are supported, while invalid or wrong-network + addresses are rejected without changing the saved pool configuration. Settings + are also left untouched while Core is unavailable, and payout checks left + pending by an older release are retried at a bounded interval. It also removes + the misleading payout warning from MAIN builds and performs a one-time, + conditional CKPool /www ownership repair so existing sharelogs remain writable. + This app-only update retains the accepted BitcoinII Core 31.1 image, existing + blockchain and pool data, and the configured payout address; it does not + trigger another blockchain reindex. Requires 5tratumOS 0.7.12 or newer. + Stratum remains on TCP port 2345, telemetry remains disabled by default, and + no public P2P port or NAT-PMP mapping is enabled. widgets: - id: "sync" type: "text-with-progress"