Skip to content

Commit 689623d

Browse files
perf: use fd_base58 algo
1 parent 4fad955 commit 689623d

14 files changed

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

0 commit comments

Comments
 (0)