From 97177655bf886b2e20bd0afc52b77541c92a1092 Mon Sep 17 00:00:00 2001 From: Samuel Manzanera Date: Wed, 12 Oct 2022 12:39:32 +0200 Subject: [PATCH] Add private page to define node settings (#619) * Use the node genesis address as reward address in dev * Set first node as synced in the network init * Avoid to lose already set fields in node chain update * Allow the node to send tokens * Provide a private page to change the node's reward address * Fix valid_address?/1 without non expecting binary * Leverage LiveViewx for a better UX & responsivness * Prevent invalid token transfers --- config/dev.exs | 7 +- lib/archethic/bootstrap/sync.ex | 1 + .../bootstrap/transaction_handler.ex | 5 +- lib/archethic/crypto.ex | 2 + .../mining/pending_transaction_validation.ex | 11 +- lib/archethic/p2p/mem_table_loader.ex | 41 ++-- .../replication/transaction_validator.ex | 15 +- lib/archethic_web/live/settings_live.ex | 212 ++++++++++++++++++ lib/archethic_web/router.ex | 5 + lib/archethic_web/transaction_subscriber.ex | 3 + lib/archethic_web/web_utils.ex | 4 + src/c/nat/miniupnp | 2 +- 12 files changed, 274 insertions(+), 34 deletions(-) create mode 100644 lib/archethic_web/live/settings_live.ex diff --git a/config/dev.exs b/config/dev.exs index 0393b6051..cc3b00597 100755 --- a/config/dev.exs +++ b/config/dev.exs @@ -37,12 +37,7 @@ config :archethic, Archethic.BeaconChain.SummaryTimer, interval: "0 * * * * *" config :archethic, Archethic.Bootstrap, - reward_address: - System.get_env( - "ARCHETHIC_REWARD_ADDRESS", - Base.encode16(<<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>) - ) - |> Base.decode16!(case: :mixed) + reward_address: System.get_env("ARCHETHIC_REWARD_ADDRESS", "") |> Base.decode16!(case: :mixed) config :archethic, Archethic.Bootstrap.NetworkInit, genesis_pools: [ diff --git a/lib/archethic/bootstrap/sync.ex b/lib/archethic/bootstrap/sync.ex index 86adfdca4..d12f1dd90 100644 --- a/lib/archethic/bootstrap/sync.ex +++ b/lib/archethic/bootstrap/sync.ex @@ -117,6 +117,7 @@ defmodule Archethic.Bootstrap.Sync do |> NetworkInit.self_replication() P2P.set_node_globally_available(Crypto.first_node_public_key()) + P2P.set_node_globally_synced(Crypto.first_node_public_key()) P2P.authorize_node(Crypto.last_node_public_key(), DateTime.utc_now()) NetworkInit.init_software_origin_chain() diff --git a/lib/archethic/bootstrap/transaction_handler.ex b/lib/archethic/bootstrap/transaction_handler.ex index 8266a40c3..3be2245c2 100644 --- a/lib/archethic/bootstrap/transaction_handler.ex +++ b/lib/archethic/bootstrap/transaction_handler.ex @@ -99,7 +99,10 @@ defmodule Archethic.Bootstrap.TransactionHandler do condition inherit: [ # We need to ensure the type stays consistent type: node, - content: true + + # Content and token transfers will be validated during tx's validation + content: true, + token_transfers: true ] """, content: diff --git a/lib/archethic/crypto.ex b/lib/archethic/crypto.ex index d8c3a4390..2520eb305 100755 --- a/lib/archethic/crypto.ex +++ b/lib/archethic/crypto.ex @@ -1052,6 +1052,8 @@ defmodule Archethic.Crypto do end end + def valid_address?(_), do: false + @doc """ Load the transaction for the Keystore indexing """ diff --git a/lib/archethic/mining/pending_transaction_validation.ex b/lib/archethic/mining/pending_transaction_validation.ex index 35b3d1dc7..a2976a62e 100644 --- a/lib/archethic/mining/pending_transaction_validation.ex +++ b/lib/archethic/mining/pending_transaction_validation.ex @@ -270,7 +270,12 @@ defmodule Archethic.Mining.PendingTransactionValidation do %Transaction{ type: :node, data: %TransactionData{ - content: content + content: content, + ledger: %Ledger{ + token: %TokenLedger{ + transfers: token_transfers + } + } }, previous_public_key: previous_public_key }, @@ -290,7 +295,9 @@ defmodule Archethic.Mining.PendingTransactionValidation do root_ca_public_key )}, {:conn, :ok} <- - {:conn, valid_connection(ip, port, previous_public_key)} do + {:conn, valid_connection(ip, port, previous_public_key)}, + {:transfers, true} <- + {:transfers, Enum.all?(token_transfers, &Reward.is_reward_token?(&1.token_address))} do :ok else :error -> diff --git a/lib/archethic/p2p/mem_table_loader.ex b/lib/archethic/p2p/mem_table_loader.ex index 236cca920..4df68ce8c 100644 --- a/lib/archethic/p2p/mem_table_loader.ex +++ b/lib/archethic/p2p/mem_table_loader.ex @@ -108,25 +108,38 @@ defmodule Archethic.P2P.MemTableLoader do {:ok, ip, port, http_port, transport, reward_address, origin_public_key, _certificate} = Node.decode_transaction_content(content) - node = %Node{ - ip: ip, - port: port, - http_port: http_port, - first_public_key: first_public_key, - last_public_key: previous_public_key, - geo_patch: GeoPatch.from_ip(ip), - transport: transport, - last_address: address, - reward_address: reward_address, - origin_public_key: origin_public_key - } - if first_node_change?(first_public_key, previous_public_key) do + node = %Node{ + ip: ip, + port: port, + http_port: http_port, + first_public_key: first_public_key, + last_public_key: previous_public_key, + geo_patch: GeoPatch.from_ip(ip), + transport: transport, + last_address: address, + reward_address: reward_address, + origin_public_key: origin_public_key + } + node |> Node.enroll(timestamp) |> MemTable.add_node() else - MemTable.add_node(node) + {:ok, node} = MemTable.get_node(first_public_key) + + MemTable.add_node(%{ + node + | ip: ip, + port: port, + http_port: http_port, + last_public_key: previous_public_key, + geo_patch: GeoPatch.from_ip(ip), + transport: transport, + last_address: address, + reward_address: reward_address, + origin_public_key: origin_public_key + }) end Logger.info("Node loaded into in memory p2p tables", node: Base.encode16(first_public_key)) diff --git a/lib/archethic/replication/transaction_validator.ex b/lib/archethic/replication/transaction_validator.ex index 5af8ba43a..a3deaf067 100644 --- a/lib/archethic/replication/transaction_validator.ex +++ b/lib/archethic/replication/transaction_validator.ex @@ -315,18 +315,13 @@ defmodule Archethic.Replication.TransactionValidator do end defp check_inputs( - tx = %Transaction{type: type, address: address}, + tx = %Transaction{address: address}, inputs ) do - cond do - address == Bootstrap.genesis_address() -> - :ok - - Transaction.network_type?(type) -> - :ok - - true -> - do_check_inputs(tx, inputs) + if address == Bootstrap.genesis_address() do + :ok + else + do_check_inputs(tx, inputs) end end diff --git a/lib/archethic_web/live/settings_live.ex b/lib/archethic_web/live/settings_live.ex new file mode 100644 index 000000000..801549c67 --- /dev/null +++ b/lib/archethic_web/live/settings_live.ex @@ -0,0 +1,212 @@ +defmodule ArchethicWeb.SettingsLive do + @moduledoc false + + use ArchethicWeb, :live_view + + alias Archethic.Crypto + + alias Archethic.P2P + alias Archethic.P2P.Node + + alias Archethic.Reward + + alias Archethic.TransactionChain + alias Archethic.TransactionChain.Transaction + alias Archethic.TransactionChain.TransactionData + alias Archethic.TransactionChain.TransactionData.Ledger + alias Archethic.TransactionChain.TransactionData.TokenLedger + alias Archethic.TransactionChain.TransactionData.TokenLedger.Transfer, as: TokenTransfer + + alias ArchethicWeb.TransactionSubscriber + + @ip_validate_regex ~r/(^127\.)|(^192\.168\.)/ + + def mount(_params, %{"remote_ip" => remote_ip}, socket) do + # Only authorized the page in the node's private network + private_ip? = + Regex.match?( + @ip_validate_regex, + :inet.ntoa(remote_ip) |> to_string() + ) + + new_socket = + socket + |> assign(:allowed, private_ip?) + |> assign(:reward_address, "") + |> assign(:error, nil) + |> assign(:sending, false) + |> assign(:notification, "") + |> assign(:notification_status, "") + + {:ok, new_socket} + end + + def handle_params(_params, _uri, socket = %{assigns: %{allowed: true}}) do + %Node{reward_address: reward_address} = P2P.get_node_info() + {:noreply, assign(socket, :reward_address, Base.encode16(reward_address))} + end + + def handle_params(_params, _uri, socket) do + {:noreply, push_redirect(socket, to: "/", replace: true)} + end + + def handle_event( + "save", + %{"reward_address" => reward_address}, + socket = %{assigns: %{error: nil, reward_address: previous_reward_address}} + ) do + if previous_reward_address != reward_address do + send_new_transaction(Base.decode16!(reward_address, case: :mixed)) + {:noreply, assign(socket, :sending, true)} + else + {:noreply, socket} + end + end + + def handle_event("save", _params, socket) do + {:noreply, socket} + end + + def handle_event("validate", %{"reward_address" => reward_address}, socket) do + with {:ok, reward_address_bin} <- Base.decode16(reward_address, case: :mixed), + true <- Crypto.valid_address?(reward_address_bin) do + {:noreply, assign(socket, :error, nil)} + else + _ -> + {:noreply, assign(socket, :error, "Invalid address")} + end + end + + def handle_info({:new_transaction, _tx_address}, socket) do + %Node{reward_address: reward_address} = P2P.get_node_info() + + new_socket = + socket + |> assign(:sending, false) + |> assign(:reward_address, Base.encode16(reward_address)) + |> assign(:notification, "Change applied!") + |> assign(:notification_status, "success") + + {:noreply, new_socket} + end + + def handle_info({:transaction_error, _address, _context, reason}, socket) do + new_socket = + socket + |> assign(:sending, false) + |> assign(:notification, "Transaction is invalid - #{reason}") + |> assign(:notification_status, "error") + + {:noreply, new_socket} + end + + def render(assigns) do + ~L""" + <%= if @notification != "" do %> +
+ is-success + <% else %> + is-danger + <% end %> + is-light" x-data="{ open: true }" x-init="() => { setTimeout(() => open = false, 3000)}" x-show="open"> + + <%= @notification %> +
+ <% end %> +
+
+
+

Node's settings

+
+
+ +
+
+
+ +
+ +
+

<%= @error %>

+
+ +
+
+ <%= if @sending do %> + + <% else %> +
+
+
+
+ + """ + end + + defp send_new_transaction(next_reward_address) do + %Node{ + ip: ip, + 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() + + token_transfers = + case genesis_address do + ^previous_reward_address -> + get_token_transfers(previous_reward_address, next_reward_address) + + _ -> + [] + end + + {:ok, %Transaction{data: %TransactionData{code: code}}} = + TransactionChain.get_last_transaction(genesis_address, data: [:code]) + + tx = + Transaction.new(:node, %TransactionData{ + ledger: %Ledger{ + token: %TokenLedger{ + transfers: token_transfers + } + }, + 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()) + ) + }) + + TransactionSubscriber.register(tx.address, System.monotonic_time()) + + Archethic.send_new_transaction(tx) + end + + defp get_token_transfers(previous_reward_address, next_reward_address) do + {:ok, last_address} = Archethic.get_last_transaction_address(previous_reward_address) + {:ok, %{token: tokens}} = Archethic.get_balance(last_address) + + tokens + |> Enum.filter(fn {{address, _}, _} -> Reward.is_reward_token?(address) end) + |> Enum.map(fn {{address, token_id}, amount} -> + %TokenTransfer{ + to: next_reward_address, + amount: amount, + token_id: token_id, + token_address: address + } + end) + end +end diff --git a/lib/archethic_web/router.ex b/lib/archethic_web/router.ex index a6d50f0be..f3fdc9091 100644 --- a/lib/archethic_web/router.ex +++ b/lib/archethic_web/router.ex @@ -97,6 +97,11 @@ defmodule ArchethicWeb.Router do ) end + scope "/settings" do + pipe_through(:browser) + live("/", ArchethicWeb.SettingsLive, session: {ArchethicWeb.WebUtils, :keep_remote_ip, []}) + end + scope "/", ArchethicWeb do get("/*path", RootController, :index) end diff --git a/lib/archethic_web/transaction_subscriber.ex b/lib/archethic_web/transaction_subscriber.ex index 2500feda3..23a666ea4 100644 --- a/lib/archethic_web/transaction_subscriber.ex +++ b/lib/archethic_web/transaction_subscriber.ex @@ -49,6 +49,9 @@ defmodule ArchethicWeb.TransactionSubscriber do {:error, tx_address, context, error}, state ) do + %{from: from} = Map.get(state, tx_address, %{from: make_ref()}) + send(from, {:transaction_error, tx_address, context, error}) + Subscription.publish( Endpoint, %{address: tx_address, context: context, reason: error}, diff --git a/lib/archethic_web/web_utils.ex b/lib/archethic_web/web_utils.ex index d3ffc14f3..d0dabef46 100644 --- a/lib/archethic_web/web_utils.ex +++ b/lib/archethic_web/web_utils.ex @@ -26,4 +26,8 @@ defmodule ArchethicWeb.WebUtils do def total_pages(tx_count), do: count_pages(tx_count) + 1 def count_pages(tx_count), do: div(tx_count, @display_limit) + + def keep_remote_ip(conn) do + %{"remote_ip" => conn.remote_ip} + end end diff --git a/src/c/nat/miniupnp b/src/c/nat/miniupnp index 1de377faf..df04310d3 160000 --- a/src/c/nat/miniupnp +++ b/src/c/nat/miniupnp @@ -1 +1 @@ -Subproject commit 1de377faf6a235354d44d3254a628ed622818269 +Subproject commit df04310d3910d92c6ac4c73607211ad6c903f7e3