diff --git a/bindings/typescript/src/base58.ts b/bindings/typescript/src/base58.ts new file mode 100644 index 0000000..be74ca3 --- /dev/null +++ b/bindings/typescript/src/base58.ts @@ -0,0 +1,188 @@ +/** + * Base58 encoding/decoding + */ + +export { + encode + // decode +} + + + +//============================================================================= +// ENCODING +//============================================================================= + +/** + * Encode a Uint8Array into base58 + */ +function +encode(binary : Uint8Array): string { + let num_leading_zeros : number = nlz(binary); + let rest : Uint8Array = binary.slice(num_leading_zeros); + let ones : string = encode_zeros(num_leading_zeros); + let rest_b58 : string = encode_rest(rest); + let result : string = ones + rest_b58; + return result; +} + + + +/** + * count the number of leading zeros in a uint8array + * + * @internal + */ +function +nlz(bytes: Uint8Array): number { + let n = 0; + for (let this_byte of bytes) { + if (0 === this_byte) + n++; + else + break; + } + return n; +} + + +/** + * Generate a bunch of '1's for however many leading zeros there are + * + * @internal + */ +function +encode_zeros(how_many : number): string { + let ones : string = ''; + for (let i = 1; + i <= how_many; + i++) + { + ones += '1'; + } + + return ones; +} + + + +/** + * Encode a Uint8Array that has no leading zeros + * + * @internal + */ +function +encode_rest(bytes : Uint8Array): string { + let bytes_bignum : bigint = bytes_to_bigint(bytes); + let result : string = bignum_to_base58(bytes_bignum); + return result; +} + + + +/** + * Convert a bytestring to a bignum + * + * @internal + */ +function +bytes_to_bigint(bytes: Uint8Array): bigint { + let acc_bigint : bigint = 0n; + for(let this_byte of bytes) { + acc_bigint <<= 8n; + acc_bigint += BigInt(this_byte); + } + return acc_bigint; +} + + + +/** + * Convert a BigInt to Base58 + * + * @internal + */ +function +bignum_to_base58(q: bigint) { + let s = ''; + while (q !== 0n) { + let this_n : bigint = q % 58n; + q /= 58n; + + let this_b58_char : string = bigint_to_char(this_n); + s = this_b58_char + s; + } + return s; +} + + + +//============================================================================= +// TRANSLATION TABLES +//============================================================================= + +function +bigint_to_char(n: bigint) { + switch(n) { + case 0n: return '1'; + case 1n: return '2'; + case 2n: return '3'; + case 3n: return '4'; + case 4n: return '5'; + case 5n: return '6'; + case 6n: return '7'; + case 7n: return '8'; + case 8n: return '9'; + case 9n: return 'A'; + case 10n: return 'B'; + case 11n: return 'C'; + case 12n: return 'D'; + case 13n: return 'E'; + case 14n: return 'F'; + case 15n: return 'G'; + case 16n: return 'H'; + case 17n: return 'J'; + case 18n: return 'K'; + case 19n: return 'L'; + case 20n: return 'M'; + case 21n: return 'N'; + case 22n: return 'P'; + case 23n: return 'Q'; + case 24n: return 'R'; + case 25n: return 'S'; + case 26n: return 'T'; + case 27n: return 'U'; + case 28n: return 'V'; + case 29n: return 'W'; + case 30n: return 'X'; + case 31n: return 'Y'; + case 32n: return 'Z'; + case 33n: return 'a'; + case 34n: return 'b'; + case 35n: return 'c'; + case 36n: return 'd'; + case 37n: return 'e'; + case 38n: return 'f'; + case 39n: return 'g'; + case 40n: return 'h'; + case 41n: return 'i'; + case 42n: return 'j'; + case 43n: return 'k'; + case 44n: return 'm'; + case 45n: return 'n'; + case 46n: return 'o'; + case 47n: return 'p'; + case 48n: return 'q'; + case 49n: return 'r'; + case 50n: return 's'; + case 51n: return 't'; + case 52n: return 'u'; + case 53n: return 'v'; + case 54n: return 'w'; + case 55n: return 'x'; + case 56n: return 'y'; + case 57n: return 'z'; + default: + throw new Error('invalid base58 bigint: ' + n) + } +} diff --git a/bindings/typescript/test/testgen/b58.erl b/bindings/typescript/test/testgen/b58.erl index 618f8b9..ab8e916 100644 --- a/bindings/typescript/test/testgen/b58.erl +++ b/bindings/typescript/test/testgen/b58.erl @@ -6,7 +6,7 @@ %% https://digitalbazaar.github.io/base58-spec/#encode main([]) -> - {ok, Cases} = file:consult("b58_cases.eterms"), + {ok, Cases} = file:consult("b58_cases_3.eterms"), test_cases(Cases). test_cases([{{encoded, E}, {decoded, D}} | Rest]) -> @@ -19,8 +19,8 @@ test_cases([{{encoded, E}, {decoded, D}} | Rest]) -> "YOU ARE A FAILURE TO ENCODE~n" "===============================~n" "decoded : ~tw~n" - "expected : ~tw~n" - "actual : ~tw~n~n", + "expected : ~ts~n" + "actual : ~ts~n~n", [D, E, enc(D)]) end, ok = @@ -29,7 +29,7 @@ test_cases([{{encoded, E}, {decoded, D}} | Rest]) -> false -> io:format("===============================~n" "YOU ARE A FAILURE TO DECODE~n" "===============================~n" - "encoded : ~tw~n" + "encoded : ~ts~n" "expected : ~tw~n" "actual : ~tw~n~n", [E, D, dec(E)]) @@ -40,10 +40,39 @@ test_cases([]) -> % this was much clearer: https://www.youtube.com/watch?v=GedV3S9X89c -enc(Bits) -> - NBits = bit_size(Bits), - <> = Bits, - enc(BitNum, []). +-spec enc(Bytes) -> Base58 + when Bytes :: binary(), + Base58 :: string(). + +enc(Bytes) -> + % grab leading 0s + {NumLeadingZeros, Rest} = split_zeros(Bytes, 0), + NBitsInRest = bit_size(Rest), + <> = Rest, + ZerosBase58 = [$1 || _ <- lists:seq(1, NumLeadingZeros)], + RestBase58 = enc(RestBigNum, []), + ZerosBase58 ++ RestBase58. + + + +-spec split_zeros(Bytes, InitZeros) -> {NumLeadingZeros, Rest} + when Bytes :: binary(), + InitZeros :: integer(), + NumLeadingZeros :: binary(), + Rest :: binary(). + +split_zeros(<<0:8, Rest/binary>>, NumZerosAcc) -> + NewNumZerosAcc = NumZerosAcc + 1, + split_zeros(Rest, NewNumZerosAcc); +split_zeros(Rest, NumZerosAcc) -> + {NumZerosAcc, Rest}. + + + +-spec enc(BytesBigNum, Base58Acc) -> Base58 + when BytesBigNum :: integer(), + Base58Acc :: [0..57], + Base58 :: string(). enc(0, Acc) -> lists:map(fun int2char/1, Acc); @@ -53,9 +82,24 @@ enc(BitNum, Acc) -> enc(Q, [R | Acc]). + +-spec dec(Base58) -> DecodedBytes + when Base58 :: string(), + DecodedBytes :: binary(). + dec(Str) -> - Ns = lists:map(fun char2int/1, Str), - dec(Ns, 0). + % the number of leading 1s tells us the number of leading zeros + {NumLeadingZeros, RestStr} = split_ones(Str, 0), + LeadingZeros = << <<0>> || _ <- lists:seq(1, NumLeadingZeros) >>, + RestNs = lists:map(fun char2int/1, RestStr), + RestBytes = dec(RestNs, 0), + <>. + +split_ones([$1 | Rest], NOnes) -> + split_ones(Rest, NOnes + 1); +split_ones(B58Str, NOnes) -> + {NOnes, B58Str}. + dec([N | Ns], Acc) -> NewAcc = (Acc*58) + N, diff --git a/bindings/typescript/test/testgen/b58.py b/bindings/typescript/test/testgen/b58.py index 454b242..9d68c3f 100644 --- a/bindings/typescript/test/testgen/b58.py +++ b/bindings/typescript/test/testgen/b58.py @@ -9,27 +9,123 @@ really checking to see if my code matches the base58 package import base58 as b import random as r +########################### +### simple cases (single bytes, two bytes, 10 bytes of same byte) +########################### + +## 1 byte cases + +def single_byte_case(byte): + ''' + return the encode/decode pair corresponding to a single byte + ''' + decoded_bytes = bytes([byte]) + encoded_bytes = b.b58encode(decoded_bytes) + return {'db': decoded_bytes, 'eb': encoded_bytes} + + +def single_byte_cases(): + ''' + return the encode/decode pairs corresponding to every single byte case for + byte between 0 and 255 + ''' + # range(0, 256) = [0, 256) intersect Z + return [single_byte_case(n) for n in range(0, 256)] + +## too many of these + +### 2 byte cases +# +#def two_bytes(n): +# ''' +# return the two bytes corresponding to <> +# ''' +# return bytes([n // 256, n % 256]) +# +# +#def two_byte_case(n): +# ''' +# return the encode/decode pair corresponding to <> +# ''' +# decoded_bytes = two_bytes(n) +# encoded_bytes = b.b58encode(decoded_bytes) +# return {'db': decoded_bytes, 'eb': encoded_bytes} +# +# +#def two_byte_cases(): +# ''' +# return the encode/decode pairs for each two bytes <> +# ''' +# return [two_byte_case(n) for n in range(0, 256*256)] + + +## 10 byte cases + +def ten_bytes(byte): + ''' + return the ten byte string which is byte repeated 10 times + ''' + return bytes([byte for _ in range(10)]) + + +def ten_byte_case(n): + ''' + return the encode/decode pair corresponding to <> * 10 + ''' + decoded_bytes = ten_bytes(n) + encoded_bytes = b.b58encode(decoded_bytes) + return {'db': decoded_bytes, 'eb': encoded_bytes} + + +def ten_byte_cases(): + ''' + return the encode/decode pairs for each string <> * 10 + ''' + return [ten_byte_case(n) for n in range(0, 256)] + + + +## all simple cases in one function + +def simple_cases(): + return single_byte_cases() + ten_byte_cases() + + ########################### ### case generation ########################### def random_bytes(): + ''' + return between 0 and 999 random bytes + ''' + # randint is between [left, right] len = r.randint(0, 999) #len = 10 acc = [] for _ in range(len): + # randint is between [left, right] randbyte = r.randint(0, 255) acc.append(randbyte) return bytes(acc) + def case(): + ''' + return a random encode/decode pair + ''' decoded_bytes = random_bytes() encoded_bytes = b.b58encode(decoded_bytes) return {'db': decoded_bytes, 'eb': encoded_bytes} + def cases(n): + ''' + return n randomly generated encode/decode pairs + ''' return [case() for _ in range(n)] + ########################### ### js formatting ########################### @@ -99,7 +195,7 @@ def format_cases_erl(cases): ########################### def main(): - c = cases(100) + c = simple_cases() + cases(100_000) #print(format_cases_js(c)) print(format_cases_erl(c)) diff --git a/bindings/typescript/test/testgen/b58_2.erl b/bindings/typescript/test/testgen/b58_2.erl new file mode 100644 index 0000000..0266493 --- /dev/null +++ b/bindings/typescript/test/testgen/b58_2.erl @@ -0,0 +1,236 @@ +%-module(b58). +%-export([enc/1, dec/1]). + +-mode(compile). +%-spec enc(binary()) -> string(). +%% https://digitalbazaar.github.io/base58-spec/#encode + +main([]) -> + {ok, Cases} = file:consult("b58_cases_3.eterms"), + DoCase = + fun(Case) -> + spawn(fun() -> test_case(Case) end) + end, + lists:foreach(DoCase, Cases). + +test_case({{encoded, E}, {decoded, D}}) -> + EncodeOk = E =:= enc(D), + DecodeOk = D =:= dec(E), + ok = + case EncodeOk of + true -> ok; + false -> io:format("===============================~n" + "YOU ARE A FAILURE TO ENCODE~n" + "===============================~n" + "decoded : ~tw~n" + "expected : ~ts~n" + "actual : ~ts~n~n", + [D, E, enc(D)]) + end, + ok = + case DecodeOk of + true -> ok; + false -> io:format("===============================~n" + "YOU ARE A FAILURE TO DECODE~n" + "===============================~n" + "encoded : ~ts~n" + "expected : ~tw~n" + "actual : ~tw~n~n", + [E, D, dec(E)]) + end, + ok. + +% this was much clearer: https://www.youtube.com/watch?v=GedV3S9X89c + +-spec enc(Bytes) -> Base58 + when Bytes :: binary(), + Base58 :: string(). + +enc(Bytes) -> + % grab leading 0s + {NumLeadingZeros, Rest} = split_zeros(Bytes, 0), + NBitsInRest = bit_size(Rest), + <> = Rest, + ZerosBase58 = [$1 || _ <- lists:seq(1, NumLeadingZeros)], + RestBase58 = enc(RestBigNum, []), + ZerosBase58 ++ RestBase58. + + + +-spec split_zeros(Bytes, InitZeros) -> {NumLeadingZeros, Rest} + when Bytes :: binary(), + InitZeros :: integer(), + NumLeadingZeros :: binary(), + Rest :: binary(). + +split_zeros(<<0:8, Rest/binary>>, NumZerosAcc) -> + NewNumZerosAcc = NumZerosAcc + 1, + split_zeros(Rest, NewNumZerosAcc); +split_zeros(Rest, NumZerosAcc) -> + {NumZerosAcc, Rest}. + + + +-spec enc(BytesBigNum, Base58Acc) -> Base58 + when BytesBigNum :: integer(), + Base58Acc :: [0..57], + Base58 :: string(). + +enc(0, Acc) -> + lists:map(fun int2char/1, Acc); +enc(BitNum, Acc) -> + Q = BitNum div 58, + R = BitNum rem 58, + enc(Q, [R | Acc]). + + + +-spec dec(Base58) -> DecodedBytes + when Base58 :: string(), + DecodedBytes :: binary(). + +dec(Str) -> + % the number of leading 1s tells us the number of leading zeros + {NumLeadingZeros, RestStr} = split_ones(Str, 0), + LeadingZeros = << <<0>> || _ <- lists:seq(1, NumLeadingZeros) >>, + RestNs = lists:map(fun char2int/1, RestStr), + RestBytes = dec(RestNs, 0), + <>. + +split_ones([$1 | Rest], NOnes) -> + split_ones(Rest, NOnes + 1); +split_ones(B58Str, NOnes) -> + {NOnes, B58Str}. + + +dec([N | Ns], Acc) -> + NewAcc = (Acc*58) + N, + dec(Ns, NewAcc); +dec([], FinalAccN) -> + bignum_to_binary_bige(FinalAccN, <<>>). + +bignum_to_binary_bige(0, Acc) -> + Acc; +bignum_to_binary_bige(N, Acc) -> + Q = N div 256, + R = N rem 256, + NewAcc = <>, + bignum_to_binary_bige(Q, NewAcc). + +int2char( 0) -> $1; +int2char( 1) -> $2; +int2char( 2) -> $3; +int2char( 3) -> $4; +int2char( 4) -> $5; +int2char( 5) -> $6; +int2char( 6) -> $7; +int2char( 7) -> $8; +int2char( 8) -> $9; +int2char( 9) -> $A; +int2char(10) -> $B; +int2char(11) -> $C; +int2char(12) -> $D; +int2char(13) -> $E; +int2char(14) -> $F; +int2char(15) -> $G; +int2char(16) -> $H; +int2char(17) -> $J; +int2char(18) -> $K; +int2char(19) -> $L; +int2char(20) -> $M; +int2char(21) -> $N; +int2char(22) -> $P; +int2char(23) -> $Q; +int2char(24) -> $R; +int2char(25) -> $S; +int2char(26) -> $T; +int2char(27) -> $U; +int2char(28) -> $V; +int2char(29) -> $W; +int2char(30) -> $X; +int2char(31) -> $Y; +int2char(32) -> $Z; +int2char(33) -> $a; +int2char(34) -> $b; +int2char(35) -> $c; +int2char(36) -> $d; +int2char(37) -> $e; +int2char(38) -> $f; +int2char(39) -> $g; +int2char(40) -> $h; +int2char(41) -> $i; +int2char(42) -> $j; +int2char(43) -> $k; +int2char(44) -> $m; +int2char(45) -> $n; +int2char(46) -> $o; +int2char(47) -> $p; +int2char(48) -> $q; +int2char(49) -> $r; +int2char(50) -> $s; +int2char(51) -> $t; +int2char(52) -> $u; +int2char(53) -> $v; +int2char(54) -> $w; +int2char(55) -> $x; +int2char(56) -> $y; +int2char(57) -> $z. + +char2int($1) -> 0; +char2int($2) -> 1; +char2int($3) -> 2; +char2int($4) -> 3; +char2int($5) -> 4; +char2int($6) -> 5; +char2int($7) -> 6; +char2int($8) -> 7; +char2int($9) -> 8; +char2int($A) -> 9; +char2int($B) -> 10; +char2int($C) -> 11; +char2int($D) -> 12; +char2int($E) -> 13; +char2int($F) -> 14; +char2int($G) -> 15; +char2int($H) -> 16; +char2int($J) -> 17; +char2int($K) -> 18; +char2int($L) -> 19; +char2int($M) -> 20; +char2int($N) -> 21; +char2int($P) -> 22; +char2int($Q) -> 23; +char2int($R) -> 24; +char2int($S) -> 25; +char2int($T) -> 26; +char2int($U) -> 27; +char2int($V) -> 28; +char2int($W) -> 29; +char2int($X) -> 30; +char2int($Y) -> 31; +char2int($Z) -> 32; +char2int($a) -> 33; +char2int($b) -> 34; +char2int($c) -> 35; +char2int($d) -> 36; +char2int($e) -> 37; +char2int($f) -> 38; +char2int($g) -> 39; +char2int($h) -> 40; +char2int($i) -> 41; +char2int($j) -> 42; +char2int($k) -> 43; +char2int($m) -> 44; +char2int($n) -> 45; +char2int($o) -> 46; +char2int($p) -> 47; +char2int($q) -> 48; +char2int($r) -> 49; +char2int($s) -> 50; +char2int($t) -> 51; +char2int($u) -> 52; +char2int($v) -> 53; +char2int($w) -> 54; +char2int($x) -> 55; +char2int($y) -> 56; +char2int($z) -> 57.