diff --git a/bindings/erlang/src/vanth.erl b/bindings/erlang/src/vanth.erl new file mode 100644 index 0000000..0d31bc3 --- /dev/null +++ b/bindings/erlang/src/vanth.erl @@ -0,0 +1,121 @@ +%% @doc +%% Vanillae data humanization +%% +%% This is similar to serialization/deserialization, but not the same thing +%% +%% This code exists to work out concepts and code structure for Vanillae TS, it +%% may eventually become productized. Please do not use this. +%% +%% References: +%% +%% 1. https://github.com/aeternity/protocol/blob/master/serializations.md +%% 2. https://github.com/aeternity/protocol/blob/master/node/api/api_encoding.md +-module(vanth). + +-compile([export_all, nowarn_export_all]). + + +%% semantic alias for "ak_" ++ string(). +-type ak_str() :: string(). +%% semantic alias for "tx_" ++ string(). +-type tx_str() :: string(). + +%% TODO: expand +%% See: https://github.com/aeternity/protocol/blob/master/serializations.md#the-id-type +-type anth_id() :: ak_str(). + +%% See: https://github.com/aeternity/protocol/blob/master/serializations.md#spend-transaction +-type anth_spendtx() :: #{sender := anth_id(), + recipient := anth_id(), + amount := integer(), + fee := integer(), + ttl := integer(), + nonce := integer(), + payload := binary()}. + +-spec humanize(API_String) -> HumanData + when API_String :: tx_str(), + HumanData :: {ok, anth_spendtx()} + | {error, Reason :: term()}. +%% @doc +%% Humanize some data +%% @end + +humanize("tx_" ++ Base64) -> + hum_tx_b64(Base64); +humanize(X) -> + {error, {nyi, X}}. + + +%% decode the base64 and check the hash thing +hum_tx_b64(B64_str) -> + B64_Bytes = list_to_binary(B64_str), + %% This has the double sha at the end + Stupid_Bytes = base64:decode(B64_Bytes), + Stupid_Size = byte_size(Stupid_Bytes), + %% pull apart data + <> = Stupid_Bytes, + ActualDoubleSha = shasha(RLP_encoded_data), + case Check =:= ActualDoubleSha of + false -> + {error, checksum_mismatch}; + true -> + decode_and_dispatch(RLP_encoded_data) + end. + +%% Double sha +shasha(Bytes) -> + <> = crypto:hash(sha256, crypto:hash(sha256, Bytes)), + Result. + +%% decode rlp data +decode_and_dispatch(RLP_encoded_bytes) -> + {DecodedData, Remainder} = vrlp:decode(RLP_encoded_bytes), + case Remainder of + <<>> -> hum_dispatch(DecodedData); + _ -> {error, trailing_data} + end. + +%% at this point we have the rlp data, and based on the first field, we are +%% going to humanize the data + +hum_dispatch([Tag_Bytes, Vsn_Bytes | Fields]) -> + Tag = binary:decode_unsigned(Tag_Bytes), + Vsn = binary:decode_unsigned(Vsn_Bytes), + hd2(Tag, Vsn, Fields); +hum_dispatch(X) -> + {error, {invalid_data, X}}. + +%% 12 = spendtx, version = 1 +hd2(_spendtx = 12, 1, Fields) -> + hum_spendtx_fields(Fields); +hd2(Tag, Vsn, Fields) -> + {error, {nyi, {hd2, Tag, Vsn, Fields}}}. + +%% See: https://github.com/aeternity/protocol/blob/master/serializations.md#spend-transaction +hum_spendtx_fields([SenderBytes, + RecipBytes, + AmountBytes, + FeeBytes, + TTLBytes, + NonceBytes, + Payload]) -> + SenderStr = humanize_id(SenderBytes), + RecipStr = humanize_id(RecipBytes), + Amount = binary:decode_unsigned(AmountBytes), + Fee = binary:decode_unsigned(FeeBytes), + TTL = binary:decode_unsigned(TTLBytes), + Nonce = binary:decode_unsigned(NonceBytes), + {ok, #{sender => SenderStr, + recipient => RecipStr, + amount => Amount, + fee => Fee, + ttl => TTL, + nonce => Nonce, + payload => Payload}}. + +humanize_id(<<1, IdBytes:32/binary>>) -> + Check = shasha(IdBytes), + Str = vb58:enc(<>), + "ak_" ++ Str. diff --git a/bindings/erlang/src/vb58.erl b/bindings/erlang/src/vb58.erl new file mode 100644 index 0000000..37a99d8 --- /dev/null +++ b/bindings/erlang/src/vb58.erl @@ -0,0 +1,249 @@ +%% @doc +%% Vanillae Base58 Encoding/Decoding module +%% +%% References +%% +%% 1. https://digitalbazaar.github.io/base58-spec/#encode +%% 2. https://www.youtube.com/watch?v=GedV3S9X89c +%% @end +-module(vb58). + +-export([enc/1, dec/1]). + +%% TODO: move these cases to test or something +%% this originated from the +%%-mode(compile). +%%-spec enc(binary()) -> string(). +%%% https://digitalbazaar.github.io/base58-spec/#encode +% +%main([]) -> +% {ok, Cases} = file:consult("b58_cases_3.eterms"), +% test_cases(Cases). +% +%test_cases([{{encoded, E}, {decoded, D}} | Rest]) -> +% 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, +% test_cases(Rest); +%test_cases([]) -> +% ok. + +% this was much clearer: https://www.youtube.com/watch?v=GedV3S9X89c + +-spec enc(Bytes) -> Base58 + when Bytes :: binary(), + Base58 :: string(). +%% @doc +%% Encode a bytestring into base58 notation + +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(). +%% @doc +%% Decode a Base58-encoded string into a bytestring + +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. diff --git a/bindings/erlang/src/vrlp.erl b/bindings/erlang/src/vrlp.erl new file mode 100644 index 0000000..b8a9c83 --- /dev/null +++ b/bindings/erlang/src/vrlp.erl @@ -0,0 +1,148 @@ +%% @doc +%% Vanillae RLP encoder/decoder +%% +%% Reference: https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp/ +%% +%% Agrees with Ethereum's Python implementation in randomized tests +-module(vrlp). + +-export_type([decoded_data/0]). +-export([encode/1, decode/1]). + +-type decoded_data() :: binary() | [decoded_data()]. + + + +-spec encode(Data) -> RLP + when Data :: decoded_data(), + RLP :: binary(). +%% @doc +%% encode some data + +encode(Binary) when is_binary(Binary) -> + encode_binary(Binary); +encode(List) when is_list(List) -> + encode_list(List). + + + +-spec encode_binary(Bytes) -> RLP + when Bytes :: binary(), + RLP :: binary(). +%% @private +%% encode a binary in rlp +%% @end + +% single byte case when the byte is between 0..127 +% result is the byte itself +encode_binary(<>) when Byte =< 127 -> + <>; +% if the bytestring is 0..55 items long, the first byte is 128 + Length, +% the rest of the string is the string +encode_binary(Bytes) when byte_size(Bytes) =< 55 -> + Size = byte_size(Bytes), + <<(128 + Size), Bytes/binary>>; +% more than 55 bytes long, first byte is 183 + ByteLengthOfLength +% max byte size is 2^64 - 1 +encode_binary(Bytes) when 55 < byte_size(Bytes), byte_size(Bytes) < (1 bsl 64) -> + SizeInt = byte_size(Bytes), + SizeBytes = binary:encode_unsigned(SizeInt, big), + SizeOfSizeInt = byte_size(SizeBytes), + %% 183 = 128 + 55 + %% SizeOfSizeInt > 0 + <<(183 + SizeOfSizeInt), + SizeBytes/binary, + Bytes/binary>>. + + + +-spec encode_list(List) -> RLP + when List :: [decoded_data()], + RLP :: binary(). +%% @private +%% encode a list in rlp +%% @end + +% first we encode the total payload of the list +% depending on how long it is, we then branch +encode_list(List) -> + Payload = << (encode(Item)) || Item <- List>>, + Payload_Size = byte_size(Payload), + if + Payload_Size =< 55 -> + <<(192 + Payload_Size), Payload/binary>>; + 55 < Payload_Size -> + SizeBytes = binary:encode_unsigned(Payload_Size, big), + SizeOfSizeInt = byte_size(SizeBytes), + %% 247 = 192 + 55 + %% SizeOfSizeInt > 0 + <<(247 + SizeOfSizeInt), + SizeBytes/binary, + Payload/binary>> + end. + + + +-spec decode(RLP) -> {Data, Rest} + when RLP :: binary(), + Data :: decoded_data(), + Rest :: binary(). +%% @doc +%% decode an RLP-encoded string +%% @end + +% if the first byte is between 0 and 127, that is the data +decode(<>) when Byte =< 127 -> + {<>, Rest}; +% if the first byte is between 128 and 183 = 128 + 55, it is a bytestring and +% the length is Byte - 128 +decode(<>) when Byte =< 183 -> + PayloadByteLength = Byte - 128, + %PayloadBitLength = 8 * PayloadByteLength, + %io:format("Byte : ~p~n" + % "Rest : ~w~n" + % "PayloadByteLength : ~p~n", + % %"PayloadBitLength : ~p~n", + % [Byte, Rest, PayloadByteLength]), + <> = Rest, + {Payload, Rest2}; +% If the first byte is between 184 = 183 + 1 and 191 = 183 + 8, it is a +% bytestring. The byte length of the byte length of bytestring is FirstByte - +% 183. Then pull out the actual data +decode(<>) when Byte =< 191 -> + ByteLengthOfByteLength = Byte - 183, + BitLengthOfByteLength = 8 * ByteLengthOfByteLength, + <> = Rest, + <> = Rest2, + {Payload, Rest3}; +% If the first byte is between 192 and 247 = 192 + 55, it is a list. The byte +% length of the list-payload is FirstByte - 192. Then the list payload, which +% needs to be decoded on its own. +decode(<>) when Byte =< 247 -> + ByteLengthOfListPayload = Byte - 192, + <> = Rest, + List = decode_list(ListPayload), + {List, Rest2}; +% If the first byte is between 248 = 247 + 1 and 255 = 247 + 8, it is a list. +% The byte length of the byte length of the list-payload is FirstByte - 247. +% Then the byte length of the list. Then the list payload, which needs to be +% decoded on its own. +decode(<>) -> + ByteLengthOfByteLengthOfListPayload_int = Byte - 247, + BitLengthOfByteLengthOfListPayload_int = 8 * ByteLengthOfByteLengthOfListPayload_int, + <> = Rest, + <> = Rest2, + List = decode_list(ListPayload_bytes), + {List, Rest3}. + +decode_list(<<>>) -> + []; +decode_list(Bytes) -> + {Item, Rest} = decode(Bytes), + [Item | decode_list(Rest)].