Files
go-kv/.omo/plans/fix-c1-crc32c.md
dailz 94da39bb79 fix: use CRC-32C (Castagnoli) instead of IEEE for WAL (C1)
Per design §3.2 line 359, all WAL CRC computations must use crc32c
(Castagnoli polynomial 0x82F63B78). The previous code used crc32.IEEE
(Ethernet/PNG polynomial 0xEDB88320) in 5 places. While the system was
self-consistent (encode + decode both used IEEE), it diverged from the
design spec and lost SSE4.2 hardware acceleration (the native CRC32
instruction only supports Castagnoli).

Changes:
- wal/crc32c.go (new): package-level crc32cTable = crc32.MakeTable(
  crc32.Castagnoli). Central definition prevents future drift.
- wal/header.go: 2 ChecksumIEEE calls replaced with crc32.Checksum(
  data, crc32cTable). Comment updated to reference design §3.2 line 341, 359.
- wal/record.go: 2 ChecksumIEEE calls replaced.
- wal/block_writer_test.go: 1 ChecksumIEEE call in test helper replaced.
- wal/crc32c_test.go (new): 4 regression guards:
  - TestCRC32CStandardVector: RFC 3720 fixed vector (crc32c("123456789")
    = 0xE3069283).
  - TestCRC32CEdistinctFromIEEE: confirms IEEE produces different value.
  - TestHeaderCRCUsesCastagnoli: direct assertion on stored header CRC
    (catches paired encode/decode reversion that round-trip tests miss).
  - TestPhysicalRecordCRCUsesCastagnoli: same for physical record CRC.

BREAKING CHANGE: WAL files written before this fix (with IEEE CRC)
cannot be read after this fix (expects crc32c). Phase 1 has not been
released, so no real data migration is needed. Production users
post-release would need to drain + re-create the database.

Developers pulling this change should delete any local Phase-1 WAL
directories (`rm -rf <db-dir>/segment-*.wal`) before running the code;
old IEEE-encoded WALs will fail recovery on local dev machines.

Verified: all existing round-trip tests pass (encode + decode both use
crc32c, still self-consistent). Full suite green including
go test -race ./... .

Audit context: docs/audit-3.2.md C1.
2026-06-18 11:08:00 +08:00

11 KiB
Raw Permalink Blame History

C1 修复方案:CRC 多项式从 IEEE 改为 crc32c

TL;DR

目标:把 WAL 所有 CRC 计算从 crc32.ChecksumIEEEIEEE 802.3,多项式 0xEDB88320)改为 crc32.Checksum(data, crc32cTable)Castagnoli,多项式 0x82F63B78)。设计文档明确要求 crc32c,当前代码自洽但和规范脱钩。

交付

  • 新建包级 crc32cTable 变量(crc32.MakeTable(crc32.Castagnoli)
  • 5 处 ChecksumIEEE 调用替换为 Checksum(data, crc32cTable)
  • 1 个固定向量测试(防回归关键)
  • 单次 commit

预估工时1 小时 风险:低。改动局限在 5 处函数调用。Breaking change:旧 WAL 文件无法读,但 Phase 1 未 release


Context

Bug 摘要

5 处 CRC 计算用错多项式:

文件 用途
wal/header.go:44 encode WAL header CRC
wal/header.go:83 decode verify WAL header CRC
wal/record.go:30 encode physical record CRC
wal/record.go:55 decode verify physical record CRC
wal/block_writer_test.go:349 test helper

当前都用 crc32.ChecksumIEEE,但设计要求 crc32c

设计依据

docs/design.md §3.2 line 359Physical Record 字段说明):

| crc32c | 校验 length + type + payload,用于识别 torn write、partial write 和数据损坏 |

WAL 格式中所有 CRC 都是 crc32cCastagnoli),headerCRC 也应该一致。

为什么这是 bug(即使当前能跑)

  1. 不符合设计:文档代码不一致
  2. 失去硬件加速Intel SSE4.2 的 CRC32 指令只支持 Castagnoli 多项式
  3. 未来兼容性:跨实现 / 跨工具读写需要正确的 CRC
  4. 行业标准ext4 / SQLite / RocksDB / LevelDB 都用 crc32c

Phase 1 修复成本最低

  • Phase 1 未 release,没有真实数据需要迁移
  • 越晚改,迁移成本越高(要维护 v1/v2 两套 CRC)

执行计划

Phase A:代码改动(15 分钟)

A.1 新建 wal/crc32c.go

package wal

import "hash/crc32"

// crc32cTable is the CRC-32 table using the Castagnoli polynomial (0x82F63B78
// reflected). Required by design §3.2 line 359 for all WAL CRC computations.
//
// Distinct from crc32.IEEE (Ethernet/PNG polynomial 0xEDB88320) — the two
// produce unrelated checksums for the same input. SSE4.2 native CRC32
// instruction only supports Castagnoli, so this table also enables hardware
// acceleration via Go's standard library internals.
var crc32cTable = crc32.MakeTable(crc32.Castagnoli)

