From 57be8b7dc2b015619d5319ee7d3a132e047eba0a Mon Sep 17 00:00:00 2001 From: Wassim Mansouri Date: Fri, 15 Nov 2024 14:53:10 +0100 Subject: [PATCH 1/2] 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" + }) } } From 49426b227f6ef1ac823f7c0a84d667898991911e Mon Sep 17 00:00:00 2001 From: Neylix Date: Fri, 7 Feb 2025 19:01:45 +0100 Subject: [PATCH 2/2] Refactor bootstrap with NodeConfig module --- lib/archethic/bootstrap.ex | 384 ++++++------------ lib/archethic/bootstrap/sync.ex | 111 ++--- .../bootstrap/transaction_handler.ex | 40 +- .../mining/pending_transaction_validation.ex | 148 +++---- lib/archethic/mining/proof_of_work.ex | 10 +- lib/archethic/networking/scheduler.ex | 55 +-- lib/archethic/p2p/mem_table_loader.ex | 18 +- lib/archethic/p2p/node.ex | 92 +---- lib/archethic/p2p/node/config.ex | 144 +++++++ .../shared_secrets/mem_tables_loader.ex | 22 +- .../explorer/live/settings_live.ex | 91 ++--- .../explorer/views/explorer_view.ex | 24 +- 12 files changed, 501 insertions(+), 638 deletions(-) create mode 100644 lib/archethic/p2p/node/config.ex diff --git a/lib/archethic/bootstrap.ex b/lib/archethic/bootstrap.ex index a53435009..05f7d9dd2 100644 --- a/lib/archethic/bootstrap.ex +++ b/lib/archethic/bootstrap.ex @@ -11,6 +11,7 @@ defmodule Archethic.Bootstrap do alias Archethic.P2P alias Archethic.P2P.Node + alias Archethic.P2P.NodeConfig alias Archethic.Replication @@ -32,40 +33,7 @@ defmodule Archethic.Bootstrap do Start the bootstrapping as a task """ @spec start_link(list()) :: {:ok, pid()} - def start_link(args \\ []) do - {:ok, ip} = Networking.get_node_ip() - port = Keyword.get(args, :port) - http_port = Keyword.get(args, :http_port) - transport = Keyword.get(args, :transport) - - reward_address = - case Keyword.get(args, :reward_address) do - nil -> - Crypto.derive_address(Crypto.first_node_public_key()) - - "" -> - Crypto.derive_address(Crypto.first_node_public_key()) - - address -> - address - end - - last_sync_date = SelfRepair.last_sync_date() - bootstrapping_seeds = P2P.list_bootstrapping_seeds() - - Logger.info("Node bootstrapping...") - Logger.info("Rewards will be transfered to #{Base.encode16(reward_address)}") - - Task.start_link(__MODULE__, :run, [ - ip, - port, - http_port, - transport, - bootstrapping_seeds, - last_sync_date, - reward_address - ]) - end + def start_link(args \\ []), do: Task.start_link(__MODULE__, :run, [args]) @doc """ Start the bootstrap workflow. @@ -80,40 +48,36 @@ defmodule Archethic.Bootstrap do Once done, the synchronization/self repair mechanism is terminated, the node will publish to the Beacon chain its readiness. Hence others nodes will be able to communicate with and support new transactions. """ - @spec run( - :inet.ip_address(), - :inet.port_number(), - :inet.port_number(), - P2P.supported_transport(), - list(Node.t()), - DateTime.t() | nil, - Crypto.versioned_hash() - ) :: :ok - def run( - ip = {_, _, _, _}, - port, - http_port, - transport, - bootstrapping_seeds, - last_sync_date, - reward_address - ) - 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) + @spec run(args :: Keyword.t()) :: :ok + def run(args) do + Logger.info("Node bootstrapping...") - closest_bootstrapping_nodes = get_closest_nodes(bootstrapping_seeds, network_patch) - - 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 - ) + node_config = + %NodeConfig{ + first_public_key: first_public_key, + geo_patch: geo_patch, + reward_address: reward_address + } = get_node_config(args) + + Logger.info("Rewards will be transfered to #{Base.encode16(reward_address)}") + + network_patch = + case P2P.get_node_info(first_public_key) do + {:ok, %Node{network_patch: patch}} -> patch + _ -> geo_patch + end + + bootstrapping_seeds = P2P.list_bootstrapping_seeds() + + closest_bootstrapping_nodes = + get_closest_nodes(bootstrapping_seeds, network_patch, first_public_key) + + last_sync_date = SelfRepair.last_sync_date() + + if should_bootstrap?(node_config, last_sync_date) do + start_bootstrap(node_config, closest_bootstrapping_nodes) + else + Logger.debug("Node chain doesn't need to be updated") end post_bootstrap(closest_bootstrapping_nodes) @@ -121,122 +85,131 @@ defmodule Archethic.Bootstrap do Logger.info("Bootstrapping finished!") end - defp get_network_patch(ip) do - case P2P.get_node_info(Crypto.first_node_public_key()) do - {:ok, %Node{network_patch: patch}} -> - patch + defp get_node_config(args) do + node_public_key = Crypto.first_node_public_key() - _ -> - P2P.get_geo_patch(ip) - end - end + ip = + case Networking.get_node_ip() do + {:ok, ip} -> ip + {:error, reason} -> raise "Cannot retrieve public ip: #{inspect(reason)}" + end - defp get_closest_nodes(bootstrapping_seeds, network_patch) do - node_first_public_key = Crypto.first_node_public_key() + port = Keyword.get(args, :port) + http_port = Keyword.get(args, :http_port) + transport = Keyword.get(args, :transport) + + reward_address = + case Keyword.get(args, :reward_address) do + nil -> Crypto.derive_address(node_public_key) + "" -> Crypto.derive_address(node_public_key) + address -> address + end + origin_public_key = Crypto.origin_node_public_key() + origin_public_certificate = Crypto.get_key_certificate(origin_public_key) + mining_public_key = Crypto.mining_node_public_key() + geo_patch = GeoPatch.from_ip(ip) + + %NodeConfig{ + first_public_key: node_public_key, + ip: ip, + port: port, + http_port: http_port, + transport: transport, + reward_address: reward_address, + origin_public_key: origin_public_key, + origin_certificate: origin_public_certificate, + mining_public_key: mining_public_key, + geo_patch: geo_patch + } + end + + defp get_closest_nodes(bootstrapping_seeds, network_patch, first_public_key) do case bootstrapping_seeds do - [%Node{first_public_key: ^node_first_public_key}] -> + [%Node{first_public_key: ^first_public_key}] -> bootstrapping_seeds nodes -> P2P.connect_nodes(nodes) case Sync.get_closest_nodes_and_renew_seeds(nodes, network_patch) do - {:ok, closest_nodes} when closest_nodes != [] -> - closest_nodes - - _ -> - [] + {:ok, closest_nodes} -> closest_nodes + _ -> [] end end end - defp should_bootstrap?(_ip, _port, _http_port, _, _, nil), do: true + defp should_bootstrap?(_, nil), do: true - 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, geo_patch, last_sync_date) do - Logger.debug("Node chain need to updated") - true - else - Logger.debug("Node chain doesn't need to be updated") - false - end - - _ -> - Logger.debug("Node doesn't exists. It will be bootstrap and create a new chain") - true + defp should_bootstrap?( + node_config = %NodeConfig{first_public_key: first_public_key}, + last_sync_date + ) do + case P2P.get_node_info(first_public_key) do + {:ok, _} -> Sync.require_update?(node_config, last_sync_date) + _ -> true end end defp start_bootstrap( - ip, - port, - http_port, - transport, - geo_patch, - closest_bootstrapping_nodes, - configured_reward_address + node_config = %NodeConfig{first_public_key: first_public_key}, + closest_bootstrapping_nodes ) do - Logger.info("Bootstrapping starting") - - cond do - Sync.should_initialize_network?(closest_bootstrapping_nodes) -> - Logger.info("This node should initialize the network!!") - Logger.debug("Create first node transaction") - - tx = - TransactionHandler.create_node_transaction( - ip, - port, - http_port, - transport, - configured_reward_address, - geo_patch - ) - - Sync.initialize_network(tx) - - SelfRepair.put_last_sync_date(DateTime.utc_now()) - - Crypto.first_node_public_key() == Crypto.previous_node_public_key() -> - Logger.info("Node initialization...") - - first_initialization( - ip, - port, - http_port, - transport, - closest_bootstrapping_nodes, - configured_reward_address, - geo_patch - ) - - true -> - Logger.info("Update node chain...") - - {:ok, %Transaction{data: %TransactionData{content: content}}} = - TransactionChain.get_last_transaction( - Crypto.derive_address(Crypto.first_node_public_key()) - ) - - {: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) - - update_node( - ip, - port, - http_port, - transport, - closest_bootstrapping_nodes, - last_reward_address, - geo_patch - ) + if Sync.should_initialize_network?(closest_bootstrapping_nodes, first_public_key) do + Logger.info("This node should initialize the network!!") + Logger.debug("Create first node transaction") + + node_config |> TransactionHandler.create_node_transaction() |> Sync.initialize_network() + + SelfRepair.put_last_sync_date(DateTime.utc_now()) + else + node_genesis_address = first_public_key |> Crypto.derive_address() + + # In case node had lose it's DB, we ask the network if the node chain already exists + {:ok, length} = + TransactionChain.fetch_size(node_genesis_address, closest_bootstrapping_nodes) + + node_config = + if length == 0 do + Logger.debug("Node doesn't exists. It will be bootstrap and create a new chain") + node_config + else + Logger.debug("Node chain need to be updated") + Crypto.set_node_key_index(length) + + last_reward_address = + get_last_reward_address(node_genesis_address, closest_bootstrapping_nodes) + + %NodeConfig{node_config | reward_address: last_reward_address} + end + + {:ok, validated_tx} = + node_config + |> TransactionHandler.create_node_transaction() + |> TransactionHandler.send_transaction(closest_bootstrapping_nodes) + + Sync.load_storage_nonce(closest_bootstrapping_nodes) + + Replication.sync_transaction_chain( + validated_tx, + node_genesis_address, + closest_bootstrapping_nodes + ) end end + defp get_last_reward_address(genesis_address, nodes) do + {:ok, last_address} = TransactionChain.fetch_last_address(genesis_address, nodes) + + {:ok, %Transaction{data: %TransactionData{content: content}}} = + TransactionChain.fetch_transaction(last_address, nodes) + + {:ok, %NodeConfig{reward_address: last_reward_address}} = + Node.decode_transaction_content(content) + + last_reward_address + end + defp post_bootstrap(closest_bootstrapping_nodes) do last_sync_date = SelfRepair.last_sync_date() @@ -268,97 +241,6 @@ defmodule Archethic.Bootstrap do Archethic.PubSub.notify_node_status(:node_up) end - defp first_initialization( - ip, - port, - http_port, - transport, - closest_bootstrapping_nodes, - 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} = - Crypto.first_node_public_key() - |> Crypto.derive_address() - |> TransactionChain.fetch_size(closest_bootstrapping_nodes) - - Crypto.set_node_key_index(length) - - node_genesis_address = Crypto.first_node_public_key() |> Crypto.derive_address() - - reward_address = - if length > 0 do - {:ok, last_address} = - TransactionChain.fetch_last_address(node_genesis_address, closest_bootstrapping_nodes) - - {:ok, %Transaction{data: %TransactionData{content: content}}} = - 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, - _geo_patch} = Node.decode_transaction_content(content) - - last_reward_address - else - configured_reward_address - end - - tx = - TransactionHandler.create_node_transaction( - ip, - port, - http_port, - transport, - reward_address, - geo_patch - ) - - {:ok, validated_tx} = TransactionHandler.send_transaction(tx, closest_bootstrapping_nodes) - - :ok = Sync.load_storage_nonce(closest_bootstrapping_nodes) - - Replication.sync_transaction_chain( - validated_tx, - node_genesis_address, - closest_bootstrapping_nodes - ) - end - - 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, - geo_patch - ) do - tx = - TransactionHandler.create_node_transaction( - ip, - port, - http_port, - transport, - reward_address, - geo_patch - ) - - {:ok, validated_tx} = TransactionHandler.send_transaction(tx, closest_bootstrapping_nodes) - - node_genesis_address = Crypto.first_node_public_key() |> Crypto.derive_address() - - Replication.sync_transaction_chain( - validated_tx, - node_genesis_address, - closest_bootstrapping_nodes - ) - end - @doc """ Return the address which performed the initial allocation """ diff --git a/lib/archethic/bootstrap/sync.ex b/lib/archethic/bootstrap/sync.ex index 50c8c2c75..49e94d1c7 100644 --- a/lib/archethic/bootstrap/sync.ex +++ b/lib/archethic/bootstrap/sync.ex @@ -15,6 +15,7 @@ defmodule Archethic.Bootstrap.Sync do alias Archethic.P2P.Message.GetStorageNonce alias Archethic.P2P.Message.NotifyEndOfNodeSync alias Archethic.P2P.Node + alias Archethic.P2P.NodeConfig alias Archethic.SharedSecrets @@ -36,95 +37,53 @@ defmodule Archethic.Bootstrap.Sync do @doc """ Determines if network should be initialized """ - @spec should_initialize_network?(list(Node.t())) :: boolean() - def should_initialize_network?([]) do + @spec should_initialize_network?(boostrapping_nodes :: list(Node.t()), node_key :: Crypto.key()) :: + boolean() + def should_initialize_network?([], _) do TransactionChain.count_transactions_by_type(:node_shared_secrets) == 0 end - def should_initialize_network?([%Node{first_public_key: node_key} | _]) do - node_key == Crypto.first_node_public_key() and - TransactionChain.count_transactions_by_type(:node_shared_secrets) == 0 + def should_initialize_network?([%Node{first_public_key: bootstrapping_node_key} | _], node_key) + when bootstrapping_node_key == node_key do + TransactionChain.count_transactions_by_type(:node_shared_secrets) == 0 end - def should_initialize_network?(_), do: false + def should_initialize_network?(_, _), do: false @doc """ Determines if the node requires an update """ - @spec require_update?( - :inet.ip_address(), - :inet.port_number(), - :inet.port_number(), - P2P.supported_transport(), - binary(), - DateTime.t() | nil - ) :: boolean() - def require_update?(_ip, _port, _http_port, _transport, _geo_patch, nil), do: false - - def require_update?(ip, port, http_port, transport, geo_patch, last_sync_date) do - first_node_public_key = Crypto.first_node_public_key() - - 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 + @spec require_update?(node_conig :: NodeConfig.t(), last_sync_date :: DateTime.t()) :: boolean() + def require_update?(_, nil), do: false + + def require_update?( + node_config = %NodeConfig{first_public_key: first_public_key}, + last_sync_date + ) do + current_config = P2P.get_node_info() |> NodeConfig.from_node() 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 - - _ -> - false + cond do + first_node?(first_public_key) -> false + diff_sync > @out_of_sync_date_threshold -> true + true -> NodeConfig.different?(node_config, current_config) end end + defp first_node?(first_node_public_key) do + nodes = P2P.authorized_and_available_nodes() + match?([%Node{first_public_key: ^first_node_public_key}], nodes) + end + @doc """ Initialize the network by predefining the storage nonce, the first node transaction and the first node shared secrets and the genesis fund allocations """ @spec initialize_network(Transaction.t()) :: :ok - def initialize_network(node_tx = %Transaction{}) do + def initialize_network(node_tx = %Transaction{previous_public_key: first_public_key}) do NetworkInit.create_storage_nonce() secret_key = :crypto.strong_rand_bytes(32) - encrypted_secret_key = Crypto.ec_encrypt(secret_key, Crypto.last_node_public_key()) + encrypted_secret_key = Crypto.ec_encrypt(secret_key, first_public_key) encrypted_daily_nonce_seed = Crypto.aes_encrypt(@genesis_daily_nonce_seed, secret_key) encrypted_transaction_seed = Crypto.aes_encrypt(:crypto.strong_rand_bytes(32), secret_key) @@ -135,19 +94,13 @@ defmodule Archethic.Bootstrap.Sync do encrypted_reward_seed::binary>> :ok = Crypto.unwrap_secrets(secrets, encrypted_secret_key, ~U[1970-01-01 00:00:00Z]) + :ok = node_tx |> NetworkInit.self_validation() |> NetworkInit.self_replication() - :ok = - node_tx - |> NetworkInit.self_validation() - |> NetworkInit.self_replication() - - P2P.set_node_globally_available(Crypto.first_node_public_key(), DateTime.utc_now()) - P2P.set_node_globally_synced(Crypto.first_node_public_key()) + now = DateTime.utc_now() - P2P.authorize_node( - Crypto.last_node_public_key(), - SharedSecrets.next_application_date(DateTime.utc_now()) - ) + P2P.set_node_globally_available(first_public_key, now) + P2P.set_node_globally_synced(first_public_key) + P2P.authorize_node(first_public_key, SharedSecrets.next_application_date(now)) NetworkInit.init_software_origin_chain() NetworkInit.init_node_shared_secrets_chain() diff --git a/lib/archethic/bootstrap/transaction_handler.ex b/lib/archethic/bootstrap/transaction_handler.ex index 2d4e2564e..44e997854 100644 --- a/lib/archethic/bootstrap/transaction_handler.ex +++ b/lib/archethic/bootstrap/transaction_handler.ex @@ -1,12 +1,11 @@ defmodule Archethic.Bootstrap.TransactionHandler do @moduledoc false - alias Archethic.Crypto - alias Archethic.P2P alias Archethic.P2P.Message.Ok alias Archethic.P2P.Message.NewTransaction alias Archethic.P2P.Node + alias Archethic.P2P.NodeConfig alias Archethic.TransactionChain.Transaction alias Archethic.TransactionChain.TransactionData @@ -68,28 +67,8 @@ defmodule Archethic.Bootstrap.TransactionHandler do @doc """ Create a new node transaction """ - @spec create_node_transaction( - ip_address :: :inet.ip_address(), - p2p_port :: :inet.port_number(), - http_port :: :inet.port_number(), - transport :: P2P.supported_transport(), - reward_address :: Crypto.versioned_hash(), - geo_patch :: binary() - ) :: - Transaction.t() - 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_certificate = Crypto.get_key_certificate(origin_public_key) - mining_public_key = Crypto.mining_node_public_key() - + @spec create_node_transaction(node_config :: NodeConfig.t()) :: Transaction.t() + def create_node_transaction(node_config) do Transaction.new(:node, %TransactionData{ code: """ condition inherit: [ @@ -101,18 +80,7 @@ defmodule Archethic.Bootstrap.TransactionHandler do token_transfers: true ] """, - 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: origin_public_certificate, - mining_public_key: mining_public_key, - geo_patch: geo_patch - }) + content: Node.encode_transaction_content(node_config) }) end end diff --git a/lib/archethic/mining/pending_transaction_validation.ex b/lib/archethic/mining/pending_transaction_validation.ex index 5ad38e175..cb6ab005d 100644 --- a/lib/archethic/mining/pending_transaction_validation.ex +++ b/lib/archethic/mining/pending_transaction_validation.ex @@ -20,6 +20,7 @@ defmodule Archethic.Mining.PendingTransactionValidation do alias Archethic.P2P.Message.FirstPublicKey alias Archethic.P2P.Message.GetFirstPublicKey alias Archethic.P2P.Node + alias Archethic.P2P.NodeConfig alias Archethic.Reward @@ -341,67 +342,18 @@ defmodule Archethic.Mining.PendingTransactionValidation do type: :node, data: %TransactionData{ content: content, - ledger: %Ledger{ - token: %TokenLedger{ - transfers: token_transfers - } - } + ledger: %Ledger{token: %TokenLedger{transfers: token_transfers}} }, previous_public_key: previous_public_key }, _ ) do - 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, - Crypto.authorized_key_origin?(origin_public_key, get_allowed_node_key_origins())}, - root_ca_public_key <- Crypto.get_root_ca_public_key(origin_public_key), - {:auth_cert, true} <- - {:auth_cert, - Crypto.verify_key_certificate?( - origin_public_key, - key_certificate, - root_ca_public_key, - true - )}, - {:conn, :ok} <- - {:conn, valid_connection(ip, port, previous_public_key)}, - {:transfers, true} <- - {:transfers, Enum.all?(token_transfers, &Reward.is_reward_token?(&1.token_address))}, - {:mining_public_key, true} <- - {:mining_public_key, - Crypto.valid_public_key?(mining_public_key) and - Crypto.get_public_key_curve(mining_public_key) == :bls}, - {:geo_patch, true} <- - {:geo_patch, valid_geopatch?(ip, geo_patch)} do - :ok - else - :error -> - {:error, "Invalid node transaction's content"} - - {:auth_cert, false} -> - {:error, "Invalid node transaction with invalid certificate"} - - {:auth_origin, false} -> - {:error, "Invalid node transaction with invalid key origin"} - - {:conn, {:error, :invalid_ip}} -> - {:error, "Invalid node's IP address"} - - {:conn, {:error, :existing_node}} -> - {:error, - "Invalid node connection (IP/Port) for for the given public key - already existing"} - - {:transfers, false} -> - {:error, "Invalid transfers, only mining rewards tokens are allowed"} - - {:mining_public_key, false} -> - {:error, "Invalid mining public key"} - - {:geo_patch, false} -> - {:error, "Invalid geo patch from IP"} + with {:ok, node_config} <- validate_node_tx_content(content), + :ok <- validate_node_tx_origin(node_config), + :ok <- validate_node_tx_connection(node_config, previous_public_key), + :ok <- validate_node_tx_transfers(token_transfers), + :ok <- vallidate_node_tx_mining_key(node_config) do + validate_node_tx_geopatch(node_config) end end @@ -725,6 +677,73 @@ defmodule Archethic.Mining.PendingTransactionValidation do def validate_type_rules(_, _), do: :ok + defp validate_node_tx_content(content) do + case Node.decode_transaction_content(content) do + {:ok, node_config} -> {:ok, node_config} + :error -> {:error, "Invalid node transaction's content"} + end + end + + defp validate_node_tx_origin(%NodeConfig{ + origin_public_key: origin_public_key, + origin_certificate: origin_certificate + }) do + root_ca_public_key = Crypto.get_root_ca_public_key(origin_public_key) + + cond do + not Crypto.authorized_key_origin?(origin_public_key, get_allowed_node_key_origins()) -> + {:error, "Invalid node transaction with invalid key origin"} + + not Crypto.verify_key_certificate?( + origin_public_key, + origin_certificate, + root_ca_public_key, + true + ) -> + {:error, "Invalid node transaction with invalid certificate"} + + true -> + :ok + end + end + + defp validate_node_tx_connection(%NodeConfig{ip: ip, port: port}, previous_public_key) do + cond do + {:error, :invalid_ip} == Networking.validate_ip(ip) -> + {:error, "Invalid node's IP address"} + + P2P.duplicating_node?(ip, port, previous_public_key) -> + {:error, + "Invalid node connection (IP/Port) for for the given public key - already existing"} + + true -> + :ok + end + end + + defp validate_node_tx_transfers(token_transfers) do + if Enum.all?(token_transfers, &Reward.is_reward_token?(&1.token_address)), + do: :ok, + else: {:error, "Invalid transfers, only mining rewards tokens are allowed"} + end + + defp vallidate_node_tx_mining_key(%NodeConfig{mining_public_key: mining_public_key}) do + cond do + not Crypto.valid_public_key?(mining_public_key) -> + {:error, "Invalid mining public key"} + + Crypto.get_public_key_curve(mining_public_key) != :bls -> + {:error, "Node mining public key should be BLS"} + + true -> + :ok + end + end + + defp validate_node_tx_geopatch(%NodeConfig{ip: ip, geo_patch: geo_patch}) do + if geo_patch == GeoPatch.from_ip(ip), do: :ok, else: {:error, "Invalid geo patch from IP"} + end + @doc """ Ensure network transactions are in the expected chain """ @@ -1007,10 +1026,6 @@ 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__, []) @@ -1038,17 +1053,4 @@ defmodule Archethic.Mining.PendingTransactionValidation do end defp get_first_public_key([], _), do: {:error, :network_issue} - - defp valid_connection(ip, port, previous_public_key) do - with :ok <- Networking.validate_ip(ip), - false <- P2P.duplicating_node?(ip, port, previous_public_key) do - :ok - else - true -> - {:error, :existing_node} - - {:error, :invalid_ip} -> - {:error, :invalid_ip} - end - end end diff --git a/lib/archethic/mining/proof_of_work.ex b/lib/archethic/mining/proof_of_work.ex index 8c8a48c87..0076c8aaf 100644 --- a/lib/archethic/mining/proof_of_work.ex +++ b/lib/archethic/mining/proof_of_work.ex @@ -16,6 +16,7 @@ defmodule Archethic.Mining.ProofOfWork do alias Archethic.Crypto alias Archethic.P2P.Node + alias Archethic.P2P.NodeConfig alias Archethic.SharedSecrets @@ -137,13 +138,10 @@ defmodule Archethic.Mining.ProofOfWork do defp do_list_origin_public_keys_candidates(%Transaction{ type: :node, - data: %TransactionData{ - content: content - } + data: %TransactionData{content: content} }) do - {:ok, _ip, _p2p_port, _http_port, _transport, _reward_address, origin_public_key, - _origin_certificate, _mining_public_key, - _geo_patch} = Node.decode_transaction_content(content) + {:ok, %NodeConfig{origin_public_key: origin_public_key}} = + Node.decode_transaction_content(content) [origin_public_key] end diff --git a/lib/archethic/networking/scheduler.ex b/lib/archethic/networking/scheduler.ex index 95f77980d..7ac4c2f2a 100644 --- a/lib/archethic/networking/scheduler.ex +++ b/lib/archethic/networking/scheduler.ex @@ -10,9 +10,10 @@ defmodule Archethic.Networking.Scheduler do alias Archethic.Networking.PortForwarding alias Archethic.P2P - alias(Archethic.P2P.GeoPatch) + alias Archethic.P2P.GeoPatch alias Archethic.P2P.Listener, as: P2PListener alias Archethic.P2P.Node + alias Archethic.P2P.NodeConfig alias Archethic.PubSub @@ -94,49 +95,35 @@ defmodule Archethic.Networking.Scheduler do defp do_update do Logger.info("Start networking update") + node = + %Node{ip: prev_ip, last_address: last_address, origin_public_key: origin_public_key} = + P2P.get_node_info() + with {:ok, p2p_port, web_port} <- open_ports(), {:ok, ip} <- IPLookup.get_node_ip(), - {:ok, %Node{ip: prev_ip, reward_address: reward_address, transport: transport}} - when prev_ip != ip <- P2P.get_node_info(Crypto.first_node_public_key()), - genesis_address <- Crypto.first_node_public_key() |> Crypto.derive_address(), + true <- prev_ip != ip, {:ok, %Transaction{data: %TransactionData{code: code}}} <- - TransactionChain.get_last_transaction(genesis_address, data: [:code]) 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) + TransactionChain.get_transaction(last_address, data: [:code]) do + node_config = %NodeConfig{ + NodeConfig.from_node(node) + | origin_certificate: Crypto.get_key_certificate(origin_public_key), + geo_patch: GeoPatch.from_ip(ip), + port: p2p_port, + http_port: web_port + } tx = Transaction.new(:node, %TransactionData{ code: code, - content: - 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 - }) + content: Node.encode_transaction_content(node_config) }) Archethic.send_new_transaction(tx, forward?: true) handle_new_ip(tx) else - :error -> - Logger.warning("Cannot open port") - - {:error, :not_found} -> - Logger.debug("Skip node update: Not yet bootstrapped") - - {:ok, %Node{}} -> - Logger.debug("Skip node update: Same IP - no need to send a new node transaction") - - {:error, _} -> - Logger.warning("Cannot fetch IP") + :error -> Logger.warning("Cannot open port") + false -> Logger.debug("Skip node update: Same IP - no need to send a new node transaction") + {:error, _} -> Logger.warning("Cannot fetch IP") end end @@ -144,8 +131,8 @@ defmodule Archethic.Networking.Scheduler do p2p_port = Application.get_env(:archethic, P2PListener) |> Keyword.fetch!(:port) web_port = Application.get_env(:archethic, WebEndpoint) |> get_in([:http, :port]) - with {:ok, _} <- PortForwarding.try_open_port(p2p_port, false), - {:ok, _} <- PortForwarding.try_open_port(web_port, false) do + with {:ok, p2p_port} <- PortForwarding.try_open_port(p2p_port, false), + {:ok, web_port} <- PortForwarding.try_open_port(web_port, false) do {:ok, p2p_port, web_port} end end diff --git a/lib/archethic/p2p/mem_table_loader.ex b/lib/archethic/p2p/mem_table_loader.ex index 013ad4d8f..d63bb89fe 100644 --- a/lib/archethic/p2p/mem_table_loader.ex +++ b/lib/archethic/p2p/mem_table_loader.ex @@ -11,6 +11,7 @@ defmodule Archethic.P2P.MemTableLoader do alias Archethic.P2P.GeoPatch alias Archethic.P2P.MemTable alias Archethic.P2P.Node + alias Archethic.P2P.NodeConfig alias Archethic.SelfRepair @@ -93,9 +94,7 @@ defmodule Archethic.P2P.MemTableLoader do type: :node, previous_public_key: previous_public_key, data: %TransactionData{content: content}, - validation_stamp: %ValidationStamp{ - timestamp: timestamp - } + validation_stamp: %ValidationStamp{timestamp: timestamp} }) do Logger.info("Loading transaction into P2P mem table", transaction_address: Base.encode16(address), @@ -104,8 +103,17 @@ 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, geo_patch} = Node.decode_transaction_content(content) + {:ok, + %NodeConfig{ + ip: ip, + port: port, + http_port: http_port, + transport: transport, + reward_address: reward_address, + origin_public_key: origin_public_key, + mining_public_key: mining_public_key, + geo_patch: geo_patch + }} = Node.decode_transaction_content(content) geo_patch = if geo_patch == nil, do: GeoPatch.from_ip(ip), else: geo_patch diff --git a/lib/archethic/p2p/node.ex b/lib/archethic/p2p/node.ex index 7d7f2b4f5..288ab67f6 100755 --- a/lib/archethic/p2p/node.ex +++ b/lib/archethic/p2p/node.ex @@ -14,6 +14,7 @@ defmodule Archethic.P2P.Node do alias Archethic.Crypto alias Archethic.P2P + alias Archethic.P2P.NodeConfig alias Archethic.Utils @@ -40,78 +41,6 @@ defmodule Archethic.P2P.Node do availability_update: ~U[2008-10-31 00:00:00Z] ] - @doc """ - Decode node information from transaction content - """ - @spec decode_transaction_content(binary()) :: - {: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, - geo_patch :: binary() | nil} - | :error - def decode_transaction_content( - <> - ) do - with <> <- ip, - {reward_address, rest} <- Utils.deserialize_address(rest), - {origin_public_key, rest} <- Utils.deserialize_public_key(rest), - <> <- 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, geo_patch} - else - _ -> - :error - end - end - - 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(%{ - 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 - <> - end - @type t() :: %__MODULE__{ first_public_key: nil | Crypto.key(), last_public_key: Crypto.key(), @@ -134,6 +63,25 @@ defmodule Archethic.P2P.Node do availability_update: DateTime.t() } + @doc """ + Encode node's transaction content + """ + @spec encode_transaction_content(node_config :: NodeConfig.t()) :: binary() + def encode_transaction_content(node_config) do + NodeConfig.serialize(node_config) + end + + @doc """ + Decode node information from transaction content + """ + @spec decode_transaction_content(binary()) :: {:ok, NodeConfig.t()} | :error + def decode_transaction_content(bin) do + case NodeConfig.deserialize(bin) do + {node_config, _rest} -> {:ok, node_config} + :error -> :error + end + end + @doc """ Convert a tuple from NodeLedger to a Node instance """ diff --git a/lib/archethic/p2p/node/config.ex b/lib/archethic/p2p/node/config.ex new file mode 100644 index 000000000..92250ab98 --- /dev/null +++ b/lib/archethic/p2p/node/config.ex @@ -0,0 +1,144 @@ +defmodule Archethic.P2P.NodeConfig do + @moduledoc """ + Configuration of the node in the network. + It contains P2P informations, reward address, mining public key and origin. + """ + + alias Archethic.Crypto + alias Archethic.P2P + alias Archethic.P2P.Node + alias Archethic.Utils + + defstruct [ + :first_public_key, + :ip, + :port, + :http_port, + :transport, + :reward_address, + :origin_public_key, + :origin_certificate, + :mining_public_key, + :geo_patch + ] + + @type t :: %__MODULE__{ + first_public_key: nil | Crypto.key(), + ip: :inet.ip_address(), + port: :inet.port_number(), + http_port: :inet.port_number(), + transport: P2P.supported_transport(), + reward_address: Crypto.prepended_hash(), + origin_public_key: Crypto.key(), + origin_certificate: nil | binary(), + mining_public_key: nil | Crypto.key(), + geo_patch: nil | binary() + } + + @doc """ + Extract the informations from the Node struct and return a NodeConfig + """ + @spec from_node(node :: Node.t()) :: t() + def from_node(%Node{ + first_public_key: first_public_key, + ip: ip, + port: port, + http_port: http_port, + transport: transport, + reward_address: reward_address, + origin_public_key: origin_public_key, + mining_public_key: mining_public_key, + geo_patch: geo_patch + }) do + %__MODULE__{ + first_public_key: first_public_key, + ip: ip, + port: port, + http_port: http_port, + transport: transport, + reward_address: reward_address, + origin_public_key: origin_public_key, + mining_public_key: mining_public_key, + geo_patch: geo_patch + } + end + + @doc """ + Returns true if the config are different + do not compare origin certificate + """ + @spec different?(config1 :: t(), config2 :: t()) :: boolean() + def different?(config1, config2) do + config1 = %__MODULE__{config1 | origin_certificate: nil} + config2 = %__MODULE__{config2 | origin_certificate: nil} + + config1 != config2 + end + + @doc """ + Serialize a config in binary. + Origin certificate should not be nil + """ + @spec serialize(node_config :: t()) :: binary() + def serialize(%__MODULE__{ + ip: {ip1, ip2, ip3, ip4}, + port: port, + http_port: http_port, + transport: transport, + reward_address: reward_address, + origin_public_key: origin_public_key, + origin_certificate: origin_certificate, + mining_public_key: mining_public_key, + geo_patch: geo_patch + }) + when origin_certificate != nil do + <> + end + + defp serialize_transport(MockTransport), do: 0 + defp serialize_transport(:tcp), do: 1 + + @doc """ + Deserialize a binary and return a NodeConfig + """ + + @spec deserialize(binary()) :: {t(), binary()} | :error + def deserialize(<>) do + with <> <- ip, + {reward_address, rest} <- Utils.deserialize_address(rest), + {origin_public_key, rest} <- Utils.deserialize_public_key(rest), + <> <- rest, + {mining_public_key, rest} <- extract_mining_public_key(rest), + {geo_patch, rest} <- extract_geo_patch(rest) do + node_config = %__MODULE__{ + ip: {ip1, ip2, ip3, ip4}, + port: port, + http_port: http_port, + transport: deserialize_transport(transport), + reward_address: reward_address, + origin_public_key: origin_public_key, + origin_certificate: origin_certificate, + mining_public_key: mining_public_key, + geo_patch: geo_patch + } + + {node_config, rest} + else + _ -> :error + end + end + + def deserialize(_), do: :error + + defp deserialize_transport(0), do: MockTransport + defp deserialize_transport(1), do: :tcp + + defp extract_mining_public_key(<<>>), do: {nil, <<>>} + defp extract_mining_public_key(rest), do: Utils.deserialize_public_key(rest) + + defp extract_geo_patch(<>), do: {geo_patch, rest} + defp extract_geo_patch(rest), do: {nil, rest} +end diff --git a/lib/archethic/shared_secrets/mem_tables_loader.ex b/lib/archethic/shared_secrets/mem_tables_loader.ex index ed7afc4b9..6cb22d378 100644 --- a/lib/archethic/shared_secrets/mem_tables_loader.ex +++ b/lib/archethic/shared_secrets/mem_tables_loader.ex @@ -8,6 +8,7 @@ defmodule Archethic.SharedSecrets.MemTablesLoader do alias Archethic.Utils alias Archethic.P2P.Node + alias Archethic.P2P.NodeConfig alias Archethic.SharedSecrets alias Archethic.SharedSecrets.MemTables.NetworkLookup @@ -56,25 +57,18 @@ defmodule Archethic.SharedSecrets.MemTablesLoader do def load_transaction(%Transaction{ address: address, type: :node, - data: %TransactionData{ - content: content - } + data: %TransactionData{content: content} }) do - {:ok, _ip, _p2p_port, _http_port, _transport, _reward_address, origin_public_key, _cert, - _mining_public_key, _geo_patch} = Node.decode_transaction_content(content) + {:ok, %NodeConfig{origin_public_key: origin_public_key}} = + Node.decode_transaction_content(content) <<_::8, origin_id::8, _::binary>> = origin_public_key family = case Crypto.key_origin(origin_id) do - :software -> - :software - - :tpm -> - :hardware - - :on_chain_wallet -> - :software + :software -> :software + :tpm -> :hardware + :on_chain_wallet -> :software end :ok = OriginKeyLookup.add_public_key(family, origin_public_key) @@ -83,8 +77,6 @@ defmodule Archethic.SharedSecrets.MemTablesLoader do transaction_address: Base.encode16(address), transaction_type: :node ) - - :ok end def load_transaction(%Transaction{ diff --git a/lib/archethic_web/explorer/live/settings_live.ex b/lib/archethic_web/explorer/live/settings_live.ex index 73504c4ba..2a3f4a516 100644 --- a/lib/archethic_web/explorer/live/settings_live.ex +++ b/lib/archethic_web/explorer/live/settings_live.ex @@ -9,6 +9,7 @@ defmodule ArchethicWeb.Explorer.SettingsLive do alias Archethic.P2P alias Archethic.P2P.Node + alias Archethic.P2P.NodeConfig alias Archethic.Reward @@ -118,49 +119,35 @@ defmodule ArchethicWeb.Explorer.SettingsLive do end defp send_new_transaction(next_reward_address) do - %Node{ - ip: ip, - geo_patch: geo_patch, - port: port, - http_port: http_port, - transport: transport, - reward_address: previous_reward_address - } = P2P.get_node_info() - - genesis_address = Crypto.first_node_public_key() |> Crypto.derive_address() + node = + %Node{ + reward_address: previous_reward_address, + origin_public_key: origin_public_key, + last_address: last_address, + first_public_key: first_public_key + } = P2P.get_node_info() + + node_config = %NodeConfig{ + NodeConfig.from_node(node) + | origin_certificate: Crypto.get_key_certificate(origin_public_key), + reward_address: next_reward_address + } + + genesis_address = Crypto.derive_address(first_public_key) token_transfers = - case genesis_address do - ^previous_reward_address -> - get_token_transfers(previous_reward_address, next_reward_address) - - _ -> - [] - end + if previous_reward_address == genesis_address, + do: get_token_transfers(previous_reward_address, next_reward_address), + else: [] {:ok, %Transaction{data: %TransactionData{code: code}}} = - TransactionChain.get_last_transaction(genesis_address, data: [:code]) + TransactionChain.get_transaction(last_address, data: [:code]) tx = Transaction.new(:node, %TransactionData{ - ledger: %Ledger{ - token: %TokenLedger{ - transfers: token_transfers - } - }, + ledger: %Ledger{token: %TokenLedger{transfers: token_transfers}}, code: code, - content: - 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 - }) + content: Node.encode_transaction_content(node_config) }) TransactionSubscriber.register(tx.address, System.monotonic_time()) @@ -169,35 +156,22 @@ defmodule ArchethicWeb.Explorer.SettingsLive do end defp send_noop_transaction() do - %Node{ - ip: ip, - geo_patch: geo_patch, - port: port, - http_port: http_port, - transport: transport, - reward_address: reward_address - } = P2P.get_node_info() + node = + %Node{origin_public_key: origin_public_key, last_address: last_address} = + P2P.get_node_info() - genesis_address = Crypto.first_node_public_key() |> Crypto.derive_address() + node_config = %NodeConfig{ + NodeConfig.from_node(node) + | origin_certificate: Crypto.get_key_certificate(origin_public_key) + } {:ok, %Transaction{data: %TransactionData{code: code}}} = - TransactionChain.get_last_transaction(genesis_address, data: [: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: 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 - }) + content: Node.encode_transaction_content(node_config) }) TransactionSubscriber.register(tx.address, System.monotonic_time()) @@ -206,8 +180,7 @@ defmodule ArchethicWeb.Explorer.SettingsLive do end defp get_token_transfers(previous_reward_address, next_reward_address) do - {:ok, genesis_address} = Archethic.fetch_genesis_address(previous_reward_address) - %{token: tokens} = Archethic.get_balance(genesis_address) + %{token: tokens} = Archethic.get_balance(previous_reward_address) tokens |> Enum.filter(fn {{address, _}, _} -> Reward.is_reward_token?(address) end) diff --git a/lib/archethic_web/explorer/views/explorer_view.ex b/lib/archethic_web/explorer/views/explorer_view.ex index 77f1775d9..449367114 100644 --- a/lib/archethic_web/explorer/views/explorer_view.ex +++ b/lib/archethic_web/explorer/views/explorer_view.ex @@ -15,6 +15,7 @@ defmodule ArchethicWeb.Explorer.ExplorerView do alias Archethic.SharedSecrets.NodeRenewal alias Archethic.P2P.Node + alias Archethic.P2P.NodeConfig alias Archethic.TransactionChain.TransactionSummary @@ -51,8 +52,18 @@ defmodule ArchethicWeb.Explorer.ExplorerView do end def format_transaction_content(:node, content) do - {:ok, ip, port, http_port, transport, reward_address, origin_public_key, key_certificate, - mining_public_key, geo_patch} = Node.decode_transaction_content(content) + {:ok, + %NodeConfig{ + ip: ip, + port: port, + http_port: http_port, + transport: transport, + reward_address: reward_address, + origin_public_key: origin_public_key, + origin_certificate: origin_certificate, + mining_public_key: mining_public_key, + geo_patch: geo_patch + }} = Node.decode_transaction_content(content) content = """ IP: #{:inet.ntoa(ip)} @@ -62,15 +73,12 @@ defmodule ArchethicWeb.Explorer.ExplorerView do Transport: #{transport} Reward address: #{Base.encode16(reward_address)} Origin public key: #{Base.encode16(origin_public_key)} - Key certificate: #{Base.encode16(key_certificate)} + Origin certificate: #{Base.encode16(origin_certificate)} """ case mining_public_key do - nil -> - content - - _ -> - content <> "Mining public key: #{Base.encode16(mining_public_key)}" + nil -> content + _ -> content <> "Mining public key: #{Base.encode16(mining_public_key)}" end end