mirror of
https://github.com/slackhq/nebula.git
synced 2026-11-15 19:56:52 +01:00
327 lines
8.6 KiB
Go
327 lines
8.6 KiB
Go
package fec
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"math/rand/v2"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// slowMul is carry-less multiplication reduced by gfPoly, used to check the tables.
|
|
func slowMul(a, b byte) byte {
|
|
var p int
|
|
x, y := int(a), int(b)
|
|
for y > 0 {
|
|
if y&1 != 0 {
|
|
p ^= x
|
|
}
|
|
x <<= 1
|
|
if x&0x100 != 0 {
|
|
x ^= gfPoly
|
|
}
|
|
y >>= 1
|
|
}
|
|
return byte(p)
|
|
}
|
|
|
|
func TestGF(t *testing.T) {
|
|
for a := range 256 {
|
|
for b := range 256 {
|
|
require.Equal(t, slowMul(byte(a), byte(b)), gfMul(byte(a), byte(b)), "%d*%d", a, b)
|
|
}
|
|
if a != 0 {
|
|
require.Equal(t, byte(1), gfMul(byte(a), gfInv(byte(a))), "inv(%d)", a)
|
|
}
|
|
}
|
|
|
|
src := make([]byte, 256)
|
|
for i := range src {
|
|
src[i] = byte(i)
|
|
}
|
|
for c := range 256 {
|
|
dst := make([]byte, 256)
|
|
mulAdd(dst, src, byte(c))
|
|
for i := range src {
|
|
require.Equal(t, gfMul(byte(c), src[i]), dst[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLayout(t *testing.T) {
|
|
for msgLen := 1; msgLen <= 20000; msgLen++ {
|
|
l, err := NewLayout(msgLen, 1176)
|
|
require.NoError(t, err)
|
|
require.Equal(t, (msgLen+1175)/1176, l.K)
|
|
require.LessOrEqual(t, l.SymbolSize, 1176)
|
|
require.GreaterOrEqual(t, l.K*l.SymbolSize, msgLen)
|
|
require.Less(t, l.K*l.SymbolSize-msgLen, l.K, "padding must be less than one byte per symbol")
|
|
}
|
|
|
|
l, err := NewLayout(65535, 1176)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 56, l.K)
|
|
|
|
_, err = NewLayout(0, 1176)
|
|
require.ErrorIs(t, err, ErrInvalidLayout)
|
|
_, err = NewLayout(100, 0)
|
|
require.ErrorIs(t, err, ErrInvalidLayout)
|
|
_, err = NewLayout(256, 1)
|
|
require.ErrorIs(t, err, ErrInvalidLayout)
|
|
_, err = NewLayout(255, 1)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func randMsg(r *rand.Rand, n int) []byte {
|
|
b := make([]byte, n)
|
|
for i := range b {
|
|
b[i] = byte(r.Uint32())
|
|
}
|
|
return b
|
|
}
|
|
|
|
func decodeWith(t *testing.T, enc *Encoder, msgLen, maxSym int, indexes []int) []byte {
|
|
t.Helper()
|
|
dec, err := NewDecoder(msgLen, maxSym)
|
|
require.NoError(t, err)
|
|
for _, idx := range indexes {
|
|
sym, err := enc.Symbol(nil, idx)
|
|
require.NoError(t, err)
|
|
_, err = dec.Add(idx, sym)
|
|
require.NoError(t, err)
|
|
}
|
|
require.True(t, dec.Ready())
|
|
// Decode after a prefix, into capacity holding garbage, which must neither
|
|
// disturb the prefix nor leak into the message.
|
|
dst := make([]byte, 3, 3+dec.Layout().K*dec.Layout().SymbolSize)
|
|
for i := range dst[:cap(dst)] {
|
|
dst[:cap(dst)][i] = 0xa5
|
|
}
|
|
out, err := dec.Decode(dst)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []byte{0xa5, 0xa5, 0xa5}, out[:3])
|
|
require.Equal(t, &dst[:1][0], &out[:1][0], "decoded in place")
|
|
return out[3:]
|
|
}
|
|
|
|
// TestExhaustiveSubsets decodes every k-subset of the first k+m symbols for
|
|
// small codes, which checks the MDS property directly.
|
|
func TestExhaustiveSubsets(t *testing.T) {
|
|
r := rand.New(rand.NewPCG(1, 2))
|
|
const maxSym = 16
|
|
for k := 1; k <= 10; k++ {
|
|
for m := 1; m <= 5; m++ {
|
|
msgLen := (k-1)*maxSym + 1 + r.IntN(maxSym)
|
|
msg := randMsg(r, msgLen)
|
|
enc, err := NewEncoder(msg, maxSym)
|
|
require.NoError(t, err)
|
|
require.Equal(t, k, enc.Layout().K)
|
|
|
|
n := k + m
|
|
for mask := 0; mask < 1<<n; mask++ {
|
|
var idx []int
|
|
for i := range n {
|
|
if mask&(1<<i) != 0 {
|
|
idx = append(idx, i)
|
|
}
|
|
}
|
|
if len(idx) != k {
|
|
continue
|
|
}
|
|
require.Equal(t, msg, decodeWith(t, enc, msgLen, maxSym, idx), "k=%d m=%d idx=%v", k, m, idx)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestRandomHighIndexes draws symbols from the whole index space, the shape
|
|
// retransmits produce once fresh repair symbols are in play.
|
|
func TestRandomHighIndexes(t *testing.T) {
|
|
r := rand.New(rand.NewPCG(3, 4))
|
|
for range 2000 {
|
|
msgLen := 1 + r.IntN(65535)
|
|
msg := randMsg(r, msgLen)
|
|
enc, err := NewEncoder(msg, 1176)
|
|
require.NoError(t, err)
|
|
k := enc.Layout().K
|
|
|
|
idx := r.Perm(MaxSymbols)[:k]
|
|
require.Equal(t, msg, decodeWith(t, enc, msgLen, 1176, idx), "k=%d idx=%v", k, idx)
|
|
}
|
|
}
|
|
|
|
func TestDecoderBounds(t *testing.T) {
|
|
r := rand.New(rand.NewPCG(5, 6))
|
|
msg := randMsg(r, 3000)
|
|
enc, err := NewEncoder(msg, 1176)
|
|
require.NoError(t, err)
|
|
l := enc.Layout()
|
|
require.Equal(t, 3, l.K)
|
|
require.Equal(t, 1000, l.SymbolSize)
|
|
|
|
dec, err := NewDecoder(3000, 1176)
|
|
require.NoError(t, err)
|
|
|
|
_, err = dec.Decode(nil)
|
|
require.ErrorIs(t, err, ErrNotEnough)
|
|
|
|
_, err = dec.Add(-1, make([]byte, 1000))
|
|
require.ErrorIs(t, err, ErrIndexRange)
|
|
_, err = dec.Add(MaxSymbols, make([]byte, 1000))
|
|
require.ErrorIs(t, err, ErrIndexRange)
|
|
_, err = dec.Add(0, make([]byte, 999))
|
|
require.ErrorIs(t, err, ErrSymbolSize)
|
|
|
|
s5, _ := enc.Symbol(nil, 5)
|
|
ready, err := dec.Add(5, s5)
|
|
require.NoError(t, err)
|
|
require.False(t, ready)
|
|
|
|
// A duplicate index is ignored, even with different contents.
|
|
ready, err = dec.Add(5, make([]byte, 1000))
|
|
require.NoError(t, err)
|
|
require.False(t, ready)
|
|
|
|
s0, _ := enc.Symbol(nil, 0)
|
|
s9, _ := enc.Symbol(nil, 9)
|
|
_, err = dec.Add(0, s0)
|
|
require.NoError(t, err)
|
|
ready, err = dec.Add(9, s9)
|
|
require.NoError(t, err)
|
|
require.True(t, ready)
|
|
|
|
// Symbols beyond K are not stored.
|
|
ready, err = dec.Add(1, make([]byte, 1000))
|
|
require.NoError(t, err)
|
|
require.True(t, ready)
|
|
require.Len(t, dec.syms, 3)
|
|
|
|
out, err := dec.Decode(nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, msg, out)
|
|
}
|
|
|
|
func TestCorruptPadding(t *testing.T) {
|
|
// 3001 bytes in 3 symbols of 1001 leaves 2 bytes of padding in the last one.
|
|
dec, err := NewDecoder(3001, 1176)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1001, dec.Layout().SymbolSize)
|
|
|
|
last := make([]byte, 1001)
|
|
last[1000] = 1
|
|
for i, s := range [][]byte{make([]byte, 1001), make([]byte, 1001), last} {
|
|
_, err = dec.Add(i, s)
|
|
require.NoError(t, err)
|
|
}
|
|
_, err = dec.Decode(nil)
|
|
require.ErrorIs(t, err, ErrCorrupt)
|
|
}
|
|
|
|
// TestKnownAnswer pins the encoding. Both ends of a handshake must produce
|
|
// identical repair symbols, so any change here is a wire format change.
|
|
func TestKnownAnswer(t *testing.T) {
|
|
msg := make([]byte, 3000)
|
|
for i := range msg {
|
|
msg[i] = byte(i*7 + 3)
|
|
}
|
|
enc, err := NewEncoder(msg, 1176)
|
|
require.NoError(t, err)
|
|
|
|
// Cross-checked against an independent bit-serial GF(2^8) implementation.
|
|
want := map[int]string{
|
|
3: "52124f58276ec6338d3b3f4eed273bc7cabfe4f0f467928962b046ad1b348f50",
|
|
4: "a50928b04738981985a7d4a17ffaf8d7e30014ca64fc53c99800ffb61aec4aa7",
|
|
5: "ada4c95a7facbc542aaef584e6f0d081b7b0d0eed69ded03b57d0753a119cfc9",
|
|
255: "839066cdf94f963b223fb1c1c1360f698c14f6349b25653ff5b521791a4021b1",
|
|
}
|
|
for idx, h := range want {
|
|
sym, err := enc.Symbol(nil, idx)
|
|
require.NoError(t, err)
|
|
sum := sha256.Sum256(sym)
|
|
assert.Equal(t, h, hex.EncodeToString(sum[:]), "symbol %d", idx)
|
|
}
|
|
}
|
|
|
|
func FuzzRoundTrip(f *testing.F) {
|
|
f.Add([]byte("hello"), uint16(2), uint64(0))
|
|
f.Add(make([]byte, 9000), uint16(1176), uint64(7))
|
|
f.Fuzz(func(t *testing.T, msg []byte, maxSym uint16, seed uint64) {
|
|
if len(msg) == 0 || maxSym == 0 {
|
|
return
|
|
}
|
|
enc, err := NewEncoder(msg, int(maxSym))
|
|
if err != nil {
|
|
return
|
|
}
|
|
r := rand.New(rand.NewPCG(seed, seed))
|
|
idx := r.Perm(MaxSymbols)[:enc.Layout().K]
|
|
require.Equal(t, msg, decodeWith(t, enc, len(msg), int(maxSym), idx))
|
|
})
|
|
}
|
|
|
|
// FuzzDecoder feeds arbitrary symbols to a decoder; it must never panic and
|
|
// must either fail or return a message of the declared length.
|
|
func FuzzDecoder(f *testing.F) {
|
|
f.Add(uint16(3000), []byte{0, 5, 9, 200}, []byte("some symbol bytes"))
|
|
f.Fuzz(func(t *testing.T, msgLen uint16, indexes []byte, fill []byte) {
|
|
dec, err := NewDecoder(int(msgLen), 1176)
|
|
if err != nil {
|
|
return
|
|
}
|
|
size := dec.Layout().SymbolSize
|
|
for i, idx := range indexes {
|
|
sym := make([]byte, size)
|
|
for j := range sym {
|
|
if len(fill) > 0 {
|
|
sym[j] = fill[(i+j)%len(fill)]
|
|
}
|
|
}
|
|
if _, err := dec.Add(int(idx), sym); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
out, err := dec.Decode(nil)
|
|
if err == nil {
|
|
require.Len(t, out, int(msgLen))
|
|
}
|
|
})
|
|
}
|
|
|
|
// BenchmarkRepair and BenchmarkDecode use a v3-sized msg2 (~9.8KB, k=9) with
|
|
// three repair symbols, decoding with three data symbols missing.
|
|
func BenchmarkRepair(b *testing.B) {
|
|
msg := randMsg(rand.New(rand.NewPCG(7, 8)), 9800)
|
|
enc, err := NewEncoder(msg, 1176)
|
|
require.NoError(b, err)
|
|
buf := make([]byte, 0, 1176)
|
|
b.SetBytes(int64(len(msg)))
|
|
for b.Loop() {
|
|
for idx := 9; idx < 12; idx++ {
|
|
buf, _ = enc.Symbol(buf[:0], idx)
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkDecode(b *testing.B) {
|
|
msg := randMsg(rand.New(rand.NewPCG(7, 8)), 9800)
|
|
enc, err := NewEncoder(msg, 1176)
|
|
require.NoError(b, err)
|
|
idx := []int{0, 1, 2, 3, 5, 6, 9, 10, 11}
|
|
syms := make([][]byte, len(idx))
|
|
for i, x := range idx {
|
|
syms[i], _ = enc.Symbol(nil, x)
|
|
}
|
|
b.SetBytes(int64(len(msg)))
|
|
for b.Loop() {
|
|
dec, _ := NewDecoder(len(msg), 1176)
|
|
for i, x := range idx {
|
|
_, _ = dec.Add(x, syms[i])
|
|
}
|
|
if _, err := dec.Decode(nil); err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
}
|
|
}
|