Added pkg/base58

This provides a base58 encoder / decoder using the Bitcoin alphabet.
This commit is contained in:
Olivier Meunier
2025-03-11 20:31:20 +01:00
parent 02afcd1d9e
commit 9cf71545cb
4 changed files with 527 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
ISC License:
Copyright (c) 2004-2010 by Internet Systems Consortium, Inc. ("ISC")
Copyright (c) 1995-2003 by Internet Software Consortium
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+191
View File
@@ -0,0 +1,191 @@
// SPDX-FileCopyrightText: © 2013-2015 The btcsuite developers
//
// SPDX-License-Identifier: ISC
// Package base58 implements base58 encoding
package base58
import (
"errors"
"fmt"
)
// An Encoding is a radix 58 encoding/decoding scheme, defined by a
// 58-character alphabet.
type Encoding struct {
encode [58]byte
decodeMap [256]uint8
}
const (
decodeMapInitialize = "" +
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" +
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"
invalidIndex = '\xff'
)
// ErrInvalidChar is returned on decoding errors.
var ErrInvalidChar = errors.New("invalid base58 string")
// NewEncoding returns a new [Encoding] defined by the given alphabet,
// which must be a 58-bit string that contains unique byte values and does
// not contain CR or LF characters.
func NewEncoding(encoder string) *Encoding {
if len(encoder) != 58 {
panic("encoding alphabet is not 58-bytes long")
}
e := new(Encoding)
copy(e.encode[:], encoder)
copy(e.decodeMap[:], decodeMapInitialize)
for i := range len(encoder) {
switch {
case encoder[i] == '\n' || encoder[i] == '\r':
panic("encoding alphabet contains newline character")
case e.decodeMap[encoder[i]] != invalidIndex:
panic("encoding alphabet includes duplicate symbols")
}
e.decodeMap[encoder[i]] = uint8(i)
}
return e
}
// StdEncoding is an [Encoding] with the Bitcoin alphabet.
var StdEncoding = NewEncoding("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz")
// EncodeToString is a shortcut to [Encoding.EncodeToString] on [StdEncoding].
func EncodeToString(src []byte) string {
return StdEncoding.EncodeToString(src)
}
// DecodeString is a shortcut to [Encoding.DecodeString] on [StdEncoding].
func DecodeString(s string) ([]byte, error) {
return StdEncoding.DecodeString(s)
}
// EncodeToString returns the base58 encoding of src.
func (enc *Encoding) EncodeToString(src []byte) string {
// Since the conversion is from base256 to base58, the max possible number
// of bytes of output per input byte is log_58(256) ~= 1.37. Thus, the max
// total output size is ceil(len(input) * 137/100). Rather than worrying
// about the ceiling, just add one even if it isn't needed since the final
// output is truncated to the right size at the end.
output := make([]byte, (len(src)*137/100)+1)
// Encode to base58 in reverse order to avoid extra calculations to
// determine the final output size in favor of just keeping track while
// iterating.
var index int
for _, r := range src {
// Multiply each byte in the output by 256 and encode to base58 while
// propagating the carry.
val := uint32(r)
for i, b := range output[:index] {
val += uint32(b) << 8
output[i] = byte(val % 58)
val /= 58
}
for ; val > 0; val /= 58 {
output[index] = byte(val % 58)
index++
}
}
// Replace the calculated remainders with their corresponding base58 digit.
for i, b := range output[:index] {
output[i] = enc.encode[b]
}
// Account for the leading zeros in the input. They are appended since the
// encoding is happening in reverse order.
for _, r := range src {
if r != 0 {
break
}
output[index] = enc.encode[0]
index++
}
// Truncate the output buffer to the actual number of encoded bytes and
// reverse it since it was calculated in reverse order.
output = output[:index:index]
for i := range index / 2 {
output[i], output[index-1-i] = output[index-1-i], output[i]
}
return string(output)
}
// DecodeString returns the bytes represented by the base58 string s.
func (enc *Encoding) DecodeString(s string) ([]byte, error) {
if len(s) == 0 {
return []byte{}, nil
}
// The max possible output size is when a base58 encoding consists of
// nothing but the alphabet character at index 0 which would result in the
// same number of bytes as the number of input chars.
output := make([]byte, len(s))
// Encode to base256 in reverse order to avoid extra calculations to
// determine the final output size in favor of just keeping track while
// iterating.
var index int
for _, r := range []byte(s) {
// Invalid base58 character.
val := uint32(enc.decodeMap[r])
if val == 255 {
return nil, fmt.Errorf("%w (got %q)", ErrInvalidChar, r)
}
// Multiply each byte in the output by 58 and encode to base256 while
// propagating the carry.
for i, b := range output[:index] {
val += uint32(b) * 58
output[i] = byte(val)
val >>= 8
}
for ; val > 0; val >>= 8 {
output[index] = byte(val)
index++
}
}
// Account for the leading zeros in the input. They are appended since the
// encoding is happening in reverse order.
for _, r := range []byte(s) {
if r != enc.encode[0] {
break
}
output[index] = 0
index++
}
// Truncate the output buffer to the actual number of decoded bytes and
// reverse it since it was calculated in reverse order.
output = output[:index:index]
for i := range index / 2 {
output[i], output[index-1-i] = output[index-1-i], output[i]
}
return output, nil
}
+288
View File
@@ -0,0 +1,288 @@
// SPDX-FileCopyrightText: © 2013-2015 The btcsuite developers
//
// SPDX-License-Identifier: ISC
package base58_test
import (
"bytes"
"crypto/rand"
"encoding/base32"
"encoding/hex"
"errors"
"io"
"math/big"
"strconv"
"testing"
"github.com/google/uuid"
"codeberg.org/readeck/readeck/pkg/base58"
)
func h2b(s string) []byte {
b, err := hex.DecodeString(s)
if err != nil {
panic(err)
}
return b
}
func uuid2b(s string) []byte {
u, err := uuid.Parse(s)
if err != nil {
panic(err)
}
return u[:]
}
func FuzzDecode(f *testing.F) {
for range 50 {
l, _ := rand.Int(rand.Reader, big.NewInt(48))
l = l.Add(l, big.NewInt(1))
data := make([]byte, l.Int64())
_, _ = io.ReadFull(rand.Reader, data)
f.Add(data)
}
f.Fuzz(func(t *testing.T, a []byte) {
s := base58.EncodeToString(a)
decoded, err := base58.DecodeString(s)
if err != nil {
t.Fatalf("unexpected error %s", err.Error())
}
if !bytes.Equal(decoded, a) {
t.Fatalf("got: %q, wanted: %q", decoded, a)
}
})
}
func TestB58(t *testing.T) {
tests := []struct {
decoded []byte
encoded string
}{
// String inputs
{[]byte(""), ""},
{[]byte(" "), "Z"},
{[]byte("-"), "n"},
{[]byte("0"), "q"},
{[]byte("1"), "r"},
{[]byte("-1"), "4SU"},
{[]byte("11"), "4k8"},
{[]byte("abc"), "ZiCa"},
{[]byte("1234598760"), "3mJr7AoUXx2Wqd"},
{[]byte("abcdefghijklmnopqrstuvwxyz"), "3yxU3u1igY8WkgtjK92fbJQCd4BZiiT1v25f"},
{[]byte("00000000000000000000000000000000000000000000000000000000000000"), "3sN2THZeE9Eh9eYrwkvZqNstbHGvrxSAM7gXUXvyFQP8XvQLUqNCS27icwUeDT7ckHm4FUHM2mTVh1vbLmk7y"},
// Hex inputs.
{h2b("61"), "2g"},
{h2b("626262"), "a3gV"},
{h2b("636363"), "aPEr"},
{h2b("73696d706c792061206c6f6e6720737472696e67"), "2cFupjhnEsSn59qHXstmK2ffpLv2"},
{h2b("00eb15231dfceb60925886b67d065299925915aeb172c06647"), "1NS17iag9jJgTHD1VXjvLCEnZuQ3rJDE9L"},
{h2b("516b6fcd0f"), "ABnLTmg"},
{h2b("bf4f89001e670274dd"), "3SEo3LWLoPntC"},
{h2b("572e4794"), "3EFU7m"},
{h2b("ecac89cad93923c02321"), "EJDM8drfXA6uyA"},
{h2b("10c8511e"), "Rt5zm"},
{h2b("00000000000000000000"), "1111111111"},
// some uuids
{uuid2b("dc5eb8be-935e-48b4-a875-bb6d56ab9484"), "UDJwD3JQyUzSXnDfcPwAPM"},
{uuid2b("b27d7e58-0c7e-4417-9840-9e68d75ac596"), "P3N2J7smMHsDXtdXWsvmLH"},
{uuid2b("fc9ab497-af12-4fbc-9ef3-346ffea2e599"), "YCB6ukdJeQQ8jbLMNauV9A"},
{uuid2b("ac95ae33-3c18-4ab3-b8c8-526132c55218"), "NK4s4dXFhbwtWwKH9yXCFq"},
{uuid2b("3f865103-a1da-45cc-b894-b9e1dec58eec"), "8qyDaaR9jNMbz4rYWTk2zf"},
{uuid2b("a318acb5-3ee9-4a61-8975-730e27549e7b"), "M97QsaYJkj7Tip6F9WLUT4"},
{uuid2b("80dd05ff-67cf-4de4-b240-5d3db038f62c"), "Guvywu2ufH7AMMUUsMPU6F"},
{uuid2b("21842526-3c7d-4e99-816e-43f7dd37350a"), "593fNsVRSkgnKrib8SFPHs"},
{uuid2b("b34e9b20-2bb5-4a09-9d99-bf4f20b649b2"), "P9DLmQXawztNSGtkrsAugd"},
{uuid2b("a2449574-ac5b-4511-87d9-99bfd001e3a7"), "M3BG6Jn4pMENMkTfXk5a7Q"},
{uuid2b("77bef40c-13d4-46a2-9116-ad7092c680e8"), "FndaZicXRsG5zSqeFFoFSw"},
{uuid2b("9159cde1-627f-4b85-9e43-da4a05f1be44"), "Jx1snke6pW2RtHkNnjDCQb"},
{uuid2b("259e84fe-2555-4367-a248-2d42bd3f96c8"), "5eS4da452PjehJ2DdpnnGP"},
{uuid2b("6357b08e-6c64-4834-bb90-a917d572bb27"), "DGVzkMz9nykcjcQNZLdRUS"},
{uuid2b("03630f49-289d-4a95-9f68-c07531319208"), "RFwimz6svzb8tTeTMBXiw"},
{uuid2b("6ba37d99-6e30-42a9-9f6a-63d284c7b460"), "EHvCo2reWqyVXMXSMLBFXV"},
{uuid2b("9e378a71-3df5-460e-b56c-5db68f9289f9"), "LYAVWbvkvBaeDA8U9N4Gba"},
{uuid2b("7ffa494b-73de-4da2-95f0-8411462bf899"), "Gob4jwjbav6BVn4DKfdmHA"},
{uuid2b("451f6ec9-d04a-40ac-83d6-9598f17303e4"), "9Y4gKd7pkQTseaLS7sUYy1"},
{uuid2b("17e5c85f-2f96-48c9-8cf4-4034d845aedd"), "3xA5raxdqfTHkPvPKqfQwv"},
}
for i, test := range tests {
if s := base58.EncodeToString(test.decoded); s != test.encoded {
t.Errorf("Encode test %d failed: got: %q, wanted: %q", i, s, test.encoded)
}
b, err := base58.DecodeString(test.encoded)
if err != nil {
t.Errorf("unexpected error in test %d: %s", i, err.Error())
}
if !bytes.Equal(b, test.decoded) {
t.Errorf("Decode test %d failed: got: %x, wanted: %x", i, b, test.decoded)
}
}
}
func TestDecodeError(t *testing.T) {
_, err := base58.DecodeString("🙂")
if err == nil || !errors.Is(err, base58.ErrInvalidChar) {
t.Errorf("wanted an error, got: %v", err)
}
}
func TestNewEncodingErrors(t *testing.T) {
tests := []struct {
encoder string
err string
}{
{
"aa",
"encoding alphabet is not 58-bytes long",
},
{
"\n23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz",
"encoding alphabet contains newline character",
},
{
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnZpqrstuvwxyz",
"encoding alphabet includes duplicate symbols",
},
}
for i, test := range tests {
t.Run(strconv.Itoa(i+1), func(t *testing.T) {
defer func() {
r := recover()
if r != test.err {
t.Fatalf("wanted: %q, got: %q", test.err, r)
}
}()
base58.NewEncoding(test.encoder)
})
}
}
func FuzzUUID(f *testing.F) {
for range 5 {
data := base58.NewUUID()
f.Add(data)
}
f.Fuzz(func(t *testing.T, a string) {
decoded, err := base58.DecodeUUID(a)
if err != nil {
t.Fatalf("unexpected error %s", err.Error())
}
encoded := base58.EncodeUUID(decoded)
if encoded != a {
t.Fatalf("got: %q, wanted: %q", decoded, a)
}
})
}
func TestEncodeUUID(t *testing.T) {
tests := []struct {
decoded []byte
encoded string
}{
// some uuids
{uuid2b("dc5eb8be-935e-48b4-a875-bb6d56ab9484"), "UDJwD3JQyUzSXnDfcPwAPM"},
{uuid2b("b27d7e58-0c7e-4417-9840-9e68d75ac596"), "P3N2J7smMHsDXtdXWsvmLH"},
{uuid2b("fc9ab497-af12-4fbc-9ef3-346ffea2e599"), "YCB6ukdJeQQ8jbLMNauV9A"},
{uuid2b("ac95ae33-3c18-4ab3-b8c8-526132c55218"), "NK4s4dXFhbwtWwKH9yXCFq"},
{uuid2b("3f865103-a1da-45cc-b894-b9e1dec58eec"), "8qyDaaR9jNMbz4rYWTk2zf"},
{uuid2b("a318acb5-3ee9-4a61-8975-730e27549e7b"), "M97QsaYJkj7Tip6F9WLUT4"},
{uuid2b("80dd05ff-67cf-4de4-b240-5d3db038f62c"), "Guvywu2ufH7AMMUUsMPU6F"},
{uuid2b("21842526-3c7d-4e99-816e-43f7dd37350a"), "593fNsVRSkgnKrib8SFPHs"},
{uuid2b("b34e9b20-2bb5-4a09-9d99-bf4f20b649b2"), "P9DLmQXawztNSGtkrsAugd"},
{uuid2b("a2449574-ac5b-4511-87d9-99bfd001e3a7"), "M3BG6Jn4pMENMkTfXk5a7Q"},
{uuid2b("77bef40c-13d4-46a2-9116-ad7092c680e8"), "FndaZicXRsG5zSqeFFoFSw"},
{uuid2b("9159cde1-627f-4b85-9e43-da4a05f1be44"), "Jx1snke6pW2RtHkNnjDCQb"},
{uuid2b("259e84fe-2555-4367-a248-2d42bd3f96c8"), "5eS4da452PjehJ2DdpnnGP"},
{uuid2b("6357b08e-6c64-4834-bb90-a917d572bb27"), "DGVzkMz9nykcjcQNZLdRUS"},
{uuid2b("03630f49-289d-4a95-9f68-c07531319208"), "RFwimz6svzb8tTeTMBXiw"},
{uuid2b("6ba37d99-6e30-42a9-9f6a-63d284c7b460"), "EHvCo2reWqyVXMXSMLBFXV"},
{uuid2b("9e378a71-3df5-460e-b56c-5db68f9289f9"), "LYAVWbvkvBaeDA8U9N4Gba"},
{uuid2b("7ffa494b-73de-4da2-95f0-8411462bf899"), "Gob4jwjbav6BVn4DKfdmHA"},
{uuid2b("451f6ec9-d04a-40ac-83d6-9598f17303e4"), "9Y4gKd7pkQTseaLS7sUYy1"},
{uuid2b("17e5c85f-2f96-48c9-8cf4-4034d845aedd"), "3xA5raxdqfTHkPvPKqfQwv"},
}
for i, test := range tests {
u := uuid.UUID(test.decoded)
if s := base58.EncodeUUID(u); s != test.encoded {
t.Errorf("Encode test %d failed: got: %q, wanted: %q", i, s, test.encoded)
}
decoded, err := base58.DecodeUUID(test.encoded)
if err != nil {
t.Errorf("unexpected error in test %d: %s", i, err.Error())
}
if !bytes.Equal(decoded[:], test.decoded) {
t.Errorf("Decode test %d failed: got: %x, wanted: %x", i, decoded, test.decoded)
}
}
}
func TestDecodeUUIDError(t *testing.T) {
_, err := base58.DecodeUUID("abcd")
if err == nil || !errors.Is(err, base58.ErrInvalidLenght) {
t.Errorf("wanted an error, got: %v", err)
}
_, err = base58.DecodeUUID("🙂")
if err == nil || !errors.Is(err, base58.ErrInvalidChar) {
t.Errorf("wanted an error, got: %v", err)
}
}
func BenchmarkB32EncodeToString(b *testing.B) {
id := uuid.New()
b.ResetTimer()
for b.Loop() {
base32.HexEncoding.EncodeToString(id[:])
}
}
func BenchmarkB32DecodeString(b *testing.B) {
id := uuid.New()
encoded := base32.HexEncoding.EncodeToString(id[:])
var err error
b.ResetTimer()
for b.Loop() {
_, err = base32.HexEncoding.DecodeString(encoded)
if err != nil {
panic(err)
}
}
}
func BenchmarkEncodeToString(b *testing.B) {
id := uuid.New()
b.ResetTimer()
for b.Loop() {
base58.EncodeToString(id[:])
}
}
func BenchmarkDecodeString(b *testing.B) {
id := uuid.New()
encoded := base58.EncodeToString(id[:])
var err error
b.ResetTimer()
for b.Loop() {
_, err = base58.DecodeString(encoded)
if err != nil {
panic(err)
}
}
}
+40
View File
@@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: © 2025 Olivier Meunier <olivier@neokraft.net>
//
// SPDX-License-Identifier: AGPL-3.0-only
package base58
import (
"errors"
"fmt"
"github.com/google/uuid"
)
// ErrInvalidLenght is returned on UUID decoding errors.
var ErrInvalidLenght = errors.New("invalid length")
// NewUUID returns a base58 encoded UUIDv4.
func NewUUID() string {
return EncodeUUID(uuid.New())
}
// EncodeUUID encodes an UUID to a base58 string.
func EncodeUUID(u uuid.UUID) string {
return EncodeToString(u[:])
}
// DecodeUUID decodes a string to an UUID.
func DecodeUUID(s string) (u uuid.UUID, err error) {
var b []byte
if b, err = DecodeString(s); err != nil {
return
}
if len(b) != 16 {
err = fmt.Errorf("%w (wants 16, got %d)", ErrInvalidLenght, len(b))
return
}
return uuid.UUID(b), nil
}