Skip to content

Commit

Permalink
Added a port of the base58 Fast decoding and Trivial decoding
Browse files Browse the repository at this point in the history
The port is based off of the github.com/trezor/trezor-crypto/base58.c code. There can be some more optimizations around buffering but it works with the tests.
Updated the tests to test Fast and Trivial decode as well
Updated the benchmarks to include decoding (seems to be just over a 6x improvement)
  • Loading branch information
Nika Jones committed Aug 19, 2017
1 parent 9906b57 commit 43b5950
Show file tree
Hide file tree
Showing 2 changed files with 203 additions and 5 deletions.
147 changes: 146 additions & 1 deletion base58.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
package main

import "math/big"
import (
"fmt"
"math/big"
)

const b58set = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"

var decodeMap [256]int8

func init() {
for i := range decodeMap {
decodeMap[i] = -1
}
for i, b := range b58set {
decodeMap[b] = int8(i)
}
}

var (
bn0 = big.NewInt(0)
bn58 = big.NewInt(58)
Expand Down Expand Up @@ -69,3 +83,134 @@ func TrivialBase58Encoding(a []byte) string {
}
return string(buf[idx:])
}

func FastBase58Decoding(str string) ([]byte, error) {
if len(str) == 0 {
return nil, fmt.Errorf("zero length string")
}

var (
t uint64
zmask, c uint32
zcount int

b58u = []rune(str)
b58sz = len(b58u)

binsz = len(b58u)
outisz = (binsz + 3) / 4 // check to see if we need to change this buffer size to optimize

This comment has been minimized.

Copy link
@ribasushi

ribasushi May 24, 2020

@njones this got addressed in mr-tron#14
Would appreciate if you give it a look along with everyone else: while all tests pass (plus extra tests in ipfs) I am still paranoid that something got over-done

Thanks!

binu = make([]byte, (binsz+3)*3)
bytesleft = binsz % 4
)

if bytesleft > 0 {
zmask = (0xffffffff << uint32(bytesleft*8))
} else {
bytesleft = 4
}

var outi = make([]uint32, outisz)

var i = 0
for ; i < b58sz && b58u[i] == '1'; i++ {
zcount++
}

for ; i < b58sz; i++ {
if b58u[i]&0x80 != 0 {
return nil, fmt.Errorf("High-bit set on invalid digit")
}

if decodeMap[b58u[i]] == -1 {
return nil, fmt.Errorf("Invalid base58 digit (%q)", b58u[i])
}

c = uint32(decodeMap[b58u[i]])

for j := (outisz - 1); j >= 0; j-- {
t = uint64(outi[j])*58 + uint64(c)
c = uint32((t & 0x3f00000000) >> 32)
outi[j] = uint32(t & 0xffffffff)
}

if c > 0 {
return nil, fmt.Errorf("Output number too big (carry to the next int32)")
}

if outi[0]&zmask != 0 {
return nil, fmt.Errorf("Output number too big (last int32 filled too far)")
}
}

// the nested for-loop below is the same as the original code:
// switch (bytesleft) {
// case 3:
// *(binu++) = (outi[0] & 0xff0000) >> 16;
// //-fallthrough
// case 2:
// *(binu++) = (outi[0] & 0xff00) >> 8;
// //-fallthrough
// case 1:
// *(binu++) = (outi[0] & 0xff);
// ++j;
// //-fallthrough
// default:
// break;
// }
//
// for (; j < outisz; ++j)
// {
// *(binu++) = (outi[j] >> 0x18) & 0xff;
// *(binu++) = (outi[j] >> 0x10) & 0xff;
// *(binu++) = (outi[j] >> 8) & 0xff;
// *(binu++) = (outi[j] >> 0) & 0xff;
// }
var j, cnt int
for j, cnt = 0, 0; j < outisz; j++ {
for mask := byte(bytesleft-1) * 8; mask <= 0x18; mask, cnt = mask-8, cnt+1 {
binu[cnt] = byte(outi[j] >> mask)
}
if j == 0 {
bytesleft = 4 // because it could be less than 4 the first time through
}
}

for n, v := range binu {
if v > 0 {
start := n - zcount
if start < 0 {
start = 0
}
return binu[start:cnt], nil
}
}

return binu[:j], nil
}

// Decode decodes the base58 encoded bytes.
// based
func TrivialBase58Decoding(str string) ([]byte, error) {
var zcount int
for i := 0; i < len(str) && str[i] == '1'; i++ {
zcount++
}
leading := make([]byte, zcount)

var padChar rune = -1
src := []byte(str)
j := 0
for ; j < len(src) && src[j] == byte(padChar); j++ {
}

n := new(big.Int)
for i := range src[j:] {
c := decodeMap[src[i]]
if c == -1 {
return nil, fmt.Errorf("illegal base58 data at input index: %d", i)
}
n.Mul(n, bn58)
n.Add(n, big.NewInt(int64(c)))
}
return append(leading, n.Bytes()...), nil
}
61 changes: 57 additions & 4 deletions base58_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,48 @@ import (
"testing"
)

func TestFastEqTrivialEncoding(t *testing.T) {
for j := 0; j < 256; j++ {
type testValues struct {
dec, enc string // decoded hex value
}

var n = 5000000
var testPairs = make([]testValues, 0, n)

func initTestPairs() {
if len(testPairs) > 0 {
return
}
// pre-make the test pairs, so it doesn't take up benchmark time...
data := make([]byte, 32)
for i := 0; i < n; i++ {
rand.Read(data)
testPairs = append(testPairs, testValues{dec: hex.EncodeToString(data), enc: FastBase58Encoding(data)})
}
}

func TestFastEqTrivialEncodingAndDecoding(t *testing.T) {
for j := 1; j < 256; j++ {
var b = make([]byte, j)
for i := 0; i < 100; i++ {
rand.Read(b)
if FastBase58Encoding(b) != TrivialBase58Encoding(b) {
t.Errorf(hex.EncodeToString(b))
fe := FastBase58Encoding(b)
te := TrivialBase58Encoding(b)

if fe != te {
t.Errorf("encoding err: %#v", hex.EncodeToString(b))
}

fd, ferr := FastBase58Decoding(fe)
if ferr != nil {
t.Errorf("fast error: %v", ferr)
}
td, terr := TrivialBase58Decoding(te)
if terr != nil {
t.Errorf("trivial error: %v", terr)
}

if hex.EncodeToString(fd) != hex.EncodeToString(td) {
t.Errorf("decoding err: [%x] %s != %s", b, hex.EncodeToString(fd), hex.EncodeToString(td))
}
}
}
Expand All @@ -33,3 +68,21 @@ func BenchmarkFastBase58Encoding(b *testing.B) {
FastBase58Encoding(data)
}
}

func BenchmarkTrivialBase58Decoding(b *testing.B) {
initTestPairs()
b.ResetTimer()

for i := 0; i < b.N; i++ {
TrivialBase58Decoding(testPairs[i].enc)
}
}

func BenchmarkFastBase58Decoding(b *testing.B) {
initTestPairs()
b.ResetTimer()

for i := 0; i < b.N; i++ {
FastBase58Decoding(testPairs[i].enc)
}
}

0 comments on commit 43b5950

Please sign in to comment.