Skip to content

Commit 380d0dc

Browse files
tests: add fuzzing
1 parent 689623d commit 380d0dc

3 files changed

Lines changed: 180 additions & 46 deletions

File tree

base58/decode.go

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package base58
33
import (
44
"encoding/binary"
55
"errors"
6-
"math/bits"
76
)
87

98
var (
@@ -93,33 +92,23 @@ func Decode64(encoded string, dst *[64]byte) error {
9392
uint64(raw[5*i+4])
9493
}
9594

96-
// For 64-byte decode, accumulation can overflow u64.
97-
// Use 96-bit (hi:lo) arithmetic.
95+
// Plain uint64 accumulation — each product is ≤ 2^62 and the sum
96+
// of 18 terms stays under 2^64 (verified by Firedancer analysis).
9897
var bin [binarySz64]uint64
99-
var binHi [binarySz64]uint64
100-
for i := range intermediateSz64 {
101-
for k := range binarySz64 {
102-
hi, lo := bits.Mul64(intermediate[i], uint64(decTable64[i][k]))
103-
newLo, carry := bits.Add64(bin[k], lo, 0)
104-
bin[k] = newLo
105-
binHi[k] += hi + carry
98+
for k := range binarySz64 {
99+
var acc uint64
100+
for i := range intermediateSz64 {
101+
acc += intermediate[i] * uint64(decTable64[i][k])
106102
}
103+
bin[k] = acc
107104
}
108105

109-
// Carry propagation: each bin[k] should reduce to 32 bits.
110-
// Carry is (binHi[k] : bin[k]) >> 32.
111106
for i := binarySz64 - 1; i >= 1; i-- {
112-
carryHi := binHi[i]
113-
carryLo := bin[i] >> 32
107+
bin[i-1] += bin[i] >> 32
114108
bin[i] &= 0xFFFFFFFF
115-
// Add (carryHi:carryLo) shifted: carry = carryHi<<32 | carryLo
116-
// But carryHi could make this > 64 bits. Add to bin[i-1] and binHi[i-1].
117-
newLo, c := bits.Add64(bin[i-1], carryLo, 0)
118-
bin[i-1] = newLo
119-
binHi[i-1] += carryHi + c
120109
}
121110

122-
if binHi[0] > 0 || bin[0] > 0xFFFFFFFF {
111+
if bin[0] > 0xFFFFFFFF {
123112
return ErrValueTooLarge
124113
}
125114

base58/encode.go

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package base58
22

33
import (
44
"encoding/binary"
5-
"math/bits"
65
"unsafe"
76
)
87

@@ -124,35 +123,40 @@ func encodeRaw32(src *[32]byte, raw *[raw58Sz32]byte) int {
124123

125124
// encodeRaw64 fills raw with the raw base-58 digits for a 64-byte input and
126125
// returns the number of leading digits to skip.
126+
//
127+
// The accumulation uses plain uint64 arithmetic. Each product is u32×u32 so
128+
// it fits in u64. After the first 8 input limbs a mini-reduction prevents
129+
// overflow before adding the remaining 8 limbs (matches Firedancer).
127130
func encodeRaw64(src *[64]byte, raw *[raw58Sz64]byte) int {
128131
var bin [binarySz64]uint32
129132
for i := range binarySz64 {
130133
bin[i] = binary.BigEndian.Uint32(src[i*4 : i*4+4])
131134
}
132135

133-
// For 64 bytes the accumulation can overflow u64, so we use
134-
// 96-bit arithmetic (hi:lo) and reduce carries during accumulation.
135136
var intermediate [intermediateSz64]uint64
136-
var intermediateHi [intermediateSz64]uint64
137-
for i := range binarySz64 {
137+
138+
// First 8 limbs.
139+
for i := 0; i < 8; i++ {
138140
for k := range intermediateSz64 - 1 {
139-
hi, lo := bits.Mul64(uint64(bin[i]), uint64(encTable64[i][k]))
140-
newLo, carry := bits.Add64(intermediate[k+1], lo, 0)
141-
intermediate[k+1] = newLo
142-
intermediateHi[k+1] += hi + carry
141+
intermediate[k+1] += uint64(bin[i]) * uint64(encTable64[i][k])
143142
}
144143
}
145144

146-
// Extended-precision carry propagation.
145+
// Mini-reduction to prevent overflow before the second half.
146+
intermediate[intermediateSz64-3] += intermediate[intermediateSz64-2] / r1div
147+
intermediate[intermediateSz64-2] %= r1div
148+
149+
// Last 8 limbs.
150+
for i := 8; i < binarySz64; i++ {
151+
for k := range intermediateSz64 - 1 {
152+
intermediate[k+1] += uint64(bin[i]) * uint64(encTable64[i][k])
153+
}
154+
}
155+
156+
// Full carry propagation.
147157
for i := intermediateSz64 - 1; i >= 1; i-- {
148-
hi := intermediateHi[i]
149-
lo := intermediate[i]
150-
q, r := div128by64(hi, lo, r1div)
151-
intermediate[i] = r
152-
intermediateHi[i] = 0
153-
newLo, carry := bits.Add64(intermediate[i-1], q, 0)
154-
intermediate[i-1] = newLo
155-
intermediateHi[i-1] += carry
158+
intermediate[i-1] += intermediate[i] / r1div
159+
intermediate[i] %= r1div
156160
}
157161

158162
for i := range intermediateSz64 {
@@ -186,11 +190,3 @@ func encodeRaw64(src *[64]byte, raw *[raw58Sz64]byte) int {
186190

187191
return rawLeading0s - inLeading0s
188192
}
189-
190-
// div128by64 computes (hi:lo) / d and (hi:lo) % d.
191-
func div128by64(hi, lo, d uint64) (q, r uint64) {
192-
if hi == 0 {
193-
return lo / d, lo % d
194-
}
195-
return bits.Div64(hi, lo, d)
196-
}

base58/fuzz_test.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package base58
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
7+
mrtronbase58 "github.com/mr-tron/base58"
8+
)
9+
10+
// --- Encode fuzz: our output must match mr-tron for every input ---
11+
12+
func FuzzEncode32_MatchesMrTron(f *testing.F) {
13+
f.Add(make([]byte, 32)) // all zeros
14+
f.Add(bytes.Repeat([]byte{0xff}, 32)) // all 0xFF
15+
f.Add(append([]byte{1}, make([]byte, 31)...)) // single leading byte
16+
f.Add(append(make([]byte, 31), 1)) // trailing 1
17+
18+
f.Fuzz(func(t *testing.T, data []byte) {
19+
if len(data) != 32 {
20+
t.Skip()
21+
}
22+
var src [32]byte
23+
copy(src[:], data)
24+
25+
ours := Encode32(&src)
26+
theirs := mrtronbase58.Encode(src[:])
27+
if ours != theirs {
28+
t.Fatalf("Encode32 mismatch for %x:\n ours: %s\n theirs: %s", src, ours, theirs)
29+
}
30+
})
31+
}
32+
33+
func FuzzEncode64_MatchesMrTron(f *testing.F) {
34+
f.Add(make([]byte, 64))
35+
f.Add(bytes.Repeat([]byte{0xff}, 64))
36+
f.Add(append([]byte{1}, make([]byte, 63)...))
37+
38+
f.Fuzz(func(t *testing.T, data []byte) {
39+
if len(data) != 64 {
40+
t.Skip()
41+
}
42+
var src [64]byte
43+
copy(src[:], data)
44+
45+
ours := Encode64(&src)
46+
theirs := mrtronbase58.Encode(src[:])
47+
if ours != theirs {
48+
t.Fatalf("Encode64 mismatch for %x:\n ours: %s\n theirs: %s", src, ours, theirs)
49+
}
50+
})
51+
}
52+
53+
// --- Decode fuzz: round-trip through mr-tron must agree ---
54+
55+
func FuzzDecode32_MatchesMrTron(f *testing.F) {
56+
f.Add("11111111111111111111111111111111")
57+
f.Add("11111111111111111111111111111112")
58+
f.Add("4cHoJNmLed5PBgFBezHmJkMJLEZrcTvr3aopjnYBRxUb")
59+
60+
f.Fuzz(func(t *testing.T, encoded string) {
61+
var dst [32]byte
62+
err := Decode32(encoded, &dst)
63+
if err != nil {
64+
// We're stricter than mr-tron (fixed size, leading-zero
65+
// validation). Just verify we don't panic.
66+
return
67+
}
68+
69+
// If we accepted it, mr-tron must decode to the same bytes.
70+
theirBytes, theirErr := mrtronbase58.Decode(encoded)
71+
if theirErr != nil {
72+
t.Fatalf("we accepted %q but mr-tron rejected it: %v", encoded, theirErr)
73+
}
74+
75+
// mr-tron strips leading zeros; pad to compare.
76+
padded := make([]byte, 32)
77+
copy(padded[32-len(theirBytes):], theirBytes)
78+
if !bytes.Equal(dst[:], padded) {
79+
t.Fatalf("decode mismatch for %q:\n ours: %x\n theirs: %x", encoded, dst, padded)
80+
}
81+
82+
// Re-encode must produce the original string.
83+
reEncoded := Encode32(&dst)
84+
if reEncoded != encoded {
85+
t.Fatalf("round-trip mismatch: %q -> %x -> %q", encoded, dst, reEncoded)
86+
}
87+
})
88+
}
89+
90+
func FuzzDecode64_MatchesMrTron(f *testing.F) {
91+
f.Add("5YBLhMBLjhAHnEPnHKLLnVwHSfXGPJMCvKAfNsiaEw2T63edrYxVFHKUxRXfP6KA1HVo7c9JZ3LAJQR72giX7Cb")
92+
93+
f.Fuzz(func(t *testing.T, encoded string) {
94+
var dst [64]byte
95+
err := Decode64(encoded, &dst)
96+
if err != nil {
97+
return
98+
}
99+
100+
theirBytes, theirErr := mrtronbase58.Decode(encoded)
101+
if theirErr != nil {
102+
t.Fatalf("we accepted %q but mr-tron rejected it: %v", encoded, theirErr)
103+
}
104+
105+
padded := make([]byte, 64)
106+
copy(padded[64-len(theirBytes):], theirBytes)
107+
if !bytes.Equal(dst[:], padded) {
108+
t.Fatalf("decode mismatch for %q:\n ours: %x\n theirs: %x", encoded, dst, padded)
109+
}
110+
111+
reEncoded := Encode64(&dst)
112+
if reEncoded != encoded {
113+
t.Fatalf("round-trip mismatch: %q -> %x -> %q", encoded, dst, reEncoded)
114+
}
115+
})
116+
}
117+
118+
// --- Invalid input fuzz: verify we never panic ---
119+
120+
func FuzzDecode32_NoPanic(f *testing.F) {
121+
f.Add([]byte(""))
122+
f.Add([]byte("0")) // invalid char
123+
f.Add([]byte("O")) // invalid char
124+
f.Add([]byte("I")) // invalid char
125+
f.Add([]byte("l")) // invalid char
126+
f.Add([]byte("\x00")) // null byte
127+
f.Add([]byte("\xff")) // high byte
128+
f.Add(bytes.Repeat([]byte("z"), 45)) // too long
129+
f.Add(bytes.Repeat([]byte("1"), 50)) // way too long
130+
131+
f.Fuzz(func(t *testing.T, data []byte) {
132+
var dst [32]byte
133+
// Must not panic regardless of input.
134+
Decode32(string(data), &dst)
135+
})
136+
}
137+
138+
func FuzzDecode64_NoPanic(f *testing.F) {
139+
f.Add([]byte(""))
140+
f.Add([]byte("0"))
141+
f.Add([]byte("\x00"))
142+
f.Add([]byte("\xff"))
143+
f.Add(bytes.Repeat([]byte("z"), 91))
144+
145+
f.Fuzz(func(t *testing.T, data []byte) {
146+
var dst [64]byte
147+
Decode64(string(data), &dst)
148+
})
149+
}

0 commit comments

Comments
 (0)