← Blog

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:

  1. init hashgeth init genesis.json แล้วดู hash ที่ได้
  2. rollup l2genesis.l2.hash จาก rollup.json
  3. 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 ในมือ

อ่านต่อในเว็บ

WS-06: Build Your Own OP Stack L2

← more posts