Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions scripts/finalize-axebc2-0.1.11-dev.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ 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"
dev_store_revision="249ab61506dc09c2151d39e2b210f5f18d75ff21"
dev_compose_sha256="93ceba92069947f47d650a5fb32205836fe070d83707f36912a2e0e83beb1244"
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"
Expand Down Expand Up @@ -77,11 +79,19 @@ grep -F _DIGEST_REQUIRED "$tmp" >/dev/null && fail "unresolved digest sentinel r
grep -Fx " image: $app_tag@$app_digest" "$tmp" >/dev/null || fail "app reference is incorrect"
grep -Fx " image: $core_tag@$core_digest" "$tmp" >/dev/null || fail "Core service reference is incorrect"
grep -Fx " BTC2D_IMAGE: \"$core_tag@$core_digest\"" "$tmp" >/dev/null || fail "BTC2D_IMAGE is incorrect"
rendered_compose_sha256="$(python3 - "$tmp" <<'PY'
import hashlib, sys
with open(sys.argv[1], "rb") as handle:
print(hashlib.sha256(handle.read()).hexdigest())
PY
)"
[[ "$rendered_compose_sha256" == "$dev_compose_sha256" ]] ||
fail "finalized DEV Compose SHA-256 differs from the accepted recipe"

evidence_tmp="$(mktemp "${evidence_output}.finalize.XXXXXX")"
python3 - "$evidence_tmp" "$app_tag" "$app_digest" "$app_revision" "$app_candidate_run" "$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" "$dev_store_revision" "$dev_compose_sha256" <<'PY'
import json,sys
path,app_image,app_digest,revision,app_candidate_run,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,dev_store_revision,dev_compose_sha256=sys.argv[1:]
payload = {
"schema": 1,
"result": "RECORD_passed_AFTER_LIVE_DEV_ACCEPTANCE",
Expand All @@ -96,6 +106,8 @@ payload = {
"core_candidate_run": 33675068951,
"tested_os_version": os_version,
"tested_os_bundle_sha256": os_bundle_sha256,
"dev_store_revision": dev_store_revision,
"dev_compose_sha256": dev_compose_sha256,
"tested_on": "RECORD_TEST_NODE",
"tested_at": "RECORD_ISO_8601_TIMESTAMP",
"acceptance": {
Expand Down Expand Up @@ -129,6 +141,7 @@ payload = {
"pending_payout_revalidation_passed": "RECORD_BOOLEAN",
"main_payout_banner_hidden": "RECORD_BOOLEAN",
"ckpool_sharelog_ownership_repaired": "RECORD_BOOLEAN",
"pool_config_directory_writable": "RECORD_BOOLEAN",
"telemetry_disabled": "RECORD_BOOLEAN",
"p2p_port_unpublished": "RECORD_BOOLEAN",
"natpmp_disabled": "RECORD_BOOLEAN",
Expand Down
145 changes: 141 additions & 4 deletions scripts/validate-axebc2-core31-dev.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#!/usr/bin/env python3
from pathlib import Path
import datetime
import hashlib
import importlib.util
import json
import os
Expand All @@ -15,14 +17,20 @@

ROOT = Path(__file__).resolve().parents[1]
APP = ROOT / "willitmod-dev-bc2"
APP_DIGEST = "sha256:23a7962e223da5549eba52697c6f4cfa16ab74cba935c68c48148a4c515302b4"
DEV_STORE_REVISION = "249ab61506dc09c2151d39e2b210f5f18d75ff21"
DEV_COMPOSE_SHA256 = "93ceba92069947f47d650a5fb32205836fe070d83707f36912a2e0e83beb1244"
DEV_STORE_COMMITTED_AT = datetime.datetime.fromisoformat("2026-09-04T17:11:52+00:00")


def require(condition, message):
if not condition:
raise SystemExit(message)


compose = (APP / "docker-compose.yml").read_text(encoding="utf-8")
compose_path = APP / "docker-compose.yml"
compose_bytes = compose_path.read_bytes()
compose = compose_bytes.decode("utf-8")
parser = argparse.ArgumentParser()
parser.add_argument("--phase", required=True, choices=("prefinalization", "finalized"))
phase = parser.parse_args().phase
Expand All @@ -34,6 +42,17 @@ 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"))

# Hash the exact finalized recipe in either lifecycle phase. In prefinalization
# there is exactly one sentinel; in finalization this replacement is a no-op.
finalized_compose_bytes = compose_bytes.replace(
b"APP_CANDIDATE_DIGEST_REQUIRED", APP_DIGEST.removeprefix("sha256:").encode()
)
computed_compose_sha256 = hashlib.sha256(finalized_compose_bytes).hexdigest()
require(
computed_compose_sha256 == DEV_COMPOSE_SHA256,
"DEV Compose content differs from the recipe accepted on 10.10.10.235",
)

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(
Expand All @@ -47,16 +66,17 @@ def require(condition, message):
)
require(
evidence.get("app_digest")
== "sha256:23a7962e223da5549eba52697c6f4cfa16ab74cba935c68c48148a4c515302b4",
== APP_DIGEST,
"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",
and evidence.get("core_source_revision") == "cdf44542dde255648008249d187fafc15f3a2f09"
and evidence.get("core_candidate_run") == 33675068951,
"evidence must retain the accepted Core 31 tag, digest, source revision, and candidate run",
)
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")
Expand All @@ -65,6 +85,123 @@ def require(condition, message):
== "11a35e68ab169eb0446485992a57b33fae018a92020b7d86bbf9a005571377af",
"evidence must be bound to the exact verified v0.7.12-dev bundle",
)
require(
evidence.get("dev_store_revision") == DEV_STORE_REVISION,
"evidence must name the exact corrected DEV store revision",
)
require(
evidence.get("dev_compose_sha256") == DEV_COMPOSE_SHA256
and evidence.get("dev_compose_sha256") == computed_compose_sha256,
"evidence must be bound to the exact corrected DEV Compose recipe",
)


def parse_utc_timestamp(value, label):
try:
parsed = datetime.datetime.fromisoformat(str(value).replace("Z", "+00:00"))
except ValueError:
raise SystemExit(f"{label} must be an ISO-8601 timestamp")
require(parsed.tzinfo is not None, f"{label} must include a timezone")
return parsed.astimezone(datetime.timezone.utc)


if phase == "finalized":
require(type(evidence.get("schema")) is int and evidence["schema"] == 1, "finalized DEV evidence schema must be 1")
require(evidence.get("result") == "passed", "finalized DEV evidence must have passed")
require(evidence.get("tested_on") == "10.10.10.235", "finalized DEV evidence must name the test node")
tested_at = parse_utc_timestamp(evidence.get("tested_at"), "tested_at")
acceptance = evidence.get("acceptance")
require(isinstance(acceptance, dict), "finalized DEV evidence requires structured acceptance observations")
observed_at = parse_utc_timestamp(acceptance.get("observed_at"), "acceptance observed_at")
require(
tested_at >= DEV_STORE_COMMITTED_AT and observed_at >= DEV_STORE_COMMITTED_AT,
"finalized DEV acceptance must post-date the corrected store recipe",
)
require(observed_at == tested_at, "tested_at and acceptance observed_at must identify the same run")

true_gates = (
"migration_required_marker_absent",
"migration_started_marker_valid",
"migration_complete_marker_valid",
"verifychain_passed",
"payout_configured",
"payout_preserved",
"app_ui_privacy_passed",
"payout_validation_passed",
"invalid_payout_rejected_without_mutation",
"rpc_unavailable_rejected_without_mutation",
"pending_payout_revalidation_passed",
"main_payout_banner_hidden",
"ckpool_sharelog_ownership_repaired",
"pool_config_directory_writable",
"telemetry_disabled",
"p2p_port_unpublished",
"natpmp_disabled",
"post_completion_restart_passed",
"reindex_not_repeated",
"app_rollback_rejected",
"os_rollback_rejected",
)
missing_true_gates = [key for key in true_gates if acceptance.get(key) is not True]
require(not missing_true_gates, "required acceptance gates are not true: " + ", ".join(missing_true_gates))
require(
acceptance.get("chain") == "main"
and type(acceptance.get("competing_valid_tips")) is int
and acceptance["competing_valid_tips"] == 0,
"main chain must have no competing valid tips",
)
require(acceptance.get("core_version") == 310100, "exact Core 31.1.0 version was not observed")
require(
acceptance.get("checkpoint_height") == 57752
and acceptance.get("checkpoint_hash")
== "000000000000000013ceffe797280c57f75a5b9f1d9e70c3503584058c322576",
"official ShockWave checkpoint observation is invalid",
)
hex64 = lambda value: isinstance(value, str) and bool(re.fullmatch(r"[0-9a-f]{64}", value))
minimum_chainwork = "0000000000000000000000000000000000000000000000959028194ff1139272"
require(
hex64(acceptance.get("chainwork")) and acceptance["chainwork"] >= minimum_chainwork,
"observed chainwork is below the accepted minimum",
)
progress = acceptance.get("verification_progress")
require(
acceptance.get("ibd") is False
and isinstance(progress, (int, float))
and not isinstance(progress, bool)
and progress >= 0.999999,
"node synchronization evidence is incomplete",
)
blocks = acceptance.get("blocks")
require(
type(blocks) is int
and blocks >= 57752
and blocks == acceptance.get("headers")
and blocks == acceptance.get("explorer_common_height"),
"node and explorer heights must match at or beyond the checkpoint",
)
require(
hex64(acceptance.get("best_block_hash"))
and acceptance.get("best_block_hash") == acceptance.get("explorer_common_hash"),
"node and explorer hashes must match",
)
require(
type(acceptance.get("outbound_core31_peers")) is int
and acceptance["outbound_core31_peers"] >= 3,
"fewer than three outbound Core 31 peers were observed",
)
require(acceptance.get("verifychain_level") == 4, "verifychain level 4 was not recorded")
require(acceptance.get("pool_stratum_result") == "passed", "pool/Stratum acceptance did not pass")
else:
require(
evidence.get("result") == "RECORD_passed_AFTER_LIVE_DEV_ACCEPTANCE",
"prefinalization evidence must remain an explicit acceptance template",
)
acceptance = evidence.get("acceptance")
require(isinstance(acceptance, dict), "prefinalization evidence template requires acceptance fields")
require(
acceptance.get("pool_config_directory_writable") == "RECORD_BOOLEAN",
"prefinalization evidence must prompt for the uid-1000 pool-config write probe",
)
require('"2345:3333/tcp"' in compose, "Stratum host port 2345 must be retained")
require("SUPPORT_CHECKIN_ENABLED: \"false\"" in compose, "telemetry must default off")
require("create_host_path: false" in compose, "build metadata bind must fail closed")
Expand Down
19 changes: 18 additions & 1 deletion tests/test_axebc2_dev_finalizer.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
import hashlib
import os
import re
from pathlib import Path
Expand All @@ -10,10 +11,12 @@
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "scripts/finalize-axebc2-0.1.11-dev.sh"
COMPOSE = ROOT / "willitmod-dev-bc2/docker-compose.yml"
APP_DIGEST = "sha256:" + "a" * 64
APP_DIGEST = "sha256:23a7962e223da5549eba52697c6f4cfa16ab74cba935c68c48148a4c515302b4"
CORE_DIGEST = "sha256:8875917ece57668fe9925d40a256ce8d429a3071511bb555d4ace1fa4370afc6"
CORE_TAG = "31.1.0-rc.cdf44542dde2"
OS_BUNDLE_SHA256 = "11a35e68ab169eb0446485992a57b33fae018a92020b7d86bbf9a005571377af"
DEV_STORE_REVISION = "249ab61506dc09c2151d39e2b210f5f18d75ff21"
DEV_COMPOSE_SHA256 = "93ceba92069947f47d650a5fb32205836fe070d83707f36912a2e0e83beb1244"

class AxeBC2DevFinalizerTests(unittest.TestCase):
def setUp(self):
Expand Down Expand Up @@ -77,6 +80,7 @@ def test_anonymous_candidate_checks_finalize_and_emit_evidence(self):
result=self.run_it(); self.assertEqual(result.returncode,0,result.stderr)
compose=(self.root/"willitmod-dev-bc2/docker-compose.yml").read_text(encoding="utf-8")
self.assertNotIn("_DIGEST_REQUIRED",compose)
self.assertEqual(hashlib.sha256(compose.encode()).hexdigest(),DEV_COMPOSE_SHA256)
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"))
Expand All @@ -87,12 +91,25 @@ def test_anonymous_candidate_checks_finalize_and_emit_evidence(self):
self.assertEqual(evidence["core_candidate_run"],33675068951)
self.assertEqual(evidence["tested_os_version"],"v0.7.12-dev")
self.assertEqual(evidence["tested_os_bundle_sha256"],OS_BUNDLE_SHA256)
self.assertEqual(evidence["dev_store_revision"],DEV_STORE_REVISION)
self.assertEqual(evidence["dev_compose_sha256"],DEV_COMPOSE_SHA256)
self.assertEqual(evidence["acceptance"]["pool_config_directory_writable"],"RECORD_BOOLEAN")
self.assertEqual(evidence["app_digest"],APP_DIGEST); self.assertEqual(evidence["core_digest"],CORE_DIGEST)
calls=self.log.read_text(encoding="utf-8")
self.assertEqual(calls.count("--platform linux/amd64"),2); self.assertEqual(calls.count("--platform linux/arm64"),2)
self.assertNotIn("buildx", calls)
self.assertTrue(all("--host unix:///tmp/test-colima.sock --config" in line for line in calls.splitlines()))

def test_recipe_drift_fails_before_compose_or_evidence_mutation(self):
compose=self.root/"willitmod-dev-bc2/docker-compose.yml"
compose.write_bytes(compose.read_bytes()+b"\n# unexpected recipe drift\n")
original=compose.read_bytes()
result=self.run_it()
self.assertNotEqual(result.returncode,0)
self.assertIn("finalized DEV Compose SHA-256 differs",result.stderr)
self.assertEqual(compose.read_bytes(),original)
self.assertFalse((self.root/"willitmod-dev-bc2/DEV-ACCEPTANCE-EVIDENCE.json").exists())

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],env=env,text=True,capture_output=True,check=False)
Expand Down
54 changes: 34 additions & 20 deletions willitmod-dev-bc2/CORE31-DEV-RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,21 @@ 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 corrected DEV recipe was merged at store revision
`249ab61506dc09c2151d39e2b210f5f18d75ff21`. The exact finalized
`docker-compose.yml` tested from that revision has SHA-256
`93ceba92069947f47d650a5fb32205836fe070d83707f36912a2e0e83beb1244`.
Acceptance evidence and every downstream promotion gate are bound to both
values so evidence from the earlier recipe cannot authorize release.

`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.
sentinel, verifies that the resulting recipe has the accepted checksum, and
emits an evidence template bound to both exact source revisions and the corrected
DEV store recipe. 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)
Expand Down Expand Up @@ -68,20 +76,26 @@ competing valid tip at or beyond ShockWave checkpoint height `57,752`.
That evidence remains valid for the unchanged Core image, but it does not replace
live acceptance of the new application candidate.

## Required 0.1.11 live DEV acceptance

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.

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.
## Completed 0.1.11 live DEV acceptance

The corrected recipe was installed through the DEV store on `10.10.10.235` and
accepted at `2026-09-04T17:22:22Z`. The update preserved the existing node chain,
migration markers, pool configuration, payout address, and rollback policy. It
did not start another blockchain reindex. The non-submitting Stratum probe passed,
and telemetry, P2P-port, and NAT-PMP controls remained unchanged.

Core `310100` reported main-chain height and headers `58,444`, chainwork
`00000000000000000000000000000000000000000000fb2888bffb8c3c655c9c`, and best
block hash `00000000000000001e6ee54b268e62f3f1306a04cdb8f08a30c712e3e1cc3996`.
The official explorer matched that exact height and hash. Level-4 `verifychain`
passed, ten outbound Core 31 peers were observed, and there were no competing
valid tips.

The payout tests covered Core-accepted mainnet `1...`, `3...`, and `bc1...`
address families. Invalid and RPC-unavailable submissions did not mutate the
saved configuration, bounded pending validation passed, private UI behavior was
retained, and the misleading MAIN payout banner remained hidden. The corrected
initializer left `/data/pool/config` owned by uid/gid 1000; an atomic create and
replace probe from the uid-1000 application container passed. The targeted config
repair did not rewrite the current pool configuration, and the conditional
CKPool `/www` sharelog repair also passed.
Loading
Loading