Skip to content

Commit c93139f

Browse files
committed
test(fuzz): add fuzz tests for CompactToBig and loadHeadersFromFile
1 parent 8731bb3 commit c93139f

File tree

2 files changed

+197
-0
lines changed

2 files changed

+197
-0
lines changed
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package chaintracks
2+
3+
import (
4+
"testing"
5+
)
6+
7+
// FuzzCompactToBig tests CompactToBig with random uint32 values
8+
// to ensure it never panics and always returns a valid big.Int.
9+
func FuzzCompactToBig(f *testing.F) {
10+
// Seed corpus with known interesting values
11+
f.Add(uint32(0x1d00ffff)) // genesis block mainnet
12+
f.Add(uint32(0x1b0404cb)) // typical difficulty
13+
f.Add(uint32(0x00000000)) // zero
14+
f.Add(uint32(0xffffffff)) // max uint32
15+
f.Add(uint32(0x01000000)) // exponent = 1
16+
f.Add(uint32(0x02000000)) // exponent = 2
17+
f.Add(uint32(0x03000000)) // exponent = 3
18+
f.Add(uint32(0x00800000)) // sign bit set, exponent = 0
19+
f.Add(uint32(0x01800000)) // sign bit set, exponent = 1
20+
21+
f.Fuzz(func(t *testing.T, compact uint32) {
22+
// Should never panic
23+
result := CompactToBig(compact)
24+
25+
// Result should never be nil
26+
if result == nil {
27+
t.Fatal("CompactToBig returned nil")
28+
}
29+
30+
// Result should be a valid big.Int
31+
if result.BitLen() < 0 {
32+
t.Errorf("CompactToBig(%x) returned invalid big.Int", compact)
33+
}
34+
35+
// Verify sign bit handling
36+
isNegative := compact&0x00800000 != 0
37+
if isNegative && result.Sign() > 0 {
38+
t.Errorf("CompactToBig(%x) should be negative but got sign %d", compact, result.Sign())
39+
}
40+
41+
// For zero mantissa, result should be zero
42+
mantissa := compact & 0x007fffff
43+
if mantissa == 0 && result.Sign() != 0 {
44+
t.Errorf("CompactToBig(%x) with zero mantissa should return zero, got %v", compact, result)
45+
}
46+
})
47+
}
48+
49+
// FuzzChainWorkFromHex tests ChainWorkFromHex with random strings
50+
// to ensure it handles invalid input gracefully without panicking.
51+
func FuzzChainWorkFromHex(f *testing.F) {
52+
// Seed corpus with valid and invalid hex strings
53+
f.Add("0000000000000000000000000000000000000000000000000000000000003039")
54+
f.Add("1234567890abcdef")
55+
f.Add("invalid")
56+
f.Add("")
57+
f.Add("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")
58+
f.Add("000000000000000000000000000000000000000000000000000000000000000g") // invalid hex char
59+
f.Add("0x1234") // 0x prefix
60+
f.Add(" 1234 ") // whitespace
61+
f.Add("\n1234\n") // newlines
62+
f.Add("😀") // unicode
63+
f.Add(string([]byte{0x00, 0x01, 0x02})) // control chars
64+
65+
f.Fuzz(func(t *testing.T, hexStr string) {
66+
// Should never panic
67+
result, err := ChainWorkFromHex(hexStr)
68+
69+
// If no error, result should not be nil
70+
if err == nil && result == nil {
71+
t.Fatal("ChainWorkFromHex returned nil without error")
72+
}
73+
74+
// If error, result should be nil
75+
if err != nil && result != nil {
76+
t.Errorf("ChainWorkFromHex returned non-nil result with error: %v", err)
77+
}
78+
79+
// If successful, result should be a valid big.Int
80+
if err == nil {
81+
if result.BitLen() < 0 {
82+
t.Errorf("ChainWorkFromHex(%q) returned invalid big.Int", hexStr)
83+
}
84+
85+
// Round-trip test: convert back to hex and parse again
86+
hexOut := result.Text(16)
87+
result2, err2 := ChainWorkFromHex(hexOut)
88+
if err2 != nil {
89+
t.Errorf("Round-trip failed for %q: %v", hexStr, err2)
90+
}
91+
if result.Cmp(result2) != 0 {
92+
t.Errorf("Round-trip mismatch for %q: %v != %v", hexStr, result, result2)
93+
}
94+
}
95+
})
96+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package chaintracks
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
// FuzzLoadHeadersFromFile tests loadHeadersFromFile with random binary data
12+
// to ensure it handles malformed header files gracefully without panicking.
13+
func FuzzLoadHeadersFromFile(f *testing.F) {
14+
// Seed corpus with interesting binary patterns
15+
16+
// Empty file
17+
f.Add([]byte{})
18+
19+
// Single valid-looking header (80 bytes of zeros)
20+
validHeader := make([]byte, 80)
21+
f.Add(validHeader)
22+
23+
// Two headers (160 bytes)
24+
twoHeaders := make([]byte, 160)
25+
f.Add(twoHeaders)
26+
27+
// Not a multiple of 80 (79 bytes)
28+
f.Add(make([]byte, 79))
29+
30+
// Not a multiple of 80 (81 bytes)
31+
f.Add(make([]byte, 81))
32+
33+
// Not a multiple of 80 (159 bytes)
34+
f.Add(make([]byte, 159))
35+
36+
// Large file (1000 headers = 80,000 bytes)
37+
largeFile := make([]byte, 80*1000)
38+
f.Add(largeFile)
39+
40+
// Random pattern (240 bytes = 3 headers worth)
41+
randomPattern := []byte{
42+
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
43+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
44+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
45+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
46+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
47+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
48+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
49+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
50+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
51+
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
52+
}
53+
f.Add(randomPattern)
54+
55+
// All 0xFF bytes (80 bytes)
56+
allOnes := make([]byte, 80)
57+
for i := range allOnes {
58+
allOnes[i] = 0xFF
59+
}
60+
f.Add(allOnes)
61+
62+
f.Fuzz(func(t *testing.T, data []byte) {
63+
// Create a temporary file with the fuzzed data
64+
tmpDir := t.TempDir()
65+
tmpFile := filepath.Join(tmpDir, "fuzz.headers")
66+
67+
err := os.WriteFile(tmpFile, data, 0o600)
68+
require.NoError(t, err, "Failed to write temp file")
69+
70+
// Should never panic
71+
headers, err := loadHeadersFromFile(tmpFile)
72+
73+
// Validate invariants based on input
74+
dataLen := len(data)
75+
76+
// If data length is not a multiple of 80, should return error
77+
if dataLen%80 != 0 {
78+
require.Error(t, err, "Expected error for non-80-byte-aligned data")
79+
require.Nil(t, headers, "Headers should be nil on error")
80+
require.ErrorIs(t, err, ErrInvalidFileSize, "Should return ErrInvalidFileSize")
81+
return
82+
}
83+
84+
// If data length is a multiple of 80, it may succeed or fail depending on header validity
85+
expectedHeaderCount := dataLen / 80
86+
87+
if err != nil {
88+
// Error is acceptable if header parsing fails
89+
require.Nil(t, headers, "Headers should be nil when error is returned")
90+
} else {
91+
// Success: validate the returned headers
92+
require.NotNil(t, headers, "Headers should not be nil on success")
93+
require.Len(t, headers, expectedHeaderCount, "Header count mismatch")
94+
95+
// Verify each header is not nil
96+
for i, header := range headers {
97+
require.NotNil(t, header, "Header at index %d should not be nil", i)
98+
}
99+
}
100+
})
101+
}

0 commit comments

Comments
 (0)