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:
@@ -1,7 +1,7 @@
|
||||
import { Router, type Request, type Response } from "express";
|
||||
import { randomUUID } from "crypto";
|
||||
import { db, bootstrapJobs, type BootstrapJob } from "@workspace/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { lnbitsService } from "../lib/lnbits.js";
|
||||
import { pricingService } from "../lib/pricing.js";
|
||||
import { provisionerService } from "../lib/provisioner.js";
|
||||
@@ -31,13 +31,18 @@ async function advanceBootstrapJob(job: BootstrapJob): Promise<BootstrapJob | nu
|
||||
const isPaid = await lnbitsService.checkInvoicePaid(job.paymentHash);
|
||||
if (!isPaid) return job;
|
||||
|
||||
// Guard: only advance if still awaiting_payment — prevents duplicate provisioning
|
||||
// on concurrent polls (each poll independently confirms payment).
|
||||
const updated = await db
|
||||
.update(bootstrapJobs)
|
||||
.set({ state: "provisioning", updatedAt: new Date() })
|
||||
.where(eq(bootstrapJobs.id, job.id))
|
||||
.where(and(eq(bootstrapJobs.id, job.id), eq(bootstrapJobs.state, "awaiting_payment")))
|
||||
.returning();
|
||||
|
||||
if (updated.length === 0) return getBootstrapJobById(job.id);
|
||||
if (updated.length === 0) {
|
||||
// Another concurrent request already advanced the state — just re-fetch.
|
||||
return getBootstrapJobById(job.id);
|
||||
}
|
||||
|
||||
console.log(`[bootstrap] Payment confirmed for ${job.id} — starting provisioning`);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user