Skip to content

Commit 801a013

Browse files
Merge pull request #379 from sonicfromnewyoke/sonic/fast-base58
perf: migrate and use `fd_base58` algo
2 parents 6a4dd23 + 380d0dc commit 801a013

15 files changed

Lines changed: 1602 additions & 52 deletions

base58/base58_test.go

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
package base58
2+
3+
import (
4+
"crypto/rand"
5+
"encoding/hex"
6+
"testing"
7+
8+
mrtronbase58 "github.com/mr-tron/base58"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
// Known test vectors cross-validated against multiple base58 implementations
14+
// (Bitcoin Core, bs58, mr-tron, five8). Any implementation that encodes these
15+
// bytes to the given strings — and decodes them back — is bit-compatible.
16+
var knownVectors32 = []struct {
17+
hex string
18+
b58 string
19+
}{
20+
{
21+
"0000000000000000000000000000000000000000000000000000000000000000",
22+
"11111111111111111111111111111111",
23+
},
24+
{
25+
"0000000000000000000000000000000000000000000000000000000000000001",
26+
"11111111111111111111111111111112",
27+
},
28+
{
29+
// Solana pubkey: 4cHoJNmLed5PBgFBezHmJkMJLEZrcTvr3aopjnYBRxUb
30+
"359d6209a1296a422463405b82829cf2f0a86b2e87077c80a74372841e185efc",
31+
"4cHoJNmLed5PBgFBezHmJkMJLEZrcTvr3aopjnYBRxUb",
32+
},
33+
}
34+
35+
var knownVectors64 = []struct {
36+
hex string
37+
b58 string
38+
}{
39+
{
40+
// Solana signature: 5YBLhMBLjhAHnEPnHKLLnVwHSfXGPJMCvKAfNsiaEw2T63edrYxVFHKUxRXfP6KA1HVo7c9JZ3LAJQR72giX7Cb
41+
// Hex cross-checked against Python's `base58` package.
42+
"03e9bb70b0ae091b4a3233dc952a2da569afaa0ae1c06aa7d3c2a4da2f2854ec76dfae30d9474b4593726761345bec7ce1a95812c1fa8ddc740314cb29fef458",
43+
"5YBLhMBLjhAHnEPnHKLLnVwHSfXGPJMCvKAfNsiaEw2T63edrYxVFHKUxRXfP6KA1HVo7c9JZ3LAJQR72giX7Cb",
44+
},
45+
}
46+
47+
func TestEncode32_KnownVectors(t *testing.T) {
48+
for _, tv := range knownVectors32 {
49+
raw, err := hex.DecodeString(tv.hex)
50+
require.NoError(t, err)
51+
var src [32]byte
52+
copy(src[:], raw)
53+
assert.Equal(t, tv.b58, Encode32(&src), "hex=%s", tv.hex)
54+
}
55+
}
56+
57+
func TestDecode32_KnownVectors(t *testing.T) {
58+
for _, tv := range knownVectors32 {
59+
expected, err := hex.DecodeString(tv.hex)
60+
require.NoError(t, err)
61+
var dst [32]byte
62+
err = Decode32(tv.b58, &dst)
63+
require.NoError(t, err)
64+
assert.Equal(t, expected, dst[:], "b58=%s", tv.b58)
65+
}
66+
}
67+
68+
func TestEncode64_KnownVectors(t *testing.T) {
69+
for _, tv := range knownVectors64 {
70+
raw, err := hex.DecodeString(tv.hex)
71+
require.NoError(t, err)
72+
var src [64]byte
73+
copy(src[:], raw)
74+
assert.Equal(t, tv.b58, Encode64(&src), "hex=%s", tv.hex)
75+
}
76+
}
77+
78+
func TestDecode64_KnownVectors(t *testing.T) {
79+
for _, tv := range knownVectors64 {
80+
expected, err := hex.DecodeString(tv.hex)
81+
require.NoError(t, err)
82+
var dst [64]byte
83+
err = Decode64(tv.b58, &dst)
84+
require.NoError(t, err)
85+
assert.Equal(t, expected, dst[:], "b58=%s", tv.b58)
86+
}
87+
}
88+
89+
func TestEncode32_Zeros(t *testing.T) {
90+
var src [32]byte
91+
assert.Equal(t, "11111111111111111111111111111111", Encode32(&src))
92+
}
93+
94+
func TestDecode32_Zeros(t *testing.T) {
95+
var dst [32]byte
96+
require.NoError(t, Decode32("11111111111111111111111111111111", &dst))
97+
assert.Equal(t, [32]byte{}, dst)
98+
}
99+
100+
func TestRoundtrip32_Random(t *testing.T) {
101+
// Cross-check the specialized fixed-size path against mr-tron's
102+
// well-tested general-purpose implementation.
103+
for range 1000 {
104+
var src [32]byte
105+
rand.Read(src[:])
106+
107+
encoded := Encode32(&src)
108+
assert.Equal(t, mrtronbase58.Encode(src[:]), encoded, "encode mismatch for %x", src)
109+
110+
var decoded [32]byte
111+
require.NoError(t, Decode32(encoded, &decoded))
112+
assert.Equal(t, src, decoded, "decode mismatch for %s", encoded)
113+
}
114+
}
115+
116+
func TestRoundtrip64_Random(t *testing.T) {
117+
for range 1000 {
118+
var src [64]byte
119+
rand.Read(src[:])
120+
121+
encoded := Encode64(&src)
122+
assert.Equal(t, mrtronbase58.Encode(src[:]), encoded, "encode mismatch for %x", src)
123+
124+
var decoded [64]byte
125+
require.NoError(t, Decode64(encoded, &decoded))
126+
assert.Equal(t, src, decoded, "decode mismatch for %s", encoded)
127+
}
128+
}
129+
130+
func TestAppendEncode32_ZeroAlloc(t *testing.T) {
131+
var src [32]byte
132+
rand.Read(src[:])
133+
expected := Encode32(&src)
134+
135+
// Pre-sized buffer: should not allocate.
136+
buf := make([]byte, 0, EncodedMaxLen32)
137+
buf = AppendEncode32(buf, &src)
138+
assert.Equal(t, expected, string(buf))
139+
140+
// Append to an existing buffer.
141+
prefix := []byte("pubkey=")
142+
buf2 := make([]byte, 0, len(prefix)+EncodedMaxLen32)
143+
buf2 = append(buf2, prefix...)
144+
buf2 = AppendEncode32(buf2, &src)
145+
assert.Equal(t, "pubkey="+expected, string(buf2))
146+
}
147+
148+
func TestAppendEncode64_ZeroAlloc(t *testing.T) {
149+
var src [64]byte
150+
rand.Read(src[:])
151+
expected := Encode64(&src)
152+
153+
buf := make([]byte, 0, EncodedMaxLen64)
154+
buf = AppendEncode64(buf, &src)
155+
assert.Equal(t, expected, string(buf))
156+
}
157+
158+
func TestDecode_InvalidChars(t *testing.T) {
159+
var dst [32]byte
160+
assert.Error(t, Decode32("0invalid", &dst)) // '0' is not in base58
161+
assert.Error(t, Decode32("I\x00nvalid", &dst))
162+
assert.Error(t, Decode32("Oinvalid", &dst)) // 'O' is not in base58
163+
}
164+
165+
// Benchmarks
166+
var (
167+
benchSrc32 [32]byte
168+
benchSrc64 [64]byte
169+
benchStr32 string
170+
benchStr64 string
171+
)
172+
173+
func init() {
174+
rand.Read(benchSrc32[:])
175+
rand.Read(benchSrc64[:])
176+
benchStr32 = Encode32(&benchSrc32)
177+
benchStr64 = Encode64(&benchSrc64)
178+
}
179+
180+
func BenchmarkBase58_Encode32(b *testing.B) {
181+
src := &benchSrc32
182+
b.SetBytes(32)
183+
for b.Loop() {
184+
Encode32(src)
185+
}
186+
}
187+
188+
func BenchmarkBase58_AppendEncode32(b *testing.B) {
189+
src := &benchSrc32
190+
buf := make([]byte, 0, EncodedMaxLen32)
191+
b.SetBytes(32)
192+
for b.Loop() {
193+
buf = AppendEncode32(buf[:0], src)
194+
}
195+
}
196+
197+
func BenchmarkBase58_AppendEncode64(b *testing.B) {
198+
src := &benchSrc64
199+
buf := make([]byte, 0, EncodedMaxLen64)
200+
b.SetBytes(64)
201+
for b.Loop() {
202+
buf = AppendEncode64(buf[:0], src)
203+
}
204+
}
205+
206+
func BenchmarkBase58_Decode32(b *testing.B) {
207+
var dst [32]byte
208+
b.SetBytes(32)
209+
for b.Loop() {
210+
Decode32(benchStr32, &dst)
211+
}
212+
}
213+
214+
func BenchmarkBase58_Encode64(b *testing.B) {
215+
src := &benchSrc64
216+
b.SetBytes(64)
217+
for b.Loop() {
218+
Encode64(src)
219+
}
220+
}
221+
222+
func BenchmarkBase58_Decode64(b *testing.B) {
223+
var dst [64]byte
224+
b.SetBytes(64)
225+
for b.Loop() {
226+
Decode64(benchStr64, &dst)
227+
}
228+
}

base58/decode.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package base58
2+
3+
import (
4+
"encoding/binary"
5+
"errors"
6+
)
7+
8+
var (
9+
ErrInvalidChar = errors.New("base58: invalid base58 character")
10+
ErrInvalidLength = errors.New("base58: invalid encoded length")
11+
ErrValueTooLarge = errors.New("base58: decoded value too large for output size")
12+
ErrLeadingZeros = errors.New("base58: leading '1' count does not match leading zero bytes")
13+
)
14+
15+
// Decode32 decodes a base58 string into a 32-byte array.
16+
func Decode32(encoded string, dst *[32]byte) error {
17+
encLen := len(encoded)
18+
if encLen == 0 || encLen > raw58Sz32 {
19+
return ErrInvalidLength
20+
}
21+
22+
var raw [raw58Sz32]byte
23+
offset := raw58Sz32 - encLen
24+
for i := range encLen {
25+
c := encoded[i]
26+
if c < '1' || c > 'z' {
27+
return ErrInvalidChar
28+
}
29+
digit := base58Inverse[c-'1']
30+
if digit == base58InvalidDigit {
31+
return ErrInvalidChar
32+
}
33+
raw[offset+i] = digit
34+
}
35+
36+
var intermediate [intermediateSz32]uint64
37+
for i := range intermediateSz32 {
38+
intermediate[i] = uint64(raw[5*i+0])*11316496 +
39+
uint64(raw[5*i+1])*195112 +
40+
uint64(raw[5*i+2])*3364 +
41+
uint64(raw[5*i+3])*58 +
42+
uint64(raw[5*i+4])
43+
}
44+
45+
// Matrix-vector multiply (assembly on arm64, Go on other archs).
46+
var bin [binarySz32]uint64
47+
decodeMatMul32(&intermediate, &bin)
48+
49+
for i := binarySz32 - 1; i >= 1; i-- {
50+
bin[i-1] += bin[i] >> 32
51+
bin[i] &= 0xFFFFFFFF
52+
}
53+
54+
if bin[0] > 0xFFFFFFFF {
55+
return ErrValueTooLarge
56+
}
57+
58+
for i := range binarySz32 {
59+
binary.BigEndian.PutUint32(dst[i*4:i*4+4], uint32(bin[i]))
60+
}
61+
62+
return validateLeadingZeros(encoded, dst[:])
63+
}
64+
65+
// Decode64 decodes a base58 string into a 64-byte array.
66+
func Decode64(encoded string, dst *[64]byte) error {
67+
encLen := len(encoded)
68+
if encLen == 0 || encLen > raw58Sz64 {
69+
return ErrInvalidLength
70+
}
71+
72+
var raw [raw58Sz64]byte
73+
offset := raw58Sz64 - encLen
74+
for i := range encLen {
75+
c := encoded[i]
76+
if c < '1' || c > 'z' {
77+
return ErrInvalidChar
78+
}
79+
digit := base58Inverse[c-'1']
80+
if digit == base58InvalidDigit {
81+
return ErrInvalidChar
82+
}
83+
raw[offset+i] = digit
84+
}
85+
86+
var intermediate [intermediateSz64]uint64
87+
for i := range intermediateSz64 {
88+
intermediate[i] = uint64(raw[5*i+0])*11316496 +
89+
uint64(raw[5*i+1])*195112 +
90+
uint64(raw[5*i+2])*3364 +
91+
uint64(raw[5*i+3])*58 +
92+
uint64(raw[5*i+4])
93+
}
94+
95+
// Plain uint64 accumulation — each product is ≤ 2^62 and the sum
96+
// of 18 terms stays under 2^64 (verified by Firedancer analysis).
97+
var bin [binarySz64]uint64
98+
for k := range binarySz64 {
99+
var acc uint64
100+
for i := range intermediateSz64 {
101+
acc += intermediate[i] * uint64(decTable64[i][k])
102+
}
103+
bin[k] = acc
104+
}
105+
106+
for i := binarySz64 - 1; i >= 1; i-- {
107+
bin[i-1] += bin[i] >> 32
108+
bin[i] &= 0xFFFFFFFF
109+
}
110+
111+
if bin[0] > 0xFFFFFFFF {
112+
return ErrValueTooLarge
113+
}
114+
115+
for i := range binarySz64 {
116+
binary.BigEndian.PutUint32(dst[i*4:i*4+4], uint32(bin[i]))
117+
}
118+
119+
return validateLeadingZeros(encoded, dst[:])
120+
}
121+
122+
// validateLeadingZeros verifies that the number of leading '1' characters in
123+
// the encoded input equals the number of leading zero bytes in the decoded
124+
// output. This is a required invariant of base58: each leading zero byte in
125+
// the raw value is represented by exactly one '1' in the encoding.
126+
func validateLeadingZeros(encoded string, dst []byte) error {
127+
inLeading1s := 0
128+
for i := 0; i < len(encoded) && encoded[i] == '1'; i++ {
129+
inLeading1s++
130+
}
131+
132+
outLeading0s := 0
133+
for _, b := range dst {
134+
if b != 0 {
135+
break
136+
}
137+
outLeading0s++
138+
}
139+
140+
if inLeading1s != outLeading0s {
141+
return ErrLeadingZeros
142+
}
143+
return nil
144+
}

0 commit comments

Comments
 (0)