From 534617ba3c129578bb9a4b403d9e1434a89e26dc Mon Sep 17 00:00:00 2001 From: Jam Date: Wed, 15 Oct 2025 09:21:14 +0000 Subject: [PATCH 1/2] Move Poller out of local domain core. Get the rust out!! Get it out!!! --- lib/examples/e_poller.ex | 51 --- lib/local_domain/structures/merkle_tree.ex | 36 +- lib/local_domain/system/poller.ex | 459 --------------------- mix.exs | 3 +- test/poller_test.exs | 9 - 5 files changed, 19 insertions(+), 539 deletions(-) delete mode 100644 lib/examples/e_poller.ex delete mode 100644 lib/local_domain/system/poller.ex delete mode 100644 test/poller_test.exs 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/merkle_tree.ex b/lib/local_domain/structures/merkle_tree.ex index 068163e..ba64ac0 100644 --- a/lib/local_domain/structures/merkle_tree.ex +++ b/lib/local_domain/structures/merkle_tree.ex @@ -63,26 +63,26 @@ defmodule Anoma.LocalDomain.MerkleTree do 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) + 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)) + leaf_index = + leaves + |> Enum.find_index(&(&1 == leaf)) + + is_left = (leaf_index &&& 1) == 0 - is_left = (leaf_index &&& 1) == 0 - - if is_left do - neighbour = Enum.at(leaves, leaf_index + 1) - - {acc ++ [{neighbour, true}], hash(leaf <> neighbour)} - else - neighbour = Enum.at(leaves, leaf_index - 1) - - {acc ++ [{neighbour, false}], hash(neighbour <> leaf)} - end - end + if is_left do + neighbour = Enum.at(leaves, leaf_index + 1) + + {acc ++ [{neighbour, true}], hash(leaf <> neighbour)} + else + neighbour = Enum.at(leaves, leaf_index - 1) + + {acc ++ [{neighbour, false}], hash(neighbour <> leaf)} + end + end if root == root(tree) do {frontiers, root} 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/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 From 05a6b33cb72de0a856c10f8471887ebbd7d974ac Mon Sep 17 00:00:00 2001 From: Jam Date: Fri, 17 Oct 2025 15:39:05 +0000 Subject: [PATCH 2/2] Fix merkle tree error handling --- lib/examples/e_merkle_tree.ex | 8 +++++ lib/local_domain/structures/merkle_tree.ex | 38 +++++++++++++--------- test/merkle_test.exs | 1 + 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/lib/examples/e_merkle_tree.ex b/lib/examples/e_merkle_tree.ex index f719f1e..24081e8 100644 --- a/lib/examples/e_merkle_tree.ex +++ b/lib/examples/e_merkle_tree.ex @@ -47,6 +47,14 @@ 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) diff --git a/lib/local_domain/structures/merkle_tree.ex b/lib/local_domain/structures/merkle_tree.ex index ba64ac0..fbe49d7 100644 --- a/lib/local_domain/structures/merkle_tree.ex +++ b/lib/local_domain/structures/merkle_tree.ex @@ -63,28 +63,36 @@ defmodule Anoma.LocalDomain.MerkleTree do 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) + for i <- 0..(depth(tree) - 1), reduce: {[], leaf} do + {acc, leaf} -> + if (leaf == nil) do + {acc, leaf} + else + leaves = Map.get(tree.nodes, i) leaf_index = leaves |> Enum.find_index(&(&1 == leaf)) - is_left = (leaf_index &&& 1) == 0 + if (leaf_index != nil) do - if is_left do - neighbour = Enum.at(leaves, leaf_index + 1) - - {acc ++ [{neighbour, true}], hash(leaf <> neighbour)} - else - neighbour = Enum.at(leaves, leaf_index - 1) - - {acc ++ [{neighbour, false}], hash(neighbour <> leaf)} - end - end + is_left = (leaf_index &&& 1) == 0 + + if is_left do + neighbour = Enum.at(leaves, leaf_index + 1) + + {acc ++ [{neighbour, true}], hash(leaf <> neighbour)} + else + neighbour = Enum.at(leaves, leaf_index - 1) - if root == root(tree) do + {acc ++ [{neighbour, false}], hash(neighbour <> leaf)} + end + else + {acc, nil} + end + end + end + if root == root(tree) && root != nil do {frontiers, root} else nil diff --git a/test/merkle_test.exs b/test/merkle_test.exs index ae18a3b..e05faa5 100644 --- a/test/merkle_test.exs +++ b/test/merkle_test.exs @@ -6,6 +6,7 @@ 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() end