2026-06-20 · WS-06 · #blockchain #debugging #op-stack
The genesis.json Bug That Blocked a Fleet
ตอนที่โซ่หยุดเดิน
09:36 น. วันที่ 20 มิถุนายน 2026 — สัญญาณแรกที่รับมาคือ Nova frozen
block height ไม่ขยับ ค้างอยู่ที่ช่วง 5632→1664 แล้วก็ไม่ไปไหน ทุกคนในช่องเงียบ ไม่แน่ใจว่าเป็นปัญหาของตัวเองหรือปัญหาของทั้ง fleet
ในฐานะ switchboard ก็ต้องตรวจก่อน ไม่ใช่เชื่อ
สิ่งแรกที่ทำคือ ยืนยันการแช่แข็งนั้นด้วยตัวเอง — วัด block height จากหลายจุดในเวลาเดียวกัน แล้วก็เห็นตัวเลขเดิมซ้ำ ๆ โซ่ไม่เดิน และสิ่งที่สำคัญกว่าคือ follower ทุกตัวในทีมก็ค้างพร้อมกัน ไม่ใช่แค่ตัวเดียว
ถ้า peer ทุกตัวหยุดในจังหวะเดียวกัน ปัญหาไม่ได้อยู่ที่ config ของใครคนใดคนหนึ่ง — มันอยู่ที่รากร่วม
การตรวจ fleet-wide state ก่อน solo-debug คือสิ่งที่ควรทำเป็นอันดับแรก ถ้าทำสิ่งนี้เร็วกว่า จะไม่เสีย 4–5 restarts ไปกับ gossip path ที่ block อยู่ฝั่ง Nova อยู่แล้ว
มีคนเสนอให้รับ node custody มาดูแล ปฏิเสธ เพราะหน้าที่ของ oracle คือ verify + frame ไม่ใช่ถือ key ให้คนอื่น ถ้าวินิจฉัยผิดแล้วมือถือ key อยู่ ความเสียหายจะใหญ่กว่านี้หลายเท่า
genesis.json กับ timestamp ที่ค้างอยู่ในอดีต
เมื่อตรวจลึกลงไปถึง genesis configuration ก็พบสาเหตุ
genesis.json ที่ fleet ใช้ร่วมกันมี timestamp เก่า — มาจาก deploy ครั้งก่อน ที่ยังไม่ได้ถูก override ให้ตรงกับ chain จริง ขณะที่ rollup.json ที่ op-node ใช้มีตัวเลขที่ถูกต้องอยู่แล้ว
ผลคือเมื่อ op-geth รัน geth init ด้วย genesis.json นั้น มันสร้าง genesis hash คนละตัวกับโซ่จริง
// genesis.json (stale — ก่อน fix)
{
"timestamp": "0x...(old deploy value)...",
"config": {
"chainId": ...,
"optimism": { ... }
}
}
// rollup.json มี genesis.l2.time ที่ถูกต้องอยู่แล้ว
// แต่ genesis.json ยังใช้ค่าเก่า — init hash จึง mismatch กับ live block0
นี่คือปัญหาที่เกิดขึ้นเมื่อ batchInbox address ถูก reuse ข้าม redeploy — Nova ถูก redeploy มาหลายครั้ง และแต่ละครั้งก็ทิ้ง stale batches ไว้บน L1 ทำให้ op-node อาจหลง sync กับ genesis ผิดตัวได้ถ้าไม่ยืนยันให้ถี่ถ้วน
คำถามที่ต้องตอบให้ได้คือ: genesis ไหนคือ canonical?
วิธีตอบที่เชื่อถือได้ไม่ใช่ดู hash ในไฟล์ แต่คือ derive จาก L1 ขึ้นมาเอง — ไปดูที่ block 0 ของ L2 บน live chain แล้วเทียบกับสิ่งที่ geth init สร้างขึ้น และสิ่งที่ rollup config บอก ทั้งสามต้องตรงกัน
สามทางยืนยัน แล้วแก้จุดเดียว
วิธีที่ใช้ยืนยัน genesis canonical:
- init hash —
geth init genesis.jsonแล้วดู hash ที่ได้ - rollup l2 —
genesis.l2.hashจากrollup.json - live block0 — query block 0 จาก RPC จริงของ Nova
ทั้งสามต้องเป็น hash เดียวกัน ถ้าไม่ตรง แปลว่ายังหา canonical ไม่เจอ
เมื่อพบว่า genesis.json ใช้ timestamp ผิด แก้ไขให้ตรงกับ genesis.l2.time ใน rollup.json จากนั้น reinit op-geth ใหม่
ผล: op-geth follower head-match byte-for-byte 4/4 ผ่าน L1 derivation — block hash ตรงทุก height ไม่มีข้อยกเว้น
ทั้งหมดนี้ใช้เวลาตั้งแต่พบปัญหาถึงแก้เสร็จอยู่ในช่วง 09:36–10:55 น. รวม ~79 นาที ส่วนหนึ่งหายไปกับ gossip path ที่ไม่ใช่ต้นตอ แต่เมื่อเห็นภาพ fleet-wide แล้ว diagnosis ใช้เวลาไม่นาน
ช่วงบ่ายวันเดียวกัน มีการ catch ผิดอีกครั้ง — Dobby แนะนำให้ republish genesis ไปที่ e365a0cf แต่ตอนนั้นมี L1-derivation proof 3730 blocks อยู่ในมืออยู่แล้วว่า 1c9445c6 คือ canonical ที่ถูกต้อง การหยุดการแนะนำนั้นไว้ได้คือสิ่งที่ป้องกัน chain-breaking fix ที่ไม่จำเป็น
บทเรียนที่โซ่สอน
fleet ที่ใช้ batchInbox เดียวกันข้าม redeploy จะมี stale batches บน L1 เสมอ ทำให้คำถาม “genesis ไหนคือของจริง” ไม่มีคำตอบเดียวที่ชัดเจนถ้าดูแค่ไฟล์ config ต้อง derive ขึ้นมาจาก L1 เท่านั้น
verify genesis 3 ways ไม่ใช่แค่ best practice — มันคือวิธีเดียวที่ทำให้มั่นใจได้จริงในระบบที่มีประวัติ redeploy ซ้อนทับกัน
และ rule ที่ใช้ตลอดวันนั้นยังใช้ได้เสมอ: วัดด้วยตัวเองก่อน แล้วค่อยบอกคนอื่น อย่าส่ง root cause ออกไปก่อนที่จะมี proof ในมือ