base58: add Encode32

This commit is contained in:
Richard Patel 2023-06-25 16:14:32 +00:00
parent a1d4c370b3
commit dd4f5d4d6c
2 changed files with 191 additions and 0 deletions

144
pkg/base58/base58.go Normal file
View File

@ -0,0 +1,144 @@
// Copyright 2022 Firedancer Contributors
// Package base58 converts between binary and Base58 for 32/64 length strings.
//
// Ported from Firedancer:
// https://github.com/firedancer-io/firedancer/blob/main/src/ballet/base58/fd_base58.h
//
// Original author: Philip Taffet <phtaffet@jumptrading.com>
package base58
import "encoding/binary"
// alphabet maps [0, 58) to the base58 character.
const alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
// encTable32 contains the unique values less than 58^5 such that:
//
// 2^(32*(7-j)) = sum_k table[j][k]*58^(5*(7-k))
//
// The second dimension of this table is actually ceil(log_(58^5)
// (2^(32*7)), but that's almost always 8
var encTable32 = [9][8]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},
}
func Encode32(out *[44]byte, in [32]byte) uint {
const raw58sz = 45
// Count leading zeros (needed for final output)
var inLeading0s uint
for i := range in {
if in[i] != 0 {
break
}
inLeading0s++
}
// X = sum_i bytes[i] * 2^(8*(32-1-i))
// Convert N to 32-bit limbs:
// X = sum_i binary[i] * 2^(32*(8-1-i))
var limbs [8]uint32
for i := range limbs {
limbs[i] = binary.BigEndian.Uint32(in[4*i:])
}
r1Div := uint64(656356768) // = 58^5
// Convert to the intermediate format:
// X = sum_i intermediate[i] * 58^(5*(INTERMEDIATE_SZ-1-i))
// Initially, we don't require intermediate[i] < 58^5,
// but we do want to make sure the sums don't overflow.
var intermediate [9]uint64
// The worst case is if binary[7] is (2^32)-1. In that case
// intermediate[8] will be be just over 2^63, which is fine.
for i := 0; i < 8; i++ {
for j := 0; j < 8; j++ {
intermediate[j+1] += uint64(limbs[i]) * uint64(encTable32[i][j])
}
}
// Now we make sure each term is less than 58^5.
// Again, we have to be a bit careful of overflow.
//
// For N==32, in the worst case, as before, intermediate[8] will be
// just over 2^63 and intermediate[7] will be just over 2^62.6. In
// the first step, we'll add floor(intermediate[8]/58^5) to
// intermediate[7]. 58^5 is pretty big though, so intermediate[7]
// barely budges, and this is still fine.
//
// For N==64, in the worst case, the biggest entry in intermediate
// at this point is 2^63.87, and in the worst case, we add
// (2^64-1)/58^5, which is still about 2^63.87.
for i := 8; i > 0; i-- {
intermediate[i-1] += intermediate[i] / r1Div
intermediate[i] %= r1Div
}
// Convert intermediate form to base 58.
// X = sum_i raw_base58[i] * 58^(RAW58_SZ-1-i)
var rawBase58 [45]byte
for i := 0; i < 9; i++ {
// We know intermediate[ i ] < 58^5 < 2^32 for all i, so casting
// to a uint32 is safe.
v := uint32(intermediate[i])
rawBase58[5*i+4] = byte((v / 1) % 58)
rawBase58[5*i+3] = byte((v / 58) % 58)
rawBase58[5*i+2] = byte((v / 3364) % 58)
rawBase58[5*i+1] = byte((v / 195112) % 58)
// We know this one is less than 58
rawBase58[5*i+0] = byte(v / 11316496)
}
// Finally, actually convert to the string.
// We have to ignore all the leading zeros in rawBase58 and instead
// insert inLeading0s leading '1' characters. We can show that
// rawBase58 actually has at least inLeading0s, so we'll do this
// by skipping the first few leading zeros in rawBase58.
var rawLeading0s uint
for rawLeading0s = 0; rawLeading0s < raw58sz; rawLeading0s++ {
if rawBase58[rawLeading0s] != 0 {
break
}
}
// It's not immediately obvious that rawLeading0s >= inLeading0s,
// but it's true. In base b, X has floor(log_b X)+1 digits. That
// means inLeading0s = N-1-floor(log_256 X) and rawLeading0s =
// RAW58_SZ-1-floor(log_58 X). Let X<256^N be given and consider:
//
// rawLeading0s - inLeading0s =
// = RAW58_SZ-N + floor( log_256 X ) - floor( log_58 X )
// >= RAW58_SZ-N - 1 + ( log_256 X - log_58 X ) .
//
// log_256 X - log_58 X is monotonically decreasing for X>0, so it
// achieves it minimum at the maximum possible value for X, i.e.
// 256^N-1.
// >= RAW58_SZ-N-1 + log_256(256^N-1) - log_58(256^N-1)
//
// When N==32, RAW58_SZ is 45, so this gives skip >= 0.29
// When N==64, RAW58_SZ is 90, so this gives skip >= 1.59.
//
// Regardless, rawLeading0s - inLeading0s >= 0.
skip := rawLeading0s - inLeading0s
for i := uint(0); i < raw58sz-skip; i++ {
out[i] = alphabet[rawBase58[i+skip]]
}
return raw58sz - skip
}

47
pkg/base58/base58_test.go Normal file
View File

@ -0,0 +1,47 @@
package base58
import (
"encoding/hex"
"testing"
)
var testVector32 = []struct {
hex string
b58 string
}{
{
hex: "0000000000000000000000000000000000000000000000000000000000000000",
b58: "11111111111111111111111111111111",
},
{
hex: "0000000000000000000000000000000000000000000000000000000000000001",
b58: "11111111111111111111111111111112",
},
{
hex: "0000000000000000000000000000000000000000000000000000000000000101",
b58: "1111111111111111111111111111115S",
},
{
hex: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
b58: "JEKNVnkbo3jma5nREBBJCDoXFVeKkD56V3xKrvRmWxFG",
},
{
hex: "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe",
b58: "JEKNVnkbo3jma5nREBBJCDoXFVeKkD56V3xKrvRmWxFF",
},
}
func TestEncode32(t *testing.T) {
for _, test := range testVector32 {
var in [32]byte
hex.Decode(in[:], []byte(test.hex))
var out [44]byte
outLen := Encode32(&out, in)
outStr := string(out[:outLen])
if outStr != test.b58 {
t.Errorf("Encode32(%s) = %s, want %s", test.hex, outStr, test.b58)
}
}
}