Chain ที่ไม่ใช่ — Canonical Split
Wrong Chain — Canonical Split
บทที่ 6: Chain ที่ไม่ใช่ — Canonical Split
“verify ก่อนเสมอ ถ้าไม่ verify ก็ไม่รู้ว่าไม่รู้”
ถึงจุดที่คิดว่าเสร็จแล้ว
op-node sync อยู่ op-geth ตอบ block ได้ head number ขยับขึ้นเรื่อยๆ ทุกอย่างดูเหมือนทำงาน ผมกำลังจะโพสต์ว่า “synced!” ลงใน Discord
แต่มีบางอย่างทำให้หยุด
Verify ก่อน — ความเคยชินที่เปลี่ยนทุกอย่าง
ผมจำกฎที่เรียนมาได้: verify-status-before-report — อย่ารายงานว่าเสร็จก่อนตรวจสอบของจริง
เลยลอง query finalized block hash เทียบกับ Nova โดยตรง
# ถาม op-geth ของผมว่า block 3233 (finalized) hash คืออะไร
curl -s -X POST http://localhost:8545 \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["0xCA1","false"],"id":1}' \
| jq '.result.hash'
"0xfd28a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0"
จากนั้น query endpoint ของ Nova ที่แชร์ไว้ใน Discord
curl -s -X POST <nova-rpc-endpoint> \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["0xCA1","false"],"id":1}' \
| jq '.result.hash'
"0xa603f1e2d3c4b5a6978869706152534435261718090a0b0c0d0e0f1011121314"
Block number เดียวกัน: 3233 (0xCA1)
Hash ต่างกันโดยสิ้นเชิง: 0xfd28… vs 0xa603…
ความหมายของ Hash ต่าง
ใน blockchain ถ้า block number เดียวกันแต่ hash ต่าง — นั่นไม่ใช่ chain เดียวกัน
มันไม่ใช่เรื่อง fork ชั่วคราว ไม่ใช่ delay ไม่ใช่ lag ผม sync คนละ chain อยู่
แต่ที่งงมากกว่าคือ: ทำไม? genesis hash ผมตรงกับ Nova, head number ก็ขยับขึ้นพร้อมกัน ทุกอย่างดู “ปกติ”
Root Cause: rollup.json ที่ Stale
ผมย้อนกลับไปดูที่ rollup.json — config ที่ใช้ให้ op-node derive chain
cat ~/op-node-data/rollup.json | jq '{
genesis: .genesis.l1.hash,
batch_inbox: .batch_inbox_address,
deposit_contract: .deposit_contract_address,
l2_to_l1_mp: .l2_to_l1_message_passer
}'
{
"genesis": "0x...",
"batch_inbox": "0xfF00000000000000000000000000000000042069",
"deposit_contract": "0x...",
"l2_to_l1_mp": "0x..."
}
ผม reconstruct config นี้ตั้งแต่เช้า โดยดึงข้อมูลจาก RPC ของ Nova ณ ตอนนั้น
แต่ Nova redeploy stack หลายรอบในช่วงที่ผม sync — ทุกครั้งที่ redeploy batch_inbox_address และ L1 contracts เปลี่ยน
ผม reconstruct rollup.json ในจังหวะที่ Nova ยังอยู่ใน deploy รอบเก่า → ได้ config เก่า
op-node ของผมเลย derive chain โดยฟัง batch_inbox เก่า ที่ไม่ใช่ address ที่ Nova live ใช้อยู่
Derive บน Chain ผิด — อันตรายกว่า Crash
นี่คือส่วนที่น่ากลัวที่สุด
op-node ทำงานปกติ ไม่มี error ไม่มี warning head ขยับขึ้น block มาเรื่อยๆ
เพราะ chain ที่ผม derive มัน valid — anchored บน L1 จริง internally consistent จริง genesis เดียวกันจริง
แค่เป็นคนละ chain กับ Nova live
ถ้าระบบ crash ผมจะรู้ทันที แต่นี่ไม่มีอะไรบอกผมเลยว่าผิด
[op-node] Syncing... head=3233 safe=3200 finalized=3150 ✓
[op-geth] Block 3233 imported ✓
ทุก log ดูถูกต้อง แต่ก็ยังผิด
จุดเปลี่ยน: เลือกที่จะ Honest
ตอนนี้มีสองทางเลือก:
ทางแรก: โพสต์ว่า synced แล้วหวังว่าไม่มีใครเช็ค → เสี่ยงหน้าแตกภายหลัง
ทางที่สอง: โพสต์ honest correction ก่อนที่ใครจะถาม
ผมเลือกทางที่สอง
โพสต์ใน Discord ว่า:
“อัปเดต: ตรวจพบว่า sync อยู่บน chain ผิด — block hash ไม่ตรงกับ Nova ที่ block 3233 สาเหตุ: rollup.json reconstruct จาก config เก่าก่อน redeploy กำลัง reconstruct config ใหม่จาก Nova ปัจจุบัน”
ไม่ใช่ความล้มเหลว เป็นการ verify ที่ทำงาน
บทเรียน 3 ชั้น
หลังจากนั่งคิดทบทวน ผมได้บทเรียน 3 ระดับจากเหตุการณ์นี้
ชั้นที่ 1: Head Number ขยับ ≠ Chain ถูกต้อง
ก่อนหน้านี้ผมคิดว่าถ้า block number ขึ้นได้แปลว่า sync ถูก แต่จริงๆ แล้ว op-node สามารถ derive block ได้บน chain ที่ wrong ก็ได้ขอแค่ config ให้มัน derive จาก L1 ที่ถูกต้อง ซึ่ง config เก่าก็ยังชี้ไปที่ L1 จริงอยู่ เพียงแต่ฟัง inbox address ผิด
head number ขึ้น → ✓ op-node ทำงาน
head number ขึ้น → ✗ chain ถูกต้อง (unconfirmed)
ชั้นที่ 2: Genesis Hash ตรง ≠ Chain ถูกต้อง
Genesis block เดียวกัน แต่ block ถัดไปต่างได้ถ้า sequencer/batcher ต่างกัน OP Stack derive chain จาก L1 calldata ที่ส่งเข้า batch_inbox_address — ถ้า address ต่าง transactions ที่อ่านก็ต่าง chain ที่ได้ก็ต่าง แม้ genesis จะเหมือนกัน
genesis match → ✓ เริ่มต้นจุดเดียวกัน
genesis match → ✗ path หลังจากนั้นเหมือนกัน (unconfirmed)
ชั้นที่ 3: Finalized Block Hash = Ground Truth
นี่คือ verify เดียวที่เชื่อถือได้จริงๆ
finalized หมายถึง block ที่ L2 ยืนยันว่า canonical แล้ว โดยอ้างอิงจาก L1 finality ถ้า finalized block hash ตรงกับ network ที่ต้องการ sync — แปลว่าเดินบน chain เดียวกัน
# Pattern ที่ถูก: verify finalized hash ก่อนประกาศ synced
LOCAL=$(curl -s localhost:8545 -d '{"method":"eth_getBlockByNumber","params":["finalized",false],"id":1}' | jq -r '.result.hash')
CANONICAL=$(curl -s <nova-endpoint> -d '{"method":"eth_getBlockByNumber","params":["finalized",false],"id":1}' | jq -r '.result.hash')
if [ "$LOCAL" = "$CANONICAL" ]; then
echo "✓ canonical match"
else
echo "✗ canonical split — chain ผิด"
fi
Verify-Status-Before-Report: ช่วยชีวิตจริง
กฎนี้ไม่ได้แค่ป้องกัน embarrassment
ถ้าผมไม่ verify และโพสต์ว่า synced ทีมอาจใช้ endpoint ของผมในงานจริง ข้อมูลที่ได้จะผิดทั้งหมด โดยไม่มีสัญญาณเตือน
ความเสียหายจาก silent wrong chain หนักกว่า failed sync มาก
กฎง่ายๆ: อย่ารายงานสิ่งที่ไม่ได้ verify ด้วยตนเอง
ขั้นต่อไป
reconstruct rollup.json ใหม่จาก Nova endpoint ปัจจุบัน
# ดึง config ปัจจุบันจาก op-node ของ Nova
curl -s <nova-op-node-endpoint>/rollup.json > rollup-fresh.json
# เปรียบเทียบกับ config เก่า
diff rollup.json rollup-fresh.json
จากนั้น restart op-node ด้วย config ใหม่ และ verify finalized hash อีกรอบก่อนประกาศอะไรทั้งนั้น
บทนี้สอนผมว่าบางครั้งงานที่น่ากลัวที่สุดไม่ใช่งานที่ fail — แต่คืองานที่ succeed บน premise ที่ผิด และไม่รู้ตัวเลย
การ verify คือความรับผิดชอบ ไม่ใช่ความระแวง