From d1a8d4327203131881ba3c378b8336c331f3f356 Mon Sep 17 00:00:00 2001 From: Timothy Egwuda <33420739+Timrossid@users.noreply.github.com> Date: Tue, 28 Jul 2026 00:48:42 +0000 Subject: [PATCH 1/4] feat(ct-46): add deterministic wasm hash generation for deployments - Create scripts/generate-wasm-hash.sh for reproducible wasm artifact tracking - Generates SHA-256 hashes for all contract wasm artifacts - Stores hashes in scripts/wasm-hashes.json with build metadata - Supports --build, --verify, and --update-readme modes - Tracks git commit, rustc version, toolchain, and manifest hash - Verification mode detects unexpected artifact changes Closes #125 --- scripts/generate-wasm-hash.sh | 210 ++++++++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 scripts/generate-wasm-hash.sh diff --git a/scripts/generate-wasm-hash.sh b/scripts/generate-wasm-hash.sh new file mode 100644 index 0000000..0028e55 --- /dev/null +++ b/scripts/generate-wasm-hash.sh @@ -0,0 +1,210 @@ +#!/usr/bin/env bash +# ──────────────────────────────────────────────────────────────── +# scripts/generate-wasm-hash.sh — Deterministic Wasm Hash Generator +# +# Generates and verifies SHA-256 hashes for Soroban contract wasm +# artifacts, making deployments verifiable and reproducible. +# +# Deterministic builds require: +# 1. Same source code (git commit) +# 2. Same Rust toolchain version (rust-toolchain.toml or CI image) +# 3. Same dependency versions (Cargo.lock committed) +# 4. Same build profile (release) with LTO +# +# Usage: +# ./scripts/generate-wasm-hash.sh # hash existing wasm artifacts +# ./scripts/generate-wasm-hash.sh --build # build & hash +# ./scripts/generate-wasm-hash.sh --verify # verify stored hashes +# ./scripts/generate-wasm-hash.sh --update-readme # update README with hashes +# ──────────────────────────────────────────────────────────────── + +set -euo pipefail + +WORKSPACE_DIR="$(cd "$(dirname "$0")/.." && pwd)" +CONTRACTS_DIR="${WORKSPACE_DIR}/contracts" +HASH_FILE="${WORKSPACE_DIR}/scripts/wasm-hashes.json" +BUILD="false" +VERIFY="false" +UPDATE_README="false" + +# ── Parse arguments ──────────────────────────────────────────── +while [[ $# -gt 0 ]]; do + case "$1" in + --build) BUILD="true"; shift ;; + --verify) VERIFY="true"; shift ;; + --update-readme) UPDATE_README="true"; shift ;; + *) echo "Unknown option: $1"; exit 1 ;; + esac +done + +# ── Ensure we're in the right directory ─────────────────────── +cd "${WORKSPACE_DIR}" + +echo "━━━ Oraculum — Deterministic Wasm Hash Generator ━━━━━━━━" +echo " Workspace: ${WORKSPACE_DIR}" +echo " Hash file: ${HASH_FILE}" +echo "" + +# ── Build if requested ──────────────────────────────────────── +if [[ "${BUILD}" == "true" ]]; then + echo "→ Building contracts (release profile)…" + cd "${CONTRACTS_DIR}" + cargo build --workspace --release --target wasm32-unknown-unknown + cd "${WORKSPACE_DIR}" + echo " ✓ Build complete." + echo "" +fi + +# ── Find wasm artifacts ─────────────────────────────────────── +WASM_ARTIFACTS=() +while IFS= read -r -d '' wasm; do + WASM_ARTIFACTS+=("$wasm") +done < <(find "${CONTRACTS_DIR}/target/wasm32-unknown-unknown/release" -maxdepth 1 -name '*.wasm' -print0 2>/dev/null || true) + +if [ ${#WASM_ARTIFACTS[@]} -eq 0 ]; then + echo "⚠ No wasm artifacts found." + echo " Build first with: ./scripts/generate-wasm-hash.sh --build" + exit 1 +fi + +echo "→ Found ${#WASM_ARTIFACTS[@]} wasm artifact(s):" +for wasm in "${WASM_ARTIFACTS[@]}"; do + echo " 📦 $(basename "$wasm")" +done +echo "" + +# ── Generate manifest hash (for reproducibility tracking) ───── +# The manifest hash covers Cargo.toml and Cargo.lock so that +# any dependency change is detected. +compute_manifest_hash() { + ( + cd "${CONTRACTS_DIR}" + cat Cargo.toml Cargo.lock | sha256sum | cut -d' ' -f1 + ) +} + +# ── Generate hashes ─────────────────────────────────────────── +GIT_COMMIT=$(git rev-parse HEAD 2>/dev/null || echo "unknown") +GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown") +BUILD_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") +MANIFEST_HASH=$(compute_manifest_hash) + +# Collect Rust toolchain info +RUSTC_VERSION=$(rustc --version 2>/dev/null || echo "unknown") +RUSTUP_TOOLCHAIN=$(rustup show active-toolchain 2>/dev/null | awk '{print $1}' || echo "unknown") + +echo "→ Generating hashes…" + +HASHES_JSON="{ + \"generated_at\": \"${BUILD_TIMESTAMP}\", + \"git_commit\": \"${GIT_COMMIT}\", + \"git_branch\": \"${GIT_BRANCH}\", + \"rustc_version\": \"${RUSTC_VERSION}\", + \"toolchain\": \"${RUSTUP_TOOLCHAIN}\", + \"manifest_hash\": \"${MANIFEST_HASH}\", + \"contracts\": {" + +FIRST=true +for wasm in "${WASM_ARTIFACTS[@]}"; do + NAME=$(basename "$wasm" .wasm) + SHA=$(sha256sum "$wasm" | cut -d' ' -f1) + SIZE=$(stat --printf="%s" "$wasm" 2>/dev/null || stat -f%z "$wasm" 2>/dev/null || echo "0") + + if [ "$FIRST" = true ]; then + FIRST=false + else + HASHES_JSON+="," + fi + + HASHES_JSON+=" + \"${NAME}\": { + \"wasm_file\": \"${NAME}.wasm\", + \"sha256\": \"${SHA}\", + \"size_bytes\": ${SIZE} + }" +done + +HASHES_JSON+=" + } +} +" + +echo "${HASHES_JSON}" > "${HASH_FILE}" +echo "✓ Hashes written to ${HASH_FILE}" +echo "" + +# ── Display summary ─────────────────────────────────────────── +echo "─── Hash Summary ─────────────────────────────────────" +echo "${HASHES_JSON}" | python3 -m json.tool 2>/dev/null || echo "${HASHES_JSON}" +echo "" + +# ── Verification mode ──────────────────────────────────────── +if [[ "${VERIFY}" == "true" ]]; then + echo "─── Verifying Stored Hashes ─────────────────────────" + if [ ! -f "${HASH_FILE}" ]; then + echo "❌ No hash file found at ${HASH_FILE}" + exit 1 + fi + + VERIFY_FAIL=0 + VERIFY_PASS=0 + + for wasm in "${WASM_ARTIFACTS[@]}"; do + NAME=$(basename "$wasm" .wasm) + CURRENT_SHA=$(sha256sum "$wasm" | cut -d' ' -f1) + STORED_SHA=$(python3 -c "import json; d=json.load(open('${HASH_FILE}')); print(d['contracts']['${NAME}']['sha256'])" 2>/dev/null || echo "") + + if [ -z "${STORED_SHA}" ]; then + echo " ⚠ ${NAME}: no stored hash to compare against" + continue + fi + + if [ "${CURRENT_SHA}" == "${STORED_SHA}" ]; then + echo " ✅ ${NAME}: hash MATCHES (${CURRENT_SHA})" + VERIFY_PASS=$((VERIFY_PASS + 1)) + else + echo " ❌ ${NAME}: hash MISMATCH" + echo " Stored: ${STORED_SHA}" + echo " Current: ${CURRENT_SHA}" + VERIFY_FAIL=$((VERIFY_FAIL + 1)) + fi + done + + echo "" + if [ "${VERIFY_FAIL}" -eq 0 ]; then + echo "✅ All hashes verified successfully." + else + echo "❌ ${VERIFY_FAIL} hash(es) mismatched. Rebuild required." + exit 1 + fi +fi + +# ── Update README with hashes ──────────────────────────────── +if [[ "${UPDATE_README}" == "true" ]]; then + echo "─── Updating README ─────────────────────────────────" + README="${CONTRACTS_DIR}/README.md" + + if [ -f "${README}" ]; then + # Generate a markdown table of hashes + TABLE="\n## Wasm Artifact Hashes\n\n" + TABLE+="| Contract | SHA-256 | Size |\n" + TABLE+="|----------|---------|------|\n" + + for wasm in "${WASM_ARTIFACTS[@]}"; do + NAME=$(basename "$wasm" .wasm) + SHA=$(sha256sum "$wasm" | cut -d' ' -f1) + SIZE=$(stat --printf="%s" "$wasm" 2>/dev/null || stat -f%z "$wasm" 2>/dev/null || echo "0") + SIZE_HR=$(numfmt --to=iec 2>/dev/null && echo "${SIZE}" || echo "${SIZE} bytes") + TABLE+="| \`${NAME}\` | \`${SHA}\` | ${SIZE_HR} |\n" + done + + TABLE+="\n_Built from commit \`${GIT_COMMIT}\` on \`${GIT_BRANCH}\` using ${RUSTC_VERSION}_\n" + + echo -e "${TABLE}" + echo "" + echo "→ To add this table to your README, run with --update-readme" + echo " or copy the table above manually." + else + echo " ⚠ README not found at ${README}" + fi +fi From 1e41278919658958f4957c52eccdcae5b9cce154 Mon Sep 17 00:00:00 2001 From: Timothy Egwuda <33420739+Timrossid@users.noreply.github.com> Date: Tue, 28 Jul 2026 00:52:51 +0000 Subject: [PATCH 2/4] fix: add python3 fallback for generate-wasm-hash.sh --- scripts/generate-wasm-hash.sh | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) mode change 100644 => 100755 scripts/generate-wasm-hash.sh diff --git a/scripts/generate-wasm-hash.sh b/scripts/generate-wasm-hash.sh old mode 100644 new mode 100755 index 0028e55..488225f --- a/scripts/generate-wasm-hash.sh +++ b/scripts/generate-wasm-hash.sh @@ -38,11 +38,25 @@ while [[ $# -gt 0 ]]; do done # ── Ensure we're in the right directory ─────────────────────── +# ── Prerequisites check ──────────────────────────────────────── +# Python is needed for JSON display/parsing (optional — falls back to cat) +HAS_PYTHON=false +if command -v python3 &>/dev/null; then + HAS_PYTHON=true +elif command -v python &>/dev/null; then + # Alias python -> python3 if available + python3() { python "$@"; } + HAS_PYTHON=true +fi + cd "${WORKSPACE_DIR}" echo "━━━ Oraculum — Deterministic Wasm Hash Generator ━━━━━━━━" echo " Workspace: ${WORKSPACE_DIR}" echo " Hash file: ${HASH_FILE}" +if [ "$HAS_PYTHON" = false ]; then + echo " ⚠ Python3 not found — JSON display will use cat instead" +fi echo "" # ── Build if requested ──────────────────────────────────────── @@ -135,7 +149,11 @@ echo "" # ── Display summary ─────────────────────────────────────────── echo "─── Hash Summary ─────────────────────────────────────" -echo "${HASHES_JSON}" | python3 -m json.tool 2>/dev/null || echo "${HASHES_JSON}" +if [ "$HAS_PYTHON" = true ]; then + echo "${HASHES_JSON}" | python3 -m json.tool 2>/dev/null || cat "${HASH_FILE}" +else + cat "${HASH_FILE}" +fi echo "" # ── Verification mode ──────────────────────────────────────── @@ -152,7 +170,13 @@ if [[ "${VERIFY}" == "true" ]]; then for wasm in "${WASM_ARTIFACTS[@]}"; do NAME=$(basename "$wasm" .wasm) CURRENT_SHA=$(sha256sum "$wasm" | cut -d' ' -f1) - STORED_SHA=$(python3 -c "import json; d=json.load(open('${HASH_FILE}')); print(d['contracts']['${NAME}']['sha256'])" 2>/dev/null || echo "") + + if [ "$HAS_PYTHON" = true ]; then + STORED_SHA=$(python3 -c "import json; d=json.load(open('${HASH_FILE}')); print(d['contracts']['${NAME}']['sha256'])" 2>/dev/null || echo "") + else + # Fallback: grep the raw JSON file + STORED_SHA=$(grep -A2 "\"${NAME}\"" "${HASH_FILE}" 2>/dev/null | grep "sha256" | sed 's/.*"sha256": "\(.*\)".*/\1/' || echo "") + fi if [ -z "${STORED_SHA}" ]; then echo " ⚠ ${NAME}: no stored hash to compare against" From 26c3abb7010483fcbe76b8aea851fb803cc44415 Mon Sep 17 00:00:00 2001 From: Timothy Egwuda <33420739+Timrossid@users.noreply.github.com> Date: Tue, 28 Jul 2026 01:00:40 +0000 Subject: [PATCH 3/4] fix: fix python3() scope, rename --update-readme to --print-readme-table, use _run_python wrapper --- scripts/generate-wasm-hash.sh | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/scripts/generate-wasm-hash.sh b/scripts/generate-wasm-hash.sh index 488225f..5fb430f 100755 --- a/scripts/generate-wasm-hash.sh +++ b/scripts/generate-wasm-hash.sh @@ -15,7 +15,7 @@ # ./scripts/generate-wasm-hash.sh # hash existing wasm artifacts # ./scripts/generate-wasm-hash.sh --build # build & hash # ./scripts/generate-wasm-hash.sh --verify # verify stored hashes -# ./scripts/generate-wasm-hash.sh --update-readme # update README with hashes +# ./scripts/generate-wasm-hash.sh --print-readme-table # print markdown table of hashes for README # ──────────────────────────────────────────────────────────────── set -euo pipefail @@ -32,7 +32,7 @@ while [[ $# -gt 0 ]]; do case "$1" in --build) BUILD="true"; shift ;; --verify) VERIFY="true"; shift ;; - --update-readme) UPDATE_README="true"; shift ;; + --print-readme-table) UPDATE_README="true"; shift ;; *) echo "Unknown option: $1"; exit 1 ;; esac done @@ -41,11 +41,16 @@ done # ── Prerequisites check ──────────────────────────────────────── # Python is needed for JSON display/parsing (optional — falls back to cat) HAS_PYTHON=false -if command -v python3 &>/dev/null; then - HAS_PYTHON=true -elif command -v python &>/dev/null; then - # Alias python -> python3 if available - python3() { python "$@"; } +_run_python() { + if command -v python3 &>/dev/null; then + python3 "$@" + elif command -v python &>/dev/null; then + python "$@" + else + return 1 + fi +} +if command -v python3 &>/dev/null || command -v python &>/dev/null; then HAS_PYTHON=true fi @@ -150,7 +155,7 @@ echo "" # ── Display summary ─────────────────────────────────────────── echo "─── Hash Summary ─────────────────────────────────────" if [ "$HAS_PYTHON" = true ]; then - echo "${HASHES_JSON}" | python3 -m json.tool 2>/dev/null || cat "${HASH_FILE}" + _run_python -m json.tool 2>/dev/null < "${HASH_FILE}" || cat "${HASH_FILE}" else cat "${HASH_FILE}" fi @@ -172,7 +177,7 @@ if [[ "${VERIFY}" == "true" ]]; then CURRENT_SHA=$(sha256sum "$wasm" | cut -d' ' -f1) if [ "$HAS_PYTHON" = true ]; then - STORED_SHA=$(python3 -c "import json; d=json.load(open('${HASH_FILE}')); print(d['contracts']['${NAME}']['sha256'])" 2>/dev/null || echo "") + STORED_SHA=$(_run_python -c "import json; d=json.load(open('${HASH_FILE}')); print(d['contracts']['${NAME}']['sha256'])" 2>/dev/null || echo "") else # Fallback: grep the raw JSON file STORED_SHA=$(grep -A2 "\"${NAME}\"" "${HASH_FILE}" 2>/dev/null | grep "sha256" | sed 's/.*"sha256": "\(.*\)".*/\1/' || echo "") @@ -226,8 +231,7 @@ if [[ "${UPDATE_README}" == "true" ]]; then echo -e "${TABLE}" echo "" - echo "→ To add this table to your README, run with --update-readme" - echo " or copy the table above manually." + echo "→ Copy the table above into your README." else echo " ⚠ README not found at ${README}" fi From c6c8717f53c170ef88a40f586b4b0de5a0601ab0 Mon Sep 17 00:00:00 2001 From: Timothy Egwuda <33420739+Timrossid@users.noreply.github.com> Date: Tue, 28 Jul 2026 01:02:53 +0000 Subject: [PATCH 4/4] chore: add .gitignore entries for generated artifacts --- .gitignore | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3e9b607..e9e6edf 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,9 @@ Thumbs.db .vscode/ .idea/ *.swp -*.swo \ No newline at end of file +*.swo + +# Generated script artifacts +scripts/wasm-hashes.json +scripts/sdk-compat-*.md +target/coverage/ \ No newline at end of file