diff --git a/CHANGELOG.md b/CHANGELOG.md index 04e7dba..30e09de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog for Tezex +## v2.0.0 + +- [forge]: TODO +- [BREAKING]: `Tezex.Micheline` is replaced with `Tezex.Forge` +- [BREAKING]: `Tezex.Micheline.Zarith` is replaced with `Tezex.Zarith`, which now returns `"-123"` instead of `-123` ## v1.2.0 diff --git a/lib/crypto.ex b/lib/crypto.ex index 8ea75ad..39a8399 100644 --- a/lib/crypto.ex +++ b/lib/crypto.ex @@ -9,7 +9,7 @@ defmodule Tezex.Crypto do alias Tezex.Crypto.PrivateKey alias Tezex.Crypto.Signature alias Tezex.Crypto.Utils - alias Tezex.Micheline + alias Tezex.Forge # public key @prefix_edpk <<13, 15, 37, 217>> @@ -66,7 +66,7 @@ defmodule Tezex.Crypto do end end - defp extract_pubkey(pubkey) do + def extract_pubkey(pubkey) do case Base58Check.decode58(pubkey) do {:ok, <<@prefix_edpk, public_key::binary-size(32)>> <> _} -> {:ok, public_key} {:ok, <<@prefix_sppk, public_key::binary-size(33)>> <> _} -> {:ok, public_key} @@ -255,8 +255,8 @@ defmodule Tezex.Crypto do iex> encoded_private_key = "spsk24EJohZHJkZnWEzj3w9wE7BFARpFmq5WAo9oTtqjdJ2t4pyoB3" iex> Tezex.Crypto.sign_message(encoded_private_key, "foo") "sigm9uJiGjdk2DpuqTmHcjzpAdTSQfqKxFuDKodyNT8JP3UvrfoPFTNkFbFgDP1WfAi2PjJ3dcpZFLTagD7gUBmwVWbPr5mk" - iex> msg = Tezex.Micheline.string_to_micheline_hex("foo") - "050100000003666F6F" + iex> msg = Tezex.Forge.pack("foo", :string) + "050100000003666f6f" iex> Tezex.Crypto.sign_message(encoded_private_key, msg) "sigm9uJiGjdk2DpuqTmHcjzpAdTSQfqKxFuDKodyNT8JP3UvrfoPFTNkFbFgDP1WfAi2PjJ3dcpZFLTagD7gUBmwVWbPr5mk" """ @@ -266,7 +266,7 @@ defmodule Tezex.Crypto do end def sign_message(privkey_param, bytes) do - sign(privkey_param, Micheline.string_to_micheline_hex(bytes)) + sign(privkey_param, Forge.pack(bytes, :string)) end @spec sign(privkey_param(), binary(), binary()) :: nonempty_binary() diff --git a/lib/crypto/base58_check.ex b/lib/crypto/base58_check.ex index e2e1023..77df130 100644 --- a/lib/crypto/base58_check.ex +++ b/lib/crypto/base58_check.ex @@ -56,4 +56,7 @@ defmodule Tezex.Crypto.Base58Check do """ @spec decode58!(binary) :: binary defdelegate decode58!(encoded), to: Base58Check + + @spec decode58check!(binary) :: binary + defdelegate decode58check!(encoded), to: Base58Check end diff --git a/lib/forge.ex b/lib/forge.ex new file mode 100644 index 0000000..b2edf84 --- /dev/null +++ b/lib/forge.ex @@ -0,0 +1,641 @@ +defmodule Tezex.Forge do + @moduledoc """ + Mostly ported from pytezos@9352c4579e436b92f8070343964af20747255197 + > pytezos / MIT License / (c) 2020 Baking Bad / (c) 2018 Arthur Breitman + """ + + import Bitwise + + alias Tezex.Crypto.Base58Check + alias Tezex.Zarith + + @base58_encodings [ + # block hash + %{e_prefix: "B", e_len: 51, d_prefix: <<1, 52>>, d_len: 32}, + # op hash + %{e_prefix: "o", e_len: 51, d_prefix: <<5, 116>>, d_len: 32}, + # op list hash + %{e_prefix: "Lo", e_len: 52, d_prefix: <<133, 233>>, d_len: 32}, + # op list list hash + %{e_prefix: "LLo", e_len: 53, d_prefix: <<29, 159, 109>>, d_len: 32}, + # protocol hash + %{e_prefix: "P", e_len: 51, d_prefix: <<2, 170>>, d_len: 32}, + # context hash + %{e_prefix: "Co", e_len: 52, d_prefix: <<79, 199>>, d_len: 32}, + # ed25519 pkh + %{e_prefix: "tz1", e_len: 36, d_prefix: <<6, 161, 159>>, d_len: 20}, + # secp256k1 pkh + %{e_prefix: "tz2", e_len: 36, d_prefix: <<6, 161, 161>>, d_len: 20}, + # p256 pkh + %{e_prefix: "tz3", e_len: 36, d_prefix: <<6, 161, 164>>, d_len: 20}, + # BLS-MinPk + %{e_prefix: "tz4", e_len: 36, d_prefix: <<6, 161, 16>>, d_len: 20}, + # originated address + %{e_prefix: "KT1", e_len: 36, d_prefix: <<2, 90, 121>>, d_len: 20}, + # tx_rollup_l2_address + %{e_prefix: "txr1", e_len: 37, d_prefix: <<1, 128, 120, 31>>, d_len: 20}, + # originated smart rollup address + %{e_prefix: "sr1", e_len: 36, d_prefix: <<6, 124, 117>>, d_len: 20}, + # smart rollup commitment hash + %{e_prefix: "src1", e_len: 54, d_prefix: <<17, 165, 134, 138>>, d_len: 32}, + # smart rollup state hash + %{e_prefix: "srs1", e_len: 54, d_prefix: <<17, 165, 235, 240>>, d_len: 32}, + # cryptobox pkh + %{e_prefix: "id", e_len: 30, d_prefix: <<153, 103>>, d_len: 16}, + # script expression + %{e_prefix: "expr", e_len: 54, d_prefix: <<13, 44, 64, 27>>, d_len: 32}, + # ed25519 seed + %{e_prefix: "edsk", e_len: 54, d_prefix: <<13, 15, 58, 7>>, d_len: 32}, + # ed25519 pubkey + %{e_prefix: "edpk", e_len: 54, d_prefix: <<13, 15, 37, 217>>, d_len: 32}, + # secp256k1 privkey + %{e_prefix: "spsk", e_len: 54, d_prefix: <<17, 162, 224, 201>>, d_len: 32}, + # p256 privkey + %{e_prefix: "p2sk", e_len: 54, d_prefix: <<16, 81, 238, 189>>, d_len: 32}, + # ed25519 enc seed + %{e_prefix: "edesk", e_len: 88, d_prefix: <<7, 90, 60, 179, 41>>, d_len: 56}, + # secp256k1 enc privkey + %{e_prefix: "spesk", e_len: 88, d_prefix: <<9, 237, 241, 174, 150>>, d_len: 56}, + # p256 enc privkey + %{e_prefix: "p2esk", e_len: 88, d_prefix: <<9, 48, 57, 115, 171>>, d_len: 56}, + # secp256k1 pubkey + %{e_prefix: "sppk", e_len: 55, d_prefix: <<3, 254, 226, 86>>, d_len: 33}, + # p256 pubkey + %{e_prefix: "p2pk", e_len: 55, d_prefix: <<3, 178, 139, 127>>, d_len: 33}, + # secp256k1 scalar + %{e_prefix: "SSp", e_len: 53, d_prefix: <<38, 248, 136>>, d_len: 33}, + # secp256k1 element + %{e_prefix: "GSp", e_len: 53, d_prefix: <<5, 92, 0>>, d_len: 33}, + # ed25519 privkey + %{e_prefix: "edsk", e_len: 98, d_prefix: <<43, 246, 78, 7>>, d_len: 64}, + # ed25519 sig + %{e_prefix: "edsig", e_len: 99, d_prefix: <<9, 245, 205, 134, 18>>, d_len: 64}, + # secp256k1 sig + %{e_prefix: "spsig", e_len: 99, d_prefix: <<13, 115, 101, 19, 63>>, d_len: 64}, + # p256 sig + %{e_prefix: "p2sig", e_len: 98, d_prefix: <<54, 240, 44, 52>>, d_len: 64}, + # generic sig + %{e_prefix: "sig", e_len: 96, d_prefix: <<4, 130, 43>>, d_len: 64}, + # chain id + %{e_prefix: "Net", e_len: 15, d_prefix: <<87, 82, 0>>, d_len: 4}, + # seed nonce hash + %{e_prefix: "nce", e_len: 53, d_prefix: <<69, 220, 169>>, d_len: 32}, + # blinded pkh + %{e_prefix: "btz1", e_len: 37, d_prefix: <<1, 2, 49, 223>>, d_len: 20}, + # block_payload_hash + %{e_prefix: "vh", e_len: 52, d_prefix: <<1, 106, 242>>, d_len: 32} + ] + @base58_e_prefix Enum.map(@base58_encodings, fn m -> + {m.e_prefix, Map.drop(m, [:e_prefix])} + end) + |> Map.new() + + # The position represents the encoding value + @primitives ~w( + parameter storage code False Elt Left None Pair Right Some True Unit PACK UNPACK BLAKE2B SHA256 SHA512 ABS ADD AMOUNT + AND BALANCE CAR CDR CHECK_SIGNATURE COMPARE CONCAT CONS CREATE_ACCOUNT CREATE_CONTRACT IMPLICIT_ACCOUNT DIP DROP DUP + EDIV EMPTY_MAP EMPTY_SET EQ EXEC FAILWITH GE GET GT HASH_KEY IF IF_CONS IF_LEFT IF_NONE INT LAMBDA LE LEFT LOOP LSL + LSR LT MAP MEM MUL NEG NEQ NIL NONE NOT NOW OR PAIR PUSH RIGHT SIZE SOME SOURCE SENDER SELF STEPS_TO_QUOTA SUB SWAP + TRANSFER_TOKENS SET_DELEGATE UNIT UPDATE XOR ITER LOOP_LEFT ADDRESS CONTRACT ISNAT CAST RENAME bool contract int key + key_hash lambda list map big_map nat option or pair set signature string bytes mutez timestamp unit operation address + SLICE DIG DUG EMPTY_BIG_MAP APPLY chain_id CHAIN_ID LEVEL SELF_ADDRESS never NEVER UNPAIR VOTING_POWER TOTAL_VOTING_POWER + KECCAK SHA3 PAIRING_CHECK bls12_381_g1 bls12_381_g2 bls12_381_fr sapling_state sapling_transaction_deprecated + SAPLING_EMPTY_STATE SAPLING_VERIFY_UPDATE ticket TICKET_DEPRECATED READ_TICKET SPLIT_TICKET JOIN_TICKETS GET_AND_UPDATE + chest chest_key OPEN_CHEST VIEW view constant SUB_MUTEZ tx_rollup_l2_address MIN_BLOCK_TIME sapling_transaction EMIT + Lambda_rec LAMBDA_REC TICKET BYTES NAT + ) + @primitive_tags Map.new(Enum.with_index(@primitives)) + @tags_primitive Map.new(Enum.map(Enum.with_index(@primitives), fn {k, v} -> {v, k} end)) + + defp prim_tag(int) when is_integer(int), do: @tags_primitive[int] + defp prim_tag(str) when is_binary(str), do: @primitive_tags[str] + + defp get_tag(args_len, annots_len) do + tag = min(args_len * 2 + 3 + if(annots_len > 0, do: 1, else: 0), 9) + <> + end + + defp read_tag(tag) do + {div(tag - 3, 2), rem(tag - 3, 2) != 0} + end + + @doc """ + Encode a signed unbounded integer into byte form. + """ + @spec forge_int(integer()) :: nonempty_binary() + def forge_int(value) when is_integer(value) do + bin = Zarith.encode(value) + + if rem(byte_size(bin), 2) == 1 do + "0" <> bin + else + bin + end + |> :binary.decode_hex() + end + + def forge_int16(value) do + <> + end + + def forge_int32(value) do + <> + end + + @doc """ + Decode a signed unbounded integer from bytes. + """ + @spec unforge_int(binary()) :: {integer(), integer()} + def unforge_int(data) do + {%{int: bin}, n} = Zarith.consume(:binary.encode_hex(data)) + {String.to_integer(bin), div(n, 2)} + end + + @doc """ + Encode a non-negative integer using LEB128 encoding. + """ + @spec forge_nat(non_neg_integer()) :: nonempty_binary() + def forge_nat(value) do + if value < 0 do + raise ArgumentError, "Value cannot be negative." + end + + forge_nat_recursive(value) + end + + defp forge_nat_recursive(value, acc \\ <<>>) do + byte = value &&& 0x7F + value = value >>> 7 + + if value != 0 do + forge_nat_recursive(value, <>) + else + <> + end + end + + @spec unforge_chain_id(binary()) :: nonempty_binary() + def unforge_chain_id(data) do + encode_with_prefix(data, "Net") + end + + @spec unforge_signature(binary()) :: nonempty_binary() + def unforge_signature(data) do + encode_with_prefix(data, "sig") + end + + def forge_bool(value) do + if value, do: <<255>>, else: <<0>> + end + + @spec forge_base58(binary()) :: binary() + def forge_base58(value) do + prefix_len = + Enum.find_value(@base58_encodings, fn m -> + if byte_size(value) == m.e_len and String.starts_with?(value, m.e_prefix) do + byte_size(m.d_prefix) + else + false + end + end) + + if is_nil(prefix_len) do + raise "Invalid encoding, prefix or length mismatch." + end + + Base58Check.decode58!(value) + |> binary_slice(prefix_len, 32) + end + + def optimize_timestamp(value) when is_binary(value) do + case DateTime.from_iso8601(value) do + {:ok, datetime, 0} -> DateTime.to_unix(datetime) + _ -> String.to_integer(value) + end + end + + @doc """ + Encode address or key hash into bytes. + + ## Parameters + - `value`: base58 encoded address or key_hash + - `tz_only`: True indicates that it's a key_hash (will be encoded in a more compact form) + """ + @spec forge_address(binary(), boolean()) :: nonempty_binary() + @spec forge_address(binary()) :: nonempty_binary() + def forge_address(value, tz_only \\ false) do + prefix_len = if String.starts_with?(value, "txr1"), do: 4, else: 3 + prefix = binary_part(value, 0, prefix_len) + + address = + Base58Check.decode58!(value) + |> binary_slice(prefix_len, 20) + + case prefix do + "tz1" -> <<0, 0, address::binary>> + "tz2" -> <<0, 1, address::binary>> + "tz3" -> <<0, 2, address::binary>> + "tz4" -> <<0, 3, address::binary>> + "KT1" -> <<1, address::binary, 0>> + "txr1" -> <<2, address::binary, 0>> + "sr1" -> <<3, address::binary, 0>> + _ -> raise "Can't forge address: unknown prefix `#{prefix}`" + end + |> then(fn res -> + if tz_only do + binary_slice(res, 1..-1//1) + else + res + end + end) + end + + @spec unforge_address(binary()) :: nonempty_binary() + def unforge_address(data) do + tz_prefixes = %{ + <<0, 0>> => "tz1", + <<0, 1>> => "tz2", + <<0, 2>> => "tz3", + <<0, 3>> => "tz4" + } + + tz_prefix = + Enum.find_value(tz_prefixes, fn {bin_prefix, tz_prefix} -> + if String.starts_with?(data, bin_prefix) do + tz_prefix + end + end) + + {data, prefix} = + cond do + is_binary(tz_prefix) -> + {binary_slice(data, 2..-1//1), tz_prefix} + + String.starts_with?(data, <<1>>) and String.ends_with?(data, <<0>>) -> + {binary_slice(data, 1..-2//1), "KT1"} + + String.starts_with?(data, <<2>>) and String.ends_with?(data, <<0>>) -> + {binary_slice(data, 1..-2//1), "txr1"} + + String.starts_with?(data, <<3>>) and String.ends_with?(data, <<0>>) -> + {binary_slice(data, 1..-2//1), "sr1"} + + true -> + {binary_slice(data, 1..-1//1), tz_prefixes[<<0, :binary.at(data, 0)>>]} + end + + encode_with_prefix(data, prefix) + end + + @spec forge_contract(binary()) :: binary() + def forge_contract(value) do + [address, entrypoint] = String.split(value, "%", parts: 2) + address_bytes = forge_address(address) + + if entrypoint != nil && entrypoint != "default" do + address_bytes <> entrypoint + else + address_bytes + end + end + + @doc """ + Decode a contract (address + optional entrypoint) from bytes. + + ## Parameters + - `data`: The binary containing the encoded contract. + + ## Returns + - A string with the base58 encoded address and, if present, the entrypoint separated by `%`. + """ + @spec unforge_contract(binary()) :: nonempty_binary() + def unforge_contract(data) do + address = unforge_address(binary_part(data, 0, 22)) + + case byte_size(data) > 22 do + true -> + entrypoint = binary_part(data, 22, byte_size(data) - 22) + address <> "%" <> entrypoint + + false -> + address + end + end + + @spec forge_public_key(binary()) :: nonempty_binary() + def forge_public_key(value) do + {:ok, res} = Tezex.Crypto.extract_pubkey(value) + prefix = binary_part(value, 0, 4) + + case prefix do + "edpk" -> <<0>> <> res + "sppk" -> <<1>> <> res + "p2pk" -> <<2>> <> res + _ -> raise "Unrecognized key type: #{prefix}" + end + end + + @spec unforge_public_key(binary()) :: nonempty_binary() + def unforge_public_key(data) do + key_prefix = + %{ + <<0>> => <<13, 15, 37, 217>>, + <<1>> => <<3, 254, 226, 86>>, + <<2>> => <<3, 178, 139, 127>> + } + + prefix = key_prefix[binary_part(data, 0, 1)] + Base58Check.encode(binary_part(data, 1, byte_size(data) - 1), prefix) + end + + @spec forge_array(binary(), non_neg_integer()) :: binary() + @spec forge_array(binary()) :: binary() + def forge_array(data, len_bytes \\ 4) do + <> <> data + end + + defp encode_byte_size(bytes) do + (byte_size(bytes) / 2) + |> trunc() + |> Integer.to_string(16) + |> String.pad_leading(8, "0") + end + + @doc """ + Decodes an encoded array of bytes. + + ## Parameters + + - `data` - The encoded array as a binary. + - `len_bytes` - The number of bytes that store the array length. + + ## Returns + + A tuple with the list of bytes and the total length of the array extracted. + """ + @spec unforge_array(binary(), non_neg_integer()) :: {binary(), non_neg_integer()} + @spec unforge_array(binary()) :: {binary(), non_neg_integer()} + def unforge_array(data, len_bytes \\ 4) do + if byte_size(data) < len_bytes do + throw("not enough bytes to parse array length, wanted #{len_bytes}") + end + + length = :binary.decode_unsigned(binary_slice(data, 0, len_bytes), :big) + + if byte_size(data) < len_bytes + length do + throw("not enough bytes to parse array body, wanted #{length}") + end + + array_body = binary_part(data, len_bytes, length) + {array_body, len_bytes + length} + end + + @doc """ + Encode a Micheline expression into byte form. + + ## Parameters + - `data`: The Micheline expression, which can be a list or map. + + ## Returns + - The encoded Micheline expression as binary data. + """ + @spec forge_micheline(list() | map()) :: binary() + def forge_micheline(data) when is_list(data) do + # Handle encoding of list data + data = + Enum.map(data, &forge_micheline/1) + |> Enum.join("") + + <<2>> <> forge_array(data) + end + + def forge_micheline(data) when is_map(data) do + # Handle encoding of map (dictionary) data + cond do + Map.has_key?(data, "prim") -> + args = Map.get(data, "args", []) + annots = Map.get(data, "annots", []) + + [ + get_tag(length(args), length(annots)), + prim_tag(data["prim"]), + if Enum.empty?(args) do + [] + else + encoded_args = Enum.join(Enum.map(args, &forge_micheline/1), "") + + args_content = + if length(args) < 3 do + encoded_args + else + forge_array(encoded_args) + end + + args_content + end, + cond do + length(annots) > 0 -> forge_array(Enum.join(annots, " ")) + length(args) >= 3 -> <<0, 0, 0, 0>> + true -> [] + end + ] + + not is_nil(data["bytes"]) -> + [<<10>>, forge_array(:binary.decode_hex(data["bytes"]))] + + not is_nil(data["int"]) -> + [<<0>>, forge_int(String.to_integer(data["int"]))] + + not is_nil(data["string"]) -> + [<<1>>, forge_array(data["string"])] + + true -> + raise "Unsupported data format: #{inspect(data)}" + end + |> IO.iodata_to_binary() + end + + def forge_micheline(_data) do + raise "Unsupported data type" + end + + @doc """ + Parse Micheline map from bytes. + + ## Parameters + - `data`: The binary containing the forged Micheline expression. + + ## Returns + - The Micheline map parsed from the bytes. + """ + @spec unforge_micheline(binary()) :: list() | map() + def unforge_micheline(data) do + {result, _ptr} = do_unforge_micheline(data, 0) + result + end + + @spec do_unforge_micheline(binary(), integer()) :: {list() | map(), integer()} + defp do_unforge_micheline(data, ptr) do + tag = :binary.at(data, ptr) + ptr = ptr + 1 + + case tag do + 0 -> + {val, offset} = unforge_int(binary_slice(data, ptr..-1//1)) + ptr = ptr + offset + {%{"int" => "#{val}"}, ptr} + + 1 -> + {val, offset} = unforge_array(binary_slice(data, ptr..-1//1)) + ptr = ptr + offset + {%{"string" => val}, ptr} + + 2 -> + unforge_sequence(data, ptr) + + tag when tag in 3..9 -> + {args_len, annots} = read_tag(tag) + unforge_prim_expr(data, ptr, args_len, annots) + + 10 -> + {val, offset} = unforge_array(binary_slice(data, ptr..-1//1)) + ptr = ptr + offset + {%{"bytes" => :binary.encode_hex(val, :lowercase)}, ptr} + + _ -> + raise "Unknown tag: #{tag} at position #{ptr}" + end + end + + @typep unforged_res :: list(unforged_res()) | map() + @spec unforge_sequence(binary(), integer()) :: {unforged_res(), integer()} + defp unforge_sequence(data, ptr) do + {_, offset} = unforge_array(binary_slice(data, ptr..-1//1)) + + end_ptr = ptr + offset + ptr = ptr + 4 + + {res, ptr} = decode_seq_elements(data, ptr, end_ptr) + + if ptr != end_ptr do + raise "Out of sequence boundaries" + end + + {res, ptr} + end + + @spec decode_seq_elements(binary(), integer(), integer(), list(unforged_res())) :: + {unforged_res(), integer()} + defp decode_seq_elements(data, ptr, end_ptr, acc \\ []) do + if ptr < end_ptr do + {element, ptr} = do_unforge_micheline(data, ptr) + decode_seq_elements(data, ptr, end_ptr, [element | acc]) + else + {Enum.reverse(acc), ptr} + end + end + + defp unforge_prim_expr(data, ptr, args_len, annots) do + tag = :binary.at(data, ptr) + ptr = ptr + 1 + + expr = %{"prim" => prim_tag(tag)} + + {expr, ptr} = + cond do + args_len > 0 and args_len < 3 -> + {args, ptr} = + Enum.reduce(1..args_len, {[], ptr}, fn _, {args, ptr} -> + {arg, ptr} = do_unforge_micheline(data, ptr) + {[arg | args], ptr} + end) + + expr = Map.put(expr, "args", Enum.reverse(args)) + {expr, ptr} + + args_len == 3 -> + {seq, ptr} = unforge_sequence(data, ptr) + expr = Map.put(expr, "args", seq) + {expr, ptr} + + args_len == 0 -> + {expr, ptr} + + true -> + raise "unexpected args len #{args_len}" + end + + if annots or args_len == 3 do + {value, offset} = unforge_array(binary_slice(data, ptr..-1//1)) + ptr = ptr + offset + + if byte_size(value) > 0 do + annots_list = String.split(value, " ") + {Map.put(expr, "annots", annots_list), ptr} + else + {expr, ptr} + end + else + {expr, ptr} + end + end + + @spec forge_script(map()) :: binary() + def forge_script(script) do + code = forge_micheline(script["code"]) + storage = forge_micheline(script["storage"]) + forge_array(code) <> forge_array(storage) + end + + @spec forge_script_expr(binary()) :: nonempty_binary() + def forge_script_expr(packed_key) do + data = Blake2.hash2b(packed_key, 32) + + Base58Check.encode(data, <<13, 44, 64, 27>>) + end + + defp encode_with_prefix(data, e_prefix) do + prefix = @base58_e_prefix[e_prefix] + + if is_nil(prefix) or prefix.d_len != byte_size(data) do + raise "Invalid encoding, prefix or length mismatch." + end + + Base58Check.encode(data, prefix.d_prefix) + end + + @typep pack_types :: nil | :address | :bytes | :int | :key_hash | :nat | :string + @spec pack(binary() | integer() | map(), pack_types()) :: nonempty_binary() + @spec pack(binary() | integer() | map()) :: nonempty_binary() + @doc """ + Serialize a piece of data to its optimized binary representation. + """ + def pack(value, type \\ nil) do + case type do + :int -> + "0500" <> Zarith.encode(value) + + :nat -> + "0500" <> Zarith.encode(value) + + :string -> + hex_bytes = :binary.encode_hex(value, :lowercase) + "0501" <> encode_byte_size(hex_bytes) <> hex_bytes + + :key_hash -> + address = :binary.encode_hex(forge_address(value), :lowercase) + "050a#{encode_byte_size(address)}#{address}" + + :address -> + address = :binary.encode_hex(forge_address(value), :lowercase) + "050a#{encode_byte_size(address)}#{address}" + + :bytes -> + hex_bytes = :binary.encode_hex(value, :lowercase) + "050a#{encode_byte_size(hex_bytes)}#{hex_bytes}" + + nil -> + "05" <> :binary.encode_hex(forge_micheline(value), :lowercase) + end + end +end diff --git a/lib/forge_operation.ex b/lib/forge_operation.ex new file mode 100644 index 0000000..715c118 --- /dev/null +++ b/lib/forge_operation.ex @@ -0,0 +1,286 @@ +defmodule Tezex.ForgeOperation do + @moduledoc """ + Mostly ported from pytezos@9352c4579e436b92f8070343964af20747255197 + > pytezos / MIT License / (c) 2020 Baking Bad / (c) 2018 Arthur Breitman + """ + + alias Tezex.Forge + + @operation_tags %{ + "endorsement" => 0, + "endorsement_with_slot" => 10, + "proposals" => 5, + "ballot" => 6, + "seed_nonce_revelation" => 1, + "double_endorsement_evidence" => 2, + "double_baking_evidence" => 3, + "activate_account" => 4, + "failing_noop" => 17, + "reveal" => 107, + "transaction" => 108, + "origination" => 109, + "delegation" => 110, + "register_global_constant" => 111, + "transfer_ticket" => 158, + "smart_rollup_add_messages" => 201, + "smart_rollup_execute_outbox_message" => 206 + } + + @reserved_entrypoints %{ + "default" => <<0>>, + "root" => <<1>>, + "do" => <<2>>, + "set_delegate" => <<3>>, + "remove_delegate" => <<4>>, + "deposit" => <<5>> + } + + # Checks if the content dictionary has parameters that are not the default 'Unit' type for 'default' entrypoint + defp has_parameters(content) do + case content do + %{"parameters" => %{"entrypoint" => "default", "value" => %{"prim" => "Unit"}}} -> false + %{"parameters" => _} -> true + _ -> false + end + end + + def forge_entrypoint(entrypoint) do + case Map.get(@reserved_entrypoints, entrypoint) do + nil -> <<255>> <> Forge.forge_array(entrypoint, 1) + code -> code + end + end + + defp forge_tag(tag) do + <> + end + + @spec forge_operation(map()) :: nonempty_binary() + def forge_operation(content) do + encoders = %{ + "failing_noop" => &forge_failing_noop/1, + "activate_account" => &forge_activate_account/1, + "reveal" => &forge_reveal/1, + "transaction" => &forge_transaction/1, + "origination" => &forge_origination/1, + "delegation" => &forge_delegation/1, + "endorsement" => &forge_endorsement/1, + "endorsement_with_slot" => &forge_endorsement_with_slot/1, + "register_global_constant" => &forge_register_global_constant/1, + "transfer_ticket" => &forge_transfer_ticket/1, + "smart_rollup_add_messages" => &forge_smart_rollup_add_messages/1, + "smart_rollup_execute_outbox_message" => &forge_smart_rollup_execute_outbox_message/1 + } + + encoder = encoders[content["kind"]] + + if is_nil(encoder) do + raise "No encoder for #{content["kind"]}" + end + + encoder.(content) + end + + @spec forge_operation_group(map()) :: nonempty_binary() + def forge_operation_group(operation_group) do + [ + Forge.forge_base58(operation_group["branch"]), + Enum.join(Enum.map(operation_group["contents"], &forge_operation/1)) + ] + |> IO.iodata_to_binary() + |> :binary.encode_hex(:lowercase) + end + + @spec forge_activate_account(map()) :: nonempty_binary() + def forge_activate_account(content) do + [ + forge_tag(@operation_tags[content["kind"]]), + binary_slice(Forge.forge_address(content["pkh"]), 2..-1//1), + Base.decode16!(content["secret"], case: :mixed) + ] + |> IO.iodata_to_binary() + end + + @spec forge_reveal(map()) :: nonempty_binary() + def forge_reveal(content) do + [ + forge_tag(@operation_tags[content["kind"]]), + Forge.forge_address(content["source"], true), + Forge.forge_nat(String.to_integer(content["fee"])), + Forge.forge_nat(String.to_integer(content["counter"])), + Forge.forge_nat(String.to_integer(content["gas_limit"])), + Forge.forge_nat(String.to_integer(content["storage_limit"])), + Forge.forge_public_key(content["public_key"]) + ] + |> IO.iodata_to_binary() + end + + @spec forge_transaction(map()) :: nonempty_binary() + def forge_transaction(content) do + [ + forge_tag(@operation_tags[content["kind"]]), + Forge.forge_address(content["source"], true), + Forge.forge_nat(String.to_integer(content["fee"])), + Forge.forge_nat(String.to_integer(content["counter"])), + Forge.forge_nat(String.to_integer(content["gas_limit"])), + Forge.forge_nat(String.to_integer(content["storage_limit"])), + Forge.forge_nat(String.to_integer(content["amount"])), + Forge.forge_address(content["destination"]), + if has_parameters(content) do + params = content["parameters"] + + [ + Forge.forge_bool(true), + forge_entrypoint(params["entrypoint"]), + Forge.forge_array(Forge.forge_micheline(params["value"])) + ] + else + Forge.forge_bool(false) + end + ] + |> IO.iodata_to_binary() + end + + @spec forge_origination(map()) :: nonempty_binary() + def forge_origination(content) do + [ + forge_tag(@operation_tags[content["kind"]]), + Forge.forge_address(content["source"], true), + Forge.forge_nat(String.to_integer(content["fee"])), + Forge.forge_nat(String.to_integer(content["counter"])), + Forge.forge_nat(String.to_integer(content["gas_limit"])), + Forge.forge_nat(String.to_integer(content["storage_limit"])), + Forge.forge_nat(String.to_integer(content["balance"])), + case Map.get(content, "delegate") do + nil -> + Forge.forge_bool(false) + + delegate -> + Forge.forge_bool(true) <> + Forge.forge_address(delegate, true) + end, + Forge.forge_script(content["script"]) + ] + |> IO.iodata_to_binary() + end + + @spec forge_delegation(map()) :: nonempty_binary() + def forge_delegation(content) do + [ + forge_tag(@operation_tags[content["kind"]]), + Forge.forge_address(content["source"], true), + Forge.forge_nat(String.to_integer(content["fee"])), + Forge.forge_nat(String.to_integer(content["counter"])), + Forge.forge_nat(String.to_integer(content["gas_limit"])), + Forge.forge_nat(String.to_integer(content["storage_limit"])), + case Map.get(content, "delegate") do + nil -> + Forge.forge_bool(false) + + delegate -> + [Forge.forge_bool(true), Forge.forge_address(delegate, true)] + end + ] + |> IO.iodata_to_binary() + end + + @spec forge_endorsement(map()) :: nonempty_binary() + def forge_endorsement(content) do + [forge_tag(content["kind"]), Forge.forge_int32(String.to_integer(content["level"]))] + |> IO.iodata_to_binary() + end + + @spec forge_inline_endorsement(map()) :: nonempty_binary() + def forge_inline_endorsement(content) do + [ + Forge.forge_base58(content["branch"]), + Forge.forge_nat(@operation_tags[content["operations"]["kind"]]), + Forge.forge_int32(String.to_integer(content["operations"]["level"])), + Forge.forge_base58(content["signature"]) + ] + |> IO.iodata_to_binary() + end + + @spec forge_endorsement_with_slot(map()) :: nonempty_binary() + def forge_endorsement_with_slot(content) do + [ + forge_tag(content["kind"]), + Forge.forge_array(forge_inline_endorsement(content["endorsement"])), + Forge.forge_int16(String.to_integer(content["slot"])) + ] + |> IO.iodata_to_binary() + end + + @spec forge_failing_noop(map()) :: nonempty_binary() + def forge_failing_noop(content) do + [forge_tag(content["kind"]), Forge.forge_array(content["arbitrary"])] |> IO.iodata_to_binary() + end + + @spec forge_register_global_constant(map()) :: nonempty_binary() + def forge_register_global_constant(content) do + [ + forge_tag(@operation_tags[content["kind"]]), + Forge.forge_address(content["source"], true), + Forge.forge_nat(String.to_integer(content["fee"])), + Forge.forge_nat(String.to_integer(content["counter"])), + Forge.forge_nat(String.to_integer(content["gas_limit"])), + Forge.forge_nat(String.to_integer(content["storage_limit"])), + Forge.forge_array(Forge.forge_micheline(content["value"])) + ] + |> IO.iodata_to_binary() + end + + @spec forge_transfer_ticket(map()) :: nonempty_binary() + def forge_transfer_ticket(content) do + [ + forge_tag(@operation_tags[content["kind"]]), + Forge.forge_address(content["source"], true), + Forge.forge_nat(String.to_integer(content["fee"])), + Forge.forge_nat(String.to_integer(content["counter"])), + Forge.forge_nat(String.to_integer(content["gas_limit"])), + Forge.forge_nat(String.to_integer(content["storage_limit"])), + Forge.forge_array(Forge.forge_micheline(content["ticket_contents"])), + Forge.forge_array(Forge.forge_micheline(content["ticket_ty"])), + Forge.forge_address(content["ticket_ticketer"]), + Forge.forge_nat(String.to_integer(content["ticket_amount"])), + Forge.forge_address(content["destination"]), + Forge.forge_array(content["entrypoint"]) + ] + |> IO.iodata_to_binary() + end + + @spec forge_smart_rollup_add_messages(map()) :: nonempty_binary() + def forge_smart_rollup_add_messages(content) do + [ + forge_tag(@operation_tags[content["kind"]]), + Forge.forge_address(content["source"], true), + Forge.forge_nat(String.to_integer(content["fee"])), + Forge.forge_nat(String.to_integer(content["counter"])), + Forge.forge_nat(String.to_integer(content["gas_limit"])), + Forge.forge_nat(String.to_integer(content["storage_limit"])), + Forge.forge_array( + Enum.join( + Enum.map(content["message"], &Forge.forge_array(:binary.decode_hex(&1))), + "" + ) + ) + ] + |> IO.iodata_to_binary() + end + + @spec forge_smart_rollup_execute_outbox_message(map()) :: nonempty_binary() + def forge_smart_rollup_execute_outbox_message(content) do + [ + forge_tag(@operation_tags[content["kind"]]), + Forge.forge_address(content["source"], true), + Forge.forge_nat(String.to_integer(content["fee"])), + Forge.forge_nat(String.to_integer(content["counter"])), + Forge.forge_nat(String.to_integer(content["gas_limit"])), + Forge.forge_nat(String.to_integer(content["storage_limit"])), + Forge.forge_base58(content["rollup"]), + Forge.forge_base58(content["cemented_commitment"]), + Forge.forge_array(:binary.decode_hex(content["output_proof"])) + ] + |> IO.iodata_to_binary() + end +end diff --git a/lib/micheline.ex b/lib/micheline.ex deleted file mode 100644 index 2caee78..0000000 --- a/lib/micheline.ex +++ /dev/null @@ -1,271 +0,0 @@ -defmodule Tezex.Micheline do - @moduledoc """ - Decode Micheline code. - """ - alias Tezex.Micheline.Zarith - - @kw ~w( - parameter storage code False Elt Left None Pair Right Some True Unit PACK UNPACK - BLAKE2B SHA256 SHA512 ABS ADD AMOUNT AND BALANCE CAR CDR CHECK_SIGNATURE COMPARE CONCAT - CONS CREATE_ACCOUNT CREATE_CONTRACT IMPLICIT_ACCOUNT DIP DROP DUP EDIV EMPTY_MAP EMPTY_SET - EQ EXEC FAILWITH GE GET GT HASH_KEY IF IF_CONS IF_LEFT IF_NONE INT LAMBDA LE LEFT - LOOP LSL LSR LT MAP MEM MUL NEG NEQ NIL NONE NOT NOW OR PAIR PUSH RIGHT SIZE - SOME SOURCE SENDER SELF STEPS_TO_QUOTA SUB SWAP TRANSFER_TOKENS SET_DELEGATE UNIT UPDATE XOR - ITER LOOP_LEFT ADDRESS CONTRACT ISNAT CAST RENAME bool contract int key key_hash lambda - list map big_map nat option or pair set signature string bytes mutez timestamp unit - operation address SLICE DIG DUG EMPTY_BIG_MAP APPLY chain_id CHAIN_ID LEVEL SELF_ADDRESS - never NEVER UNPAIR VOTING_POWER TOTAL_VOTING_POWER KECCAK SHA3 PAIRING_CHECK bls12_381_g1 - bls12_381_g2 bls12_381_fr sapling_state sapling_transaction SAPLING_EMPTY_STATE SAPLING_VERIFY_UPDATE - ticket TICKET READ_TICKET SPLIT_TICKET JOIN_TICKETS GET_AND_UPDATE chest chest_key OPEN_CHEST - VIEW view constant - ) - - @doc """ - Parse a single message from a Micheline packed message. - - ## Examples - iex> Tezex.Micheline.read_packed("02000000210061010000000574657a6f730100000000010000000b63727970746f6e6f6d6963") - %{int: 33} - - iex> Tezex.Micheline.read_packed("0200e1d22c") - %{int: -365_729} - """ - @spec read_packed(binary()) :: map() | list(map()) - def read_packed(<<_::binary-size(2), rest::binary>>) do - {val, _consumed} = hex_to_micheline(rest) - val - end - - @doc """ - Parse a Micheline hex string, return a tuple `{result, consumed}` containing - a list of Micheline objects as maps, and the number of bytes that was consumed in the process. - - ## Examples - iex> Tezex.Micheline.hex_to_micheline("02000000210061010000000574657a6f730100000000010000000b63727970746f6e6f6d6963") - {[%{int: -33}, %{string: "tezos"}, %{string: ""}, %{string: "cryptonomic"}], 76} - - iex> Tezex.Micheline.hex_to_micheline("00e1d22c") - {%{int: -365729}, 8} - """ - @spec hex_to_micheline(binary()) :: {map() | list(map()), pos_integer()} - # literal int or nat - def hex_to_micheline("00" <> rest) do - {result, consumed} = Zarith.consume(rest) - {result, consumed + 2} - end - - # literal string - def hex_to_micheline("01" <> rest) do - {hex_string, length} = micheline_hex_to_string(rest) - - {%{string: :binary.decode_hex(hex_string)}, length + 2} - end - - # sequence - def hex_to_micheline(<<"02", length::binary-size(8), rest::binary>>) do - {xs, consumed} = read_sequence(rest, 0, hex_to_dec(length) * 2, []) - - {Enum.reverse(xs), consumed + 8 + 2} - end - - # primitive / no arg / no annot - def hex_to_micheline("03" <> rest) do - {kw, consumed} = code_to_kw(rest) - {%{prim: kw}, 2 + consumed} - end - - # primitive / no arg / annot - def hex_to_micheline("04" <> rest) do - tot_consumed = 2 - {kw, consumed} = code_to_kw(rest) - tot_consumed = consumed + tot_consumed - <<_consumed::binary-size(consumed), rest::binary>> = rest - - {annot, consumed} = micheline_hex_to_string(rest) - annot = :binary.decode_hex(annot) - - {%{prim: kw, annot: annot}, consumed + tot_consumed} - end - - # primitive / 1 arg / no annot - def hex_to_micheline("05" <> rest) do - tot_consumed = 2 - - {kw, consumed} = code_to_kw(rest) - tot_consumed = consumed + tot_consumed - <<_consumed::binary-size(consumed), rest::binary>> = rest - - {arg, consumed} = hex_to_micheline(rest) - tot_consumed = consumed + tot_consumed - - {%{prim: kw, args: [arg]}, tot_consumed} - end - - # primitive / 1 arg / annot - def hex_to_micheline("06" <> rest) do - tot_consumed = 2 - - {kw, consumed} = code_to_kw(rest) - tot_consumed = consumed + tot_consumed - <<_consumed::binary-size(consumed), rest::binary>> = rest - - {arg, consumed} = hex_to_micheline(rest) - tot_consumed = consumed + tot_consumed - <<_consumed::binary-size(consumed), rest::binary>> = rest - - {annot, consumed} = micheline_hex_to_string(rest) - tot_consumed = consumed + tot_consumed - - {%{prim: kw, args: [arg], annots: decode_annotations(annot)}, tot_consumed} - end - - # primitive / 2 arg / no annot - def hex_to_micheline("07" <> rest) do - tot_consumed = 2 - - {kw, consumed} = code_to_kw(rest) - tot_consumed = consumed + tot_consumed - <<_consumed::binary-size(consumed), rest::binary>> = rest - - {arg1, consumed} = hex_to_micheline(rest) - tot_consumed = consumed + tot_consumed - <<_consumed::binary-size(consumed), rest::binary>> = rest - - {arg2, consumed} = hex_to_micheline(rest) - tot_consumed = consumed + tot_consumed - - {%{prim: kw, args: [arg1, arg2]}, tot_consumed} - end - - # primitive / 2 arg / annot - def hex_to_micheline("08" <> rest) do - tot_consumed = 2 - - {kw, consumed} = code_to_kw(rest) - tot_consumed = consumed + tot_consumed - <<_consumed::binary-size(consumed), rest::binary>> = rest - - {arg1, consumed} = hex_to_micheline(rest) - tot_consumed = consumed + tot_consumed - <<_consumed::binary-size(consumed), rest::binary>> = rest - - {arg2, consumed} = hex_to_micheline(rest) - tot_consumed = consumed + tot_consumed - <<_consumed::binary-size(consumed), rest::binary>> = rest - - {annot, consumed} = micheline_hex_to_string(rest) - tot_consumed = consumed + tot_consumed - - {%{prim: kw, args: [arg1, arg2], annots: decode_annotations(annot)}, tot_consumed} - end - - # primitive / N arg / maybe annot - def hex_to_micheline("09" <> rest) do - tot_consumed = 2 - - {kw, consumed} = code_to_kw(rest) - tot_consumed = consumed + tot_consumed - <<_consumed::binary-size(consumed), rest::binary>> = rest - - {args, consumed} = hex_to_micheline("02" <> rest) - # -2 to factor out the "02" we just added - consumed = consumed - 2 - tot_consumed = consumed + tot_consumed - <<_consumed::binary-size(consumed), rest::binary>> = rest - - case rest do - # no annotation to parse - <<"00000000", _rest::binary>> -> - {%{prim: kw, args: args}, tot_consumed + 8} - - _ -> - {annot, consumed} = micheline_hex_to_string(rest) - tot_consumed = consumed + tot_consumed - {%{prim: kw, args: args, annots: decode_annotations(annot)}, tot_consumed} - end - end - - # raw bytes - def hex_to_micheline(<<"0a", rest::binary>>) do - {bytes, length} = micheline_hex_to_string(rest) - {%{bytes: bytes}, length + 2} - end - - @spec micheline_hex_to_string(binary()) :: {binary(), pos_integer()} - def micheline_hex_to_string(<>) do - length = hex_to_dec(length) * 2 - <> = rest - {text, length + 8} - end - - @doc """ - Encode a string to its Micheline representation: - * `"05"` to indicate that it is a Micheline expression - * `"01"` to indicate that it is a Micheline string - * byte size encoded on 4 bytes - * hex representation of the string - """ - @spec string_to_micheline_hex(binary()) :: binary() - def string_to_micheline_hex(bytes) do - hex_bytes = :binary.encode_hex(bytes) - padded_bytes_size = String.pad_leading("#{trunc(byte_size(hex_bytes) / 2)}", 8, "0") - - "0501" <> padded_bytes_size <> hex_bytes - end - - @doc """ - Decode optimized Micheline representation of an address value - - ## Examples - iex> Tezex.Micheline.decode_optimized_address("00007fc95c97fd368cd9055610ee79e64ff9e0b5285c") - {:ok, "tz1XHhjLXQuG9rf9n7o1VbgegMkiggy1oktu"} - iex> Tezex.Micheline.decode_optimized_address("10007fc95c97fd368cd9055610ee79e64ff9e0b5285c") - {:error, :invalid} - """ - @spec decode_optimized_address(binary()) :: {:error, :invalid} | {:ok, nonempty_binary()} - def decode_optimized_address(hex) do - {prefix, pkh} = - case :binary.decode_hex(hex) do - <<0, 0, pkh::binary-size(20)>> -> {<<6, 161, 159>>, pkh} - <<0, 1, pkh::binary-size(20)>> -> {<<6, 161, 161>>, pkh} - <<0, 2, pkh::binary-size(20)>> -> {<<6, 161, 164>>, pkh} - <<1, pkh::binary-size(20), 0>> -> {<<2, 90, 121>>, pkh} - _ -> {:error, :invalid} - end - - case {prefix, pkh} do - {:error, :invalid} -> {:error, :invalid} - {prefix, pkh} -> {:ok, Tezex.Crypto.Base58Check.encode(pkh, prefix)} - end - end - - defp code_to_kw(code) when is_integer(code) do - {Enum.at(@kw, code), 2} - end - - defp code_to_kw(<>) when is_binary(code) do - {d, _} = Integer.parse(code, 16) - code_to_kw(d) - end - - defp decode_annotations(annots_hex) do - annots_hex - |> :binary.decode_hex() - |> String.split(" ") - end - - defp hex_to_dec(hex) do - {d, ""} = Integer.parse(hex, 16) - d - end - - defp read_sequence(to_read, consumed, length, acc) when consumed < length do - {content, l} = hex_to_micheline(to_read) - <<_consumed::binary-size(l), to_read::binary>> = to_read - read_sequence(to_read, consumed + l, length, [content | acc]) - end - - defp read_sequence(_to_read, consumed, _length, acc) do - {acc, consumed} - end -end diff --git a/lib/micheline/zarith.ex b/lib/zarith.ex similarity index 86% rename from lib/micheline/zarith.ex rename to lib/zarith.ex index e47d164..185dcb7 100644 --- a/lib/micheline/zarith.ex +++ b/lib/zarith.ex @@ -1,4 +1,4 @@ -defmodule Tezex.Micheline.Zarith do +defmodule Tezex.Zarith do @moduledoc """ A Zarith number is an integer encoded as a variable length sequence of bytes. @@ -14,16 +14,16 @@ defmodule Tezex.Micheline.Zarith do Takes a binary and returns the integer in base 10. ## Examples - iex> Tezex.Micheline.Zarith.decode("00a1d22c") + iex> Tezex.Zarith.decode("a1d22c") 365_729 - iex> Tezex.Micheline.Zarith.decode("00e1d22c") + iex> Tezex.Zarith.decode("e1d22c") -365_729 """ @spec decode(nonempty_binary()) :: integer() def decode(binary_input) when is_binary(binary_input) do {%{int: integer}, _} = consume(binary_input) - integer + String.to_integer(integer) end @doc """ @@ -33,10 +33,10 @@ defmodule Tezex.Micheline.Zarith do Implementation based on [anchorageoss/tezosprotocol (MIT License - Copyright (c) 2019 Anchor Labs, Inc.)](https://github.com/anchorageoss/tezosprotocol/blob/23a051d34fcfda8393940141f8151113a1aca10b/zarith/zarith.go#L153) ## Examples - iex> Tezex.Micheline.Zarith.encode(365_729) + iex> Tezex.Zarith.encode(365_729) "a1d22c" - iex> Tezex.Micheline.Zarith.encode(-365_729) + iex> Tezex.Zarith.encode(-365_729) "e1d22c" """ @spec encode(integer()) :: nonempty_binary() @@ -108,7 +108,7 @@ defmodule Tezex.Micheline.Zarith do Takes a binary and returns the decoded integer in base 10 along with how many characters of the input binary were used to decode the integer. """ - @spec consume(nonempty_binary()) :: {%{int: integer()}, pos_integer()} + @spec consume(nonempty_binary()) :: {%{int: binary()}, pos_integer()} def consume(binary_input) when is_binary(binary_input) do {carved_int, rest} = find_int(binary_input) @@ -128,16 +128,22 @@ defmodule Tezex.Micheline.Zarith do {integer, consumed} end - defp find_int(<>, acc \\ []) do + defp find_int(_bin, _acc \\ []) + + defp find_int(<>, acc) do dec = hex_to_dec(n) - if dec != 0 and dec < 128 do + if dec < 128 do {Enum.reverse([n | acc]) |> Enum.join(""), rest} else find_int(rest, [n | acc]) end end + defp find_int("", acc) do + {Enum.reverse(acc) |> Enum.join(""), ""} + end + defp read([<<_halt::1, sign::1, tail::integer-size(6)>> | rest]) do bits = for bits_part <- read_next(rest, [<>]), into: <<>> do @@ -148,8 +154,8 @@ defmodule Tezex.Micheline.Zarith do <> = bits case sign do - 1 -> %{int: -integer} - 0 -> %{int: integer} + 1 -> %{int: "-#{integer}"} + 0 -> %{int: "#{integer}"} end end @@ -173,5 +179,14 @@ defmodule Tezex.Micheline.Zarith do defp dec_to_hex(dec) do Integer.to_string(dec, 16) |> String.downcase() + |> then(fn hex -> + if rem(byte_size(hex), 2) == 1 do + "0" <> hex + else + hex + end + end) + + # |> tap(fn x -> IO.inspect(byte_size(x)) end) end end diff --git a/test/crypto_test.exs b/test/crypto_test.exs index 5069d95..6dc6008 100644 --- a/test/crypto_test.exs +++ b/test/crypto_test.exs @@ -3,7 +3,7 @@ defmodule Tezex.CryptoTest do doctest Tezex.Crypto alias Tezex.Crypto - alias Tezex.Micheline + alias Tezex.Forge describe "check_signature/4" do @msg_sig_pubkey [ @@ -358,7 +358,7 @@ defmodule Tezex.CryptoTest do defp sign_and_verify(encoded_private_key, pubkey) do msg = "aaøfË" - msg = Micheline.string_to_micheline_hex(msg) + msg = Forge.pack(msg, :string) signature = Crypto.sign_message(encoded_private_key, msg) Crypto.verify_signature(signature, msg, pubkey) diff --git a/test/forge_operation_test.exs b/test/forge_operation_test.exs new file mode 100644 index 0000000..a8d714b --- /dev/null +++ b/test/forge_operation_test.exs @@ -0,0 +1,230 @@ +defmodule Tezex.ForgeOperationTest do + use ExUnit.Case + + alias Tezex.ForgeOperation + + describe "Tezos P2P message encoder test suite" do + test "correctly encode some transactions" do + transaction = %{ + "kind" => "transaction", + "source" => "tz1VJAdH2HRUZWfohXW59NPYQKFMe1csroaX", + "fee" => "10000", + "counter" => "9", + "storage_limit" => "10001", + "gas_limit" => "10002", + "amount" => "10000000", + "destination" => "tz2G4TwEbsdFrJmApAxJ1vdQGmADnBp95n9m" + } + + result = + ForgeOperation.forge_transaction(transaction) + |> :binary.encode_hex(:lowercase) + + assert result == + "6c0069ef8fb5d47d8a4321c94576a2316a632be8ce89904e09924e914e80ade204000154f5d8f71ce18f9f05bb885a4120e64c667bc1b400" + + transaction = %{ + "destination" => "KT1X1rMCkifsoDJ1ynsHFqdvyagJKc9J6wEq", + "amount" => "10000", + "storage_limit" => "0", + "gas_limit" => "11697", + "counter" => "29892", + "fee" => "100000", + "source" => "tz1b2icJC4E7Y2ED1xsZXuqYpF7cxHDtduuP", + "kind" => "transaction", + "parameters" => %{"entrypoint" => "default", "value" => %{"prim" => "Unit"}} + } + + result = + ForgeOperation.forge_transaction(transaction) + |> :binary.encode_hex(:lowercase) + + assert result == + "6c00a8d45bdc966ddaaac83188a1e1c1fde2a3e05e5ca08d06c4e901b15b00904e01f61128c6abd2426d0c49b1fee1fa8c98dcc4ce0a0000" + end + + test "Dexter 2021Q1 tests" do + transaction = %{ + "kind" => "transaction", + "amount" => "100000000", + "destination" => "KT1XTUGj7Rkgh6vLVDu91h81Xu2WGfyTxpqi", + "parameters" => %{ + "entrypoint" => "xtzToToken", + "value" => %{ + "prim" => "Pair", + "args" => [ + %{"string" => "tz1RhnGx9hCxbrN8zKEKLbwU1zKLYZTqRs63"}, + %{ + "prim" => "Pair", + "args" => [ + %{"int" => "198180477354428686667"}, + %{"string" => "2021-01-21T18:09:14.519Z"} + ] + } + ] + } + }, + "storage_limit" => "0", + "gas_limit" => "11697", + "counter" => "29892", + "fee" => "100000", + "source" => "tz1RUGhq8sQpfGu1W2kf7MixqWX7oxThBFLr" + } + + result = + ForgeOperation.forge_transaction(transaction) + |> :binary.encode_hex(:lowercase) + + assert result == + "6c003ff84abc64319bda01968fd5269981d7615a6f75a08d06c4e901b15b0080c2d72f01fae98b912bb3644d56b8409cb98f40c779a9befe00ffff0a78747a546f546f6b656e0000005507070100000024747a3152686e47783968437862724e387a4b454b4c627755317a4b4c595a5471527336330707008b858b81c289bfcefc2a0100000018323032312d30312d32315431383a30393a31342e3531395a" + end + + test "correctly encode a reveal operation" do + reveal = %{ + "kind" => "reveal", + "source" => "tz1VJAdH2HRUZWfohXW59NPYQKFMe1csroaX", + "fee" => "0", + "counter" => "425748", + "storage_limit" => "0", + "gas_limit" => "10000", + "public_key" => "edpkuDuXgPVJi3YK2GKL6avAK3GyjqyvpJjG9gTY5r2y72R7Teo65i" + } + + result = + ForgeOperation.forge_reveal(reveal) + |> :binary.encode_hex(:lowercase) + + assert result == + "6b0069ef8fb5d47d8a4321c94576a2316a632be8ce890094fe19904e00004c7b0501f6ea08f472b7e88791d3b8da49d64ac1e2c90f93c27e6531473305c6" + end + + test "correctly encode a contract origination operation" do + origination = %{ + "kind" => "origination", + "source" => "tz1VJAdH2HRUZWfohXW59NPYQKFMe1csroaX", + "fee" => "10000", + "counter" => "9", + "storage_limit" => "10001", + "gas_limit" => "10002", + "balance" => "10003", + "script" => %{ + "code" => [ + %{"prim" => "parameter", "args" => [%{"prim" => "int"}]}, + %{"prim" => "storage", "args" => [%{"prim" => "int"}]}, + %{ + "prim" => "code", + "args" => [ + [ + %{"prim" => "CAR"}, + %{"prim" => "PUSH", "args" => [%{"prim" => "int"}, %{"int" => "1"}]}, + %{"prim" => "ADD"}, + %{ + "prim" => "PUSH", + "args" => [%{"prim" => "bytes"}, %{"bytes" => "0123456789abcdef"}] + }, + %{"prim" => "DROP"}, + %{"prim" => "NIL", "args" => [%{"prim" => "operation"}]}, + %{"prim" => "PAIR"} + ] + ] + } + ], + "storage" => %{"int" => "30"} + } + } + + result = + ForgeOperation.forge_operation(origination) + |> :binary.encode_hex(:lowercase) + + assert result == + "6d0069ef8fb5d47d8a4321c94576a2316a632be8ce89904e09924e914e934e000000003702000000320500035b0501035b0502020000002303160743035b00010312074303690a000000080123456789abcdef0320053d036d034200000002001e" + end + + test "correctly encode a contract origination operation 2" do + origination = %{ + "kind" => "origination", + "source" => "tz1VJAdH2HRUZWfohXW59NPYQKFMe1csroaX", + "delegate" => "tz1MRXFvJdkZdsr4CpGNB9dwA37LvMoNf7pM", + "fee" => "10000", + "counter" => "9", + "storage_limit" => "10001", + "gas_limit" => "10002", + "balance" => "10003", + "script" => %{ + "code" => [ + %{"prim" => "parameter", "args" => [%{"prim" => "int"}]}, + %{"prim" => "storage", "args" => [%{"prim" => "int"}]}, + %{ + "prim" => "code", + "args" => [ + [ + %{"prim" => "CAR"}, + %{"prim" => "PUSH", "args" => [%{"prim" => "int"}, %{"int" => "1"}]}, + %{"prim" => "ADD"}, + %{ + "prim" => "PUSH", + "args" => [%{"prim" => "bytes"}, %{"bytes" => "0123456789abcdef"}] + }, + %{"prim" => "DROP"}, + %{"prim" => "NIL", "args" => [%{"prim" => "operation"}]}, + %{"prim" => "PAIR"} + ] + ] + } + ], + "storage" => %{"int" => "30"} + } + } + + result = + ForgeOperation.forge_operation(origination) + |> :binary.encode_hex(:lowercase) + + assert result == + "6d0069ef8fb5d47d8a4321c94576a2316a632be8ce89904e09924e914e934eff001392b07a567de5cb3a4301fbef2030696b4dfd8b0000003702000000320500035b0501035b0502020000002303160743035b00010312074303690a000000080123456789abcdef0320053d036d034200000002001e" + end + + test "correctly encode a delegation operation" do + delegation = %{ + "kind" => "delegation", + "source" => "tz1VJAdH2HRUZWfohXW59NPYQKFMe1csroaX", + "fee" => "10000", + "counter" => "9", + "storage_limit" => "10001", + "gas_limit" => "10002", + "delegate" => "tz3WXYtyDUNL91qfiCJtVUX746QpNv5i5ve5" + } + + result = + ForgeOperation.forge_delegation(delegation) + |> :binary.encode_hex(:lowercase) + + assert result == + "6e0069ef8fb5d47d8a4321c94576a2316a632be8ce89904e09924e914eff026fde46af0356a0476dae4e4600172dc9309b3aa4" + + delegation = %{delegation | "delegate" => nil} + + result = + ForgeOperation.forge_delegation(delegation) + |> :binary.encode_hex(:lowercase) + + assert result == "6e0069ef8fb5d47d8a4321c94576a2316a632be8ce89904e09924e914e00" + end + + test "correctly encode an activation operation" do + activation = %{ + "kind" => "activate_account", + "pkh" => "tz1LoKbFyYHTkCnj9mgRKFb9g8pP4Lr3zniP", + "secret" => "9b7f631e52f877a1d363474404da8130b0b940ee" + } + + result = + ForgeOperation.forge_activate_account(activation) + |> :binary.encode_hex(:lowercase) + + assert result == + "040cb9f9da085607c05cac1ca4c62a3f3cfb8146aa9b7f631e52f877a1d363474404da8130b0b940ee" + end + end +end diff --git a/test/forge_test.exs b/test/forge_test.exs new file mode 100644 index 0000000..1b9f6b6 --- /dev/null +++ b/test/forge_test.exs @@ -0,0 +1,228 @@ +defmodule Tezex.ForgeTest do + use ExUnit.Case + + alias Tezex.Forge + alias Tezex.ForgeOperation + + describe "test_micheline" do + test "address" do + addr = "tz1LKpeN8ZSSFNyTWiBNaE4u4sjaq7J1Vz2z" + + assert "0000078694ecd15392219b7e47814ecfa11f90192642" == + :binary.encode_hex(Forge.forge_address(addr), :lowercase) + + assert Forge.unforge_address(Forge.forge_address(addr)) == addr + end + + test "public_key" do + sig = "edpktsPhZ8weLEXqf4Fo5FS9Qx8ZuX4QpEBEwe63L747G8iDjTAF6w" + + assert "001de67a53b0d3ab18dd6c415da17c9f83015489cde2c7165a3ada081a6049b78f" == + :binary.encode_hex(Forge.forge_public_key(sig), :lowercase) + + assert Forge.unforge_public_key(Forge.forge_public_key(sig)) == sig + end + + test "forge_base58" do + v = "BKpLvH3E3bUa5Z2nb3RkH2p6EKLfymvxUAEgtRJnu4m9UX1TWUb" + + assert :binary.encode_hex(Forge.forge_base58(v), :lowercase) == + "0dc397b7865779d87bd47d406e8b4eee84498f22ab01dff124433c7f057af5ae" + end + + test "unforge_signature" do + input = + :binary.decode_hex( + "49d47dba27bd76208b092f3e500f64818920c817491b8b9094f28c2c2b9c6721b257b8878ce47182122b8ea84aeacd84a8aa28cb1f1fe48a26355a7bca4b8306" + ) + + output = + "sigXeXB5JD5TaLb3xgTPKjgf9W45judiCmNP9UBdZBdmtHSGBxL1M8ZSUb6LpjGP2MdfUBTB4WHs5APnvyRV1LooU6QHJuDe" + + assert Forge.unforge_signature(input) == output + end + + test "un/forge int" do + xs = [ + {{-1, 1}, <<65>>}, + {{0, 1}, <<0>>}, + {{1, 1}, <<1>>}, + {{63, 1}, <<63>>}, + {{64, 2}, <<128, 1>>}, + {{12_349_129_381_238, 7}, <<182, 213, 194, 151, 232, 206, 5>>}, + {{-12_349_129_381_238, 7}, <<246, 213, 194, 151, 232, 206, 5>>} + ] + + Enum.each(xs, fn {{decoded, len}, encoded} -> + assert Forge.forge_int(decoded) == encoded + assert Forge.unforge_int(encoded) == {decoded, len} + end) + + assert Forge.unforge_int( + :binary.decode_hex( + "000521000307430359030a0743035d0a00000015002523250b271e153be6c2668954114be101d04d3d05700005054200030342034d" + ) + ) == {0, 1} + end + end + + describe "pytezos tests" do + test "test_forge_combs" do + expr = %{ + "prim" => "Pair", + "args" => [%{"int" => "1"}, %{"int" => "2"}, %{"int" => "3"}, %{"int" => "4"}] + } + + assert expr == Forge.unforge_micheline(Forge.forge_micheline(expr)) + end + + test "test_prim_sequence_three_args" do + packed = + "0502000000f003200743036e0a00000016010cd84cb6f78f1e146e5e86b3648327edfd45618e0007430368010000000c63616c6c6261636b2d343034037706550765096500000031046e0000000625766f746572045d0000000a2563616e64696461746504590000000f25657865637574655f766f74696e670000000c25766f74655f706172616d73046e00000007256275636b657400000010256c61756e63685f63616c6c6261636b072f020000000203270200000004034c03200743036a00000521000307430359030a0743035d0a00000015002523250b271e153be6c2668954114be101d04d3d05700005054200030342034d" + + data = :binary.decode_hex(packed) + + result = + Forge.unforge_micheline(binary_slice(data, 1..-1//1)) + + expected_result = [ + %{"prim" => "DROP"}, + %{ + "prim" => "PUSH", + "args" => [ + %{"prim" => "address"}, + %{"bytes" => "010cd84cb6f78f1e146e5e86b3648327edfd45618e00"} + ] + }, + %{"prim" => "PUSH", "args" => [%{"prim" => "string"}, %{"string" => "callback-404"}]}, + %{"prim" => "SELF_ADDRESS"}, + %{ + "prim" => "CONTRACT", + "args" => [ + %{ + "prim" => "pair", + "args" => [ + %{ + "prim" => "pair", + "args" => [ + %{"prim" => "address", "annots" => ["%voter"]}, + %{"prim" => "key_hash", "annots" => ["%candidate"]}, + %{"prim" => "bool", "annots" => ["%execute_voting"]} + ], + "annots" => ["%vote_params"] + }, + %{"prim" => "address", "annots" => ["%bucket"]} + ] + } + ], + "annots" => ["%launch_callback"] + }, + %{ + "prim" => "IF_NONE", + "args" => [[%{"prim" => "FAILWITH"}], [%{"prim" => "SWAP"}, %{"prim" => "DROP"}]] + }, + %{"prim" => "PUSH", "args" => [%{"prim" => "mutez"}, %{"int" => "0"}]}, + %{"prim" => "DUP", "args" => [%{"int" => "3"}]}, + %{"prim" => "PUSH", "args" => [%{"prim" => "bool"}, %{"prim" => "True"}]}, + %{ + "prim" => "PUSH", + "args" => [ + %{"prim" => "key_hash"}, + %{"bytes" => "002523250b271e153be6c2668954114be101d04d3d"} + ] + }, + %{"prim" => "DIG", "args" => [%{"int" => "5"}]}, + %{"prim" => "PAIR", "args" => [%{"int" => "3"}]}, + %{"prim" => "PAIR"}, + %{"prim" => "TRANSFER_TOKENS"} + ] + + assert result == expected_result + + assert expected_result == Forge.unforge_micheline(Forge.forge_micheline(expected_result)) + end + + test "test_regr_local_remote_diff" do + opg = %{ + "branch" => "BKpLvH3E3bUa5Z2nb3RkH2p6EKLfymvxUAEgtRJnu4m9UX1TWUb", + "contents" => [ + %{ + "amount" => "0", + "counter" => "446245", + "destination" => "KT1VYUxhLoSvouozCaDGL1XcswnagNfwr3yi", + "fee" => "104274", + "gas_limit" => "1040000", + "kind" => "transaction", + "parameters" => %{"entrypoint" => "default", "value" => %{"prim" => "Unit"}}, + "source" => "tz1grSQDByRpnVs7sPtaprNZRp531ZKz6Jmm", + "storage_limit" => "60000" + } + ], + "protocol" => "PsCARTHAGazKbHtnKfLzQg3kms52kSRpgnDY982a9oYsSXRLQEb", + "signature" => nil + } + + local = ForgeOperation.forge_operation_group(opg) + + remote = + "0dc397b7865779d87bd47d406e8b4eee84498f22ab01dff124433c7f057af5ae6c00e8b36c80efb51ec85a14562426049aa182a3ce38d2ae06a59e1b80bd3fe0d4030001e5ebf2dcc7dcc9d13c2c45cd76823dd604740c7f0000" + + assert local == remote + end + end + + describe "pack" do + result = Forge.pack(9, :int) + assert result == "050009" + + result = Forge.pack(9, :nat) + assert result == "050009" + + result = Forge.pack(-9, :int) + assert result == "050049" + + result = Forge.pack(-6407, :int) + assert result == "0500c764" + + result = Forge.pack(98_978_654, :int) + assert result == "05009eadb25e" + + result = Forge.pack(-78_181_343_541, :int) + assert result == "0500f584c5bfc604" + + result = Forge.pack("tz1eEnQhbwf6trb8Q8mPb2RaPkNk2rN7BKi8", :address) + assert result == "050a000000160000cc04e65d3e38e4e8059041f27a649c76630f95e2" + + result = Forge.pack("tz1eEnQhbwf6trb8Q8mPb2RaPkNk2rN7BKi8", :key_hash) + assert result == "050a000000160000cc04e65d3e38e4e8059041f27a649c76630f95e2" + + result = Forge.pack("Tezos Tacos Nachos", :string) + assert result == "05010000001254657a6f73205461636f73204e6163686f73" + + result = Forge.pack(:binary.decode_hex("0a0a0a"), :bytes) + assert result == "050a000000030a0a0a" + + result = Forge.pack(%{"prim" => "Pair", "args" => [%{"int" => "1"}, %{"int" => "12"}]}) + assert result == "0507070001000c" + + value = %{ + "prim" => "Pair", + "args" => [ + %{"int" => "42"}, + %{ + "prim" => "Left", + "args" => [ + %{ + "prim" => "Left", + "args" => [ + %{"prim" => "Pair", "args" => [%{"int" => "1585470660"}, %{"int" => "900100"}]} + ] + } + ] + } + ] + } + + assert Forge.pack(value) == "050707002a0505050507070084f382e80b0084f06d" + end +end diff --git a/test/micheline/zarith_test.exs b/test/micheline/zarith_test.exs deleted file mode 100644 index d2575c3..0000000 --- a/test/micheline/zarith_test.exs +++ /dev/null @@ -1,73 +0,0 @@ -defmodule Tezex.Micheline.ZarithTest do - use ExUnit.Case - alias Tezex.Micheline - alias Tezex.Micheline.Zarith - doctest Micheline.Zarith - - test "encode/decode" do - n = 1_000_000_000_000 - random = fn -> trunc(:rand.uniform(n) - n / 2) end - - Enum.each(1..5000, fn _ -> - number = random.() - assert number == Zarith.decode(Zarith.encode(number)) - end) - end - - test "decode/1" do - assert 1_000_000 = Zarith.decode("0080897a") - assert 917_431_994 = Zarith.decode("00ba9af7ea06") - assert -917_431_994 = Zarith.decode("00fa9af7ea06") - assert 365_729 = Zarith.decode("00a1d22c") - assert -365_729 = Zarith.decode("00e1d22c") - assert 610_913_435_200 = Zarith.decode("0080f9b9d4c723") - assert -610_913_435_200 = Zarith.decode("00c0f9b9d4c723") - assert -33 = Zarith.decode("00610100") - end - - test "encode/1" do - assert "80897a" = Zarith.encode(1_000_000) - assert "a1d22c" = Zarith.encode(365_729) - assert "e1d22c" = Zarith.encode(-365_729) - assert "61" = Zarith.encode(-33) - assert "ba9af7ea06" = Zarith.encode(917_431_994) - assert "fa9af7ea06" = Zarith.encode(-917_431_994) - assert "80f9b9d4c723" = Zarith.encode(610_913_435_200) - assert "c0f9b9d4c723" = Zarith.encode(-610_913_435_200) - end - - describe "consume/1" do - test "1_000_000" do - assert {%{int: 1_000_000}, 8} = Zarith.consume("0080897a") - end - - test "917_431_994" do - assert {%{int: 917_431_994}, 12} = Zarith.consume("00ba9af7ea06") - end - - test "-917_431_994" do - assert {%{int: -917_431_994}, 12} = Zarith.consume("00fa9af7ea06") - end - - test "365_729" do - assert {%{int: 365_729}, 8} = Zarith.consume("00a1d22c") - end - - test "-365_729" do - assert {%{int: -365_729}, 8} = Zarith.consume("00e1d22c") - end - - test "610_913_435_200" do - assert {%{int: 610_913_435_200}, 14} = Zarith.consume("0080f9b9d4c723") - end - - test "-610_913_435_200" do - assert {%{int: -610_913_435_200}, 14} = Zarith.consume("00c0f9b9d4c723") - end - - test "-33" do - assert {%{int: -33}, 4} = Zarith.consume("00610100") - assert {%{int: -33}, 4} = Micheline.hex_to_micheline("00610100") - end - end -end diff --git a/test/micheline_test.exs b/test/micheline_test.exs deleted file mode 100644 index 89c6c8d..0000000 --- a/test/micheline_test.exs +++ /dev/null @@ -1,115 +0,0 @@ -defmodule Tezex.MichelineTest do - use ExUnit.Case - alias Tezex.Micheline - doctest Tezex.Micheline - - test "Small int: hex -> Micheline" do - assert {%{int: 6}, _} = Micheline.hex_to_micheline("0006") - - assert {%{int: -6}, _} = Micheline.hex_to_micheline("0046") - - assert {%{int: 63}, _} = Micheline.hex_to_micheline("003f") - - assert {%{int: -63}, _} = Micheline.hex_to_micheline("007f") - end - - test "Medium int: hex -> Micheline" do - assert {%{int: 97}, _} = Micheline.hex_to_micheline("00a101") - - assert {%{int: -127}, _} = Micheline.hex_to_micheline("00ff01") - - assert {%{int: 900}, _} = Micheline.hex_to_micheline("00840e") - - assert {%{int: -900}, _} = Micheline.hex_to_micheline("00c40e") - end - - test "Large int: hex -> Micheline" do - assert {%{int: 917_431_994}, _} = Micheline.hex_to_micheline("00ba9af7ea06") - - assert {%{int: -917_431_994}, _} = Micheline.hex_to_micheline("00fa9af7ea06") - - assert {%{int: 365_729}, _} = Micheline.hex_to_micheline("00a1d22c") - - assert {%{int: -365_729}, _} = Micheline.hex_to_micheline("00e1d22c") - - assert {%{int: 610_913_435_200}, _} = Micheline.hex_to_micheline("0080f9b9d4c723") - - assert {%{int: -610_913_435_200}, _} = Micheline.hex_to_micheline("00c0f9b9d4c723") - end - - test "strings" do - {result, 122} = - Micheline.hex_to_micheline( - "0100000038697066733a2f2f516d54556177504451526557325754514d6869725967416b707854427a487a616e3646465846776b685071654d6a2f3434" - ) - - assert result == %{string: "ipfs://QmTUawPDQReW2WTQMhirYgAkpxTBzHzan6FFXFwkhPqeMj/44"} - end - - test "bytes" do - {result, _} = Micheline.hex_to_micheline("0a000000080123456789abcdef") - assert result == %{bytes: "0123456789abcdef"} - end - - test "Mixed literal value array" do - assert {[%{int: -33}, %{string: "tezos"}, %{string: ""}, %{string: "cryptonomic"}], 76} = - Micheline.hex_to_micheline( - "02000000210061010000000574657a6f730100000000010000000b63727970746f6e6f6d6963" - ) - end - - test "Bare primitive" do - assert {%{prim: "PUSH"}, 4} = Micheline.hex_to_micheline("0343") - end - - test "Single primitive with a single annotation" do - assert {%{annot: "@cba", prim: "PUSH"}, 20} = - Micheline.hex_to_micheline("04430000000440636261") - end - - test "Single primitive with a single argument" do - {result, _} = Micheline.hex_to_micheline("053d036d") - assert result == %{prim: "NIL", args: [%{prim: "operation"}]} - end - - test "Single primitive with two arguments" do - assert {%{annots: ["@cba"], args: [%{prim: "operation"}], prim: "NIL"}, 24} = - Micheline.hex_to_micheline("063d036d0000000440636261") - - assert {%{args: [%{prim: "operation"}, %{prim: "operation"}], prim: "NIL"}, 12} = - Micheline.hex_to_micheline("073d036d036d") - end - - test "Single primitive with two arguments and annotation" do - assert {%{ - annots: ["@cba"], - args: [%{prim: "operation"}, %{prim: "operation"}], - prim: "NIL" - }, 28} = Micheline.hex_to_micheline("083d036d036d0000000440636261") - end - - test "Single primitive with more than two arguments and no annotations" do - assert {%{ - args: [%{prim: "operation"}, %{prim: "operation"}, %{prim: "operation"}], - prim: "NIL" - }, 32} = Micheline.hex_to_micheline("093d00000006036d036d036d00000000") - end - - test "Single primitive with more than two arguments and multiple annotations" do - assert {%{ - annots: ["@red", "@green", "@blue"], - args: [%{prim: "operation"}, %{prim: "operation"}, %{prim: "operation"}], - prim: "NIL" - }, - 66} = - Micheline.hex_to_micheline( - "093d00000006036d036d036d00000011407265642040677265656e2040626c7565" - ) - end - - test "Encode/decode string" do - assert "050100000003666F6F" == Micheline.string_to_micheline_hex("foo") - s = "0100000003666F6F" - assert {%{string: "foo"}, byte_size(s)} == Micheline.hex_to_micheline(s) - end -end diff --git a/test/zarith_test.exs b/test/zarith_test.exs new file mode 100644 index 0000000..ab8d4ef --- /dev/null +++ b/test/zarith_test.exs @@ -0,0 +1,75 @@ +defmodule Tezex.ZarithTest do + use ExUnit.Case + + alias Tezex.Zarith + doctest Zarith + + test "encode/decode" do + n = 1_000_000_000_000 + random = fn -> trunc(:rand.uniform(n) - n / 2) end + + Enum.each(1..5000, fn _ -> + number = random.() + assert number == Zarith.decode(Zarith.encode(number)) + end) + end + + test "decode/1" do + assert 1_000_000 = Zarith.decode("80897a") + assert 917_431_994 = Zarith.decode("ba9af7ea06") + assert -917_431_994 = Zarith.decode("fa9af7ea06") + assert 365_729 = Zarith.decode("a1d22c") + assert -365_729 = Zarith.decode("e1d22c") + assert 610_913_435_200 = Zarith.decode("80f9b9d4c723") + assert -610_913_435_200 = Zarith.decode("c0f9b9d4c723") + assert -33 = Zarith.decode("610100") + end + + test "encode/1" do + assert "80897a" = Zarith.encode(1_000_000) + assert "a1d22c" = Zarith.encode(365_729) + assert "e1d22c" = Zarith.encode(-365_729) + assert "61" = Zarith.encode(-33) + assert "ba9af7ea06" = Zarith.encode(917_431_994) + assert "fa9af7ea06" = Zarith.encode(-917_431_994) + assert "80f9b9d4c723" = Zarith.encode(610_913_435_200) + assert "c0f9b9d4c723" = Zarith.encode(-610_913_435_200) + end + + describe "consume/1" do + test "1_000_000" do + packed = "80897a" + assert {%{int: "1000000"}, byte_size(packed)} == Zarith.consume(packed) + end + + test "917_431_994" do + packed = "ba9af7ea06" + assert {%{int: "917431994"}, byte_size(packed)} == Zarith.consume(packed) + end + + test "-917_431_994" do + packed = "fa9af7ea06" + assert {%{int: "-917431994"}, byte_size(packed)} == Zarith.consume(packed) + end + + test "365_729" do + packed = "a1d22c" + assert {%{int: "365729"}, byte_size(packed)} == Zarith.consume(packed) + end + + test "-365_729" do + packed = "e1d22c" + assert {%{int: "-365729"}, byte_size(packed)} == Zarith.consume(packed) + end + + test "610_913_435_200" do + packed = "80f9b9d4c723" + assert {%{int: "610913435200"}, byte_size(packed)} == Zarith.consume(packed) + end + + test "-610_913_435_200" do + packed = "c0f9b9d4c723" + assert {%{int: "-610913435200"}, byte_size(packed)} == Zarith.consume(packed) + end + end +end