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
8 changes: 4 additions & 4 deletions .github/workflows/validate-axebc2-core31-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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:
Expand All @@ -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"
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
20 changes: 10 additions & 10 deletions scripts/axebc2_release_state.py
Original file line number Diff line number Diff line change
@@ -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 {}
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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"
37 changes: 34 additions & 3 deletions scripts/validate-axebc2-core31-dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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",
Expand Down Expand Up @@ -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",
)
Expand Down
57 changes: 56 additions & 1 deletion tests/test_axebc2_core31_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
)
Expand Down Expand Up @@ -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()
Expand Down
Loading
Loading