Files
go-kv/wal/header.go
T
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

99 lines
2.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package wal
import (
"encoding/binary"
"errors"
"hash/crc32"
)
const (
walFileHeaderSize = 32
walMagic = 0x57414C4B // "WALK" in ASCII
walFormatVersion = 1
)
// WalFileHeader is the 32-byte header written at the start of every WAL segment file.
type WalFileHeader struct {
Magic uint32
FormatVersion uint16
HeaderSize uint16
BlockSize uint32
SegmentID uint64
StartSequence uint64
HeaderCRC uint32
}
// EncodeWalHeader serializes h into a fixed-size 32-byte array using little-endian byte order.
// The HeaderCRC field is computed over bytes 027 (everything except the CRC itself).
func EncodeWalHeader(h *WalFileHeader) [walFileHeaderSize]byte {
h.HeaderSize = walFileHeaderSize
h.Magic = walMagic
h.FormatVersion = walFormatVersion
var buf [walFileHeaderSize]byte
le := binary.LittleEndian
le.PutUint32(buf[0:4], h.Magic)
le.PutUint16(buf[4:6], h.FormatVersion)
le.PutUint16(buf[6:8], h.HeaderSize)
le.PutUint32(buf[8:12], h.BlockSize)
le.PutUint64(buf[12:20], h.SegmentID)
le.PutUint64(buf[20:28], h.StartSequence)
// CRC32C over bytes 027 (per design §3.2 line 341, 359)
h.HeaderCRC = crc32.Checksum(buf[0:28], crc32cTable)
le.PutUint32(buf[28:32], h.HeaderCRC)
return buf
}
var (
errBadMagic = errors.New("wal: bad magic number")
errBadVersion = errors.New("wal: unsupported format version")
errBadHeaderSize = errors.New("wal: bad header size")
errCRCMismatch = errors.New("wal: header CRC mismatch")
errHeaderTooShort = errors.New("wal: header data too short")
)
// DecodeWalHeader parses a 32-byte little-endian header and validates magic,
// format version, header size, and CRC.
func DecodeWalHeader(data []byte) (*WalFileHeader, error) {
if len(data) < walFileHeaderSize {
return nil, errHeaderTooShort
}
le := binary.LittleEndian
magic := le.Uint32(data[0:4])
if magic != walMagic {
return nil, errBadMagic
}
version := le.Uint16(data[4:6])
if version != walFormatVersion {
return nil, errBadVersion
}
hdrSize := le.Uint16(data[6:8])
if hdrSize != walFileHeaderSize {
return nil, errBadHeaderSize
}
// Verify CRC before trusting any other fields
gotCRC := crc32.Checksum(data[0:28], crc32cTable)
storedCRC := le.Uint32(data[28:32])
if gotCRC != storedCRC {
return nil, errCRCMismatch
}
return &WalFileHeader{
Magic: magic,
FormatVersion: version,
HeaderSize: hdrSize,
BlockSize: le.Uint32(data[8:12]),
SegmentID: le.Uint64(data[12:20]),
StartSequence: le.Uint64(data[20:28]),
HeaderCRC: storedCRC,
}, nil
}