diff --git a/lib/examples/e_merkle_tree.ex b/lib/examples/e_merkle_tree.ex index f719f1e..91f8815 100644 --- a/lib/examples/e_merkle_tree.ex +++ b/lib/examples/e_merkle_tree.ex @@ -1,6 +1,7 @@ defmodule Examples.EMerkleTree do require ExUnit.Assertions import ExUnit.Assertions + alias Anoma.LocalDomain.BatchMerkleTree alias Anoma.LocalDomain.MerkleTree def empty_tree() do @@ -12,7 +13,7 @@ defmodule Examples.EMerkleTree do empty_tree() |> MerkleTree.add([:crypto.hash(:sha256, "a")]) - assert Enum.at(Map.get(new_tree.nodes, 1), 0) == + assert Map.get(Map.get(new_tree.nodes, 1), 0) == :crypto.hash( :sha256, :crypto.hash(:sha256, "a") <> @@ -47,6 +48,15 @@ defmodule Examples.EMerkleTree do {frontiers, root} end + def generate_a_proof_wrongly() do + tree = expand_merkle_tree() + + assert nil == + MerkleTree.generate_proof(tree, :crypto.hash(:sha256, "d")) + + :ok + end + def verify_a_proof() do {frontiers, root} = generate_a_proof() MerkleTree.verify_proof(:crypto.hash(:sha256, "b"), frontiers, root) @@ -65,8 +75,14 @@ defmodule Examples.EMerkleTree do new_tree = MerkleTree.add(empty_tree(), leaves) + sorted_leaves = + Map.get(new_tree.nodes, 0) + |> Map.to_list() + |> Enum.sort(fn {ind1, _}, {ind2, _} -> ind1 < ind2 end) + |> Enum.map(&elem(&1, 1)) + # Check that the leaves are as expected - assert List.starts_with?(Map.get(new_tree.nodes, 0), leaves) + assert List.starts_with?(sorted_leaves, leaves) # Check that index is as expected assert new_tree.next_index == leaves_length @@ -78,4 +94,33 @@ defmodule Examples.EMerkleTree do new_tree end + + def merkle_tree_differential_root_comparisson(leaves_number \\ 10_000) do + tree1 = MerkleTree.new() + tree2 = BatchMerkleTree.new() + + for i <- 1..leaves_number, reduce: {tree1, tree2} do + {tree1, tree2} -> + leaf = :crypto.hash(:sha256, :erlang.term_to_binary(i)) + upd_tree1 = MerkleTree.add(tree1, [leaf]) + upd_tree2 = BatchMerkleTree.add(tree2, [leaf]) + + assert MerkleTree.root(upd_tree1) == + BatchMerkleTree.root(upd_tree2) + + {upd_tree1, upd_tree2} + end + end + + def merkle_tree_differential_proof_comparisson(leaves_number \\ 100) do + {tree1, tree2} = + merkle_tree_differential_root_comparisson(leaves_number) + + leaves = Map.get(tree1.nodes, 0) + + for leaf <- leaves do + assert MerkleTree.generate_proof(tree1, leaf) == + BatchMerkleTree.generate_proof(tree2, leaf) + end + end end diff --git a/lib/examples/e_poller.ex b/lib/examples/e_poller.ex deleted file mode 100644 index a495066..0000000 --- a/lib/examples/e_poller.ex +++ /dev/null @@ -1,51 +0,0 @@ -defmodule Examples.EPoller do - alias Anoma.LocalDomain.System.Poller - import ExUnit.Assertions - use Anoma.LocalDomain - - def decrypt_payload() do - secret_key_hex = - "e458b7d0ea3333c9ffbc4a1b50ac5b786fa0fdf91789898c25ccdc3dff1c48e6" - - public_key_hex = - "0385ef12ce29127dbf15a84e23cd9a1e9761a7704351641015f63996b2fcafe95d" - - discovery_payload_hex = - "110000000000000000866b72791189682aaac5b81e387fdb0bae5b2a457aedc7c87af3334a210000000000000003b2fc87e9b9067e74db1f9e4f92bef38765977cbfe163a49725fad06ed178d21e0000" - - Anoma.LocalDomain.System.Poller.can_decrypt( - %{secret_key: secret_key_hex, public_key: public_key_hex}, - discovery_payload_hex - ) - end - - def cipher_keypair_storage_retrieval() do - secret_key_hex = - "e458b7d0ea3333c9ffbc4a1b50ac5b786fa0fdf91789898c25ccdc3dff1c48e6" - - public_key_hex = - "0385ef12ce29127dbf15a84e23cd9a1e9761a7704351641015f63996b2fcafe95d" - - keypair = %{secret_key: secret_key_hex, public_key: public_key_hex} - - contract_name = "contract" - - {:ok, node_id, pid} = Examples.ENode.start_node() - - Poller.write_keypair(node_id, contract_name, keypair) - - {:ok, keypairs} = - Anoma.LocalDomain.Storage.ls( - node_id, - ~k"/!contract_name/discovery_keypair" - ) - - assert keypairs == - MapSet.new([ - ~k"/!contract_name/discovery_keypair/!public_key_hex" - ]) - - Examples.ENode.stop_node(pid) - keypairs - end -end diff --git a/lib/local_domain/structures/batch_merkle_tree.ex b/lib/local_domain/structures/batch_merkle_tree.ex new file mode 100644 index 0000000..8992f20 --- /dev/null +++ b/lib/local_domain/structures/batch_merkle_tree.ex @@ -0,0 +1,212 @@ +defmodule Anoma.LocalDomain.BatchMerkleTree do + @moduledoc """ + I implement merkle tree behaviour for use within local domain applications. + + Has a variable depth. + """ + + import Bitwise + use TypedStruct + + alias __MODULE__ + + typedstruct enforce: true do + # map from levels to the map from index to the node + field(:nodes, %{integer() => %{integer() => binary()}}, + default: %{ + 0 => %{0 => :crypto.hash(:sha256, "EMPTY")} + } + ) + + field(:empty_nodes, %{integer() => binary()}) + field(:next_index, non_neg_integer(), default: 0) + field(:capacity, non_neg_integer(), default: 1) + + # leaf map with leaves as keys + # for efficient path computation + field(:leaf_map, %{binary() => integer()}, default: %{}) + end + + def hash(bytes) do + :crypto.hash(:sha256, bytes) + end + + def empty() do + hash("EMPTY") + end + + def new() do + # Assume we have a tree at most of depth 32 + empty_nodes = + for i <- 1..31, reduce: %{0 => :crypto.hash(:sha256, "EMPTY")} do + acc -> + previous_empty_hash = Map.get(acc, i - 1) + + Map.put( + acc, + i, + :crypto.hash( + :sha256, + previous_empty_hash <> previous_empty_hash + ) + ) + end + + %BatchMerkleTree{empty_nodes: empty_nodes} + end + + def depth(tree) do + tree.capacity |> :math.log2() |> trunc() + end + + def root(tree) do + Map.get(Map.get(tree.nodes, depth(tree)), 0) + end + + def add(tree, leaves) do + index = tree.next_index + + # Compute the new leaf map and new commitment length + # in the same loop + {new_leaf_map, new_commitment_length} = + for leaf <- leaves, reduce: {tree.leaf_map, index} do + {map_acc, index_acc} -> + {Map.put(map_acc, index_acc, leaf), index + 1} + end + + {depth, capacity} = + if new_commitment_length >= tree.capacity do + # If the tree capacity is exceeded, calculate the new minimal depth + new_depth = + (new_commitment_length |> :math.log2() |> trunc()) + 1 + + {new_depth, Integer.pow(2, new_depth)} + else + {depth(tree), tree.capacity} + end + + # Add leaves and recompute needed intermediary nodes + new_nodes = + compute_nodes(depth, tree.nodes, tree.empty_nodes, index, leaves) + + %BatchMerkleTree{ + tree + | nodes: new_nodes, + next_index: new_commitment_length, + capacity: capacity, + leaf_map: new_leaf_map + } + end + + def generate_proof(tree, leaf) do + # Get the index of the leaf + index_found = Map.get(tree.leaf_map, leaf) + + if index_found do + {path, root, _index} = + for i <- 0..(depth(tree) - 1), + reduce: {[], leaf, index_found} do + {path, node, index} -> + # Take the current level of the tree + current_nodes = Map.get(tree.nodes, i) + + is_left = (index &&& 1) == 0 + + if is_left do + # If the node is a left one, take its right sibling + sibling = Map.get(current_nodes, index + 1, empty()) + + # Hash the node on the left and sibling on the right + # The index of its parents is going to be index / 2 + {path ++ [{sibling, true}], hash(node <> sibling), + div(index, 2)} + else + # If the node is a right one, take its left sibling + sibling = Map.get(current_nodes, index - 1, empty()) + + # Hash the node on the right and sibling on the left + # The index of its parents is going to be (index - 1) / 2 + {path ++ [{sibling, false}], hash(sibling <> node), + div(index - 1, 2)} + end + end + + if root == root(tree) do + {path, root} + else + nil + end + end + end + + def verify_proof(leaf, path, root) do + calculated_root = + Enum.reduce(path, leaf, fn {neighbour, is_left}, acc -> + if is_left do + hash(acc <> neighbour) + else + hash(neighbour <> acc) + end + end) + + calculated_root == root + end + + defp compute_nodes(depth, nodes, empty_nodes, index, leaves) do + # Iterate over each level of the tree, updating + # only the parent nodes of the given leaves + {new_nodes, _, _} = + for i <- 0..depth, reduce: {nodes, index, leaves} do + {acc_nodes, index, nodes} -> + # Fetch the current level of the tree + current_nodes = Map.get(acc_nodes, i, %{}) + + # If the first node is the right one, fetch its sibling + initial_left_sibling = + if is_left(index) do + nil + else + Map.get(current_nodes, index - 1) + end + + # Iterate over all the nodes + # Populate the current level with them + # Also calculate the list of parent nodes + {updated_current_nodes, parents, _, final_left_sibling} = + for node <- nodes, + reduce: {current_nodes, [], index, initial_left_sibling} do + {acc_current_nodes, parents, j, left_sibling} -> + if left_sibling do + # Hash the left sibling with the current node + {Map.put(acc_current_nodes, j, node), + [hash(left_sibling <> node) | parents], j + 1, nil} + else + # record the current node as a left sibling + {Map.put(acc_current_nodes, j, node), parents, j + 1, + node} + end + end + + # If the final node was a left one, we have to compute one more parent + # by hashing with an empty node of the appropriate level + final_parents = + if final_left_sibling do + [ + hash(final_left_sibling <> Map.get(empty_nodes, i)) + | parents + ] + else + parents + end + + {Map.put(acc_nodes, i, updated_current_nodes), div(index, 2), + Enum.reverse(final_parents)} + end + + new_nodes + end + + defp is_left(index) do + (index &&& 1) == 0 + end +end diff --git a/lib/local_domain/structures/merkle_tree.ex b/lib/local_domain/structures/merkle_tree.ex index 068163e..0e08aab 100644 --- a/lib/local_domain/structures/merkle_tree.ex +++ b/lib/local_domain/structures/merkle_tree.ex @@ -8,13 +8,17 @@ defmodule Anoma.LocalDomain.MerkleTree do import Bitwise use TypedStruct + alias __MODULE__ + typedstruct enforce: true do - field(:nodes, %{integer() => list(binary())}, + # map from levels to the map from index to the node + field(:nodes, %{integer() => %{integer() => binary()}}, default: %{ - 0 => [:crypto.hash(:sha256, "EMPTY")] + 0 => %{0 => :crypto.hash(:sha256, "EMPTY")} } ) + field(:empty_nodes, %{integer() => binary()}) field(:next_index, non_neg_integer(), default: 0) field(:capacity, non_neg_integer(), default: 1) end @@ -28,72 +32,98 @@ defmodule Anoma.LocalDomain.MerkleTree do end def new() do - %Anoma.LocalDomain.MerkleTree{} + # Assume we have a tree at most of depth 32 + empty_nodes = + for i <- 1..31, reduce: %{0 => :crypto.hash(:sha256, "EMPTY")} do + acc -> + previous_empty_hash = Map.get(acc, i - 1) + + Map.put( + acc, + i, + :crypto.hash( + :sha256, + previous_empty_hash <> previous_empty_hash + ) + ) + end + + %Anoma.LocalDomain.MerkleTree{empty_nodes: empty_nodes} end def depth(tree) do - length(Map.keys(tree.nodes)) - 1 + tree.capacity |> :math.log2() |> trunc() end def root(tree) do - Enum.at(Map.get(tree.nodes, depth(tree)), 0) - end - - def generate_empty_branch(n) do - Enum.map(1..n, fn _ -> empty() end) + Map.get(Map.get(tree.nodes, depth(tree)), 0) end def add(tree, values) do - {new_leaves, next_index, capacity} = - Enum.reduce( - values, - {Map.get(tree.nodes, 0), tree.next_index, tree.capacity}, - fn leaf, {new_leaves, next_index, new_capacity} -> - add_leaf(new_leaves, next_index, new_capacity, leaf) - end - ) - - %Anoma.LocalDomain.MerkleTree{ - nodes: - Map.merge(%{0 => new_leaves}, calculate_nodes(new_leaves, 1)), - next_index: next_index, - capacity: capacity - } + Enum.reduce( + values, + tree, + fn leaf, acc_tree -> + add_leaf(acc_tree, leaf) + end + ) end def generate_proof(tree, leaf) do - {frontiers, root} = - for i <- 0..(depth(tree) - 1), reduce: {[], leaf} do - {acc, leaf} -> - leaves = Map.get(tree.nodes, i) - - leaf_index = - leaves - |> Enum.find_index(&(&1 == leaf)) - - is_left = (leaf_index &&& 1) == 0 - - if is_left do - neighbour = Enum.at(leaves, leaf_index + 1) + leaves = Map.get(tree.nodes, 0) + + # Get the index of the leaf + found_elem = + leaves + |> Map.to_list() + |> Enum.find(fn + {_, ^leaf} -> + true + + _ -> + false + end) - {acc ++ [{neighbour, true}], hash(leaf <> neighbour)} - else - neighbour = Enum.at(leaves, leaf_index - 1) + if found_elem do + {path, root, _index} = + for i <- 0..(depth(tree) - 1), + reduce: {[], leaf, elem(found_elem, 0)} do + {path, node, index} -> + # Take the current level of the tree + current_nodes = Map.get(tree.nodes, i) + + is_left = (index &&& 1) == 0 + + if is_left do + # If the node is a left one, take its right sibling + sibling = Map.get(current_nodes, index + 1, empty()) + + # Hash the node on the left and sibling on the right + # The index of its parents is going to be index / 2 + {path ++ [{sibling, true}], hash(node <> sibling), + div(index, 2)} + else + # If the node is a right one, take its left sibling + sibling = Map.get(current_nodes, index - 1, empty()) + + # Hash the node on the right and sibling on the left + # The index of its parents is going to be (index - 1) / 2 + {path ++ [{sibling, false}], hash(sibling <> node), + div(index - 1, 2)} + end + end - {acc ++ [{neighbour, false}], hash(neighbour <> leaf)} - end + if root == root(tree) do + {path, root} + else + nil end - - if root == root(tree) do - {frontiers, root} - else - nil end end - def verify_proof(leaf, frontiers, root) do + def verify_proof(leaf, path, root) do calculated_root = - Enum.reduce(frontiers, leaf, fn {neighbour, is_left}, acc -> + Enum.reduce(path, leaf, fn {neighbour, is_left}, acc -> if is_left do hash(acc <> neighbour) else @@ -104,27 +134,69 @@ defmodule Anoma.LocalDomain.MerkleTree do calculated_root == root end - defp add_leaf(leaves, index, capacity, leaf) do - new_leaves = List.replace_at(leaves, index, leaf) - new_index = index + 1 - - if new_index == capacity do - {new_leaves ++ generate_empty_branch(length(leaves)), new_index, - capacity * 2} + defp add_leaf(tree, leaf) do + index = tree.next_index + depth = depth(tree) + + # Add a leaf and recompute needed intermediary nodes + new_nodes = + compute_nodes(depth, tree.nodes, tree.empty_nodes, index, leaf) + + if index + 1 == tree.capacity do + # If the tree is fully filled, we need to recompute a new + # root as if by adding an extra empty leaf at next index + expanded_nodes = + compute_nodes( + depth + 1, + new_nodes, + tree.empty_nodes, + index + 1, + empty() + ) + + %MerkleTree{ + tree + | nodes: expanded_nodes, + next_index: index + 1, + capacity: tree.capacity * 2 + } else - {new_leaves, new_index, capacity} + %MerkleTree{tree | nodes: new_nodes, next_index: index + 1} end end - defp calculate_nodes(leaves, i) do - next_level = - Enum.chunk_every(leaves, 2, 2, :discard) - |> Enum.map(fn [a, b] -> hash(a <> b) end) + defp compute_nodes(depth, nodes, empty_nodes, index, leaf) do + # Iterate over each level of the tree, updating + # only the parent nodes of the leaf + {new_nodes, _, _} = + for i <- 0..depth, reduce: {nodes, index, leaf} do + {acc_nodes, index, node} -> + # Fetch the current level of the tree + current_nodes = Map.get(acc_nodes, i, %{}) - if length(next_level) > 1 do - Map.merge(%{i => next_level}, calculate_nodes(next_level, i + 1)) - else - %{i => next_level} - end + # Put the updated node at the given index + updated_nodes = + Map.put(acc_nodes, i, Map.put(current_nodes, index, node)) + + is_left = (index &&& 1) == 0 + + if is_left do + # If the node is a left one, fetch its right sibling + sibling = + Map.get(current_nodes, index + 1, Map.get(empty_nodes, i)) + + # Hash the node on the left and sibling on the right + # The index of its parents is going to be index / 2 + {updated_nodes, div(index, 2), hash(node <> sibling)} + else + # If the node is a right one, fetch its left sibling + sibling = Map.get(current_nodes, index - 1) + # Hash the node on the left and sibling on the right + # The index of its parents is going to be (index - 1) / 2 + {updated_nodes, div(index - 1, 2), hash(sibling <> node)} + end + end + + new_nodes end end diff --git a/lib/local_domain/system/poller.ex b/lib/local_domain/system/poller.ex deleted file mode 100644 index a7f9352..0000000 --- a/lib/local_domain/system/poller.ex +++ /dev/null @@ -1,459 +0,0 @@ -defmodule Anoma.LocalDomain.System.Poller do - @moduledoc """ - I poll for events from a graphQL endpoint for a protocol adapter contract indexer. - """ - - @behaviour :gen_statem - use Anoma.LocalDomain - require Logger - - def child_spec(opts) do - %{ - id: {__MODULE__, opts[:node_id]}, - start: {__MODULE__, :start_link, [opts]}, - type: :worker, - restart: :temporary, - shutdown: 5_000 - } - end - - @doc """ - Starts a poller for indexing a ProtocolAdapter contract - """ - def start(node_id, contract, endpoint) do - Logger.debug("STARTING POLLER PROCESS") - - args = %{ - contract: contract, - cipher_keypairs: [], - endpoint: endpoint, - node_id: node_id - } - - DynamicSupervisor.start_child( - AppTasksSupervisor, - {__MODULE__, args} - ) - end - - @doc """ - Stops the PA contract poller - """ - def stop(pid) do - DynamicSupervisor.terminate_child(AppTasksSupervisor, pid) - end - - @doc """ - Adds a cipher keypair - """ - def add_cipher_keypair(node_id, cipher_keypair) do - name = Anoma.LocalDomain.Registry.via(node_id, __MODULE__) - :gen_statem.cast(name, {:add_keypair, cipher_keypair}) - end - - def transactionExecutedQuery() do - """ - query($min: Int!) { - ProtocolAdapter_TransactionExecuted(where: {blockNumber: {_gt: $min}}) { - transactions { - tag - isConsumed - resourcePayloads { - id - blob - } - discoveryPayloads { - id - blob - } - } - blockNumber - } - } - """ - end - - @doc """ - Writes a transaction resource to storage - """ - def write_transaction_resource( - node_id, - contract, - tag, - discovery, - resource, - is_consumed - ) do - Anoma.LocalDomain.Storage.write_local( - node_id, - ~k"/!contract/resource/!tag", - %{ - discovery: discovery, - resource: resource, - is_consumed: is_consumed, - tag: tag - } - ) - end - - @doc """ - Writes a transaction resource to storage, associated with a public key representing the keypair the discovery payload was decrypted with - """ - def write_transaction_resource( - node_id, - contract, - tag, - public_key, - discovery, - resource, - is_consumed - ) do - Anoma.LocalDomain.Storage.write_local( - node_id, - ~k"/!contract/resource/!public_key/!tag", - %{ - discovery: discovery, - resource: resource, - is_consumed: is_consumed, - tag: tag - } - ) - end - - @doc """ - Reads a transaction resource - """ - def read_transaction_resource(node_id, contract, tag) do - Anoma.LocalDomain.Storage.read_latest( - node_id, - ~k"/!contract/resource/!tag" - ) - end - - @doc """ - Reads a transaction resource associated with a public key - """ - def read_transaction_resource(node_id, contract, tag, public_key) do - Anoma.LocalDomain.Storage.read_latest( - node_id, - ~k"/!contract/resource/!public_key/!tag" - ) - end - - @doc """ - Reads current known blockheight - """ - def read_blockheight(node_id, contract) do - Anoma.LocalDomain.Storage.read_latest( - node_id, - ~k"/!contract/blockheight" - ) - end - - @doc """ - Writes the current known blockheight - """ - def write_blockheight(node_id, contract, height) do - Anoma.LocalDomain.Storage.write_local( - node_id, - ~k"/!contract/blockheight", - height - ) - end - - @doc """ - Writes a cipher keypair to storage - """ - def write_keypair(node_id, contract, %{ - secret_key: secret, - public_key: public - }) do - Anoma.LocalDomain.Storage.write_local( - node_id, - ~k"/!contract/discovery_keypair/!public", - secret - ) - end - - def read_commitment_tree(node_id, contract) do - Anoma.LocalDomain.Storage.read_latest( - node_id, - ~k"/!contract/commitments" - ) - end - - def write_commitment_tree(node_id, contract, tree) do - Anoma.LocalDomain.Storage.write_local( - node_id, - ~k"/!contract/commitments", - tree - ) - end - - def prepare_payload_and_keypair( - payload_bytes, - secret_key_bytes, - public_key_bytes - ) do - public_key_with_prefix = - case byte_size(public_key_bytes) do - 33 -> <<33, 0, 0, 0, 0, 0, 0, 0>> <> public_key_bytes - _ -> public_key_bytes - end - - payload_list = :binary.bin_to_list(payload_bytes) - - keypair = - AnomaSDK.Arm.Keypair.from_map(%{ - secret_key: Base.encode64(secret_key_bytes), - public_key: Base.encode64(public_key_with_prefix) - }) - - {payload_list, keypair} - end - - @doc """ - Attempts to discovery payload, given a keypair - """ - def can_decrypt( - %{secret_key: secret_key_hex, public_key: public_key_hex}, - discovery_payload_hex - ) do - with {:ok, secret_key_bytes} <- - Base.decode16(String.trim(secret_key_hex, "0x"), - case: :mixed - ), - {:ok, public_key_bytes} <- - Base.decode16(String.trim(public_key_hex, "0x"), - case: :mixed - ), - {:ok, payload_bytes} <- - Base.decode16(String.trim(discovery_payload_hex, "0x"), - case: :mixed - ), - {payload_list, keypair} <- - prepare_payload_and_keypair( - payload_bytes, - secret_key_bytes, - public_key_bytes - ) do - case AnomaSDK.Arm.decrypt_cipher(payload_list, keypair) do - {:ok, _} -> :ok - decrypted when is_list(decrypted) -> :ok - nil -> {:error, nil} - {:error, reason} -> {:error, reason} - end - else - {:error, reason} -> {:error, reason} - :error -> {:error, :bad_hex} - other -> {:error, {:unexpected, other}} - end - end - - def start_link(opts) do - name = Anoma.LocalDomain.Registry.via(opts[:node_id], __MODULE__) - :gen_statem.start_link(name, __MODULE__, opts, []) - end - - @impl true - def callback_mode, do: :handle_event_function - - @impl true - def init(opts) do - data = - Map.merge( - opts, - %{ - blockheight: - case read_blockheight(opts[:node_id], opts[:contract]) do - {:ok, blockheight} -> blockheight - :absent -> 0 - end, - commitments: - case read_commitment_tree(opts[:node_id], opts[:contract]) do - {:ok, tree} -> tree - :absent -> Anoma.LocalDomain.MerkleTree.new() - end - } - ) - - {:ok, :polling, data, {:state_timeout, 0, :tick}} - end - - @impl true - def handle_event( - :state_timeout, - :tick, - _state, - %{ - cipher_keypairs: cipher_keypairs, - endpoint: endpoint, - blockheight: current_blockheight, - contract: contract, - node_id: node_id, - commitments: commitment_tree - } = data - ) do - Logger.info("POLLING #{endpoint}") - Logger.debug("Current Blockheight #{current_blockheight}") - Logger.debug("Current Keypairs #{inspect(cipher_keypairs)}") - - ## TODO optimise the graphQL so we don't have to do two queries - case Req.post(endpoint, - json: %{ - query: transactionExecutedQuery(), - variables: %{"min" => current_blockheight} - } - ) do - {:ok, %{status: 200, body: body}} -> - events = - body["data"]["ProtocolAdapter_TransactionExecuted"] - - if length(events) > 0 do - Logger.debug("New blocks found") - - next_blockheight = Enum.at(events, -1)["blockNumber"] - - transactions = - events - |> Enum.map(fn event -> event["transactions"] end) - |> Enum.concat() - - Logger.debug("FOUND #{length(transactions)} TRANSACTIONS") - - next_tree = - Anoma.LocalDomain.MerkleTree.add( - commitment_tree, - transactions - |> Enum.filter(fn tx -> tx["isConsumed"] == false end) - |> Enum.map(fn tx -> - "0x" <> tag_hex = tx["tag"] - {:ok, tag_bin} = Base.decode16(tag_hex, case: :mixed) - tag_bin - end) - ) - - for tx <- transactions do - Logger.debug("WRITING TAG RESOURCE #{tx["tag"]}") - - write_transaction_resource( - node_id, - contract, - tx["tag"], - tx["discoveryPayloads"], - tx["resourcePayloads"], - tx["isConsumed"] - ) - - # Attempt decryption + store per cipher key - for keypair <- cipher_keypairs, - discovery_payload <- tx["discoveryPayloads"] do - "0x" <> blob = discovery_payload["blob"] - - case can_decrypt(keypair, blob) do - :ok -> - Logger.debug("CAN DECRYPT #{blob}") - - write_transaction_resource( - node_id, - contract, - tx["tag"], - keypair[:public_key], - tx["discoveryPayloads"], - tx["resourcePayloads"], - tx["isConsumed"] - ) - - {:error, reason} -> - Logger.debug( - "#{keypair[:public_key]} can't decrypt #{blob} #{inspect(reason)}" - ) - end - end - end - - write_commitment_tree(node_id, contract, next_tree) - write_blockheight(node_id, contract, next_blockheight) - - {:keep_state, - %{ - data - | blockheight: next_blockheight, - commitments: next_tree - }, {:state_timeout, 12_000, :tick}} - else - Logger.debug("No new blocks") - {:keep_state, data, {:state_timeout, 12_000, :tick}} - end - - {:error, reason} -> - Logger.error("Query failed #{inspect(reason)}") - {:keep_state, data, {:state_timeout, 12_000, :tick}} - end - end - - @impl true - def handle_event( - :cast, - {:add_keypair, keypair}, - :polling, - %{ - cipher_keypairs: cipher_keypairs, - contract: contract, - node_id: node_id - } = data - ) do - Logger.debug("Adding cipher keypair #{inspect(keypair)}") - - :ok = write_keypair(node_id, contract, keypair) - - {:next_state, :paused, - %{data | cipher_keypairs: cipher_keypairs ++ [keypair]}, - {:next_event, :internal, {:reindex, keypair}}} - end - - @impl true - def handle_event( - :internal, - {:reindex, keypair}, - :paused, - %{contract: contract, node_id: node_id} = data - ) do - {:ok, resource_keys} = - Anoma.LocalDomain.Storage.ls(node_id, ~k"/!contract/resource") - - for resource_key <- resource_keys do - {:ok, resource} = - Anoma.LocalDomain.Storage.read_latest(node_id, resource_key) - - for discovery <- resource[:discovery] do - blob = String.trim(discovery["blob"], "0x") - - Logger.debug("Blob #{blob}") - - case can_decrypt(keypair, blob) do - :ok -> - Logger.debug("CAN DECRYPT #{blob}") - - write_transaction_resource( - node_id, - contract, - List.last(resource_key), - keypair[:public_key], - resource[:discovery], - resource[:resource], - resource[:is_consumed] - ) - - {:error, reason} -> - Logger.debug("Failed to decrypt #{blob} #{inspect(reason)}") - - r -> - IO.puts(r) - end - end - end - - {:next_state, :polling, data, {:state_timeout, 0, :tick}} - end -end diff --git a/mix.exs b/mix.exs index 1bf2443..bef38ad 100644 --- a/mix.exs +++ b/mix.exs @@ -29,8 +29,7 @@ defmodule Anoma.LocalDomain.MixProject do # non-runtime dependencies here {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:dialyxir, "~> 1.4", only: :dev, runtime: false}, - {:ex_doc, "~> 0.38.2", only: :dev, runtime: false}, - {:anoma_sdk, git: "https://github.com/anoma/anoma-sdk"} + {:ex_doc, "~> 0.38.2", only: :dev, runtime: false} # {:dep_from_hexpm, "~> 0.3.0"}, # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} ] diff --git a/test/merkle_test.exs b/test/merkle_test.exs index ae18a3b..b39e5b5 100644 --- a/test/merkle_test.exs +++ b/test/merkle_test.exs @@ -6,7 +6,10 @@ defmodule MerkleTest do Examples.EMerkleTree.write_to_merkle_tree() Examples.EMerkleTree.expand_merkle_tree() Examples.EMerkleTree.generate_a_proof() + Examples.EMerkleTree.generate_a_proof_wrongly() Examples.EMerkleTree.verify_a_proof() Examples.EMerkleTree.random_tree() + Examples.EMerkleTree.merkle_tree_differential_root_comparisson() + Examples.EMerkleTree.merkle_tree_differential_proof_comparisson() end end diff --git a/test/poller_test.exs b/test/poller_test.exs deleted file mode 100644 index 343c922..0000000 --- a/test/poller_test.exs +++ /dev/null @@ -1,9 +0,0 @@ -defmodule PollerTest do - use ExUnit.Case - doctest Anoma.LocalDomain.System.Poller - - test "Run the examples" do - Examples.EPoller.decrypt_payload() - Examples.EPoller.cipher_keypair_storage_retrieval() - end -end