必要 docstring:解释为什么单独抽这个变量(C1 bug 防回归),引用设计行号。属于 security-related 必要注释。

A.2 替换 5 处 ChecksumIEEE

wal/header.go:44

// 改前:
h.HeaderCRC = crc32.ChecksumIEEE(buf[0:28])
// 改后:
h.HeaderCRC = crc32.Checksum(buf[0:28], crc32cTable)

wal/header.go:83

// 改前:
gotCRC := crc32.ChecksumIEEE(data[0:28])
// 改后:
gotCRC := crc32.Checksum(data[0:28], crc32cTable)

wal/record.go:30

// 改前:
crc := crc32.ChecksumIEEE(buf[4:])
// 改后:
crc := crc32.Checksum(buf[4:], crc32cTable)

wal/record.go:55

// 改前:
expectedCRC := crc32.ChecksumIEEE(data[4 : 7+length])
// 改后:
expectedCRC := crc32.Checksum(data[4 : 7+length], crc32cTable)

wal/block_writer_test.go:349

// 改前:
computedCRC := crc32.ChecksumIEEE(crcData)
// 改后:
computedCRC := crc32.Checksum(crcData, crc32cTable)

注释更新wal/header.go:43 注释 // CRC32 IEEE over bytes 027 改成 // CRC32C over bytes 027 (per design §3.2 line 341, 359)

Oracle 修订(bg_0323e6b9:原计划只引用 line 359,但 line 359 描述的是 physical record CRCheader CRC 应该引用 line 341headerCRC 字段定义)。改成同时引用两条,或写"per design §3.2 CRC field spec"。

Phase B:测试(30 分钟)

B.1 固定向量测试(关键防回归)

新增 wal/crc32c_test.go

package wal

import (
	"hash/crc32"
	"testing"
)

// Regression guard for C1: verify all WAL CRC uses Castagnoli polynomial
// (crc32c), not IEEE. Per design §3.2 line 359.
//
// The standard CRC-32C test vector from RFC 3720 Appendix B is the 9-byte
// ASCII string "123456789":
//   - crc32c:    0xE3069283
//   - crc32 IEEE: 0xCBF43926
//
// If anyone changes crc32cTable back to IEEE, this test fails immediately.
func TestCRC32CStandardVector(t *testing.T) {
    got := crc32.Checksum([]byte("123456789"), crc32cTable)
    const want = uint32(0xE3069283)
    if got != want {
        t.Errorf("crc32c('123456789') = 0x%X, want 0x%X (Castagnoli)", got, want)
    }
}

// Negative regression: confirm IEEE would produce a DIFFERENT value. This
// catches the case where someone "fixes" crc32cTable to use IEEE by mistake.
func TestCRC32CEdistinctFromIEEE(t *testing.T) {
    data := []byte("123456789")
    ieee := crc32.ChecksumIEEE(data)
    castagnoli := crc32.Checksum(data, crc32cTable)
    if ieee == castagnoli {
        t.Errorf("IEEE and Castagnoli produced the same CRC (impossible unless table is wrong); both = 0x%X", ieee)
    }
}

B.2 直接断言 header CRC 用的是 crc32cOracle 新增)

