From 322e8f2b33f03f131d9384edbb97ebaa169a9ca2 Mon Sep 17 00:00:00 2001 From: Torkel Rogstad Date: Fri, 24 Jul 2026 09:06:54 +0200 Subject: [PATCH] Remove grpcurl usage, use plain HTTP requests The enforcer provides a ConnectRPC API. ConnectRPC is compatible with gRPC, but is also compatible with simple HTTP requests. Also fixes some outdated/incorrect documentation/scripts. --- INSTALL.md | 7 +- OPERATOR_GUIDE.md | 45 ++++++----- PPS_THUNDER.md | 2 - VERIFY.md | 33 ++++---- dashboard/lib/actions.js | 53 +++++------- dashboard/lib/admin-router.js | 4 +- dashboard/lib/admin.js | 23 +++--- dashboard/lib/enforcer.js | 35 ++++++++ dashboard/server.js | 2 - deploy/docker/.env.example | 5 +- deploy/docker/Dockerfile.dashboard | 21 +---- deploy/docker/docker-compose.yml | 1 - deploy/systemd/simplepool-dashboard.service | 5 +- scripts/enforcer-rpc.sh | 59 ++++++++++++++ scripts/regtest/README.md | 19 +++-- scripts/regtest/activate-thunder.sh | 89 ++++++++++----------- scripts/regtest/setup.sh | 10 ++- scripts/regtest/start.sh | 4 +- scripts/regtest/thunder-init.sh | 9 +-- 19 files changed, 236 insertions(+), 190 deletions(-) create mode 100644 dashboard/lib/enforcer.js create mode 100755 scripts/enforcer-rpc.sh diff --git a/INSTALL.md b/INSTALL.md index f6dd52b..157587a 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -74,13 +74,11 @@ but the deposit flow doesn't work end-to-end anyway. - `libhiredis` headers (Redis client — required by the broadcast module, even if you don't enable Redis broadcast) - `node` v20+ (for the dashboard and payout worker) -- `grpcurl` (only if you want to drive the enforcer's gRPC by hand — - the operator guide relies on it) ### macOS (Homebrew) ```sh -brew install sqlite curl hiredis node grpcurl +brew install sqlite curl hiredis node xcode-select --install # if `cc` isn't available ``` @@ -93,9 +91,6 @@ sudo apt install -y build-essential libsqlite3-dev libcurl4-openssl-dev \ # Node from NodeSource so you get a modern version curl -fsSL https://deb.nodesource.com/setup_20.x | sudo bash - sudo apt install -y nodejs -# grpcurl (via GitHub release) if you plan to run the deposit runbook -GRPCURL_VER=1.9.3 -curl -fsSL https://github.com/fullstorydev/grpcurl/releases/download/v${GRPCURL_VER}/grpcurl_${GRPCURL_VER}_linux_x86_64.tar.gz | sudo tar -xz -C /usr/local/bin grpcurl ``` --- diff --git a/OPERATOR_GUIDE.md b/OPERATOR_GUIDE.md index 13aac94..b764131 100644 --- a/OPERATOR_GUIDE.md +++ b/OPERATOR_GUIDE.md @@ -74,7 +74,7 @@ password](#rotating-the-admin-password) below. │ │ └────────┬──────────┘ │ operator triggers deposit - │ (admin dashboard button OR grpcurl) + │ (admin dashboard button OR curl) ▼ ┌──────────────────────┐ │ BIP300 deposit tx │ @@ -134,21 +134,23 @@ POOL_ADDR=$(sudo -u forknet $TCLI get-new-address) echo "deposit target: $POOL_ADDR" # 2. Trigger the deposit via the enforcer's wallet. AMOUNT + FEE are yours to pick. -GRPCURL=/home/forknet/forknet-software/grpcurl +# The enforcer speaks ConnectRPC on the gRPC port, so unary calls are +# plain JSON POSTs. +ENFORCER=http://127.0.0.1:50051 AMOUNT_SATS=1000000 # 0.01 BTC = 1_000_000 sats FEE_SATS=1000 -$GRPCURL -plaintext \ +curl -sS -H 'content-type: application/json' \ -d "{\"sidechain_id\":9,\"address\":\"$POOL_ADDR\",\"value_sats\":$AMOUNT_SATS,\"fee_sats\":$FEE_SATS}" \ - 127.0.0.1:50051 cusf.mainchain.v1.WalletService/CreateDepositTransaction + $ENFORCER/cusf.mainchain.v1.WalletService/CreateDepositTransaction -# 3. Wait for a natural mainchain block from your miner (or on regtest, -# force one with GenerateBlocks). -$GRPCURL -plaintext -d '{"blocks":1}' \ - 127.0.0.1:50051 cusf.mainchain.v1.WalletService/GenerateBlocks +# 3. Wait for a natural mainchain block from your miner. (On regtest you +# can force one — GenerateBlocks is a streaming RPC, so use the helper: +# scripts/enforcer-rpc.sh --stream \ +# cusf.mainchain.v1.WalletService/GenerateBlocks '{"blocks":1}' ) # 4. Confirm the Ctip moved -$GRPCURL -plaintext -d '{"sidechain_number":9}' \ - 127.0.0.1:50051 cusf.mainchain.v1.ValidatorService/GetCtip +curl -sS -H 'content-type: application/json' -d '{"sidechain_number":9}' \ + $ENFORCER/cusf.mainchain.v1.ValidatorService/GetCtip # 5. Poke Thunder to include the new deposit in a sidechain block. # `mine` may time out on the client side but the block gets produced. @@ -308,8 +310,8 @@ running. Check with: ```sh # enforcer says the deposit was consensus-accepted? -grpcurl -plaintext -d '{"sidechain_number":9}' \ - 127.0.0.1:50051 cusf.mainchain.v1.ValidatorService/GetCtip +curl -sS -H 'content-type: application/json' -d '{"sidechain_number":9}' \ + http://127.0.0.1:50051/cusf.mainchain.v1.ValidatorService/GetCtip # Thunder wallet sees the balance? sudo -u forknet /home/forknet/forknet-software/thunder-rust/target/debug/thunder_app_cli balance ``` @@ -387,15 +389,15 @@ new shares landing while you deposit. Read `totals.owed` off ```sh ssh -i root@ -GRPCURL=/home/forknet/forknet-software/grpcurl +ENFORCER=http://127.0.0.1:50051 BCLI="/home/forknet/forknet-software/drivechain-forknet/build/bin/bitcoin-cli \ -datadir=/home/forknet/.drivechain-forknet \ -rpcuser= -rpcpassword= \ -rpcwallet=" # Fresh enforcer-owned receive address (one per deposit). -ENF=$($GRPCURL -plaintext 127.0.0.1:50051 \ - cusf.mainchain.v1.WalletService/CreateNewAddress \ +ENF=$(curl -sS -H 'content-type: application/json' -d '{}' \ + $ENFORCER/cusf.mainchain.v1.WalletService/CreateNewAddress \ | python3 -c "import json,sys; print(json.load(sys.stdin)['address'])") echo "enforcer receive: $ENF" @@ -418,24 +420,25 @@ POOL_ADDR=$(sudo -u forknet $TCLI get-new-address) echo "deposit target: $POOL_ADDR" # Capture Ctip before so we can verify it moves. -CTIP_BEFORE=$($GRPCURL -plaintext -d '{"sidechain_number":9}' \ - 127.0.0.1:50051 cusf.mainchain.v1.ValidatorService/GetCtip) +CTIP_BEFORE=$(curl -sS -H 'content-type: application/json' \ + -d '{"sidechain_number":9}' \ + $ENFORCER/cusf.mainchain.v1.ValidatorService/GetCtip) echo "$CTIP_BEFORE" # Deposit — value is in sats. E.g. 5 BTC = 500_000_000, 50 BTC = 5_000_000_000. DEPOSIT_SATS= -$GRPCURL -plaintext \ +curl -sS -H 'content-type: application/json' \ -d "{\"sidechain_id\":9,\"address\":\"$POOL_ADDR\",\ \"value_sats\":$DEPOSIT_SATS,\"fee_sats\":1000}" \ - 127.0.0.1:50051 cusf.mainchain.v1.WalletService/CreateDepositTransaction + $ENFORCER/cusf.mainchain.v1.WalletService/CreateDepositTransaction ``` Wait for a mainchain block, then confirm the Ctip grew by roughly `DEPOSIT_SATS`: ```sh -$GRPCURL -plaintext -d '{"sidechain_number":9}' \ - 127.0.0.1:50051 cusf.mainchain.v1.ValidatorService/GetCtip +curl -sS -H 'content-type: application/json' -d '{"sidechain_number":9}' \ + $ENFORCER/cusf.mainchain.v1.ValidatorService/GetCtip ``` **Step 3 — poke Thunder to include the deposit in a sidechain block.** diff --git a/PPS_THUNDER.md b/PPS_THUNDER.md index e93385c..9a6fe56 100644 --- a/PPS_THUNDER.md +++ b/PPS_THUNDER.md @@ -122,8 +122,6 @@ BTC address as the username. Same as before. ### PPS mode against the regtest BIP300 stack ```sh -brew install grpcurl # one-time, needed by activate-thunder.sh - scripts/regtest/setup.sh # downloads ~50 MB of prebuilt binaries scripts/regtest/start.sh # starts bitcoind + electrs + enforcer scripts/regtest/validate.sh # activates sidechain #9, bootstraps 150 blocks, diff --git a/VERIFY.md b/VERIFY.md index c604f94..20bc6bb 100644 --- a/VERIFY.md +++ b/VERIFY.md @@ -16,8 +16,7 @@ behaviour, so `git show ` is a quick way to inspect. On Linux/x86 some sections still work (unit tests, payout worker, audit, Redis broadcast) — only the regtest stack needs the aarch64 binaries. -- [ ] `brew install hiredis sqlite curl grpcurl` (hiredis for the C - build, grpcurl for sidechain activation) +- [ ] `brew install hiredis sqlite curl` (hiredis for the C build) - [ ] `node --version` ≥ 20 --- @@ -335,12 +334,12 @@ then observe the enforcer's TwoWayPeg event stream: ``` DEP='' -grpcurl -plaintext -d "{\"sidechain_id\":9,\"address\":\"$DEP\",\"value_sats\":100000000,\"fee_sats\":1000}" \ - 127.0.0.1:50051 cusf.mainchain.v1.WalletService/CreateDepositTransaction -grpcurl -plaintext -d '{"blocks":1}' \ - 127.0.0.1:50051 cusf.mainchain.v1.WalletService/GenerateBlocks -grpcurl -plaintext -d '{"sidechain_number":9}' \ - 127.0.0.1:50051 cusf.mainchain.v1.ValidatorService/GetCtip +scripts/enforcer-rpc.sh cusf.mainchain.v1.WalletService/CreateDepositTransaction \ + "{\"sidechain_id\":9,\"address\":\"$DEP\",\"value_sats\":100000000,\"fee_sats\":1000}" +scripts/enforcer-rpc.sh --stream cusf.mainchain.v1.WalletService/GenerateBlocks \ + '{"blocks":1}' +scripts/enforcer-rpc.sh cusf.mainchain.v1.ValidatorService/GetCtip \ + '{"sidechain_number":9}' ``` - [ ] Ctip returns non-empty with `value: "100000000"` (or the running @@ -425,8 +424,8 @@ scripts/regtest/inspect-coinbase.sh The critical empirical result. With section 11 finished: ``` -grpcurl -plaintext -d '{"sidechain_number":9}' 127.0.0.1:50051 \ - cusf.mainchain.v1.ValidatorService/GetCtip +scripts/enforcer-rpc.sh cusf.mainchain.v1.ValidatorService/GetCtip \ + '{"sidechain_number":9}' ``` - [ ] Returns `{}`. **The coinbase deposit did NOT credit the Ctip**, @@ -435,15 +434,15 @@ grpcurl -plaintext -d '{"sidechain_number":9}' 127.0.0.1:50051 \ Now issue a canonical deposit to confirm the rule difference: ``` -grpcurl -plaintext -d '{"sidechain_id":9, "address":"11111111111111111111", - "value_sats":100000000, "fee_sats":1000}' \ - 127.0.0.1:50051 cusf.mainchain.v1.WalletService/CreateDepositTransaction +scripts/enforcer-rpc.sh cusf.mainchain.v1.WalletService/CreateDepositTransaction \ + '{"sidechain_id":9, "address":"11111111111111111111", + "value_sats":100000000, "fee_sats":1000}' -grpcurl -plaintext -d '{"blocks":1}' 127.0.0.1:50051 \ - cusf.mainchain.v1.WalletService/GenerateBlocks +scripts/enforcer-rpc.sh --stream cusf.mainchain.v1.WalletService/GenerateBlocks \ + '{"blocks":1}' -grpcurl -plaintext -d '{"sidechain_number":9}' 127.0.0.1:50051 \ - cusf.mainchain.v1.ValidatorService/GetCtip +scripts/enforcer-rpc.sh cusf.mainchain.v1.ValidatorService/GetCtip \ + '{"sidechain_number":9}' ``` - [ ] Now returns `{"ctip": {"txid": {...}, "value": "100000000"}}`. diff --git a/dashboard/lib/actions.js b/dashboard/lib/actions.js index 52cb1cd..bee9ef4 100644 --- a/dashboard/lib/actions.js +++ b/dashboard/lib/actions.js @@ -7,13 +7,10 @@ * External systems these talk to: * - Thunder JSON-RPC — nudgeMine, removeFromMempool * - payout worker HTTP — triggerPayout - * - bip300301_enforcer — createDeposit (via grpcurl on the host) + * - bip300301_enforcer — createDeposit (ConnectRPC over HTTP) */ -import { execFile } from 'node:child_process'; -import { promisify } from 'node:util'; - -const execFileAsync = promisify(execFile); +import { enforcerRpc } from './enforcer.js'; /* ---------- Thunder RPC helper ---------- */ @@ -119,22 +116,20 @@ export async function triggerPayout({ payoutAdminUrl }) { /* ---------- Action #1: BTC → Thunder deposit (via bip300301_enforcer) ---------- */ -/* We shell out to `grpcurl` because the enforcer's gRPC on :50051 uses - * server reflection — no .proto files needed, one static Go binary handles - * it. The exact call matches ~/scripts/deposit_all_sidechains.sh on the - * pool host: +/* The enforcer speaks ConnectRPC on the same port as its gRPC (:50051), + * so this is a plain JSON POST: * - * grpcurl -d '{sidechain_id, address, value_sats, fee_sats}' -plaintext \ - * ENFORCER_ADDR cusf.mainchain.v1.WalletService/CreateDepositTransaction + * POST http://ENFORCER_ADDR/cusf.mainchain.v1.WalletService/CreateDepositTransaction + * { sidechain_id, address, value_sats, fee_sats } * * On success the enforcer returns a txid; we insert into the pool DB's * `deposits` table so the admin ledger shows the operator action. */ export async function createDeposit({ - grpcurlBin, enforcerGrpcAddr, sidechainId, address, valueSats, feeSats, + enforcerGrpcAddr, sidechainId, address, valueSats, feeSats, db, /* writable handle */ }) { - /* Validate inputs up front — grpcurl errors are less friendly. */ + /* Validate inputs up front — enforcer errors are less friendly. */ const sid = Number(sidechainId); if (!Number.isInteger(sid) || sid < 0 || sid > 255) { return { ok: false, msg: 'invalid sidechain_id (expected 0..255)' }; @@ -147,28 +142,20 @@ export async function createDeposit({ const fee = BigInt(feeSats || 0); if (fee < 0n) return { ok: false, msg: 'fee_sats must be >= 0' }; - const payload = JSON.stringify({ - sidechain_id: sid, - address, - value_sats: Number(val), - fee_sats: Number(fee), - }); - let stdout; + let j; try { - const res = await execFileAsync(grpcurlBin, [ - '-plaintext', - '-d', payload, - enforcerGrpcAddr, - 'cusf.mainchain.v1.WalletService/CreateDepositTransaction', - ], { timeout: 60_000, maxBuffer: 1024 * 1024 }); - stdout = res.stdout; + j = await enforcerRpc(enforcerGrpcAddr, + 'cusf.mainchain.v1.WalletService/CreateDepositTransaction', { + sidechain_id: sid, + address, + value_sats: Number(val), + fee_sats: Number(fee), + }, 60_000); } catch (e) { - const stderr = (e.stderr || '').toString().slice(0, 500); - const stdouterr = (e.stdout || '').toString().slice(0, 300); return { ok: false, msg: 'CreateDepositTransaction failed', - detail: stderr || stdouterr || e.message, + detail: (e.message || String(e)).slice(0, 500), }; } @@ -176,8 +163,6 @@ export async function createDeposit({ * depending on version). Parse whatever it gave us; if we can't find * a txid, still consider the call a success and echo the raw output * so the operator can inspect. */ - let j = {}; - try { j = JSON.parse(stdout); } catch { /* ignore */ } const txid = j.txid?.hex || j.txid?.value?.hex || (typeof j.txid === 'string' ? j.txid : null); @@ -199,7 +184,7 @@ export async function createDeposit({ return { ok: true, msg: `deposit tx created (DB log failed: ${e.message})`, - detail: txid ? `txid=${txid}` : stdout.slice(0, 300), + detail: txid ? `txid=${txid}` : JSON.stringify(j).slice(0, 300), }; } } @@ -207,6 +192,6 @@ export async function createDeposit({ ok: true, msg: `deposit tx submitted`, detail: txid ? `txid=${txid}, ${val} sats to ${address}` : - `see raw enforcer response: ${stdout.slice(0, 200)}`, + `see raw enforcer response: ${JSON.stringify(j).slice(0, 200)}`, }; } diff --git a/dashboard/lib/admin-router.js b/dashboard/lib/admin-router.js index 2e2e334..3c9b81d 100644 --- a/dashboard/lib/admin-router.js +++ b/dashboard/lib/admin-router.js @@ -18,7 +18,6 @@ export function createAdminRouter({ THUNDER_RPC_URL, PAYOUT_ADMIN_URL, ENFORCER_GRPC_ADDR, - GRPCURL_BIN, THUNDER_SIDECHAIN_ID, RESERVE_ADDRESS, PPS_SATS_PER_DIFF, @@ -31,7 +30,7 @@ export function createAdminRouter({ const [reserve, enforcer, totals, workers, inFlight, payouts, deposits, blocks] = await Promise.all([ admin.thunderBalance(THUNDER_RPC_URL), - admin.enforcerBalance(GRPCURL_BIN, ENFORCER_GRPC_ADDR), + admin.enforcerBalance(ENFORCER_GRPC_ADDR), Promise.resolve(admin.poolTotals(db)), Promise.resolve(admin.perWorkerBalances(db)), Promise.resolve(admin.inFlight(db)), @@ -158,7 +157,6 @@ export function createAdminRouter({ router.post('/action/deposit', parseAdminForm, requireCsrf, async (req, res) => { const r = await actions.createDeposit({ - grpcurlBin: GRPCURL_BIN, enforcerGrpcAddr: ENFORCER_GRPC_ADDR, sidechainId: req.body?.sidechain_id ?? THUNDER_SIDECHAIN_ID, address: (req.body?.address || '').trim(), diff --git a/dashboard/lib/admin.js b/dashboard/lib/admin.js index 5a323a9..0e72960 100644 --- a/dashboard/lib/admin.js +++ b/dashboard/lib/admin.js @@ -5,6 +5,8 @@ // http://127.0.0.1:6009) with a short timeout — we never block a // request on a slow / down Thunder node. +import { enforcerRpc } from './enforcer.js'; + function unwrap(handle) { if (typeof handle?.get === 'function') return handle.get(); return handle; @@ -254,21 +256,14 @@ export function recentBlocksFound(handle, limit = 15) { })); } -/* Probe the enforcer wallet's BTC balance via gRPC (WalletService.GetBalance). - * Shells out to grpcurl — same binary the deposit action uses. Short - * timeout so the admin page renders even when the enforcer is slow. +/* Probe the enforcer wallet's BTC balance (WalletService.GetBalance) via + * ConnectRPC — a plain JSON POST, same transport the deposit action uses. + * Short timeout so the admin page renders even when the enforcer is slow. * Returns { ok, confirmed_sats, pending_sats, has_synced, error? }. */ -export async function enforcerBalance(grpcurlBin, enforcerGrpcAddr, timeoutMs = 3000) { - const { execFile } = await import('node:child_process'); - const { promisify } = await import('node:util'); - const execFileAsync = promisify(execFile); +export async function enforcerBalance(enforcerGrpcAddr, timeoutMs = 3000) { try { - const { stdout } = await execFileAsync(grpcurlBin, [ - '-plaintext', '-d', '{}', - enforcerGrpcAddr, - 'cusf.mainchain.v1.WalletService/GetBalance', - ], { timeout: timeoutMs, maxBuffer: 256 * 1024 }); - const j = JSON.parse(stdout); + const j = await enforcerRpc(enforcerGrpcAddr, + 'cusf.mainchain.v1.WalletService/GetBalance', {}, timeoutMs); return { ok: true, confirmed_sats: Number(j.confirmedSats ?? 0), @@ -276,7 +271,7 @@ export async function enforcerBalance(grpcurlBin, enforcerGrpcAddr, timeoutMs = has_synced: Boolean(j.hasSynced), }; } catch (e) { - const msg = (e.stderr || '').toString().slice(0, 200) || e.message; + const msg = (e.message || String(e)).slice(0, 200); return { ok: false, error: msg, confirmed_sats: 0, pending_sats: 0, has_synced: false }; } } diff --git a/dashboard/lib/enforcer.js b/dashboard/lib/enforcer.js new file mode 100644 index 0000000..b9b09db --- /dev/null +++ b/dashboard/lib/enforcer.js @@ -0,0 +1,35 @@ +/* Minimal ConnectRPC client for the bip300301_enforcer. + * + * Only unary RPCs — the streaming ones (e.g. GenerateBlocks) are + * regtest-script territory; see scripts/enforcer-rpc.sh. + */ + +export async function enforcerRpc(enforcerAddr, rpcPath, body, timeoutMs = 30_000) { + const base = /^https?:\/\//.test(enforcerAddr) ? enforcerAddr : `http://${enforcerAddr}`; + const ctl = new AbortController(); + const t = setTimeout(() => ctl.abort(), timeoutMs); + try { + const r = await fetch(`${base}/${rpcPath}`, { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'connect-protocol-version': '1', + }, + body: JSON.stringify(body ?? {}), + signal: ctl.signal, + }); + const text = await r.text(); + let j; + try { j = JSON.parse(text); } + catch { + throw new Error(`enforcer ${rpcPath}: non-json response (http ${r.status}): ${text.slice(0, 200)}`); + } + if (!r.ok) { + const code = j.code || `http ${r.status}`; + throw new Error(`enforcer ${rpcPath}: ${code}${j.message ? `: ${j.message}` : ''}`); + } + return j; + } finally { + clearTimeout(t); + } +} diff --git a/dashboard/server.js b/dashboard/server.js index 478a7be..6d8c47c 100644 --- a/dashboard/server.js +++ b/dashboard/server.js @@ -61,7 +61,6 @@ const RESERVE_ADDRESS = process.env.POOL_THUNDER_RESERVE_ADDRESS || '(unset const THUNDER_RPC_URL = process.env.THUNDER_RPC_URL || 'http://127.0.0.1:6009'; const PAYOUT_ADMIN_URL = process.env.PAYOUT_ADMIN_URL || ''; const ENFORCER_GRPC_ADDR = process.env.ENFORCER_GRPC_ADDR || '127.0.0.1:50051'; -const GRPCURL_BIN = process.env.GRPCURL_BIN || 'grpcurl'; const THUNDER_SIDECHAIN_ID = parseInt(process.env.THUNDER_SIDECHAIN_ID || '9', 10); function requireAdminAuth(req, res, next) { @@ -149,7 +148,6 @@ app.use('/admin', THUNDER_RPC_URL, PAYOUT_ADMIN_URL, ENFORCER_GRPC_ADDR, - GRPCURL_BIN, THUNDER_SIDECHAIN_ID, RESERVE_ADDRESS, PPS_SATS_PER_DIFF, diff --git a/deploy/docker/.env.example b/deploy/docker/.env.example index d523920..4b63973 100644 --- a/deploy/docker/.env.example +++ b/deploy/docker/.env.example @@ -48,9 +48,6 @@ PAYOUT_ADMIN_URL=http://host.docker.internal:9080 PAYOUT_ADMIN_BIND=0.0.0.0 PAYOUT_ADMIN_PORT=9080 -# bip300301_enforcer gRPC address the deposit action targets. Uses -# grpcurl (baked into the dashboard image) with reflection, so no .proto -# files needed. +# bip300301_enforcer address the deposit action targets. ENFORCER_GRPC_ADDR=host.docker.internal:50051 -GRPCURL_BIN=/usr/local/bin/grpcurl THUNDER_SIDECHAIN_ID=9 diff --git a/deploy/docker/Dockerfile.dashboard b/deploy/docker/Dockerfile.dashboard index 5f5b518..f3ac658 100644 --- a/deploy/docker/Dockerfile.dashboard +++ b/deploy/docker/Dockerfile.dashboard @@ -16,26 +16,9 @@ RUN npm ci --omit=dev # ----------------------------------------------------------------------------- FROM node:20-bookworm-slim -# grpcurl is used by the "Deposit BTC → Thunder" admin action — one -# static Go binary, uses gRPC reflection so no .proto files needed. -# Fetched from GitHub releases (Debian's apt version lags). -ARG TARGETARCH -ARG GRPCURL_VERSION=1.9.1 RUN apt-get update && apt-get install -y --no-install-recommends \ - tini ca-certificates curl \ - && rm -rf /var/lib/apt/lists/* \ - && case "$TARGETARCH" in \ - amd64) GRPCURL_ARCH=x86_64 ;; \ - arm64) GRPCURL_ARCH=arm64 ;; \ - *) echo "unsupported arch: $TARGETARCH"; exit 1 ;; \ - esac \ - && curl -fsSL -o /tmp/grpcurl.tgz \ - "https://github.com/fullstorydev/grpcurl/releases/download/v${GRPCURL_VERSION}/grpcurl_${GRPCURL_VERSION}_linux_${GRPCURL_ARCH}.tar.gz" \ - && tar -xzf /tmp/grpcurl.tgz --no-same-owner -C /usr/local/bin grpcurl \ - && chmod +x /usr/local/bin/grpcurl \ - && rm /tmp/grpcurl.tgz \ - # Smoke-test the binary - && grpcurl -version + tini ca-certificates \ + && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY dashboard/ ./ diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml index e78e22e..cd474c9 100644 --- a/deploy/docker/docker-compose.yml +++ b/deploy/docker/docker-compose.yml @@ -60,7 +60,6 @@ services: # --- Write-action wiring --- PAYOUT_ADMIN_URL: ${PAYOUT_ADMIN_URL:-http://host.docker.internal:9080} ENFORCER_GRPC_ADDR: ${ENFORCER_GRPC_ADDR:-host.docker.internal:50051} - GRPCURL_BIN: ${GRPCURL_BIN:-/usr/local/bin/grpcurl} THUNDER_SIDECHAIN_ID: ${THUNDER_SIDECHAIN_ID:-9} volumes: # Dashboard now writes to the deposits table when the operator diff --git a/deploy/systemd/simplepool-dashboard.service b/deploy/systemd/simplepool-dashboard.service index beb2fdb..d1456a7 100644 --- a/deploy/systemd/simplepool-dashboard.service +++ b/deploy/systemd/simplepool-dashboard.service @@ -23,10 +23,9 @@ Environment=THUNDER_RPC_URL=http://127.0.0.1:6009 # --- Write-action wiring (POST buttons on /admin) --- # Payout worker admin HTTP endpoint (see simplepool-payout.service). Environment=PAYOUT_ADMIN_URL=http://127.0.0.1:9080 -# bip300301_enforcer gRPC address — the deposit action shells out to -# grpcurl (must be on PATH) to hit CreateDepositTransaction. +# bip300301_enforcer address — the deposit action POSTs JSON to its +# ConnectRPC endpoint for CreateDepositTransaction. Environment=ENFORCER_GRPC_ADDR=127.0.0.1:50051 -Environment=GRPCURL_BIN=/usr/local/bin/grpcurl Environment=THUNDER_SIDECHAIN_ID=9 # Environment=POOL_THUNDER_RESERVE_ADDRESS= # Environment=ADMIN_USER=admin diff --git a/scripts/enforcer-rpc.sh b/scripts/enforcer-rpc.sh new file mode 100755 index 0000000..d6c3d57 --- /dev/null +++ b/scripts/enforcer-rpc.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Call the bip300301_enforcer over ConnectRPC — replaces grpcurl. +# +# The enforcer serves the Connect protocol on the same port as its gRPC +# (default 127.0.0.1:50051), so a unary RPC is a plain JSON POST: +# +# scripts/enforcer-rpc.sh cusf.mainchain.v1.ValidatorService/GetSidechains +# scripts/enforcer-rpc.sh cusf.mainchain.v1.WalletService/CreateDepositTransaction \ +# '{"sidechain_id":9, "address":"...", "value_sats":100000000, "fee_sats":1000}' +# +# Server-streaming RPCs (e.g. GenerateBlocks) can't take a bare JSON +# POST — Connect wraps the request in a 5-byte envelope (flag byte + +# big-endian payload length) under Content-Type application/connect+json. +# --stream sends that envelope, then simply dumps the raw response +# frames until the server closes the stream (for GenerateBlocks that's +# when mining is done). No response parsing — verify the outcome with a +# follow-up unary call: +# +# scripts/enforcer-rpc.sh --stream cusf.mainchain.v1.WalletService/GenerateBlocks \ +# '{"blocks":1}' +# +# Env: +# ENFORCER_URL base URL (default http://127.0.0.1:50051) +# RPC_TIMEOUT max seconds for the whole call (default 60) +# +# Requires curl only. + +set -euo pipefail + +ENFORCER_URL="${ENFORCER_URL:-http://127.0.0.1:50051}" +RPC_TIMEOUT="${RPC_TIMEOUT:-60}" + +STREAM=0 +if [[ "${1:-}" == "--stream" ]]; then + STREAM=1 + shift +fi +RPC="${1:?usage: enforcer-rpc.sh [--stream] [json-request]}" +BODY="${2:-}" +[[ -n "$BODY" ]] || BODY='{}' + +if [[ "$STREAM" == 0 ]]; then + exec curl -sS --fail-with-body -m "$RPC_TIMEOUT" \ + -H 'Content-Type: application/json' \ + -H 'Connect-Protocol-Version: 1' \ + -d "$BODY" \ + "$ENFORCER_URL/$RPC" +fi + +# 5-byte Connect envelope: 0x00 flag + big-endian uint32 payload length. +LEN=$(printf '%s' "$BODY" | wc -c) +HDR="$(printf '\\x00\\x%02x\\x%02x\\x%02x\\x%02x' \ + $(( LEN >> 24 & 255 )) $(( LEN >> 16 & 255 )) $(( LEN >> 8 & 255 )) $(( LEN & 255 )))" +{ printf "$HDR"; printf '%s' "$BODY"; } | curl -sS -m "$RPC_TIMEOUT" \ + -H 'Content-Type: application/connect+json' \ + -H 'Connect-Protocol-Version: 1' \ + --data-binary @- \ + "$ENFORCER_URL/$RPC" +echo # raw frames aren't newline-terminated diff --git a/scripts/regtest/README.md b/scripts/regtest/README.md index 6b905a8..72382da 100644 --- a/scripts/regtest/README.md +++ b/scripts/regtest/README.md @@ -12,7 +12,7 @@ LayerTwo-Labs enforcer. │ │ ┌────────┤ ├────────┐ │ ZMQ │ │ gRPC │ - │ 29000 └──────────────┘ 50051 │ + │ 29010 └──────────────┘ 50051 │ ▼ │ ▼ ┌──────────┐ │ (events, │ bitcoind │◀────────┴───────┐ sidechain CRUD) @@ -52,7 +52,6 @@ scripts/regtest/stop.sh `activate-thunder.sh` and `thunder-init.sh` are both idempotent — re-running once the state is set up is a no-op. -Requires `grpcurl` (`brew install grpcurl`). Everything lives under `.regtest/` (gitignored): binaries in `.regtest/bin/`, chain state in `.regtest/data/`, logs in @@ -146,19 +145,19 @@ with a canonical deposit: ``` # Ctip BEFORE a canonical deposit (after the coinbase deposit attempt): -grpcurl -plaintext -d '{"sidechain_number":9}' 127.0.0.1:50051 \ - cusf.mainchain.v1.ValidatorService/GetCtip +scripts/enforcer-rpc.sh cusf.mainchain.v1.ValidatorService/GetCtip \ + '{"sidechain_number":9}' # → {} (no Ctip — coinbase deposit was ignored) # Issue a canonical deposit: -grpcurl -plaintext -d '{"sidechain_id":9, "address":"11111111111111111111", "value_sats":100000000, "fee_sats":1000}' \ - 127.0.0.1:50051 cusf.mainchain.v1.WalletService/CreateDepositTransaction -grpcurl -plaintext -d '{"blocks":1}' 127.0.0.1:50051 \ - cusf.mainchain.v1.WalletService/GenerateBlocks +scripts/enforcer-rpc.sh cusf.mainchain.v1.WalletService/CreateDepositTransaction \ + '{"sidechain_id":9, "address":"11111111111111111111", "value_sats":100000000, "fee_sats":1000}' +scripts/enforcer-rpc.sh --stream cusf.mainchain.v1.WalletService/GenerateBlocks \ + '{"blocks":1}' # Ctip AFTER: -grpcurl -plaintext -d '{"sidechain_number":9}' 127.0.0.1:50051 \ - cusf.mainchain.v1.ValidatorService/GetCtip +scripts/enforcer-rpc.sh cusf.mainchain.v1.ValidatorService/GetCtip \ + '{"sidechain_number":9}' # → {"ctip": {"txid": {...}, "value": "100000000"}} ``` diff --git a/scripts/regtest/activate-thunder.sh b/scripts/regtest/activate-thunder.sh index 97217df..d9abefe 100755 --- a/scripts/regtest/activate-thunder.sh +++ b/scripts/regtest/activate-thunder.sh @@ -1,32 +1,36 @@ #!/usr/bin/env bash # Activate sidechain #9 (Thunder) on the regtest BIP300 enforcer. # -# Flow (driven via the enforcer's gRPC at 127.0.0.1:50051): -# 1. CreateSidechainProposal — writes an M1 message into the enforcer's -# wallet DB. The next mined coinbase will carry it. +# Flow (driven via the enforcer's ConnectRPC HTTP API at 127.0.0.1:50051): +# 1. SubmitSidechainProposal — writes an M1 message into the +# enforcer's DB. The next mined coinbase +# will carry it. # 2. GenerateBlocks N --ack_all_proposals — mine + ack until the # proposal accumulates enough votes to activate (regtest threshold # is small; we mine 60 to be safe). # 3. GetSidechains — confirm sidechain 9 is now in the active list. # -# Requires grpcurl. Install: brew install grpcurl -# # Idempotent: if sidechain 9 is already active, exits 0 immediately. set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -GRPC="127.0.0.1:50051" +RPC="$ROOT/scripts/enforcer-rpc.sh" +export ENFORCER_URL="${ENFORCER_URL:-http://127.0.0.1:50051}" SIDECHAIN_ID=9 -if ! command -v grpcurl >/dev/null 2>&1; then - echo "grpcurl not installed; run: brew install grpcurl" >&2 - exit 1 -fi +for dep in curl jq; do + if ! command -v "$dep" >/dev/null 2>&1; then + echo "$dep not installed" >&2 + exit 1 + fi +done active() { - grpcurl -plaintext "$GRPC" \ - cusf.mainchain.v1.ValidatorService/GetSidechains 2>/dev/null \ - | grep -c "\"sidechainNumber\": $SIDECHAIN_ID" || true + "$RPC" cusf.mainchain.v1.ValidatorService/GetSidechains 2>/dev/null \ + | jq --argjson id "$SIDECHAIN_ID" \ + '[.sidechains[]? | select(.sidechainNumber == $id)] | length' \ + 2>/dev/null \ + || echo 0 } if [[ "$(active)" -gt 0 ]]; then @@ -35,12 +39,8 @@ if [[ "$(active)" -gt 0 ]]; then fi echo "==> proposing sidechain $SIDECHAIN_ID (Thunder)" -# CreateSidechainProposal is a streaming RPC. Run it backgrounded so it -# stays alive long enough to observe the confirmation events while -# GenerateBlocks runs in the foreground. -cat > /tmp/regtest-sc-proposal.json < /tmp/regtest-sc-proposal.json < /tmp/regtest-sc-proposal.out 2>&1 || true ) & -PROPOSE_PID=$! -sleep 1.5 +}' +# Unary; {} on success. Re-running while a previous attempt's proposal +# is still pending gets already_exists back — fine, that pending +# proposal is exactly what the mining below acks. +if OUT="$("$RPC" cusf.mainchain.v1.BlockProducerService/SubmitSidechainProposal "$PROPOSAL" 2>&1)"; then + echo " proposal submitted" +elif [[ "$OUT" == *already_exists* ]]; then + echo " proposal already pending" +else + echo "$OUT" >&2 + exit 1 +fi echo "==> mining 60 blocks acking the proposal" -grpcurl -plaintext -d '{"blocks":60, "ack_all_proposals":true}' \ - -max-time 60 "$GRPC" \ - cusf.mainchain.v1.WalletService/GenerateBlocks > /tmp/regtest-mine.out 2>&1 +RPC_TIMEOUT=60 "$RPC" --stream \ + cusf.mainchain.v1.WalletService/GenerateBlocks \ + '{"blocks":60, "ack_all_proposals":true}' > /tmp/regtest-mine.out 2>&1 -# Streaming RPC; let it wind down then collect the last event. +# Give the enforcer a beat to finish processing the last block. sleep 1 -kill "$PROPOSE_PID" 2>/dev/null || true -wait "$PROPOSE_PID" 2>/dev/null || true if [[ "$(active)" -gt 0 ]]; then echo "==> sidechain $SIDECHAIN_ID is now ACTIVE" - grpcurl -plaintext "$GRPC" cusf.mainchain.v1.ValidatorService/GetSidechains \ - | python3 -c " -import json, sys -data = json.loads(sys.stdin.read()) -for s in data.get('sidechains', []): - if s.get('sidechainNumber') == $SIDECHAIN_ID: - print(f' sidechainNumber={s[\"sidechainNumber\"]}') - print(f' proposalHeight={s.get(\"proposalHeight\")}') - print(f' activationHeight={s.get(\"activationHeight\")}') - print(f' voteCount={s.get(\"voteCount\")}') -" + "$RPC" cusf.mainchain.v1.ValidatorService/GetSidechains \ + | jq -r --argjson id "$SIDECHAIN_ID" ' + .sidechains[]? | select(.sidechainNumber == $id) | + " sidechainNumber=\(.sidechainNumber)", + " proposalHeight=\(.proposalHeight)", + " activationHeight=\(.activationHeight)", + " voteCount=\(.voteCount)"' exit 0 fi -echo "!!! sidechain $SIDECHAIN_ID did NOT activate. proposal stream:" >&2 -cat /tmp/regtest-sc-proposal.out >&2 +echo "!!! sidechain $SIDECHAIN_ID did NOT activate. mine output:" >&2 +cat /tmp/regtest-mine.out >&2 exit 2 diff --git a/scripts/regtest/setup.sh b/scripts/regtest/setup.sh index 226bb7b..1e5b9e1 100755 --- a/scripts/regtest/setup.sh +++ b/scripts/regtest/setup.sh @@ -114,12 +114,18 @@ listen=1 txindex=1 rest=1 fallbackfee=0.0001 +# ZMQ options are global-only: inside the [regtest] section bitcoind +# parses but silently ignores them (getzmqnotifications returns []), +# and the enforcer's validator then never advances past startup tip. +# Port 29010, not the conventional 29000 — a ZMQ bind conflict with +# another local node is silent, and the enforcer will happily subscribe +# to whichever process won the port. +zmqpubrawblock=tcp://127.0.0.1:29010 +zmqpubsequence=tcp://127.0.0.1:29010 [regtest] rpcuser=user rpcpassword=password rpcport=18443 -zmqpubrawblock=tcp://127.0.0.1:29000 -zmqpubsequence=tcp://127.0.0.1:29000 EOF cat > "$DATA/electrs/config.toml" <