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.
99 lines
2.5 KiB
Go
99 lines
2.5 KiB
Go
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 0–27 (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 0–27 (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
|
||
}
|