Skip to content

Commit

Permalink
Add private page to define node settings (#619)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
samuelmanzanera committed Oct 12, 2022
1 parent bae20fd commit 9717765
Show file tree
Hide file tree
Showing 12 changed files with 274 additions and 34 deletions.
7 changes: 1 addition & 6 deletions config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
1 change: 1 addition & 0 deletions lib/archethic/bootstrap/sync.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
5 changes: 4 additions & 1 deletion lib/archethic/bootstrap/transaction_handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions lib/archethic/crypto.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1052,6 +1052,8 @@ defmodule Archethic.Crypto do
end
end

def valid_address?(_), do: false

@doc """
Load the transaction for the Keystore indexing
"""
Expand Down
11 changes: 9 additions & 2 deletions lib/archethic/mining/pending_transaction_validation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
Expand All @@ -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 ->
Expand Down
41 changes: 27 additions & 14 deletions lib/archethic/p2p/mem_table_loader.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
15 changes: 5 additions & 10 deletions lib/archethic/replication/transaction_validator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
212 changes: 212 additions & 0 deletions lib/archethic_web/live/settings_live.ex
Original file line number Diff line number Diff line change
@@ -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 %>
<div class="notification
<%= if @notification_status == "success" do %>
is-success
<% else %>
is-danger
<% end %>
is-light" x-data="{ open: true }" x-init="() => { setTimeout(() => open = false, 3000)}" x-show="open">
<button class="delete"></button>
<%= @notification %>
</div>
<% end %>
<div class="box">
<div class="columns">
<div class="column">
<h1 class="subtitle is-5">Node's settings</h1>
</div>
</div>
<form class="columns" phx-submit="save" phx-change="validate">
<div class="column is-5-desktop">
<div class="field">
<label class="label">Reward's address</label>
<div class="control">
<input class="input <%= if @error, do: 'is-danger'%>" type="text" placeholder="Enter your new reward address" value="<%= @reward_address %>" name="reward_address" />
</div>
<p class="help is-danger"><%= @error %></p>
</div>
<div class="field">
<div class="control">
<%= if @sending do %>
<button class="button is-link is-loading" disabled>Save</button>
<% else %>
<button class="button is-link">Save</submit>
<% end %>
</div>
</div>
</div>
</div>
</div>
"""
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
5 changes: 5 additions & 0 deletions lib/archethic_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions lib/archethic_web/transaction_subscriber.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
4 changes: 4 additions & 0 deletions lib/archethic_web/web_utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion src/c/nat/miniupnp
Submodule miniupnp updated 1 files
+1 −0 README

0 comments on commit 9717765

Please sign in to comment.