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
7 changes: 1 addition & 6 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand All @@ -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
```

---
Expand Down
45 changes: 24 additions & 21 deletions OPERATOR_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 │
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
```
Expand Down Expand Up @@ -387,15 +389,15 @@ new shares landing while you deposit. Read `totals.owed` off

```sh
ssh -i <ssh-key> root@<pool-host>
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=<rpcuser> -rpcpassword=<rpcpassword> \
-rpcwallet=<wallet-name>"

# 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"

Expand All @@ -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=<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.**
Expand Down
2 changes: 0 additions & 2 deletions PPS_THUNDER.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
33 changes: 16 additions & 17 deletions VERIFY.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ behaviour, so `git show <hash>` 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

---
Expand Down Expand Up @@ -335,12 +334,12 @@ then observe the enforcer's TwoWayPeg event stream:

```
DEP='<paste the deposit format from thunder-init.sh>'
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
Expand Down Expand Up @@ -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**,
Expand All @@ -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"}}`.
Expand Down
53 changes: 19 additions & 34 deletions dashboard/lib/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---------- */

Expand Down Expand Up @@ -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)' };
Expand All @@ -147,37 +142,27 @@ 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),
};
}

/* Enforcer returns JSON with a `txid` field (bytes as base64 or hex
* 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);
Expand All @@ -199,14 +184,14 @@ 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),
};
}
}
return {
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)}`,
};
}
4 changes: 1 addition & 3 deletions dashboard/lib/admin-router.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)),
Expand Down Expand Up @@ -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(),
Expand Down
23 changes: 9 additions & 14 deletions dashboard/lib/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -254,29 +256,22 @@ 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),
pending_sats: Number(j.pendingSats ?? 0),
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 };
}
}
Expand Down
Loading
Loading