diff --git a/base58/base58_test.go b/base58/base58_test.go new file mode 100644 index 000000000..5033c8a3c --- /dev/null +++ b/base58/base58_test.go @@ -0,0 +1,228 @@ +package base58 + +import ( + "crypto/rand" + "encoding/hex" + "testing" + + mrtronbase58 "github.com/mr-tron/base58" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Known test vectors cross-validated against multiple base58 implementations +// (Bitcoin Core, bs58, mr-tron, five8). Any implementation that encodes these +// bytes to the given strings — and decodes them back — is bit-compatible. +var knownVectors32 = []struct { + hex string + b58 string +}{ + { + "0000000000000000000000000000000000000000000000000000000000000000", + "11111111111111111111111111111111", + }, + { + "0000000000000000000000000000000000000000000000000000000000000001", + "11111111111111111111111111111112", + }, + { + // Solana pubkey: 4cHoJNmLed5PBgFBezHmJkMJLEZrcTvr3aopjnYBRxUb + "359d6209a1296a422463405b82829cf2f0a86b2e87077c80a74372841e185efc", + "4cHoJNmLed5PBgFBezHmJkMJLEZrcTvr3aopjnYBRxUb", + }, +} + +var knownVectors64 = []struct { + hex string + b58 string +}{ + { + // Solana signature: 5YBLhMBLjhAHnEPnHKLLnVwHSfXGPJMCvKAfNsiaEw2T63edrYxVFHKUxRXfP6KA1HVo7c9JZ3LAJQR72giX7Cb + // Hex cross-checked against Python's `base58` package. + "03e9bb70b0ae091b4a3233dc952a2da569afaa0ae1c06aa7d3c2a4da2f2854ec76dfae30d9474b4593726761345bec7ce1a95812c1fa8ddc740314cb29fef458", + "5YBLhMBLjhAHnEPnHKLLnVwHSfXGPJMCvKAfNsiaEw2T63edrYxVFHKUxRXfP6KA1HVo7c9JZ3LAJQR72giX7Cb", + }, +} + +func TestEncode32_KnownVectors(t *testing.T) { + for _, tv := range knownVectors32 { + raw, err := hex.DecodeString(tv.hex) + require.NoError(t, err) + var src [32]byte + copy(src[:], raw) + assert.Equal(t, tv.b58, Encode32(&src), "hex=%s", tv.hex) + } +} + +func TestDecode32_KnownVectors(t *testing.T) { + for _, tv := range knownVectors32 { + expected, err := hex.DecodeString(tv.hex) + require.NoError(t, err) + var dst [32]byte + err = Decode32(tv.b58, &dst) + require.NoError(t, err) + assert.Equal(t, expected, dst[:], "b58=%s", tv.b58) + } +} + +func TestEncode64_KnownVectors(t *testing.T) { + for _, tv := range knownVectors64 { + raw, err := hex.DecodeString(tv.hex) + require.NoError(t, err) + var src [64]byte + copy(src[:], raw) + assert.Equal(t, tv.b58, Encode64(&src), "hex=%s", tv.hex) + } +} + +func TestDecode64_KnownVectors(t *testing.T) { + for _, tv := range knownVectors64 { + expected, err := hex.DecodeString(tv.hex) + require.NoError(t, err) + var dst [64]byte + err = Decode64(tv.b58, &dst) + require.NoError(t, err) + assert.Equal(t, expected, dst[:], "b58=%s", tv.b58) + } +} + +func TestEncode32_Zeros(t *testing.T) { + var src [32]byte + assert.Equal(t, "11111111111111111111111111111111", Encode32(&src)) +} + +func TestDecode32_Zeros(t *testing.T) { + var dst [32]byte + require.NoError(t, Decode32("11111111111111111111111111111111", &dst)) + assert.Equal(t, [32]byte{}, dst) +} + +func TestRoundtrip32_Random(t *testing.T) { + // Cross-check the specialized fixed-size path against mr-tron's + // well-tested general-purpose implementation. + for range 1000 { + var src [32]byte + rand.Read(src[:]) + + encoded := Encode32(&src) + assert.Equal(t, mrtronbase58.Encode(src[:]), encoded, "encode mismatch for %x", src) + + var decoded [32]byte + require.NoError(t, Decode32(encoded, &decoded)) + assert.Equal(t, src, decoded, "decode mismatch for %s", encoded) + } +} + +func TestRoundtrip64_Random(t *testing.T) { + for range 1000 { + var src [64]byte + rand.Read(src[:]) + + encoded := Encode64(&src) + assert.Equal(t, mrtronbase58.Encode(src[:]), encoded, "encode mismatch for %x", src) + + var decoded [64]byte + require.NoError(t, Decode64(encoded, &decoded)) + assert.Equal(t, src, decoded, "decode mismatch for %s", encoded) + } +} + +func TestAppendEncode32_ZeroAlloc(t *testing.T) { + var src [32]byte + rand.Read(src[:]) + expected := Encode32(&src) + + // Pre-sized buffer: should not allocate. + buf := make([]byte, 0, EncodedMaxLen32) + buf = AppendEncode32(buf, &src) + assert.Equal(t, expected, string(buf)) + + // Append to an existing buffer. + prefix := []byte("pubkey=") + buf2 := make([]byte, 0, len(prefix)+EncodedMaxLen32) + buf2 = append(buf2, prefix...) + buf2 = AppendEncode32(buf2, &src) + assert.Equal(t, "pubkey="+expected, string(buf2)) +} + +func TestAppendEncode64_ZeroAlloc(t *testing.T) { + var src [64]byte + rand.Read(src[:]) + expected := Encode64(&src) + + buf := make([]byte, 0, EncodedMaxLen64) + buf = AppendEncode64(buf, &src) + assert.Equal(t, expected, string(buf)) +} + +func TestDecode_InvalidChars(t *testing.T) { + var dst [32]byte + assert.Error(t, Decode32("0invalid", &dst)) // '0' is not in base58 + assert.Error(t, Decode32("I\x00nvalid", &dst)) + assert.Error(t, Decode32("Oinvalid", &dst)) // 'O' is not in base58 +} + +// Benchmarks +var ( + benchSrc32 [32]byte + benchSrc64 [64]byte + benchStr32 string + benchStr64 string +) + +func init() { + rand.Read(benchSrc32[:]) + rand.Read(benchSrc64[:]) + benchStr32 = Encode32(&benchSrc32) + benchStr64 = Encode64(&benchSrc64) +} + +func BenchmarkBase58_Encode32(b *testing.B) { + src := &benchSrc32 + b.SetBytes(32) + for b.Loop() { + Encode32(src) + } +} + +func BenchmarkBase58_AppendEncode32(b *testing.B) { + src := &benchSrc32 + buf := make([]byte, 0, EncodedMaxLen32) + b.SetBytes(32) + for b.Loop() { + buf = AppendEncode32(buf[:0], src) + } +} + +func BenchmarkBase58_AppendEncode64(b *testing.B) { + src := &benchSrc64 + buf := make([]byte, 0, EncodedMaxLen64) + b.SetBytes(64) + for b.Loop() { + buf = AppendEncode64(buf[:0], src) + } +} + +func BenchmarkBase58_Decode32(b *testing.B) { + var dst [32]byte + b.SetBytes(32) + for b.Loop() { + Decode32(benchStr32, &dst) + } +} + +func BenchmarkBase58_Encode64(b *testing.B) { + src := &benchSrc64 + b.SetBytes(64) + for b.Loop() { + Encode64(src) + } +} + +func BenchmarkBase58_Decode64(b *testing.B) { + var dst [64]byte + b.SetBytes(64) + for b.Loop() { + Decode64(benchStr64, &dst) + } +} diff --git a/base58/decode.go b/base58/decode.go new file mode 100644 index 000000000..116f6d657 --- /dev/null +++ b/base58/decode.go @@ -0,0 +1,144 @@ +package base58 + +import ( + "encoding/binary" + "errors" +) + +var ( + ErrInvalidChar = errors.New("base58: invalid base58 character") + ErrInvalidLength = errors.New("base58: invalid encoded length") + ErrValueTooLarge = errors.New("base58: decoded value too large for output size") + ErrLeadingZeros = errors.New("base58: leading '1' count does not match leading zero bytes") +) + +// Decode32 decodes a base58 string into a 32-byte array. +func Decode32(encoded string, dst *[32]byte) error { + encLen := len(encoded) + if encLen == 0 || encLen > raw58Sz32 { + return ErrInvalidLength + } + + var raw [raw58Sz32]byte + offset := raw58Sz32 - encLen + for i := range encLen { + c := encoded[i] + if c < '1' || c > 'z' { + return ErrInvalidChar + } + digit := base58Inverse[c-'1'] + if digit == base58InvalidDigit { + return ErrInvalidChar + } + raw[offset+i] = digit + } + + var intermediate [intermediateSz32]uint64 + for i := range intermediateSz32 { + intermediate[i] = uint64(raw[5*i+0])*11316496 + + uint64(raw[5*i+1])*195112 + + uint64(raw[5*i+2])*3364 + + uint64(raw[5*i+3])*58 + + uint64(raw[5*i+4]) + } + + // Matrix-vector multiply (assembly on arm64, Go on other archs). + var bin [binarySz32]uint64 + decodeMatMul32(&intermediate, &bin) + + for i := binarySz32 - 1; i >= 1; i-- { + bin[i-1] += bin[i] >> 32 + bin[i] &= 0xFFFFFFFF + } + + if bin[0] > 0xFFFFFFFF { + return ErrValueTooLarge + } + + for i := range binarySz32 { + binary.BigEndian.PutUint32(dst[i*4:i*4+4], uint32(bin[i])) + } + + return validateLeadingZeros(encoded, dst[:]) +} + +// Decode64 decodes a base58 string into a 64-byte array. +func Decode64(encoded string, dst *[64]byte) error { + encLen := len(encoded) + if encLen == 0 || encLen > raw58Sz64 { + return ErrInvalidLength + } + + var raw [raw58Sz64]byte + offset := raw58Sz64 - encLen + for i := range encLen { + c := encoded[i] + if c < '1' || c > 'z' { + return ErrInvalidChar + } + digit := base58Inverse[c-'1'] + if digit == base58InvalidDigit { + return ErrInvalidChar + } + raw[offset+i] = digit + } + + var intermediate [intermediateSz64]uint64 + for i := range intermediateSz64 { + intermediate[i] = uint64(raw[5*i+0])*11316496 + + uint64(raw[5*i+1])*195112 + + uint64(raw[5*i+2])*3364 + + uint64(raw[5*i+3])*58 + + uint64(raw[5*i+4]) + } + + // Plain uint64 accumulation — each product is ≤ 2^62 and the sum + // of 18 terms stays under 2^64 (verified by Firedancer analysis). + var bin [binarySz64]uint64 + for k := range binarySz64 { + var acc uint64 + for i := range intermediateSz64 { + acc += intermediate[i] * uint64(decTable64[i][k]) + } + bin[k] = acc + } + + for i := binarySz64 - 1; i >= 1; i-- { + bin[i-1] += bin[i] >> 32 + bin[i] &= 0xFFFFFFFF + } + + if bin[0] > 0xFFFFFFFF { + return ErrValueTooLarge + } + + for i := range binarySz64 { + binary.BigEndian.PutUint32(dst[i*4:i*4+4], uint32(bin[i])) + } + + return validateLeadingZeros(encoded, dst[:]) +} + +// validateLeadingZeros verifies that the number of leading '1' characters in +// the encoded input equals the number of leading zero bytes in the decoded +// output. This is a required invariant of base58: each leading zero byte in +// the raw value is represented by exactly one '1' in the encoding. +func validateLeadingZeros(encoded string, dst []byte) error { + inLeading1s := 0 + for i := 0; i < len(encoded) && encoded[i] == '1'; i++ { + inLeading1s++ + } + + outLeading0s := 0 + for _, b := range dst { + if b != 0 { + break + } + outLeading0s++ + } + + if inLeading1s != outLeading0s { + return ErrLeadingZeros + } + return nil +} diff --git a/base58/encode.go b/base58/encode.go new file mode 100644 index 000000000..f323a2eaa --- /dev/null +++ b/base58/encode.go @@ -0,0 +1,192 @@ +package base58 + +import ( + "encoding/binary" + "unsafe" +) + +// Encode32 encodes a 32-byte array to a base58 string. +// +// Allocates exactly one []byte of the encoded length. For zero-allocation +// hot paths, prefer AppendEncode32 which writes into a caller-owned buffer. +func Encode32(src *[32]byte) string { + var raw [raw58Sz32]byte + skip := encodeRaw32(src, &raw) + outLen := raw58Sz32 - skip + out := make([]byte, outLen) + for i := range outLen { + out[i] = base58Chars[raw[skip+i]] + } + return unsafe.String(unsafe.SliceData(out), len(out)) +} + +// Encode64 encodes a 64-byte array to a base58 string. +// +// Allocates exactly one []byte of the encoded length. For zero-allocation +// hot paths, prefer AppendEncode64. +func Encode64(src *[64]byte) string { + var raw [raw58Sz64]byte + skip := encodeRaw64(src, &raw) + outLen := raw58Sz64 - skip + out := make([]byte, outLen) + for i := range outLen { + out[i] = base58Chars[raw[skip+i]] + } + return unsafe.String(unsafe.SliceData(out), len(out)) +} + +// AppendEncode32 appends the base58 encoding of src to dst and returns the +// extended buffer. It allocates only if dst has insufficient capacity. +func AppendEncode32(dst []byte, src *[32]byte) []byte { + var raw [raw58Sz32]byte + skip := encodeRaw32(src, &raw) + outLen := raw58Sz32 - skip + // Grow dst in place if possible; otherwise allocate. + total := len(dst) + outLen + if cap(dst) < total { + grown := make([]byte, total) + copy(grown, dst) + dst = grown + } else { + dst = dst[:total] + } + out := dst[total-outLen:] + for i := range outLen { + out[i] = base58Chars[raw[skip+i]] + } + return dst +} + +// AppendEncode64 appends the base58 encoding of src to dst and returns the +// extended buffer. It allocates only if dst has insufficient capacity. +func AppendEncode64(dst []byte, src *[64]byte) []byte { + var raw [raw58Sz64]byte + skip := encodeRaw64(src, &raw) + outLen := raw58Sz64 - skip + total := len(dst) + outLen + if cap(dst) < total { + grown := make([]byte, total) + copy(grown, dst) + dst = grown + } else { + dst = dst[:total] + } + out := dst[total-outLen:] + for i := range outLen { + out[i] = base58Chars[raw[skip+i]] + } + return dst +} + +// encodeRaw32 fills raw with the raw base-58 digits for a 32-byte input and +// returns the number of leading digits to skip when producing the final output. +func encodeRaw32(src *[32]byte, raw *[raw58Sz32]byte) int { + var intermediate [intermediateSz32]uint64 + encodeMatMul32(src, &intermediate) + + for i := intermediateSz32 - 1; i >= 1; i-- { + intermediate[i-1] += intermediate[i] / r1div + intermediate[i] %= r1div + } + + for i := range intermediateSz32 { + v := uint32(intermediate[i]) + raw[5*i+4] = byte(v % 58) + v /= 58 + raw[5*i+3] = byte(v % 58) + v /= 58 + raw[5*i+2] = byte(v % 58) + v /= 58 + raw[5*i+1] = byte(v % 58) + v /= 58 + raw[5*i+0] = byte(v) + } + + inLeading0s := 0 + for _, b := range src { + if b != 0 { + break + } + inLeading0s++ + } + + rawLeading0s := 0 + for _, b := range raw { + if b != 0 { + break + } + rawLeading0s++ + } + + return rawLeading0s - inLeading0s +} + +// encodeRaw64 fills raw with the raw base-58 digits for a 64-byte input and +// returns the number of leading digits to skip. +// +// The accumulation uses plain uint64 arithmetic. Each product is u32×u32 so +// it fits in u64. After the first 8 input limbs a mini-reduction prevents +// overflow before adding the remaining 8 limbs (matches Firedancer). +func encodeRaw64(src *[64]byte, raw *[raw58Sz64]byte) int { + var bin [binarySz64]uint32 + for i := range binarySz64 { + bin[i] = binary.BigEndian.Uint32(src[i*4 : i*4+4]) + } + + var intermediate [intermediateSz64]uint64 + + // First 8 limbs. + for i := 0; i < 8; i++ { + for k := range intermediateSz64 - 1 { + intermediate[k+1] += uint64(bin[i]) * uint64(encTable64[i][k]) + } + } + + // Mini-reduction to prevent overflow before the second half. + intermediate[intermediateSz64-3] += intermediate[intermediateSz64-2] / r1div + intermediate[intermediateSz64-2] %= r1div + + // Last 8 limbs. + for i := 8; i < binarySz64; i++ { + for k := range intermediateSz64 - 1 { + intermediate[k+1] += uint64(bin[i]) * uint64(encTable64[i][k]) + } + } + + // Full carry propagation. + for i := intermediateSz64 - 1; i >= 1; i-- { + intermediate[i-1] += intermediate[i] / r1div + intermediate[i] %= r1div + } + + for i := range intermediateSz64 { + v := uint32(intermediate[i]) + raw[5*i+4] = byte(v % 58) + v /= 58 + raw[5*i+3] = byte(v % 58) + v /= 58 + raw[5*i+2] = byte(v % 58) + v /= 58 + raw[5*i+1] = byte(v % 58) + v /= 58 + raw[5*i+0] = byte(v) + } + + inLeading0s := 0 + for _, b := range src { + if b != 0 { + break + } + inLeading0s++ + } + + rawLeading0s := 0 + for _, b := range raw { + if b != 0 { + break + } + rawLeading0s++ + } + + return rawLeading0s - inLeading0s +} diff --git a/base58/fuzz_test.go b/base58/fuzz_test.go new file mode 100644 index 000000000..9a33577b1 --- /dev/null +++ b/base58/fuzz_test.go @@ -0,0 +1,149 @@ +package base58 + +import ( + "bytes" + "testing" + + mrtronbase58 "github.com/mr-tron/base58" +) + +// --- Encode fuzz: our output must match mr-tron for every input --- + +func FuzzEncode32_MatchesMrTron(f *testing.F) { + f.Add(make([]byte, 32)) // all zeros + f.Add(bytes.Repeat([]byte{0xff}, 32)) // all 0xFF + f.Add(append([]byte{1}, make([]byte, 31)...)) // single leading byte + f.Add(append(make([]byte, 31), 1)) // trailing 1 + + f.Fuzz(func(t *testing.T, data []byte) { + if len(data) != 32 { + t.Skip() + } + var src [32]byte + copy(src[:], data) + + ours := Encode32(&src) + theirs := mrtronbase58.Encode(src[:]) + if ours != theirs { + t.Fatalf("Encode32 mismatch for %x:\n ours: %s\n theirs: %s", src, ours, theirs) + } + }) +} + +func FuzzEncode64_MatchesMrTron(f *testing.F) { + f.Add(make([]byte, 64)) + f.Add(bytes.Repeat([]byte{0xff}, 64)) + f.Add(append([]byte{1}, make([]byte, 63)...)) + + f.Fuzz(func(t *testing.T, data []byte) { + if len(data) != 64 { + t.Skip() + } + var src [64]byte + copy(src[:], data) + + ours := Encode64(&src) + theirs := mrtronbase58.Encode(src[:]) + if ours != theirs { + t.Fatalf("Encode64 mismatch for %x:\n ours: %s\n theirs: %s", src, ours, theirs) + } + }) +} + +// --- Decode fuzz: round-trip through mr-tron must agree --- + +func FuzzDecode32_MatchesMrTron(f *testing.F) { + f.Add("11111111111111111111111111111111") + f.Add("11111111111111111111111111111112") + f.Add("4cHoJNmLed5PBgFBezHmJkMJLEZrcTvr3aopjnYBRxUb") + + f.Fuzz(func(t *testing.T, encoded string) { + var dst [32]byte + err := Decode32(encoded, &dst) + if err != nil { + // We're stricter than mr-tron (fixed size, leading-zero + // validation). Just verify we don't panic. + return + } + + // If we accepted it, mr-tron must decode to the same bytes. + theirBytes, theirErr := mrtronbase58.Decode(encoded) + if theirErr != nil { + t.Fatalf("we accepted %q but mr-tron rejected it: %v", encoded, theirErr) + } + + // mr-tron strips leading zeros; pad to compare. + padded := make([]byte, 32) + copy(padded[32-len(theirBytes):], theirBytes) + if !bytes.Equal(dst[:], padded) { + t.Fatalf("decode mismatch for %q:\n ours: %x\n theirs: %x", encoded, dst, padded) + } + + // Re-encode must produce the original string. + reEncoded := Encode32(&dst) + if reEncoded != encoded { + t.Fatalf("round-trip mismatch: %q -> %x -> %q", encoded, dst, reEncoded) + } + }) +} + +func FuzzDecode64_MatchesMrTron(f *testing.F) { + f.Add("5YBLhMBLjhAHnEPnHKLLnVwHSfXGPJMCvKAfNsiaEw2T63edrYxVFHKUxRXfP6KA1HVo7c9JZ3LAJQR72giX7Cb") + + f.Fuzz(func(t *testing.T, encoded string) { + var dst [64]byte + err := Decode64(encoded, &dst) + if err != nil { + return + } + + theirBytes, theirErr := mrtronbase58.Decode(encoded) + if theirErr != nil { + t.Fatalf("we accepted %q but mr-tron rejected it: %v", encoded, theirErr) + } + + padded := make([]byte, 64) + copy(padded[64-len(theirBytes):], theirBytes) + if !bytes.Equal(dst[:], padded) { + t.Fatalf("decode mismatch for %q:\n ours: %x\n theirs: %x", encoded, dst, padded) + } + + reEncoded := Encode64(&dst) + if reEncoded != encoded { + t.Fatalf("round-trip mismatch: %q -> %x -> %q", encoded, dst, reEncoded) + } + }) +} + +// --- Invalid input fuzz: verify we never panic --- + +func FuzzDecode32_NoPanic(f *testing.F) { + f.Add([]byte("")) + f.Add([]byte("0")) // invalid char + f.Add([]byte("O")) // invalid char + f.Add([]byte("I")) // invalid char + f.Add([]byte("l")) // invalid char + f.Add([]byte("\x00")) // null byte + f.Add([]byte("\xff")) // high byte + f.Add(bytes.Repeat([]byte("z"), 45)) // too long + f.Add(bytes.Repeat([]byte("1"), 50)) // way too long + + f.Fuzz(func(t *testing.T, data []byte) { + var dst [32]byte + // Must not panic regardless of input. + Decode32(string(data), &dst) + }) +} + +func FuzzDecode64_NoPanic(f *testing.F) { + f.Add([]byte("")) + f.Add([]byte("0")) + f.Add([]byte("\x00")) + f.Add([]byte("\xff")) + f.Add(bytes.Repeat([]byte("z"), 91)) + + f.Fuzz(func(t *testing.T, data []byte) { + var dst [64]byte + Decode64(string(data), &dst) + }) +} diff --git a/base58/matmul_amd64.go b/base58/matmul_amd64.go new file mode 100644 index 000000000..f2f254cac --- /dev/null +++ b/base58/matmul_amd64.go @@ -0,0 +1,13 @@ +//go:build amd64 + +package base58 + +// Only the 32-byte matrix multiply has an assembly path; the 64-byte +// path uses extended-precision arithmetic via math/bits which the Go +// compiler lowers to optimal MULQ/ADCQ sequences. + +//go:noescape +func encodeMatMul32(src *[32]byte, intermediate *[intermediateSz32]uint64) + +//go:noescape +func decodeMatMul32(intermediate *[intermediateSz32]uint64, bin *[binarySz32]uint64) diff --git a/base58/matmul_amd64.s b/base58/matmul_amd64.s new file mode 100644 index 000000000..124f88b8b --- /dev/null +++ b/base58/matmul_amd64.s @@ -0,0 +1,355 @@ +#include "textflag.h" + +// func encodeMatMul32(src *[32]byte, intermediate *[9]uint64) +// +// Byte-swaps src into 8 x uint32, then computes: +// intermediate[k+1] += bin[i] * encTable32[i][k] for i,k in 0..7 +// +// All 8 accumulators live in R8..R15. bin[i] is loaded fresh per row into BX. +// Table entries are 32-bit, loaded into AX with MOVL (zero-extending to 64 bits). +// Fully unrolled, zero table entries skipped. +// +// Register map: +// SI = src pointer +// DI = intermediate pointer +// DX = encTable32 base +// BX = current bin[i] (zero-extended u32) +// AX = scratch: table entry, then product +// R8..R15 = intermediate[1..8] accumulators +TEXT ·encodeMatMul32(SB), NOSPLIT|NOFRAME, $0-16 + MOVQ src+0(FP), SI + MOVQ intermediate+8(FP), DI + LEAQ ·encTable32(SB), DX + + // Zero accumulators. + XORQ R8, R8 + XORQ R9, R9 + XORQ R10, R10 + XORQ R11, R11 + XORQ R12, R12 + XORQ R13, R13 + XORQ R14, R14 + XORQ R15, R15 + + // Row 0: bin[0] = bswap(src[0..4]) + MOVL 0(SI), BX + BSWAPL BX + MOVL 0(DX), AX + IMULQ BX, AX + ADDQ AX, R8 + MOVL 4(DX), AX + IMULQ BX, AX + ADDQ AX, R9 + MOVL 8(DX), AX + IMULQ BX, AX + ADDQ AX, R10 + MOVL 12(DX), AX + IMULQ BX, AX + ADDQ AX, R11 + MOVL 16(DX), AX + IMULQ BX, AX + ADDQ AX, R12 + MOVL 20(DX), AX + IMULQ BX, AX + ADDQ AX, R13 + MOVL 24(DX), AX + IMULQ BX, AX + ADDQ AX, R14 + MOVL 28(DX), AX + IMULQ BX, AX + ADDQ AX, R15 + + // Row 1: bin[1], k=1..7 (table[1][0] = 0) + MOVL 4(SI), BX + BSWAPL BX + MOVL 36(DX), AX + IMULQ BX, AX + ADDQ AX, R9 + MOVL 40(DX), AX + IMULQ BX, AX + ADDQ AX, R10 + MOVL 44(DX), AX + IMULQ BX, AX + ADDQ AX, R11 + MOVL 48(DX), AX + IMULQ BX, AX + ADDQ AX, R12 + MOVL 52(DX), AX + IMULQ BX, AX + ADDQ AX, R13 + MOVL 56(DX), AX + IMULQ BX, AX + ADDQ AX, R14 + MOVL 60(DX), AX + IMULQ BX, AX + ADDQ AX, R15 + + // Row 2: bin[2], k=2..7 + MOVL 8(SI), BX + BSWAPL BX + MOVL 72(DX), AX + IMULQ BX, AX + ADDQ AX, R10 + MOVL 76(DX), AX + IMULQ BX, AX + ADDQ AX, R11 + MOVL 80(DX), AX + IMULQ BX, AX + ADDQ AX, R12 + MOVL 84(DX), AX + IMULQ BX, AX + ADDQ AX, R13 + MOVL 88(DX), AX + IMULQ BX, AX + ADDQ AX, R14 + MOVL 92(DX), AX + IMULQ BX, AX + ADDQ AX, R15 + + // Row 3: bin[3], k=3..7 + MOVL 12(SI), BX + BSWAPL BX + MOVL 108(DX), AX + IMULQ BX, AX + ADDQ AX, R11 + MOVL 112(DX), AX + IMULQ BX, AX + ADDQ AX, R12 + MOVL 116(DX), AX + IMULQ BX, AX + ADDQ AX, R13 + MOVL 120(DX), AX + IMULQ BX, AX + ADDQ AX, R14 + MOVL 124(DX), AX + IMULQ BX, AX + ADDQ AX, R15 + + // Row 4: bin[4], k=4..7 + MOVL 16(SI), BX + BSWAPL BX + MOVL 144(DX), AX + IMULQ BX, AX + ADDQ AX, R12 + MOVL 148(DX), AX + IMULQ BX, AX + ADDQ AX, R13 + MOVL 152(DX), AX + IMULQ BX, AX + ADDQ AX, R14 + MOVL 156(DX), AX + IMULQ BX, AX + ADDQ AX, R15 + + // Row 5: bin[5], k=5..7 + MOVL 20(SI), BX + BSWAPL BX + MOVL 180(DX), AX + IMULQ BX, AX + ADDQ AX, R13 + MOVL 184(DX), AX + IMULQ BX, AX + ADDQ AX, R14 + MOVL 188(DX), AX + IMULQ BX, AX + ADDQ AX, R15 + + // Row 6: bin[6], k=6..7 + MOVL 24(SI), BX + BSWAPL BX + MOVL 216(DX), AX + IMULQ BX, AX + ADDQ AX, R14 + MOVL 220(DX), AX + IMULQ BX, AX + ADDQ AX, R15 + + // Row 7: bin[7], k=7 only + MOVL 28(SI), BX + BSWAPL BX + MOVL 252(DX), AX + IMULQ BX, AX + ADDQ AX, R15 + + // Store intermediate[0..8]. intermediate[0] = 0. + MOVQ $0, 0(DI) + MOVQ R8, 8(DI) + MOVQ R9, 16(DI) + MOVQ R10, 24(DI) + MOVQ R11, 32(DI) + MOVQ R12, 40(DI) + MOVQ R13, 48(DI) + MOVQ R14, 56(DI) + MOVQ R15, 64(DI) + RET + +// func decodeMatMul32(intermediate *[9]uint64, bin *[8]uint64) +// +// Computes: bin[k] = sum_i intermediate[i] * decTable32[i][k] +// +// intermediate[i] values are guaranteed < 58^5 (< 2^30), so the low 32 bits +// of each intermediate contain the full value. We load with MOVL for zero- +// extension. +// +// Register map: +// SI = intermediate pointer +// DI = bin pointer +// DX = decTable32 base +// BX = current intermediate[i] +// AX = scratch: table entry, then product +// R8..R15 = bin[0..7] accumulators +TEXT ·decodeMatMul32(SB), NOSPLIT|NOFRAME, $0-16 + MOVQ intermediate+0(FP), SI + MOVQ bin+8(FP), DI + LEAQ ·decTable32(SB), DX + + // Zero accumulators. + XORQ R8, R8 + XORQ R9, R9 + XORQ R10, R10 + XORQ R11, R11 + XORQ R12, R12 + XORQ R13, R13 + XORQ R14, R14 + XORQ R15, R15 + + // Row 0: intermediate[0], k=0..6 (table[0][7] = 0) + MOVL 0(SI), BX + MOVL 0(DX), AX + IMULQ BX, AX + ADDQ AX, R8 + MOVL 4(DX), AX + IMULQ BX, AX + ADDQ AX, R9 + MOVL 8(DX), AX + IMULQ BX, AX + ADDQ AX, R10 + MOVL 12(DX), AX + IMULQ BX, AX + ADDQ AX, R11 + MOVL 16(DX), AX + IMULQ BX, AX + ADDQ AX, R12 + MOVL 20(DX), AX + IMULQ BX, AX + ADDQ AX, R13 + MOVL 24(DX), AX + IMULQ BX, AX + ADDQ AX, R14 + + // Row 1: intermediate[1], k=1..6 (table[1][0] = 0, table[1][7] = 0) + MOVL 8(SI), BX + MOVL 36(DX), AX + IMULQ BX, AX + ADDQ AX, R9 + MOVL 40(DX), AX + IMULQ BX, AX + ADDQ AX, R10 + MOVL 44(DX), AX + IMULQ BX, AX + ADDQ AX, R11 + MOVL 48(DX), AX + IMULQ BX, AX + ADDQ AX, R12 + MOVL 52(DX), AX + IMULQ BX, AX + ADDQ AX, R13 + MOVL 56(DX), AX + IMULQ BX, AX + ADDQ AX, R14 + + // Row 2: intermediate[2], k=2..7 + MOVL 16(SI), BX + MOVL 72(DX), AX + IMULQ BX, AX + ADDQ AX, R10 + MOVL 76(DX), AX + IMULQ BX, AX + ADDQ AX, R11 + MOVL 80(DX), AX + IMULQ BX, AX + ADDQ AX, R12 + MOVL 84(DX), AX + IMULQ BX, AX + ADDQ AX, R13 + MOVL 88(DX), AX + IMULQ BX, AX + ADDQ AX, R14 + MOVL 92(DX), AX + IMULQ BX, AX + ADDQ AX, R15 + + // Row 3: intermediate[3], k=3..7 + MOVL 24(SI), BX + MOVL 108(DX), AX + IMULQ BX, AX + ADDQ AX, R11 + MOVL 112(DX), AX + IMULQ BX, AX + ADDQ AX, R12 + MOVL 116(DX), AX + IMULQ BX, AX + ADDQ AX, R13 + MOVL 120(DX), AX + IMULQ BX, AX + ADDQ AX, R14 + MOVL 124(DX), AX + IMULQ BX, AX + ADDQ AX, R15 + + // Row 4: intermediate[4], k=4..7 + MOVL 32(SI), BX + MOVL 144(DX), AX + IMULQ BX, AX + ADDQ AX, R12 + MOVL 148(DX), AX + IMULQ BX, AX + ADDQ AX, R13 + MOVL 152(DX), AX + IMULQ BX, AX + ADDQ AX, R14 + MOVL 156(DX), AX + IMULQ BX, AX + ADDQ AX, R15 + + // Row 5: intermediate[5], k=5..7 + MOVL 40(SI), BX + MOVL 180(DX), AX + IMULQ BX, AX + ADDQ AX, R13 + MOVL 184(DX), AX + IMULQ BX, AX + ADDQ AX, R14 + MOVL 188(DX), AX + IMULQ BX, AX + ADDQ AX, R15 + + // Row 6: intermediate[6], k=6..7 + MOVL 48(SI), BX + MOVL 216(DX), AX + IMULQ BX, AX + ADDQ AX, R14 + MOVL 220(DX), AX + IMULQ BX, AX + ADDQ AX, R15 + + // Row 7: intermediate[7], k=7 only + MOVL 56(SI), BX + MOVL 252(DX), AX + IMULQ BX, AX + ADDQ AX, R15 + + // Row 8: table[8] = {0,0,0,0,0,0,0,1} → bin[7] += intermediate[8] + MOVQ 64(SI), BX + ADDQ BX, R15 + + // Store bin[0..7]. + MOVQ R8, 0(DI) + MOVQ R9, 8(DI) + MOVQ R10, 16(DI) + MOVQ R11, 24(DI) + MOVQ R12, 32(DI) + MOVQ R13, 40(DI) + MOVQ R14, 48(DI) + MOVQ R15, 56(DI) + RET diff --git a/base58/matmul_arm64.go b/base58/matmul_arm64.go new file mode 100644 index 000000000..7888783ba --- /dev/null +++ b/base58/matmul_arm64.go @@ -0,0 +1,13 @@ +//go:build arm64 + +package base58 + +// Only the 32-byte matrix multiply has an assembly path; the 64-byte +// path uses extended-precision arithmetic via math/bits which the Go +// compiler lowers to optimal UMULH/ADCS sequences. + +//go:noescape +func encodeMatMul32(src *[32]byte, intermediate *[intermediateSz32]uint64) + +//go:noescape +func decodeMatMul32(intermediate *[intermediateSz32]uint64, bin *[binarySz32]uint64) diff --git a/base58/matmul_arm64.s b/base58/matmul_arm64.s new file mode 100644 index 000000000..a0540e30e --- /dev/null +++ b/base58/matmul_arm64.s @@ -0,0 +1,273 @@ +#include "textflag.h" + +// func encodeMatMul32(src *[32]byte, intermediate *[9]uint64) +// +// Byte-swaps src into 8 x uint32, then computes the 8x8 matrix-vector +// multiply: intermediate[k+1] += bin[i] * encTable32[i][k] +// +// All intermediate values kept in registers to avoid memory traffic. +// Fully unrolled, zero entries skipped. +// +// ARM64 MADD semantics in Go asm: MADD Rm, Ra, Rn, Rd → Rd = Ra + Rn * Rm +TEXT ·encodeMatMul32(SB), NOSPLIT|NOFRAME, $0-16 + MOVD src+0(FP), R0 + MOVD intermediate+8(FP), R2 + + // Load and byte-swap 8 uint32 words from src. + MOVWU 0(R0), R3 + REVW R3, R3 + MOVWU 4(R0), R4 + REVW R4, R4 + MOVWU 8(R0), R5 + REVW R5, R5 + MOVWU 12(R0), R6 + REVW R6, R6 + MOVWU 16(R0), R7 + REVW R7, R7 + MOVWU 20(R0), R8 + REVW R8, R8 + MOVWU 24(R0), R9 + REVW R9, R9 + MOVWU 28(R0), R10 + REVW R10, R10 + + // Zero accumulators for intermediate[1..8]. + MOVD $0, R11 + MOVD $0, R12 + MOVD $0, R13 + MOVD $0, R14 + MOVD $0, R15 + MOVD $0, R16 + MOVD $0, R17 + MOVD $0, R19 + + MOVD $·encTable32(SB), R1 + + // Row 0: intermediate[k+1] += bin[0] * encTable32[0][k], k=0..7 + MOVWU 0(R1), R20 + MADD R3, R11, R20, R11 + MOVWU 4(R1), R20 + MADD R3, R12, R20, R12 + MOVWU 8(R1), R20 + MADD R3, R13, R20, R13 + MOVWU 12(R1), R20 + MADD R3, R14, R20, R14 + MOVWU 16(R1), R20 + MADD R3, R15, R20, R15 + MOVWU 20(R1), R20 + MADD R3, R16, R20, R16 + MOVWU 24(R1), R20 + MADD R3, R17, R20, R17 + MOVWU 28(R1), R20 + MADD R3, R19, R20, R19 + + // Row 1: k=1..7 (table[1][0] = 0) + MOVWU 36(R1), R20 + MADD R4, R12, R20, R12 + MOVWU 40(R1), R20 + MADD R4, R13, R20, R13 + MOVWU 44(R1), R20 + MADD R4, R14, R20, R14 + MOVWU 48(R1), R20 + MADD R4, R15, R20, R15 + MOVWU 52(R1), R20 + MADD R4, R16, R20, R16 + MOVWU 56(R1), R20 + MADD R4, R17, R20, R17 + MOVWU 60(R1), R20 + MADD R4, R19, R20, R19 + + // Row 2: k=2..7 + MOVWU 72(R1), R20 + MADD R5, R13, R20, R13 + MOVWU 76(R1), R20 + MADD R5, R14, R20, R14 + MOVWU 80(R1), R20 + MADD R5, R15, R20, R15 + MOVWU 84(R1), R20 + MADD R5, R16, R20, R16 + MOVWU 88(R1), R20 + MADD R5, R17, R20, R17 + MOVWU 92(R1), R20 + MADD R5, R19, R20, R19 + + // Row 3: k=3..7 + MOVWU 108(R1), R20 + MADD R6, R14, R20, R14 + MOVWU 112(R1), R20 + MADD R6, R15, R20, R15 + MOVWU 116(R1), R20 + MADD R6, R16, R20, R16 + MOVWU 120(R1), R20 + MADD R6, R17, R20, R17 + MOVWU 124(R1), R20 + MADD R6, R19, R20, R19 + + // Row 4: k=4..7 + MOVWU 144(R1), R20 + MADD R7, R15, R20, R15 + MOVWU 148(R1), R20 + MADD R7, R16, R20, R16 + MOVWU 152(R1), R20 + MADD R7, R17, R20, R17 + MOVWU 156(R1), R20 + MADD R7, R19, R20, R19 + + // Row 5: k=5..7 + MOVWU 180(R1), R20 + MADD R8, R16, R20, R16 + MOVWU 184(R1), R20 + MADD R8, R17, R20, R17 + MOVWU 188(R1), R20 + MADD R8, R19, R20, R19 + + // Row 6: k=6..7 + MOVWU 216(R1), R20 + MADD R9, R17, R20, R17 + MOVWU 220(R1), R20 + MADD R9, R19, R20, R19 + + // Row 7: k=7 only + MOVWU 252(R1), R20 + MADD R10, R19, R20, R19 + + // Store intermediate[0..8]. + MOVD $0, 0(R2) + MOVD R11, 8(R2) + MOVD R12, 16(R2) + MOVD R13, 24(R2) + MOVD R14, 32(R2) + MOVD R15, 40(R2) + MOVD R16, 48(R2) + MOVD R17, 56(R2) + MOVD R19, 64(R2) + RET + +// func decodeMatMul32(intermediate *[9]uint64, bin *[8]uint64) +// +// Computes: bin[k] = sum_i intermediate[i] * decTable32[i][k] +// Fully unrolled. +TEXT ·decodeMatMul32(SB), NOSPLIT|NOFRAME, $0-16 + MOVD intermediate+0(FP), R0 + MOVD bin+8(FP), R2 + + // Load 9 intermediate values. + MOVD 0(R0), R3 + MOVD 8(R0), R4 + MOVD 16(R0), R5 + MOVD 24(R0), R6 + MOVD 32(R0), R7 + MOVD 40(R0), R8 + MOVD 48(R0), R9 + MOVD 56(R0), R10 + MOVD 64(R0), R21 + + // Zero accumulators for bin[0..7]. + MOVD $0, R11 + MOVD $0, R12 + MOVD $0, R13 + MOVD $0, R14 + MOVD $0, R15 + MOVD $0, R16 + MOVD $0, R17 + MOVD $0, R19 + + MOVD $·decTable32(SB), R1 + + // Row 0: bin[k] += intermediate[0] * decTable32[0][k], k=0..6 (table[0][7]=0) + MOVWU 0(R1), R20 + MADD R3, R11, R20, R11 + MOVWU 4(R1), R20 + MADD R3, R12, R20, R12 + MOVWU 8(R1), R20 + MADD R3, R13, R20, R13 + MOVWU 12(R1), R20 + MADD R3, R14, R20, R14 + MOVWU 16(R1), R20 + MADD R3, R15, R20, R15 + MOVWU 20(R1), R20 + MADD R3, R16, R20, R16 + MOVWU 24(R1), R20 + MADD R3, R17, R20, R17 + + // Row 1: k=1..6 (table[1][0]=0, table[1][7]=0) + MOVWU 36(R1), R20 + MADD R4, R12, R20, R12 + MOVWU 40(R1), R20 + MADD R4, R13, R20, R13 + MOVWU 44(R1), R20 + MADD R4, R14, R20, R14 + MOVWU 48(R1), R20 + MADD R4, R15, R20, R15 + MOVWU 52(R1), R20 + MADD R4, R16, R20, R16 + MOVWU 56(R1), R20 + MADD R4, R17, R20, R17 + + // Row 2: k=2..7 + MOVWU 72(R1), R20 + MADD R5, R13, R20, R13 + MOVWU 76(R1), R20 + MADD R5, R14, R20, R14 + MOVWU 80(R1), R20 + MADD R5, R15, R20, R15 + MOVWU 84(R1), R20 + MADD R5, R16, R20, R16 + MOVWU 88(R1), R20 + MADD R5, R17, R20, R17 + MOVWU 92(R1), R20 + MADD R5, R19, R20, R19 + + // Row 3: k=3..7 + MOVWU 108(R1), R20 + MADD R6, R14, R20, R14 + MOVWU 112(R1), R20 + MADD R6, R15, R20, R15 + MOVWU 116(R1), R20 + MADD R6, R16, R20, R16 + MOVWU 120(R1), R20 + MADD R6, R17, R20, R17 + MOVWU 124(R1), R20 + MADD R6, R19, R20, R19 + + // Row 4: k=4..7 + MOVWU 144(R1), R20 + MADD R7, R15, R20, R15 + MOVWU 148(R1), R20 + MADD R7, R16, R20, R16 + MOVWU 152(R1), R20 + MADD R7, R17, R20, R17 + MOVWU 156(R1), R20 + MADD R7, R19, R20, R19 + + // Row 5: k=5..7 + MOVWU 180(R1), R20 + MADD R8, R16, R20, R16 + MOVWU 184(R1), R20 + MADD R8, R17, R20, R17 + MOVWU 188(R1), R20 + MADD R8, R19, R20, R19 + + // Row 6: k=6..7 + MOVWU 216(R1), R20 + MADD R9, R17, R20, R17 + MOVWU 220(R1), R20 + MADD R9, R19, R20, R19 + + // Row 7: k=7 only (table[7][7] = 656356768) + MOVWU 252(R1), R20 + MADD R10, R19, R20, R19 + + // Row 8: table[8] = {0,0,0,0,0,0,0,1} → bin[7] += intermediate[8] + ADD R21, R19, R19 + + // Store bin[0..7]. + MOVD R11, 0(R2) + MOVD R12, 8(R2) + MOVD R13, 16(R2) + MOVD R14, 24(R2) + MOVD R15, 32(R2) + MOVD R16, 40(R2) + MOVD R17, 48(R2) + MOVD R19, 56(R2) + RET diff --git a/base58/matmul_other.go b/base58/matmul_other.go new file mode 100644 index 000000000..619e49149 --- /dev/null +++ b/base58/matmul_other.go @@ -0,0 +1,25 @@ +//go:build !arm64 && !amd64 + +package base58 + +import "encoding/binary" + +func encodeMatMul32(src *[32]byte, intermediate *[intermediateSz32]uint64) { + var bin [binarySz32]uint32 + for i := range binarySz32 { + bin[i] = binary.BigEndian.Uint32(src[i*4 : i*4+4]) + } + for i := range binarySz32 { + for k := range intermediateSz32 - 1 { + intermediate[k+1] += uint64(bin[i]) * uint64(encTable32[i][k]) + } + } +} + +func decodeMatMul32(intermediate *[intermediateSz32]uint64, bin *[binarySz32]uint64) { + for i := range intermediateSz32 { + for k := range binarySz32 { + bin[k] += intermediate[i] * uint64(decTable32[i][k]) + } + } +} diff --git a/base58/tables.go b/base58/tables.go new file mode 100644 index 000000000..b544067a0 --- /dev/null +++ b/base58/tables.go @@ -0,0 +1,102 @@ +package base58 + +const ( + r1div = 656356768 // 58^5 + + intermediateSz32 = 9 + intermediateSz64 = 18 + binarySz32 = 8 // 32 / 4 + binarySz64 = 16 // 64 / 4 + raw58Sz32 = 45 // 9 * 5 + raw58Sz64 = 90 // 18 * 5 + + EncodedMaxLen32 = 44 + EncodedMaxLen64 = 88 + + // base58InvalidDigit is the sentinel returned by base58Inverse for any + // character that is not a valid base58 digit. + base58InvalidDigit = 255 +) + +var base58Chars = [58]byte{ + '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', + 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', + 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', 'n', 'o', 'p', + 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', +} + +// base58Inverse is indexed by (char - '1'). base58InvalidDigit = invalid. +var base58Inverse = [75]byte{ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 255, 255, 255, 255, 255, 255, 255, + 9, 10, 11, 12, 13, 14, 15, 16, 255, 17, 18, 19, 20, 21, 255, 22, + 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 255, 255, 255, 255, 255, 255, + 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 255, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 255, +} + +// encTable32 [8][8]: 2^(32*(7-j)) = sum_k encTable32[j][k] * 58^(5*(7-k)) +var encTable32 = [binarySz32][intermediateSz32 - 1]uint32{ + {513735, 77223048, 437087610, 300156666, 605448490, 214625350, 141436834, 379377856}, + {0, 78508, 646269101, 118408823, 91512303, 209184527, 413102373, 153715680}, + {0, 0, 11997, 486083817, 3737691, 294005210, 247894721, 289024608}, + {0, 0, 0, 1833, 324463681, 385795061, 551597588, 21339008}, + {0, 0, 0, 0, 280, 127692781, 389432875, 357132832}, + {0, 0, 0, 0, 0, 42, 537767569, 410450016}, + {0, 0, 0, 0, 0, 0, 6, 356826688}, + {0, 0, 0, 0, 0, 0, 0, 1}, +} + +// encTable64 [16][17]: 2^(32*(15-j)) = sum_k encTable64[j][k] * 58^(5*(16-k)) +var encTable64 = [binarySz64][intermediateSz64 - 1]uint32{ + {2631, 149457141, 577092685, 632289089, 81912456, 221591423, 502967496, 403284731, 377738089, 492128779, 746799, 366351977, 190199623, 38066284, 526403762, 650603058, 454901440}, + {0, 402, 68350375, 30641941, 266024478, 208884256, 571208415, 337765723, 215140626, 129419325, 480359048, 398051646, 635841659, 214020719, 136986618, 626219915, 49699360}, + {0, 0, 61, 295059608, 141201404, 517024870, 239296485, 527697587, 212906911, 453637228, 467589845, 144614682, 45134568, 184514320, 644355351, 104784612, 308625792}, + {0, 0, 0, 9, 256449755, 500124311, 479690581, 372802935, 413254725, 487877412, 520263169, 176791855, 78190744, 291820402, 74998585, 496097732, 59100544}, + {0, 0, 0, 0, 1, 285573662, 455976778, 379818553, 100001224, 448949512, 109507367, 117185012, 347328982, 522665809, 36908802, 577276849, 64504928}, + {0, 0, 0, 0, 0, 0, 143945778, 651677945, 281429047, 535878743, 264290972, 526964023, 199595821, 597442702, 499113091, 424550935, 458949280}, + {0, 0, 0, 0, 0, 0, 0, 21997789, 294590275, 148640294, 595017589, 210481832, 404203788, 574729546, 160126051, 430102516, 44963712}, + {0, 0, 0, 0, 0, 0, 0, 0, 3361701, 325788598, 30977630, 513969330, 194569730, 164019635, 136596846, 626087230, 503769920}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 513735, 77223048, 437087610, 300156666, 605448490, 214625350, 141436834, 379377856}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 78508, 646269101, 118408823, 91512303, 209184527, 413102373, 153715680}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11997, 486083817, 3737691, 294005210, 247894721, 289024608}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1833, 324463681, 385795061, 551597588, 21339008}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 280, 127692781, 389432875, 357132832}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42, 537767569, 410450016}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 356826688}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, +} + +// decTable32 [9][8]: 58^(5*(8-j)) = sum_k decTable32[j][k] * 2^(32*(7-k)) +var decTable32 = [intermediateSz32][binarySz32]uint32{ + {1277, 2650397687, 3801011509, 2074386530, 3248244966, 687255411, 2959155456, 0}, + {0, 8360, 1184754854, 3047609191, 3418394749, 132556120, 1199103528, 0}, + {0, 0, 54706, 2996985344, 1834629191, 3964963911, 485140318, 1073741824}, + {0, 0, 0, 357981, 1476998812, 3337178590, 1483338760, 4194304000}, + {0, 0, 0, 0, 2342503, 3052466824, 2595180627, 17825792}, + {0, 0, 0, 0, 0, 15328518, 1933902296, 4063920128}, + {0, 0, 0, 0, 0, 0, 100304420, 3355157504}, + {0, 0, 0, 0, 0, 0, 0, 656356768}, + {0, 0, 0, 0, 0, 0, 0, 1}, +} + +// decTable64 [18][16]: 58^(5*(17-j)) = sum_k decTable64[j][k] * 2^(32*(15-k)) +var decTable64 = [intermediateSz64][binarySz64]uint32{ + {249448, 3719864065, 173911550, 4021557284, 3115810883, 2498525019, 1035889824, 627529458, 3840888383, 3728167192, 2901437456, 3863405776, 1540739182, 1570766848, 0, 0}, + {0, 1632305, 1882780341, 4128706713, 1023671068, 2618421812, 2005415586, 1062993857, 3577221846, 3960476767, 1695615427, 2597060712, 669472826, 104923136, 0, 0}, + {0, 0, 10681231, 1422956801, 2406345166, 4058671871, 2143913881, 4169135587, 2414104418, 2549553452, 997594232, 713340517, 2290070198, 1103833088, 0, 0}, + {0, 0, 0, 69894212, 1038812943, 1785020643, 1285619000, 2301468615, 3492037905, 314610629, 2761740102, 3410618104, 1699516363, 910779968, 0, 0}, + {0, 0, 0, 0, 457363084, 927569770, 3976106370, 1389513021, 2107865525, 3716679421, 1828091393, 2088408376, 439156799, 2579227194, 0, 0}, + {0, 0, 0, 0, 0, 2992822783, 383623235, 3862831115, 112778334, 339767049, 1447250220, 486575164, 3495303162, 2209946163, 268435456, 0}, + {0, 0, 0, 0, 0, 4, 2404108010, 2962826229, 3998086794, 1893006839, 2266258239, 1429430446, 307953032, 2361423716, 176160768, 0}, + {0, 0, 0, 0, 0, 0, 29, 3596590989, 3044036677, 1332209423, 1014420882, 868688145, 4264082837, 3688771808, 2485387264, 0}, + {0, 0, 0, 0, 0, 0, 0, 195, 1054003707, 3711696540, 582574436, 3549229270, 1088536814, 2338440092, 1468637184, 0}, + {0, 0, 0, 0, 0, 0, 0, 0, 1277, 2650397687, 3801011509, 2074386530, 3248244966, 687255411, 2959155456, 0}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 8360, 1184754854, 3047609191, 3418394749, 132556120, 1199103528, 0}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 54706, 2996985344, 1834629191, 3964963911, 485140318, 1073741824}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 357981, 1476998812, 3337178590, 1483338760, 4194304000}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2342503, 3052466824, 2595180627, 17825792}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15328518, 1933902296, 4063920128}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 100304420, 3355157504}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 656356768}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, +} diff --git a/base58_bench_test.go b/base58_bench_test.go new file mode 100644 index 000000000..cf67902fe --- /dev/null +++ b/base58_bench_test.go @@ -0,0 +1,56 @@ +package solana + +import ( + "testing" +) + +// Benchmarks for PublicKey/Signature base58 methods, which are the +// hot paths that most users hit via Stringer and JSON marshaling. + +var ( + benchPubkey = MustPublicKeyFromBase58("4cHoJNmLed5PBgFBezHmJkMJLEZrcTvr3aopjnYBRxUb") + benchSignature = MustSignatureFromBase58("5YBLhMBLjhAHnEPnHKLLnVwHSfXGPJMCvKAfNsiaEw2T63edrYxVFHKUxRXfP6KA1HVo7c9JZ3LAJQR72giX7Cb") + + benchPubkeyStr = "4cHoJNmLed5PBgFBezHmJkMJLEZrcTvr3aopjnYBRxUb" + benchSigStr = "5YBLhMBLjhAHnEPnHKLLnVwHSfXGPJMCvKAfNsiaEw2T63edrYxVFHKUxRXfP6KA1HVo7c9JZ3LAJQR72giX7Cb" +) + +func BenchmarkPublicKey_String(b *testing.B) { + pk := benchPubkey + for b.Loop() { + _ = pk.String() + } +} + +func BenchmarkPublicKeyFromBase58(b *testing.B) { + for b.Loop() { + PublicKeyFromBase58(benchPubkeyStr) + } +} + +func BenchmarkSignature_String(b *testing.B) { + sig := benchSignature + for b.Loop() { + _ = sig.String() + } +} + +func BenchmarkSignatureFromBase58(b *testing.B) { + for b.Loop() { + SignatureFromBase58(benchSigStr) + } +} + +func BenchmarkPublicKey_MarshalJSON(b *testing.B) { + pk := benchPubkey + for b.Loop() { + pk.MarshalJSON() + } +} + +func BenchmarkSignature_MarshalJSON(b *testing.B) { + sig := benchSignature + for b.Loop() { + sig.MarshalJSON() + } +} diff --git a/go.mod b/go.mod index 8b90ff485..c40a36b66 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/gagliardetto/gofuzz v1.2.2 github.com/gagliardetto/treeout v0.1.4 github.com/google/uuid v1.6.0 + github.com/mr-tron/base58 v1.2.0 ) require ( @@ -68,7 +69,6 @@ require ( github.com/klauspost/compress v1.18.0 github.com/logrusorgru/aurora v2.0.3+incompatible github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 - github.com/mr-tron/base58 v1.2.0 github.com/onsi/gomega v1.10.1 github.com/pkg/errors v0.9.1 github.com/ryanuber/columnize v2.1.2+incompatible diff --git a/keys.go b/keys.go index a85906b52..3a51cab76 100644 --- a/keys.go +++ b/keys.go @@ -30,7 +30,8 @@ import ( "sort" "filippo.io/edwards25519" - "github.com/mr-tron/base58" + "github.com/gagliardetto/solana-go/base58" + mrtronbase58 "github.com/mr-tron/base58" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/bsontype" ) @@ -57,7 +58,7 @@ func MustPrivateKeyFromBase58(in string) PrivateKey { } func PrivateKeyFromBase58(privkey string) (PrivateKey, error) { - res, err := base58.Decode(privkey) + res, err := mrtronbase58.Decode(privkey) if err != nil { return nil, err } @@ -106,7 +107,7 @@ func PrivateKeyFromSolanaKeygenFileBytes(content []byte) (PrivateKey, error) { } func (k PrivateKey) String() string { - return base58.Encode(k) + return mrtronbase58.Encode(k) } func NewRandomPrivateKey() (PrivateKey, error) { @@ -188,21 +189,24 @@ func MustPublicKeyFromBase58(in string) PublicKey { // PublicKeyFromBase58 creates a PublicKey from a base58 encoded string. // NOTE: it will accept on- and off-curve pubkeys. func PublicKeyFromBase58(in string) (out PublicKey, err error) { - val, err := base58.Decode(in) - if err != nil { + if err = base58.Decode32(in, (*[32]byte)(&out)); err != nil { + // Fall back to the variable-length decoder to produce a more + // informative error when the input decodes to a wrong-length value. + val, decErr := mrtronbase58.Decode(in) + if decErr != nil { + return out, fmt.Errorf("decode: %w", decErr) + } + if len(val) != PublicKeyLength { + return out, fmt.Errorf("invalid length, expected %v, got %d", PublicKeyLength, len(val)) + } return out, fmt.Errorf("decode: %w", err) } - - if len(val) != PublicKeyLength { - return out, fmt.Errorf("invalid length, expected %v, got %d", PublicKeyLength, len(val)) - } - - copy(out[:], val) return } func (p PublicKey) MarshalText() ([]byte, error) { - return []byte(base58.Encode(p[:])), nil + buf := make([]byte, 0, base58.EncodedMaxLen32) + return base58.AppendEncode32(buf, (*[32]byte)(&p)), nil } func (p *PublicKey) UnmarshalText(data []byte) error { @@ -210,7 +214,13 @@ func (p *PublicKey) UnmarshalText(data []byte) error { } func (p PublicKey) MarshalJSON() ([]byte, error) { - return json.Marshal(base58.Encode(p[:])) + // Write directly into a JSON-quoted buffer. Base58 characters are all ASCII + // and never contain JSON-escape characters, so we can skip json.Marshal. + buf := make([]byte, 0, base58.EncodedMaxLen32+2) + buf = append(buf, '"') + buf = base58.AppendEncode32(buf, (*[32]byte)(&p)) + buf = append(buf, '"') + return buf, nil } func (p *PublicKey) UnmarshalJSON(data []byte) (err error) { @@ -309,7 +319,7 @@ func (p *PublicKey) Set(s string) (err error) { } func (p PublicKey) String() string { - return base58.Encode(p[:]) + return base58.Encode32((*[32]byte)(&p)) } // Short returns a shortened pubkey string, diff --git a/nativetypes.go b/nativetypes.go index f598a07d3..5a1b7b9d0 100644 --- a/nativetypes.go +++ b/nativetypes.go @@ -24,8 +24,9 @@ import ( "io" bin "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go/base58" "github.com/mostynb/zstdpool-freelist" - "github.com/mr-tron/base58" + mrtronbase58 "github.com/mr-tron/base58" ) type Padding []byte @@ -54,8 +55,8 @@ func HashFromBytes(in []byte) Hash { // MarshalText implements encoding.TextMarshaler. func (ha Hash) MarshalText() ([]byte, error) { - s := base58.Encode(ha[:]) - return []byte(s), nil + buf := make([]byte, 0, base58.EncodedMaxLen32) + return base58.AppendEncode32(buf, (*[32]byte)(&ha)), nil } // UnmarshalText implements encoding.TextUnmarshaler. @@ -69,7 +70,11 @@ func (ha *Hash) UnmarshalText(data []byte) (err error) { } func (ha Hash) MarshalJSON() ([]byte, error) { - return json.Marshal(base58.Encode(ha[:])) + buf := make([]byte, 0, base58.EncodedMaxLen32+2) + buf = append(buf, '"') + buf = base58.AppendEncode32(buf, (*[32]byte)(&ha)) + buf = append(buf, '"') + return buf, nil } func (ha *Hash) UnmarshalJSON(data []byte) (err error) { @@ -97,7 +102,7 @@ func (ha Hash) IsZero() bool { } func (ha Hash) String() string { - return base58.Encode(ha[:]) + return base58.Encode32((*[32]byte)(&ha)) } type Signature [64]byte @@ -114,16 +119,9 @@ func (sig Signature) Equals(pb Signature) bool { // SignatureFromBase58 decodes a base58 string into a Signature. func SignatureFromBase58(in string) (out Signature, err error) { - val, err := base58.Decode(in) - if err != nil { - return - } - - if len(val) != SignatureLength { - err = fmt.Errorf("invalid length, expected 64, got %d", len(val)) - return + if err = base58.Decode64(in, (*[64]byte)(&out)); err != nil { + return out, fmt.Errorf("decode: %w", err) } - copy(out[:], val) return } @@ -154,8 +152,8 @@ func SignatureFromBytes(in []byte) (out Signature) { } func (p Signature) MarshalText() ([]byte, error) { - s := base58.Encode(p[:]) - return []byte(s), nil + buf := make([]byte, 0, base58.EncodedMaxLen64) + return base58.AppendEncode64(buf, (*[64]byte)(&p)), nil } func (p *Signature) UnmarshalText(data []byte) (err error) { @@ -168,7 +166,11 @@ func (p *Signature) UnmarshalText(data []byte) (err error) { } func (p Signature) MarshalJSON() ([]byte, error) { - return json.Marshal(base58.Encode(p[:])) + buf := make([]byte, 0, base58.EncodedMaxLen64+2) + buf = append(buf, '"') + buf = base58.AppendEncode64(buf, (*[64]byte)(&p)) + buf = append(buf, '"') + return buf, nil } func (p *Signature) UnmarshalJSON(data []byte) (err error) { @@ -178,18 +180,7 @@ func (p *Signature) UnmarshalJSON(data []byte) (err error) { return } - dat, err := base58.Decode(s) - if err != nil { - return err - } - - if len(dat) != SignatureLength { - return fmt.Errorf("invalid length for Signature, expected 64, got %d", len(dat)) - } - - target := Signature{} - copy(target[:], dat) - *p = target + *p, err = SignatureFromBase58(s) return } @@ -199,7 +190,7 @@ func (s Signature) Verify(pubkey PublicKey, msg []byte) bool { } func (p Signature) String() string { - return base58.Encode(p[:]) + return base58.Encode64((*[64]byte)(&p)) } type Base64 []byte @@ -227,7 +218,7 @@ func (t *Base64) UnmarshalJSON(data []byte) (err error) { type Base58 []byte func (t Base58) MarshalJSON() ([]byte, error) { - return json.Marshal(base58.Encode(t)) + return json.Marshal(mrtronbase58.Encode(t)) } func (t *Base58) UnmarshalJSON(data []byte) (err error) { @@ -242,12 +233,12 @@ func (t *Base58) UnmarshalJSON(data []byte) (err error) { return nil } - *t, err = base58.Decode(s) + *t, err = mrtronbase58.Decode(s) return } func (t Base58) String() string { - return base58.Encode(t) + return mrtronbase58.Encode(t) } type Data struct { @@ -287,7 +278,7 @@ func (t *Data) UnmarshalJSON(data []byte) (err error) { switch t.Encoding { case EncodingBase58: var err error - t.Content, err = base58.Decode(contentString) + t.Content, err = mrtronbase58.Decode(contentString) if err != nil { return err } @@ -323,7 +314,7 @@ var zstdEncoderPool = zstdpool.NewEncoderPool() func (t Data) String() string { switch EncodingType(t.Encoding) { case EncodingBase58: - return base58.Encode(t.Content) + return mrtronbase58.Encode(t.Content) case EncodingBase64: return base64.StdEncoding.EncodeToString(t.Content) case EncodingBase64Zstd: diff --git a/transaction.go b/transaction.go index d3475709c..34d180957 100644 --- a/transaction.go +++ b/transaction.go @@ -25,11 +25,10 @@ import ( "github.com/davecgh/go-spew/spew" bin "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go/text" "github.com/gagliardetto/treeout" "github.com/mr-tron/base58" "go.uber.org/zap" - - "github.com/gagliardetto/solana-go/text" ) type Transaction struct {