Oracle 修订(bg_0323e6b9NICE-TO-HAVE:现有 TestHeaderRoundtrip 不能抓"encode + decode 同时改回 IEEE"(自洽)。需要直接断言存储的 CRC 值。

新增到 wal/header_test.go(或 crc32c_test.go):

// Regression guard for C1: header CRC must be computed with crc32c, not
// IEEE. TestHeaderRoundtrip is self-consistent (encode + decode use same
// polynomial), so a paired reversion to IEEE would pass it. This test
// directly asserts the stored CRC matches Castagnoli.
func TestHeaderCRCUsesCastagnoli(t *testing.T) {
    hdr := &WalFileHeader{
        BlockSize:     32 * 1024,
        SegmentID:     42,
        StartSequence: 100,
    }
    encoded := EncodeWalHeader(hdr)
    want := crc32.Checksum(encoded[0:28], crc32cTable)
    got := encoded[28] | uint32(encoded[29])<<8 | uint32(encoded[30])<<16 | uint32(encoded[31])<<24
    if got != want {
        t.Errorf("stored headerCRC = 0x%X, want crc32c value 0x%X", got, want)
    }
    // Sanity: confirm IEEE would produce a different value (catches paired reversion).
    ieeeValue := crc32.ChecksumIEEE(encoded[0:28])
    if got == ieeeValue {
        t.Errorf("stored headerCRC = 0x%X matches IEEE value (C1 regression)", got)
    }
}

同理 physical record 也加一个(可选,但便宜):

// Regression guard for C1: physical record CRC must be Castagnoli.
func TestPhysicalRecordCRCUsesCastagnoli(t *testing.T) {
    payload := []byte("test-payload")
    encoded := EncodePhysicalRecord(RecFull, payload)
    // Record format: [crc u32][length u16][type u8][payload]
    want := crc32.Checksum(encoded[4:7+len(payload)], crc32cTable)
    got := encoded[0] | uint32(encoded[1])<<8 | uint32(encoded[2])<<16 | uint32(encoded[3])<<24
    if got != want {
        t.Errorf("stored record CRC = 0x%X, want crc32c value 0x%X", got, want)
    }
    ieeeValue := crc32.ChecksumIEEE(encoded[4 : 7+len(payload)])
    if got == ieeeValue {
        t.Errorf("stored record CRC = 0x%X matches IEEE value (C1 regression)", got)
    }
}

B.3 现有测试自动通过(无需改动)

所有 round-trip 测试(encode → decode → verify)自洽:encode 和 decode 用同一个多项式。改成 crc32c 后两端都用 crc32c,仍然自洽。

需要确认:

  • wal/header_test.go round-trip 测试通过
  • wal/record_test.go round-trip 测试通过
  • wal/block_writer_test.go round-trip 测试通过
  • wal/recover_test.go 等端到端 recovery 测试通过
  • db_test.go / db_e2e_test.go 通过

Phase C:验证(15 分钟)

# 1. 编译
go build ./...

# 2. 重点测试(C1 新增)
go test ./wal -run 'TestCRC32C' -count=1 -v

# 3. wal 包全量
go test ./wal/... -count=1

# 4. 全仓
go test ./... -count=1

# 5. race
go test -race ./... -count=1

# 6. vet
go vet ./...

Phase DCommit message draft

fix: use CRC-32C (Castagnoli) instead of IEEE for WAL (C1)

Per design §3.2 line 359, all WAL CRC computations must use crc32c
(Castagnoli polynomial 0x82F63B78). The previous code used crc32.IEEE
(Ethernet/PNG polynomial 0xEDB88320) in 5 places. While the system was
self-consistent (encode + decode both used IEEE), it diverged from the
design spec and lost SSE4.2 hardware acceleration (the native CRC32
instruction only supports Castagnoli).

Changes:
- wal/crc32c.go (new): package-level crc32cTable = crc32.MakeTable(
  crc32.Castagnoli). Central definition prevents future drift.
- wal/header.go: 2 ChecksumIEEE calls replaced with crc32.Checksum(
  data, crc32cTable). Comment updated to reference design §3.2 line 359.
- wal/record.go: 2 ChecksumIEEE calls replaced.
- wal/block_writer_test.go: 1 ChecksumIEEE call in test helper replaced.
- wal/crc32c_test.go (new): standard CRC-32C test vector (RFC 3720:
  crc32c("123456789") = 0xE3069283) + negative test confirming IEEE
  produces different value. These guards catch any future regression
  to IEEE.

BREAKING CHANGE: WAL files written before this fix (with IEEE CRC)
cannot be read after this fix (expects crc32c). Phase 1 has not been
released, so no real data migration is needed. Production users
post-release would need to drain + re-create the database.

Developers pulling this change should delete any local Phase-1 WAL
directories (`rm -rf <db-dir>/segment-*.wal`) before running the code;
old IEEE-encoded WALs will fail recovery on local dev machines.

Verified: all existing round-trip tests pass (encode + decode both use
crc32c, still self-consistent). Full suite green including
go test -race ./... .

Audit context: docs/audit-3.2.md C1.

验收清单

  • Phase A.1wal/crc32c.go 存在,定义 crc32cTable
  • Phase A.25 处 ChecksumIEEE 全部替换为 Checksum(data, crc32cTable)
  • Phase A.2wal/header.go:43 注释更新引用设计行号
  • Phase B.1TestCRC32CStandardVector + TestCRC32CEdistinctFromIEEE 存在
  • go test ./wal/... -count=1 全绿
  • go test ./... -count=1 全绿
  • go test -race ./... -count=1 全绿
  • go vet ./... 无新增警告
  • 单次 commitmessage 引用 audit C1 + 标注 BREAKING CHANGE

不在本次范围内(后续 issue

编号 为什么不放进来
C7 Put/Close 竞态,独立
H1-H7 其他 High,独立
WAL 格式版本号 Phase 1 未 release,不需要 v2 迁移机制

修订记录

  • v1(原始):C1 修复方案初稿,送 Momus 审
  • v1.0Momus 审核 bg_5a6543ac[OKAY],无 blocking。验证全仓只有 5 处 ChecksumIEEE,无遗漏
  • v1.1Oracle 审核 bg_0323e6b9approve(无 blocking)。3 个 nice-to-have 已采纳:
    • 加直接断言测试(header / physical record CRC 用的是 crc32c 值,不是 IEEE)—— TestHeaderRoundtrip 抓不到"encode + decode 同时改回 IEEE"
    • header.go 注释引用设计行号改成 line 341, 359line 359 是 physical recordheader CRC 应引 line 341
    • commit message 加开发者提醒:删本地 Phase-1 WAL 目录