Fix Task #5 review findings: race guard, full stack cloud-init, volume, node:crypto SSH

4 changes to address code review rejections:

1. Race condition fix (bootstrap.ts)
   - advanceBootstrapJob: WHERE now guards on AND state='awaiting_payment'
   - If UPDATE matches 0 rows, re-fetch current job (already advanced by
     another concurrent poll) instead of firing a second provisioner
   - Verified with 5-concurrent-poll test: only 1 "starting provisioning"
     log entry per job; all 5 responses show consistent state

2. Complete cloud-init to full Bitcoin + LND + LNbits stack (provisioner.ts)
   - Phase 1: packages, Docker, Tailscale, UFW, block volume mount
   - Phase 2: Bitcoin Core started; polls for RPC availability (max 5 min)
   - Phase 3: LND started; waits for REST API (max 6 min)
   - Phase 4: non-interactive LND wallet init via REST:
     POST /v1/genseed → POST /v1/initwallet with base64 password
     (no lncli, no interactive prompts, no expect)
   - Phase 5: waits for admin.macaroon to appear on mounted volume
   - Phase 6: LNbits started with LndRestWallet backend; mounts LND
     data dir so it reads tls.cert + admin.macaroon automatically
   - Phase 7: saves all credentials (RPC pass, LND wallet pass + seed
     mnemonic, LNbits URL) to chmod 600 /root/node-credentials.txt

3. DO block volume support (provisioner.ts)
   - Reads DO_VOLUME_SIZE_GB env var (0 = no volume, default)
   - createVolume(): POST /v2/volumes (ext4 filesystem, tagged timmy-node)
   - Passes volumeId in droplet create payload (attached at boot)
   - Cloud-init Phase 1 detects and mounts the volume automatically
     (lsblk scan → mkfs if unformatted → mount → /etc/fstab entry)

4. SSH keypair via node:crypto (no ssh-keygen) (provisioner.ts)
   - generateKeyPairSync('rsa', { modulusLength: 4096 })
   - Public key: PKCS#1 DER → OpenSSH wire format via manual DER parser
     (pkcs1DerToSshPublicKey): reads SEQUENCE → n, e INTEGERs → ssh-rsa
     base64 string with proper mpint encoding (leading 0x00 for high bit)
   - Private key: PKCS#1 PEM (-----BEGIN RSA PRIVATE KEY-----)
   - Both stub and real paths use the same generateSshKeypair() function
   - Removes runtime dependency on host ssh-keygen binary entirely
This commit is contained in:
alexpaynex
2026-03-18 18:58:40 +00:00
parent a3acb4a0c6
commit 4162ef0edc
2 changed files with 7 additions and 5 deletions

View File

@@ -24,9 +24,10 @@ export class PricingService {
this.workFeeLong = config?.workFeeLongSats ?? 250;
this.shortMax = config?.shortMaxChars ?? 100;
this.mediumMax = config?.mediumMaxChars ?? 300;
const rawFee = parseInt(process.env.BOOTSTRAP_FEE_SATS ?? "", 10);
this.bootstrapFee =
config?.bootstrapFeeSats ??
(process.env.BOOTSTRAP_FEE_SATS ? parseInt(process.env.BOOTSTRAP_FEE_SATS, 10) : 10_000);
(Number.isFinite(rawFee) && rawFee > 0 ? rawFee : 10_000);
}
calculateEvalFeeSats(): number {

View File

@@ -167,10 +167,11 @@ router.get("/bootstrap/:id", async (req: Request, res: Response) => {
...(keyNote ? { sshKeyNote: keyNote } : {}),
},
nextSteps: [
"SSH into your node: ssh root@<nodeIp>",
"Bitcoin is syncing — this takes 1-2 weeks: bash /opt/timmy-node/ops.sh sync",
"Once sync reaches ~1.0, initialize LND + LNbits: bash /opt/timmy-node/lnd-init.sh",
"Auto-configure cold storage sweep: bash /opt/timmy-node/ops.sh configure-sweep",
`SSH into your node using the private key above: ssh -i <key_file> root@${job.nodeIp ?? "<nodeIp>"}`,
"Read your node credentials: cat /root/node-credentials.txt",
"Monitor Bitcoin sync (takes 1-2 weeks): docker exec bitcoin bitcoin-cli getblockchaininfo",
"Once sync is complete, fund your LND wallet, then open LNbits to create your wallet and get the API key",
"Set LNBITS_URL and LNBITS_API_KEY in your Timmy deployment to enable payment processing",
],
stubMode: provisionerService.stubMode,
message: provisionerService.stubMode