From 57be8b7dc2b015619d5319ee7d3a132e047eba0a Mon Sep 17 00:00:00 2001 From: Wassim Mansouri Date: Fri, 15 Nov 2024 14:53:10 +0100 Subject: [PATCH] Add geopatch to node transactions --- lib/archethic/bootstrap.ex | 55 +++-- lib/archethic/bootstrap/sync.ex | 73 ++++--- .../bootstrap/transaction_handler.ex | 35 ++-- .../mining/pending_transaction_validation.ex | 15 +- lib/archethic/mining/proof_of_work.ex | 3 +- lib/archethic/networking/scheduler.ex | 23 ++- lib/archethic/p2p/mem_table_loader.ex | 8 +- lib/archethic/p2p/node.ex | 74 ++++--- .../shared_secrets/mem_tables_loader.ex | 2 +- .../explorer/live/settings_live.ex | 44 ++-- .../explorer/views/explorer_view.ex | 3 +- .../1.5.14@add_geopatch_node_transactiosn.ex | 193 ++++++++++++++++++ test/archethic/bootstrap/sync_test.exs | 99 +++++++-- .../bootstrap/transaction_handler_test.exs | 10 +- .../mining/distributed_workflow_test.exs | 31 ++- .../pending_transaction_validation_test.exs | 175 ++++++++++++++-- test/archethic/p2p/node_test.exs | 25 +-- .../shared_secrets/mem_tables_loader_test.exs | 42 ++-- 18 files changed, 698 insertions(+), 212 deletions(-) create mode 100644 priv/migration_tasks/prod/1.5.14@add_geopatch_node_transactiosn.ex diff --git a/lib/archethic/bootstrap.ex b/lib/archethic/bootstrap.ex index 9387392dd..a53435009 100644 --- a/lib/archethic/bootstrap.ex +++ b/lib/archethic/bootstrap.ex @@ -5,6 +5,8 @@ defmodule Archethic.Bootstrap do alias Archethic.Crypto + alias Archethic.P2P.GeoPatch + alias Archethic.Networking alias Archethic.P2P @@ -98,15 +100,17 @@ defmodule Archethic.Bootstrap do ) when is_number(port) and is_list(bootstrapping_seeds) and is_binary(reward_address) do network_patch = get_network_patch(ip) + geo_patch = GeoPatch.from_ip(ip) closest_bootstrapping_nodes = get_closest_nodes(bootstrapping_seeds, network_patch) - if should_bootstrap?(ip, port, http_port, transport, last_sync_date) do + if should_bootstrap?(ip, port, http_port, transport, geo_patch, last_sync_date) do start_bootstrap( ip, port, http_port, transport, + geo_patch, closest_bootstrapping_nodes, reward_address ) @@ -147,12 +151,12 @@ defmodule Archethic.Bootstrap do end end - defp should_bootstrap?(_ip, _port, _http_port, _, nil), do: true + defp should_bootstrap?(_ip, _port, _http_port, _, _, nil), do: true - defp should_bootstrap?(ip, port, http_port, transport, last_sync_date) do + defp should_bootstrap?(ip, port, http_port, transport, geo_patch, last_sync_date) do case P2P.get_node_info(Crypto.first_node_public_key()) do {:ok, _} -> - if Sync.require_update?(ip, port, http_port, transport, last_sync_date) do + if Sync.require_update?(ip, port, http_port, transport, geo_patch, last_sync_date) do Logger.debug("Node chain need to updated") true else @@ -171,6 +175,7 @@ defmodule Archethic.Bootstrap do port, http_port, transport, + geo_patch, closest_bootstrapping_nodes, configured_reward_address ) do @@ -187,7 +192,8 @@ defmodule Archethic.Bootstrap do port, http_port, transport, - configured_reward_address + configured_reward_address, + geo_patch ) Sync.initialize_network(tx) @@ -203,7 +209,8 @@ defmodule Archethic.Bootstrap do http_port, transport, closest_bootstrapping_nodes, - configured_reward_address + configured_reward_address, + geo_patch ) true -> @@ -215,7 +222,8 @@ defmodule Archethic.Bootstrap do ) {:ok, _ip, _p2p_port, _http_port, _transport, last_reward_address, _origin_public_key, - _key_certificate, _mining_public_key} = Node.decode_transaction_content(content) + _key_certificate, _mining_public_key, + _geo_patch} = Node.decode_transaction_content(content) update_node( ip, @@ -223,7 +231,8 @@ defmodule Archethic.Bootstrap do http_port, transport, closest_bootstrapping_nodes, - last_reward_address + last_reward_address, + geo_patch ) end end @@ -265,7 +274,8 @@ defmodule Archethic.Bootstrap do http_port, transport, closest_bootstrapping_nodes, - configured_reward_address + configured_reward_address, + geo_patch ) do # In case node had lose it's DB, we ask the network if the node chain already exists {:ok, length} = @@ -286,7 +296,8 @@ defmodule Archethic.Bootstrap do TransactionChain.fetch_transaction(last_address, closest_bootstrapping_nodes) {:ok, _ip, _p2p_port, _http_port, _transport, last_reward_address, _origin_public_key, - _key_certificate, _mining_public_key} = Node.decode_transaction_content(content) + _key_certificate, _mining_public_key, + _geo_patch} = Node.decode_transaction_content(content) last_reward_address else @@ -294,7 +305,14 @@ defmodule Archethic.Bootstrap do end tx = - TransactionHandler.create_node_transaction(ip, port, http_port, transport, reward_address) + TransactionHandler.create_node_transaction( + ip, + port, + http_port, + transport, + reward_address, + geo_patch + ) {:ok, validated_tx} = TransactionHandler.send_transaction(tx, closest_bootstrapping_nodes) @@ -307,18 +325,27 @@ defmodule Archethic.Bootstrap do ) end - defp update_node(_ip, _port, _http_port, _transport, [], _reward_address) do + defp update_node(_ip, _port, _http_port, _transport, [], _reward_address, _geo_patch) do Logger.warning("Not enough nodes in the network. No node update") end - defp update_node(ip, port, http_port, transport, closest_bootstrapping_nodes, reward_address) do + defp update_node( + ip, + port, + http_port, + transport, + closest_bootstrapping_nodes, + reward_address, + geo_patch + ) do tx = TransactionHandler.create_node_transaction( ip, port, http_port, transport, - reward_address + reward_address, + geo_patch ) {:ok, validated_tx} = TransactionHandler.send_transaction(tx, closest_bootstrapping_nodes) diff --git a/lib/archethic/bootstrap/sync.ex b/lib/archethic/bootstrap/sync.ex index 1540cf5b1..50c8c2c75 100644 --- a/lib/archethic/bootstrap/sync.ex +++ b/lib/archethic/bootstrap/sync.ex @@ -56,36 +56,63 @@ defmodule Archethic.Bootstrap.Sync do :inet.port_number(), :inet.port_number(), P2P.supported_transport(), + binary(), DateTime.t() | nil ) :: boolean() - def require_update?(_ip, _port, _http_port, _transport, nil), do: false + def require_update?(_ip, _port, _http_port, _transport, _geo_patch, nil), do: false - def require_update?(ip, port, http_port, transport, last_sync_date) do + def require_update?(ip, port, http_port, transport, geo_patch, last_sync_date) do first_node_public_key = Crypto.first_node_public_key() - case P2P.authorized_and_available_nodes() do - [%Node{first_public_key: ^first_node_public_key}] -> - false + if is_node_active?(first_node_public_key) do + false + else + needs_update?( + ip, + port, + http_port, + transport, + geo_patch, + last_sync_date, + first_node_public_key + ) + end + end + + defp is_node_active?(first_node_public_key) do + P2P.authorized_and_available_nodes() + |> Enum.any?(fn %Node{first_public_key: pk} -> pk == first_node_public_key end) + end + + defp needs_update?( + ip, + port, + http_port, + transport, + geo_patch, + last_sync_date, + first_node_public_key + ) do + diff_sync = DateTime.diff(DateTime.utc_now(), last_sync_date, :second) + + case P2P.get_node_info(first_node_public_key) do + {:ok, + %Node{ + ip: prev_ip, + port: prev_port, + http_port: prev_http_port, + transport: prev_transport, + geo_patch: prev_geo_patch + }} -> + ip != prev_ip or + port != prev_port or + http_port != prev_http_port or + geo_patch != prev_geo_patch or + diff_sync > @out_of_sync_date_threshold or + prev_transport != transport _ -> - diff_sync = DateTime.diff(DateTime.utc_now(), last_sync_date, :second) - - case P2P.get_node_info(first_node_public_key) do - {:ok, - %Node{ - ip: prev_ip, - port: prev_port, - http_port: prev_http_port, - transport: prev_transport - }} - when ip != prev_ip or port != prev_port or http_port != prev_http_port or - diff_sync > @out_of_sync_date_threshold or - prev_transport != transport -> - true - - _ -> - false - end + false end end diff --git a/lib/archethic/bootstrap/transaction_handler.ex b/lib/archethic/bootstrap/transaction_handler.ex index 18d22645d..2d4e2564e 100644 --- a/lib/archethic/bootstrap/transaction_handler.ex +++ b/lib/archethic/bootstrap/transaction_handler.ex @@ -73,13 +73,21 @@ defmodule Archethic.Bootstrap.TransactionHandler do p2p_port :: :inet.port_number(), http_port :: :inet.port_number(), transport :: P2P.supported_transport(), - reward_address :: Crypto.versioned_hash() + reward_address :: Crypto.versioned_hash(), + geo_patch :: binary() ) :: Transaction.t() - def create_node_transaction(ip = {_, _, _, _}, port, http_port, transport, reward_address) + def create_node_transaction( + ip = {_, _, _, _}, + port, + http_port, + transport, + reward_address, + geo_patch + ) when is_number(port) and port >= 0 and is_binary(reward_address) do origin_public_key = Crypto.origin_node_public_key() - origin_public_key_certificate = Crypto.get_key_certificate(origin_public_key) + origin_public_certificate = Crypto.get_key_certificate(origin_public_key) mining_public_key = Crypto.mining_node_public_key() Transaction.new(:node, %TransactionData{ @@ -94,16 +102,17 @@ defmodule Archethic.Bootstrap.TransactionHandler do ] """, content: - Node.encode_transaction_content( - ip, - port, - http_port, - transport, - reward_address, - origin_public_key, - origin_public_key_certificate, - mining_public_key - ) + Node.encode_transaction_content(%{ + ip: ip, + port: port, + http_port: http_port, + transport: transport, + reward_address: reward_address, + origin_public_key: origin_public_key, + key_certificate: origin_public_certificate, + mining_public_key: mining_public_key, + geo_patch: geo_patch + }) }) end end diff --git a/lib/archethic/mining/pending_transaction_validation.ex b/lib/archethic/mining/pending_transaction_validation.ex index 9616d6980..5ad38e175 100644 --- a/lib/archethic/mining/pending_transaction_validation.ex +++ b/lib/archethic/mining/pending_transaction_validation.ex @@ -16,6 +16,7 @@ defmodule Archethic.Mining.PendingTransactionValidation do alias Archethic.OracleChain alias Archethic.P2P + alias Archethic.P2P.GeoPatch alias Archethic.P2P.Message.FirstPublicKey alias Archethic.P2P.Message.GetFirstPublicKey alias Archethic.P2P.Node @@ -350,7 +351,8 @@ defmodule Archethic.Mining.PendingTransactionValidation do }, _ ) do - with {:ok, ip, port, _http_port, _, _, origin_public_key, key_certificate, mining_public_key} <- + with {:ok, ip, port, _http_port, _, _, origin_public_key, key_certificate, mining_public_key, + geo_patch} <- Node.decode_transaction_content(content), {:auth_origin, true} <- {:auth_origin, @@ -371,7 +373,9 @@ defmodule Archethic.Mining.PendingTransactionValidation do {:mining_public_key, true} <- {:mining_public_key, Crypto.valid_public_key?(mining_public_key) and - Crypto.get_public_key_curve(mining_public_key) == :bls} do + Crypto.get_public_key_curve(mining_public_key) == :bls}, + {:geo_patch, true} <- + {:geo_patch, valid_geopatch?(ip, geo_patch)} do :ok else :error -> @@ -395,6 +399,9 @@ defmodule Archethic.Mining.PendingTransactionValidation do {:mining_public_key, false} -> {:error, "Invalid mining public key"} + + {:geo_patch, false} -> + {:error, "Invalid geo patch from IP"} end end @@ -1000,6 +1007,10 @@ defmodule Archethic.Mining.PendingTransactionValidation do end end + defp valid_geopatch?(ip, calculated_geopatch) do + calculated_geopatch == GeoPatch.from_ip(ip) + end + defp get_allowed_node_key_origins do :archethic |> Application.get_env(__MODULE__, []) diff --git a/lib/archethic/mining/proof_of_work.ex b/lib/archethic/mining/proof_of_work.ex index 58d12907f..8c8a48c87 100644 --- a/lib/archethic/mining/proof_of_work.ex +++ b/lib/archethic/mining/proof_of_work.ex @@ -142,7 +142,8 @@ defmodule Archethic.Mining.ProofOfWork do } }) do {:ok, _ip, _p2p_port, _http_port, _transport, _reward_address, origin_public_key, - _origin_certificate, _mining_public_key} = Node.decode_transaction_content(content) + _origin_certificate, _mining_public_key, + _geo_patch} = Node.decode_transaction_content(content) [origin_public_key] end diff --git a/lib/archethic/networking/scheduler.ex b/lib/archethic/networking/scheduler.ex index a9eb66974..95f77980d 100644 --- a/lib/archethic/networking/scheduler.ex +++ b/lib/archethic/networking/scheduler.ex @@ -10,6 +10,7 @@ defmodule Archethic.Networking.Scheduler do alias Archethic.Networking.PortForwarding alias Archethic.P2P + alias(Archethic.P2P.GeoPatch) alias Archethic.P2P.Listener, as: P2PListener alias Archethic.P2P.Node @@ -103,21 +104,23 @@ defmodule Archethic.Networking.Scheduler do origin_public_key = Crypto.origin_node_public_key() mining_public_key = Crypto.mining_node_public_key() key_certificate = Crypto.get_key_certificate(origin_public_key) + new_geo_patch = GeoPatch.from_ip(ip) tx = Transaction.new(:node, %TransactionData{ code: code, content: - Node.encode_transaction_content( - ip, - p2p_port, - web_port, - transport, - reward_address, - origin_public_key, - key_certificate, - mining_public_key - ) + Node.encode_transaction_content(%{ + ip: ip, + port: p2p_port, + http_port: web_port, + transport: transport, + reward_address: reward_address, + origin_public_key: origin_public_key, + key_certificate: key_certificate, + mining_public_key: mining_public_key, + geo_patch: new_geo_patch + }) }) Archethic.send_new_transaction(tx, forward?: true) diff --git a/lib/archethic/p2p/mem_table_loader.ex b/lib/archethic/p2p/mem_table_loader.ex index 8b1e3fc20..013ad4d8f 100644 --- a/lib/archethic/p2p/mem_table_loader.ex +++ b/lib/archethic/p2p/mem_table_loader.ex @@ -105,7 +105,9 @@ defmodule Archethic.P2P.MemTableLoader do first_public_key = TransactionChain.get_first_public_key(previous_public_key) {:ok, ip, port, http_port, transport, reward_address, origin_public_key, _certificate, - mining_public_key} = Node.decode_transaction_content(content) + mining_public_key, geo_patch} = Node.decode_transaction_content(content) + + geo_patch = if geo_patch == nil, do: GeoPatch.from_ip(ip), else: geo_patch if first_node_change?(first_public_key, previous_public_key) do node = %Node{ @@ -114,7 +116,7 @@ defmodule Archethic.P2P.MemTableLoader do http_port: http_port, first_public_key: first_public_key, last_public_key: previous_public_key, - geo_patch: GeoPatch.from_ip(ip), + geo_patch: geo_patch, transport: transport, last_address: address, reward_address: reward_address, @@ -135,7 +137,7 @@ defmodule Archethic.P2P.MemTableLoader do port: port, http_port: http_port, last_public_key: previous_public_key, - geo_patch: GeoPatch.from_ip(ip), + geo_patch: geo_patch, transport: transport, last_address: address, reward_address: reward_address, diff --git a/lib/archethic/p2p/node.ex b/lib/archethic/p2p/node.ex index 45b9175e1..7d7f2b4f5 100755 --- a/lib/archethic/p2p/node.ex +++ b/lib/archethic/p2p/node.ex @@ -47,7 +47,8 @@ defmodule Archethic.P2P.Node do {:ok, ip_address :: :inet.ip_address(), p2p_port :: :inet.port_number(), http_port :: :inet.port_number(), P2P.supported_transport(), reward_address :: binary(), origin_public_key :: Crypto.key(), - key_certificate :: binary(), mining_public_key :: binary() | nil} + key_certificate :: binary(), mining_public_key :: binary() | nil, + geo_patch :: binary() | nil} | :error def decode_transaction_content( <> @@ -56,18 +57,11 @@ defmodule Archethic.P2P.Node do {reward_address, rest} <- Utils.deserialize_address(rest), {origin_public_key, rest} <- Utils.deserialize_public_key(rest), <> <- rest do - mining_public_key = - case rest do - "" -> - nil - - mining_public_key -> - mining_public_key |> Utils.deserialize_public_key() |> elem(0) - end - + rest::binary>> <- rest, + {mining_public_key, rest} <- extract_mining_public_key(rest), + {geo_patch, _rest} <- extract_geo_patch(rest) do {:ok, {ip0, ip1, ip2, ip3}, port, http_port, deserialize_transport(transport), - reward_address, origin_public_key, key_certificate, mining_public_key} + reward_address, origin_public_key, key_certificate, mining_public_key, geo_patch} else _ -> :error @@ -76,32 +70,46 @@ defmodule Archethic.P2P.Node do def decode_transaction_content(<<>>), do: :error + @spec extract_mining_public_key(binary()) :: {Crypto.key() | nil, binary()} + defp extract_mining_public_key(<<>>), do: {nil, <<>>} + + defp extract_mining_public_key(rest) do + Utils.deserialize_public_key(rest) + end + + @spec extract_geo_patch(binary()) :: {binary() | nil, binary()} + defp extract_geo_patch(<>), do: {geo_patch, rest} + + defp extract_geo_patch(rest), do: {nil, rest} + @doc """ Encode node's transaction content """ - @spec encode_transaction_content( - :inet.ip_address(), - :inet.port_number(), - :inet.port_number(), - P2P.supported_transport(), - reward_address :: binary(), - origin_public_key :: Crypto.key(), - origin_key_certificate :: binary(), - mining_public_key :: Crypto.key() - ) :: binary() - def encode_transaction_content( - {ip1, ip2, ip3, ip4}, - port, - http_port, - transport, - reward_address, - origin_public_key, - key_certificate, - mining_public_key - ) do + @spec encode_transaction_content(%{ + ip: :inet.ip_address(), + port: :inet.port_number(), + http_port: :inet.port_number(), + transport: P2P.supported_transport(), + reward_address: reward_address :: binary(), + origin_public_key: origin_public_key :: Crypto.key(), + key_certificate: origin_key_certificate :: binary(), + mining_public_key: mining_public_key :: Crypto.key(), + geo_patch: geo_patch :: binary() + }) :: binary() + def encode_transaction_content(%{ + ip: {ip1, ip2, ip3, ip4}, + port: port, + http_port: http_port, + transport: transport, + reward_address: reward_address, + origin_public_key: origin_public_key, + key_certificate: key_certificate, + mining_public_key: mining_public_key, + geo_patch: geo_patch + }) do <> + key_certificate::binary, mining_public_key::binary, geo_patch::binary-size(3)>> end @type t() :: %__MODULE__{ diff --git a/lib/archethic/shared_secrets/mem_tables_loader.ex b/lib/archethic/shared_secrets/mem_tables_loader.ex index b4f7aedcc..ed7afc4b9 100644 --- a/lib/archethic/shared_secrets/mem_tables_loader.ex +++ b/lib/archethic/shared_secrets/mem_tables_loader.ex @@ -61,7 +61,7 @@ defmodule Archethic.SharedSecrets.MemTablesLoader do } }) do {:ok, _ip, _p2p_port, _http_port, _transport, _reward_address, origin_public_key, _cert, - _mining_public_key} = Node.decode_transaction_content(content) + _mining_public_key, _geo_patch} = Node.decode_transaction_content(content) <<_::8, origin_id::8, _::binary>> = origin_public_key diff --git a/lib/archethic_web/explorer/live/settings_live.ex b/lib/archethic_web/explorer/live/settings_live.ex index 6f926c090..73504c4ba 100644 --- a/lib/archethic_web/explorer/live/settings_live.ex +++ b/lib/archethic_web/explorer/live/settings_live.ex @@ -120,6 +120,7 @@ defmodule ArchethicWeb.Explorer.SettingsLive do defp send_new_transaction(next_reward_address) do %Node{ ip: ip, + geo_patch: geo_patch, port: port, http_port: http_port, transport: transport, @@ -149,16 +150,17 @@ defmodule ArchethicWeb.Explorer.SettingsLive do }, code: code, content: - Node.encode_transaction_content( - ip, - port, - http_port, - transport, - next_reward_address, - Crypto.origin_node_public_key(), - Crypto.get_key_certificate(Crypto.origin_node_public_key()), - Crypto.mining_node_public_key() - ) + Node.encode_transaction_content(%{ + ip: ip, + port: port, + http_port: http_port, + transport: transport, + reward_address: next_reward_address, + origin_public_key: Crypto.origin_node_public_key(), + key_certificate: Crypto.get_key_certificate(Crypto.origin_node_public_key()), + mining_public_key: Crypto.mining_node_public_key(), + geo_patch: geo_patch + }) }) TransactionSubscriber.register(tx.address, System.monotonic_time()) @@ -169,6 +171,7 @@ defmodule ArchethicWeb.Explorer.SettingsLive do defp send_noop_transaction() do %Node{ ip: ip, + geo_patch: geo_patch, port: port, http_port: http_port, transport: transport, @@ -184,16 +187,17 @@ defmodule ArchethicWeb.Explorer.SettingsLive do Transaction.new(:node, %TransactionData{ code: code, content: - Node.encode_transaction_content( - ip, - port, - http_port, - transport, - reward_address, - Crypto.origin_node_public_key(), - Crypto.get_key_certificate(Crypto.origin_node_public_key()), - Crypto.mining_node_public_key() - ) + Node.encode_transaction_content(%{ + ip: ip, + port: port, + http_port: http_port, + transport: transport, + reward_address: reward_address, + origin_public_key: Crypto.origin_node_public_key(), + key_certificate: Crypto.get_key_certificate(Crypto.origin_node_public_key()), + mining_public_key: Crypto.mining_node_public_key(), + geo_patch: geo_patch + }) }) TransactionSubscriber.register(tx.address, System.monotonic_time()) diff --git a/lib/archethic_web/explorer/views/explorer_view.ex b/lib/archethic_web/explorer/views/explorer_view.ex index 6d7df2fab..77f1775d9 100644 --- a/lib/archethic_web/explorer/views/explorer_view.ex +++ b/lib/archethic_web/explorer/views/explorer_view.ex @@ -52,10 +52,11 @@ defmodule ArchethicWeb.Explorer.ExplorerView do def format_transaction_content(:node, content) do {:ok, ip, port, http_port, transport, reward_address, origin_public_key, key_certificate, - mining_public_key} = Node.decode_transaction_content(content) + mining_public_key, geo_patch} = Node.decode_transaction_content(content) content = """ IP: #{:inet.ntoa(ip)} + GeoPatch: #{geo_patch} P2P Port: #{port} HTTP Port: #{http_port} Transport: #{transport} diff --git a/priv/migration_tasks/prod/1.5.14@add_geopatch_node_transactiosn.ex b/priv/migration_tasks/prod/1.5.14@add_geopatch_node_transactiosn.ex new file mode 100644 index 000000000..743925c02 --- /dev/null +++ b/priv/migration_tasks/prod/1.5.14@add_geopatch_node_transactiosn.ex @@ -0,0 +1,193 @@ +defmodule Migration_1_5_14 do + @moduledoc """ + Migration script to add geopatch to a node's transaction. + """ + + alias Archethic.Crypto + alias Archethic.P2P + alias Archethic.P2P.Node + alias Archethic.TransactionChain + alias Archethic.TransactionChain.Transaction + alias Archethic.TransactionChain.TransactionData + alias Archethic.Utils + alias Archethic.PubSub + + require Logger + + def run() do + nodes = P2P.list_nodes() |> Enum.sort_by(& &1.first_public_key) + + execute_migration(nodes) + end + + defp execute_migration([]) do + :ok + end + + defp execute_migration(nodes) do + current_node_pk = Crypto.first_node_public_key() + transaction_cache = %{} + + Enum.reduce_while(nodes, transaction_cache, fn node, transaction_cache -> + node_pk = node.first_public_key + Logger.info("Processing node", node: Base.encode16(node_pk)) + + if geopatch_in_last_transaction?(node_pk) do + Logger.info("Migration not needed for node", node: Base.encode16(node_pk)) + {:cont, Map.delete(transaction_cache, node_pk)} + else + if node_pk == current_node_pk do + Logger.info("Starting migration for node", node: Base.encode16(node_pk)) + + case send_node_transaction() do + :ok -> + Logger.info("Migration complete for node", node: Base.encode16(node_pk)) + {:halt, :ok} + + {:error, reason} -> + Logger.error( + "Migration failed (reason: #{inspect(reason)}) for", + node: Base.encode16(node_pk) + ) + + {:halt, {:error, reason}} + end + else + case Map.fetch(transaction_cache, node_pk) do + {:ok, transaction} -> + {:cont, process_transaction(transaction, Map.delete(transaction_cache, node_pk))} + + :error -> + PubSub.register_to_new_transaction_by_type(:node) + + receive do + {:new_transaction, address, :node, _timestamp} -> + with {:ok, %Transaction{previous_public_key: previous_pk} = transaction} <- + TransactionChain.get_transaction(address) do + first_pk = TransactionChain.get_first_public_key(previous_pk) + + if first_pk == node_pk do + {:cont, process_transaction(transaction, transaction_cache)} + else + updated_cache = Map.put(transaction_cache, first_pk, transaction) + {:cont, updated_cache} + end + else + {:error, reason} -> + Logger.error( + "Failed to fetch transaction: #{inspect(reason)} for address", + address: Base.encode16(address) + ) + + {:cont, transaction_cache} + end + after + 60_000 -> + Logger.error("Timeout waiting for updates from node", + node: Base.encode16(node_pk) + ) + + PubSub.unregister_to_new_transaction_by_type(:node) + {:cont, transaction_cache} + end + end + end + end + end) + end + + defp process_transaction( + %Transaction{data: %TransactionData{content: content}}, + transaction_cache + ) do + case geopatch_in_transaction_content?(content) do + true -> + PubSub.unregister_to_new_transaction_by_type(:node) + transaction_cache + + false -> + transaction_cache + end + end + + defp geopatch_in_last_transaction?(node_pk) do + case P2P.get_node_info(node_pk) do + {:ok, %Node{last_address: last_address}} -> + case TransactionChain.get_transaction(last_address) do + {:ok, %Transaction{data: %TransactionData{content: content}}} -> + geopatch_in_transaction_content?(content) + + {:error, _} -> + false + end + + {:error, _} -> + false + end + end + + defp geopatch_in_transaction_content?(content) do + with {:ok, _ip, _p2p_port, _http_port, _transport, _last_reward_address, _origin_public_key, + _key_certificate, _mining_public_key, + geo_patch} <- Node.decode_transaction_content(content) do + geo_patch != nil + else + error -> + false + end + end + + defp send_node_transaction() do + %Node{ + ip: ip, + port: port, + http_port: http_port, + transport: transport, + reward_address: reward_address, + origin_public_key: origin_public_key, + last_address: last_address + } = P2P.get_node_info() + + geopatch = Archethic.P2P.GeoPatch.from_ip(ip) + + mining_public_key = Crypto.mining_node_public_key() + key_certificate = Crypto.get_key_certificate(origin_public_key) + + {:ok, %Transaction{data: %TransactionData{code: code}}} = + TransactionChain.get_transaction(last_address, data: [:code]) + + tx = + Transaction.new(:node, %TransactionData{ + code: code, + content: + Node.encode_transaction_content(%{ + ip: ip, + port: port, + http_port: http_port, + transport: transport, + reward_address: reward_address, + origin_public_key: origin_public_key, + key_certificate: key_certificate, + mining_public_key: mining_public_key, + geo_patch: geopatch + }) + }) + + :ok = Archethic.send_new_transaction(tx, forward?: true) + + nodes = + P2P.authorized_and_available_nodes() + |> Enum.filter(&P2P.node_connected?/1) + |> P2P.sort_by_nearest_nodes() + + case Utils.await_confirmation(tx.address, nodes) do + {:ok, _} -> + Logger.error("Mining node transaction successful.") + :ok + + {:error, reason} -> + Logger.error("Cannot update node transaction: #{inspect(reason)}") + {:error, reason} + end + end +end diff --git a/test/archethic/bootstrap/sync_test.exs b/test/archethic/bootstrap/sync_test.exs index 94dea3858..5c7f1179d 100644 --- a/test/archethic/bootstrap/sync_test.exs +++ b/test/archethic/bootstrap/sync_test.exs @@ -146,13 +146,21 @@ defmodule Archethic.Bootstrap.SyncTest do first_public_key: Crypto.first_node_public_key(), last_public_key: Crypto.last_node_public_key(), transport: :tcp, + geo_patch: "AAA", authorized?: true, available?: true, authorization_date: DateTime.utc_now() }) assert false == - Sync.require_update?({193, 101, 10, 202}, 3000, 4000, :tcp, DateTime.utc_now()) + Sync.require_update?( + {193, 101, 10, 202}, + 3000, + 4000, + :tcp, + "AAA", + DateTime.utc_now() + ) end test "should return true when the node ip change" do @@ -161,7 +169,8 @@ defmodule Archethic.Bootstrap.SyncTest do port: 3000, first_public_key: Crypto.first_node_public_key(), last_public_key: Crypto.last_node_public_key(), - transport: :tcp + transport: :tcp, + geo_patch: "AAA" }) P2P.add_and_connect_node(%Node{ @@ -170,10 +179,18 @@ defmodule Archethic.Bootstrap.SyncTest do http_port: 4000, first_public_key: "other_node_key", last_public_key: "other_node_key", - transport: :tcp + transport: :tcp, + geo_patch: "AAA" }) - assert Sync.require_update?({193, 101, 10, 202}, 3000, 4000, :tcp, DateTime.utc_now()) + assert Sync.require_update?( + {193, 101, 10, 202}, + 3000, + 4000, + :tcp, + "AAA", + DateTime.utc_now() + ) end test "should return true when the node port change" do @@ -182,7 +199,31 @@ defmodule Archethic.Bootstrap.SyncTest do port: 3000, first_public_key: Crypto.first_node_public_key(), last_public_key: Crypto.last_node_public_key(), - transport: :tcp + transport: :tcp, + geo_patch: "AAA" + }) + + P2P.add_and_connect_node(%Node{ + ip: {127, 0, 0, 1}, + port: 3050, + http_port: 4000, + first_public_key: "other_node_key", + last_public_key: "other_node_key", + transport: :tcp, + geo_patch: "AAA" + }) + + assert Sync.require_update?({127, 0, 0, 1}, 3010, 4000, :tcp, "AAA", DateTime.utc_now()) + end + + test "should return true when the geopatch changes" do + P2P.add_and_connect_node(%Node{ + ip: {127, 0, 0, 1}, + port: 3000, + first_public_key: Crypto.first_node_public_key(), + last_public_key: Crypto.last_node_public_key(), + transport: :tcp, + geo_patch: "AAA" }) P2P.add_and_connect_node(%Node{ @@ -191,10 +232,11 @@ defmodule Archethic.Bootstrap.SyncTest do http_port: 4000, first_public_key: "other_node_key", last_public_key: "other_node_key", - transport: :tcp + transport: :tcp, + geo_patch: "AAA" }) - assert Sync.require_update?({127, 0, 0, 1}, 3010, 4000, :tcp, DateTime.utc_now()) + assert Sync.require_update?({127, 0, 0, 1}, 3000, 4000, :tcp, "BBB", DateTime.utc_now()) end test "should return true when the last date of sync diff is greater than 3 seconds" do @@ -204,7 +246,8 @@ defmodule Archethic.Bootstrap.SyncTest do http_port: 4000, first_public_key: Crypto.first_node_public_key(), last_public_key: Crypto.last_node_public_key(), - transport: :tcp + transport: :tcp, + geo_patch: "AAA" }) P2P.add_and_connect_node(%Node{ @@ -213,7 +256,8 @@ defmodule Archethic.Bootstrap.SyncTest do http_port: 4000, first_public_key: "other_node_key", last_public_key: "other_node_key", - transport: :tcp + transport: :tcp, + geo_patch: "AAA" }) assert Sync.require_update?( @@ -221,6 +265,7 @@ defmodule Archethic.Bootstrap.SyncTest do 3000, 4000, :tcp, + "AAA", DateTime.utc_now() |> DateTime.add(-10) ) @@ -233,7 +278,8 @@ defmodule Archethic.Bootstrap.SyncTest do http_port: 4000, first_public_key: Crypto.first_node_public_key(), last_public_key: Crypto.last_node_public_key(), - transport: :tcp + transport: :tcp, + geo_patch: "AAA" }) P2P.add_and_connect_node(%Node{ @@ -242,11 +288,19 @@ defmodule Archethic.Bootstrap.SyncTest do http_port: 4000, first_public_key: "other_node_key", last_public_key: "other_node_key", - transport: :tcp + transport: :tcp, + geo_patch: "AAA" }) assert true == - Sync.require_update?({193, 101, 10, 202}, 3000, 4000, :sctp, DateTime.utc_now()) + Sync.require_update?( + {193, 101, 10, 202}, + 3000, + 4000, + :sctp, + "AAA", + DateTime.utc_now() + ) end end @@ -308,16 +362,17 @@ defmodule Archethic.Bootstrap.SyncTest do node_tx = Transaction.new(:node, %TransactionData{ content: - Node.encode_transaction_content( - {127, 0, 0, 1}, - 3000, - 4000, - :tcp, - ArchethicCase.random_public_key(), - ArchethicCase.random_public_key(), - :crypto.strong_rand_bytes(64), - Crypto.generate_random_keypair(:bls) |> elem(0) - ) + Node.encode_transaction_content(%{ + ip: {127, 0, 0, 1}, + port: 3000, + http_port: 4000, + transport: :tcp, + reward_address: ArchethicCase.random_public_key(), + origin_public_key: ArchethicCase.random_public_key(), + key_certificate: :crypto.strong_rand_bytes(64), + mining_public_key: Crypto.generate_random_keypair(:bls) |> elem(0), + geo_patch: "000" + }) }) :ok = Sync.initialize_network(node_tx) diff --git a/test/archethic/bootstrap/transaction_handler_test.exs b/test/archethic/bootstrap/transaction_handler_test.exs index f0053f2c1..a82cad19c 100644 --- a/test/archethic/bootstrap/transaction_handler_test.exs +++ b/test/archethic/bootstrap/transaction_handler_test.exs @@ -18,7 +18,7 @@ defmodule Archethic.Bootstrap.TransactionHandlerTest do import Mox - test "create_node_transaction/4 should create transaction with ip and port encoded in the content" do + test "create_node_transaction/4 should create transaction with ip, geopatch and port encoded in the content" do assert %Transaction{ data: %TransactionData{ content: content @@ -29,11 +29,12 @@ defmodule Archethic.Bootstrap.TransactionHandlerTest do 3000, 4000, :tcp, - <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>> + <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, + "000" ) assert {:ok, {127, 0, 0, 1}, 3000, 4000, :tcp, _reward_address, _origin_public_key, _cert, - mining_public_key} = Node.decode_transaction_content(content) + mining_public_key, "000"} = Node.decode_transaction_content(content) assert Archethic.Crypto.mining_node_public_key() == mining_public_key end @@ -59,7 +60,8 @@ defmodule Archethic.Bootstrap.TransactionHandlerTest do 3000, 4000, :tcp, - "00610F69B6C5C3449659C99F22956E5F37AA6B90B473585216CF4931DAF7A0AB45" + "00610F69B6C5C3449659C99F22956E5F37AA6B90B473585216CF4931DAF7A0AB45", + "000" ) validated_transaction = %Transaction{ diff --git a/test/archethic/mining/distributed_workflow_test.exs b/test/archethic/mining/distributed_workflow_test.exs index 4fde39277..82c35357a 100644 --- a/test/archethic/mining/distributed_workflow_test.exs +++ b/test/archethic/mining/distributed_workflow_test.exs @@ -104,19 +104,28 @@ defmodule Archethic.Mining.DistributedWorkflowTest do tx = Transaction.new(:node, %TransactionData{ content: - Node.encode_transaction_content( - {80, 10, 20, 102}, - 3000, - 4000, - MockTransport, - <<0, 0, 16, 233, 156, 172, 143, 228, 236, 12, 227, 76, 1, 80, 12, 236, 69, 10, 209, 6, - 234, 172, 97, 188, 240, 207, 70, 115, 64, 117, 44, 82, 132, 186>>, - origin_public_key, - certificate, - Crypto.generate_random_keypair(:bls) |> elem(0) - ) + Node.encode_transaction_content(%{ + ip: {80, 10, 20, 102}, + port: 3000, + http_port: 4000, + transport: MockTransport, + reward_address: + <<0, 0, 16, 233, 156, 172, 143, 228, 236, 12, 227, 76, 1, 80, 12, 236, 69, 10, 209, + 6, 234, 172, 97, 188, 240, 207, 70, 115, 64, 117, 44, 82, 132, 186>>, + origin_public_key: origin_public_key, + key_certificate: certificate, + mining_public_key: Crypto.generate_random_keypair(:bls) |> elem(0), + geo_patch: "F1B" + }) }) + stub(MockGeoIP, :get_coordinates, fn ip -> + case ip do + {80, 10, 20, 102} -> + {38.345170, -0.481490} + end + end) + {:ok, %{ genesis: Transaction.previous_address(tx), diff --git a/test/archethic/mining/pending_transaction_validation_test.exs b/test/archethic/mining/pending_transaction_validation_test.exs index 4e977e69b..fd24904a8 100644 --- a/test/archethic/mining/pending_transaction_validation_test.exs +++ b/test/archethic/mining/pending_transaction_validation_test.exs @@ -7,6 +7,7 @@ defmodule Archethic.Mining.PendingTransactionValidationTest do alias Archethic.Mining.PendingTransactionValidation alias Archethic.P2P + alias Archethic.P2P.GeoPatch alias Archethic.P2P.Message.FirstPublicKey alias Archethic.P2P.Message.GenesisAddress alias Archethic.P2P.Message.GetFirstPublicKey @@ -602,17 +603,19 @@ defmodule Archethic.Mining.PendingTransactionValidationTest do certificate = Crypto.ECDSA.sign(:secp256r1, ca_pv, origin_key) content = - Node.encode_transaction_content( - {80, 20, 10, 200}, - 3000, - 4000, - :tcp, - <<0, 0, 4, 221, 19, 74, 75, 69, 16, 50, 149, 253, 24, 115, 128, 241, 110, 118, 139, 7, - 48, 217, 58, 43, 145, 233, 77, 125, 190, 207, 31, 64, 157, 137>>, - origin_public_key, - certificate, - Crypto.generate_random_keypair(:bls) |> elem(0) - ) + Node.encode_transaction_content(%{ + ip: {88, 22, 30, 229}, + port: 3000, + http_port: 4000, + transport: :tcp, + reward_address: + <<0, 0, 4, 221, 19, 74, 75, 69, 16, 50, 149, 253, 24, 115, 128, 241, 110, 118, 139, 7, + 48, 217, 58, 43, 145, 233, 77, 125, 190, 207, 31, 64, 157, 137>>, + origin_public_key: origin_public_key, + key_certificate: certificate, + mining_public_key: Crypto.generate_random_keypair(:bls) |> elem(0), + geo_patch: "F1B" + }) tx = TransactionFactory.create_non_valided_transaction(type: :node, content: content) @@ -621,9 +624,135 @@ defmodule Archethic.Mining.PendingTransactionValidationTest do address end) + stub(MockGeoIP, :get_coordinates, fn ip -> + case ip do + {88, 22, 30, 229} -> + {38.345170, -0.481490} + end + end) + assert :ok = PendingTransactionValidation.validate_type_rules(tx, DateTime.utc_now()) end + test "should return an error if the geo_patch is not the expected one" do + {origin_public_key, _} = + Crypto.generate_deterministic_keypair(:crypto.strong_rand_bytes(32), :secp256r1) + + {_, ca_pv} = :crypto.generate_key(:ecdh, :secp256r1, "ca_root_key") + <<_::8, _::8, origin_key::binary>> = origin_public_key + certificate = Crypto.ECDSA.sign(:secp256r1, ca_pv, origin_key) + + content = + Node.encode_transaction_content(%{ + ip: {88, 22, 30, 229}, + port: 3000, + http_port: 4000, + transport: :tcp, + reward_address: + <<0, 0, 4, 221, 19, 74, 75, 69, 16, 50, 149, 253, 24, 115, 128, 241, 110, 118, 139, 7, + 48, 217, 58, 43, 145, 233, 77, 125, 190, 207, 31, 64, 157, 137>>, + origin_public_key: origin_public_key, + key_certificate: certificate, + mining_public_key: Crypto.generate_random_keypair(:bls) |> elem(0), + geo_patch: "WRONG" + }) + + tx = TransactionFactory.create_non_valided_transaction(type: :node, content: content) + + MockDB + |> stub(:get_last_chain_address, fn address -> + address + end) + + stub(MockGeoIP, :get_coordinates, fn ip -> + case ip do + {88, 22, 30, 229} -> + {38.345170, -0.481490} + end + end) + + assert {:error, "Invalid geo patch from IP"} = + PendingTransactionValidation.validate_type_rules(tx, DateTime.utc_now()) + end + + test "Should include and validate geopatch in a node transaction" do + ip = {127, 0, 0, 1} + expected_geopatch = GeoPatch.from_ip(ip) + + assert byte_size(expected_geopatch) == 3 + + port = 3000 + http_port = 4000 + transport = :tcp + reward_address = ArchethicCase.random_address() + origin_public_key = ArchethicCase.random_public_key() + key_certificate = "" + mining_public_key = ArchethicCase.random_public_key() + + content = + Node.encode_transaction_content(%{ + ip: ip, + port: port, + http_port: http_port, + transport: transport, + reward_address: reward_address, + origin_public_key: origin_public_key, + key_certificate: key_certificate, + mining_public_key: mining_public_key, + geo_patch: expected_geopatch + }) + + assert {:ok, decoded_ip, decoded_port, decoded_http_port, decoded_transport, + decoded_reward_address, decoded_origin_public_key, decoded_key_certificate, + decoded_mining_public_key, + decoded_geopatch} = Node.decode_transaction_content(content) + + assert decoded_ip == ip + assert decoded_port == port + assert decoded_http_port == http_port + assert decoded_transport == transport + assert decoded_reward_address == reward_address + assert decoded_origin_public_key == origin_public_key + assert decoded_key_certificate == key_certificate + assert decoded_mining_public_key == mining_public_key + assert decoded_geopatch == expected_geopatch + + assert GeoPatch.from_ip(decoded_ip) == decoded_geopatch + end + + test "Should reject invalid geopatch in node transaction" do + ip = {127, 0, 0, 1} + invalid_geopatch = "BAD" + assert byte_size(invalid_geopatch) == 3 + + port = 3000 + http_port = 4000 + transport = :tcp + reward_address = ArchethicCase.random_address() + origin_public_key = ArchethicCase.random_public_key() + key_certificate = "" + mining_public_key = ArchethicCase.random_public_key() + + content = + Node.encode_transaction_content(%{ + ip: ip, + port: port, + http_port: http_port, + transport: transport, + reward_address: reward_address, + origin_public_key: origin_public_key, + key_certificate: key_certificate, + mining_public_key: mining_public_key, + geo_patch: invalid_geopatch + }) + + assert {:ok, decoded_ip, _, _, _, _, _, _, _, decoded_geopatch} = + Node.decode_transaction_content(content) + + assert decoded_ip == ip + refute GeoPatch.from_ip(decoded_ip) == decoded_geopatch + end + test "should return an error when a node transaction public key used on non allowed origin" do Application.put_env(:archethic, Archethic.Mining.PendingTransactionValidation, allowed_node_key_origins: [:tpm] @@ -633,17 +762,19 @@ defmodule Archethic.Mining.PendingTransactionValidationTest do certificate = Crypto.get_key_certificate(public_key) content = - Node.encode_transaction_content( - {80, 20, 10, 200}, - 3000, - 4000, - :tcp, - <<0, 0, 4, 221, 19, 74, 75, 69, 16, 50, 149, 253, 24, 115, 128, 241, 110, 118, 139, 7, - 48, 217, 58, 43, 145, 233, 77, 125, 190, 207, 31, 64, 157, 137>>, - <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, - certificate, - Crypto.generate_random_keypair(:bls) |> elem(0) - ) + Node.encode_transaction_content(%{ + ip: {80, 20, 10, 200}, + port: 3000, + http_port: 4000, + transport: :tcp, + reward_address: + <<0, 0, 4, 221, 19, 74, 75, 69, 16, 50, 149, 253, 24, 115, 128, 241, 110, 118, 139, 7, + 48, 217, 58, 43, 145, 233, 77, 125, 190, 207, 31, 64, 157, 137>>, + origin_public_key: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, + key_certificate: certificate, + mining_public_key: Crypto.generate_random_keypair(:bls) |> elem(0), + geo_patch: "BBB" + }) tx = TransactionFactory.create_non_valided_transaction( diff --git a/test/archethic/p2p/node_test.exs b/test/archethic/p2p/node_test.exs index 74be43266..1a40ed190 100644 --- a/test/archethic/p2p/node_test.exs +++ b/test/archethic/p2p/node_test.exs @@ -41,18 +41,19 @@ defmodule Archethic.P2P.NodeTest do mining_public_key = ArchethicCase.random_public_key() assert {:ok, {127, 0, 0, 1}, 3000, 4000, :tcp, ^reward_address, ^origin_public_key, - ^certificate, - ^mining_public_key} = - Node.encode_transaction_content( - {127, 0, 0, 1}, - 3000, - 4000, - :tcp, - reward_address, - origin_public_key, - certificate, - mining_public_key - ) + ^certificate, ^mining_public_key, + "000"} = + Node.encode_transaction_content(%{ + ip: {127, 0, 0, 1}, + port: 3000, + http_port: 4000, + transport: :tcp, + reward_address: reward_address, + origin_public_key: origin_public_key, + key_certificate: certificate, + mining_public_key: mining_public_key, + geo_patch: "000" + }) |> Node.decode_transaction_content() end end diff --git a/test/archethic/shared_secrets/mem_tables_loader_test.exs b/test/archethic/shared_secrets/mem_tables_loader_test.exs index 21d820c0f..0e333c01e 100644 --- a/test/archethic/shared_secrets/mem_tables_loader_test.exs +++ b/test/archethic/shared_secrets/mem_tables_loader_test.exs @@ -40,16 +40,17 @@ defmodule Archethic.SharedSecrets.MemTablesLoaderTest do type: :node, data: %TransactionData{ content: - Node.encode_transaction_content( - {127, 0, 0, 1}, - 3000, - 4000, - :tcp, - ArchethicCase.random_address(), - origin_public_key, - :crypto.strong_rand_bytes(32), - Crypto.generate_random_keypair(:bls) |> elem(0) - ) + Node.encode_transaction_content(%{ + ip: {127, 0, 0, 1}, + port: 3000, + http_port: 4000, + transport: :tcp, + reward_address: ArchethicCase.random_address(), + origin_public_key: origin_public_key, + key_certificate: :crypto.strong_rand_bytes(32), + mining_public_key: Crypto.generate_random_keypair(:bls) |> elem(0), + geo_patch: "000" + }) } } @@ -152,16 +153,17 @@ defmodule Archethic.SharedSecrets.MemTablesLoaderTest do type: :node, data: %TransactionData{ content: - Node.encode_transaction_content( - {127, 0, 0, 1}, - 3000, - 4000, - :tcp, - ArchethicCase.random_address(), - node_origin_public_key, - :crypto.strong_rand_bytes(32), - Crypto.generate_random_keypair(:bls) |> elem(0) - ) + Node.encode_transaction_content(%{ + ip: {127, 0, 0, 1}, + port: 3000, + http_port: 4000, + transport: :tcp, + reward_address: ArchethicCase.random_address(), + origin_public_key: node_origin_public_key, + key_certificate: :crypto.strong_rand_bytes(32), + mining_public_key: Crypto.generate_random_keypair(:bls) |> elem(0), + geo_patch: "000" + }) } }