From aaeeebd8b099f926f25de879d01e954dce2939d4 Mon Sep 17 00:00:00 2001 From: Neylix Date: Tue, 26 Jul 2022 09:27:19 +0200 Subject: [PATCH] Mint mining rewards token on self repair (#402) * Create new mint rewards transaction on self repair * Ensure that transaction is not corrupted * Rename scheduler * Fix fungible properties control * Add test * Rename scheduler module * Fix initial pool amount to 3.34% of supply * Fix tests * Implement unresponsive node --- config/config.exs | 2 +- config/dev.exs | 6 +- config/prod.exs | 6 +- config/test.exs | 3 +- lib/archethic/db.ex | 3 +- lib/archethic/db/embedded_impl.ex | 17 ++- lib/archethic/db/embedded_impl/stats_info.ex | 50 +++++--- .../mining/pending_transaction_validation.ex | 82 +++++++++---- lib/archethic/reward.ex | 43 ++++++- ...network_pool_scheduler.ex => scheduler.ex} | 108 ++++++++---------- lib/archethic/reward/supervisor.ex | 4 +- lib/archethic/self_repair/sync.ex | 25 +++- .../archethic/bootstrap/network_init_test.exs | 2 +- test/archethic/db/embedded_impl_test.exs | 23 +++- .../pending_transaction_validation_test.exs | 102 +++++++++++++++++ test/archethic/reward/scheduler_test.exs | 100 ++++++++++++++++ test/archethic/reward_test.exs | 8 ++ test/archethic/self_repair/sync_test.exs | 7 +- 18 files changed, 460 insertions(+), 131 deletions(-) rename lib/archethic/reward/{network_pool_scheduler.ex => scheduler.ex} (60%) create mode 100644 test/archethic/reward/scheduler_test.exs create mode 100644 test/archethic/reward_test.exs diff --git a/config/config.exs b/config/config.exs index 158014c7d..b9de5de0d 100644 --- a/config/config.exs +++ b/config/config.exs @@ -92,7 +92,7 @@ config :archethic, Archethic.Bootstrap.NetworkInit, "010104AB41291F847A601055AEDD1AF24FF76FA970D6441E2DCA3818A8319B004C96B27B8FEB1DA31A044BA0A4800B4353359735719EBB3A05F98393A9CC599C3FAFD6" |> Base.decode16!(case: :mixed) ], - genesis_network_pool_amount: 3_400_000_000_000_000 + genesis_network_pool_amount: 3_340_000_000_000_000 config :archethic, Archethic.P2P.BootstrappingSeeds, backup_file: "p2p/seeds", diff --git a/config/dev.exs b/config/dev.exs index 64f7f9952..0272c0b9e 100755 --- a/config/dev.exs +++ b/config/dev.exs @@ -107,14 +107,10 @@ config :archethic, Archethic.Networking.Scheduler, interval: "0 * * * * * *" # -----end-of-Networking-dev-configs----- -config :archethic, Archethic.Reward.NetworkPoolScheduler, +config :archethic, Archethic.Reward.Scheduler, # At the 30th second interval: "30 * * * * *" -config :archethic, Archethic.Reward.WithdrawScheduler, - # Every 10s - interval: "*/10 * * * * *" - config :archethic, Archethic.SelfRepair.Scheduler, # Every minute at the 5th second interval: "5 * * * * * *" diff --git a/config/prod.exs b/config/prod.exs index 457d098c0..da06b5094 100755 --- a/config/prod.exs +++ b/config/prod.exs @@ -184,9 +184,9 @@ config :archethic, Archethic.OracleChain.Scheduler, # Aggregate chain every day at midnight summary_interval: System.get_env("ARCHETHIC_ORACLE_CHAIN_SUMMARY_INTERVAL", "0 0 0 * * * *") -config :archethic, Archethic.Reward.NetworkPoolScheduler, - # Every month - interval: System.get_env("ARCHETHIC_REWARD_SCHEDULER_INTERVAL", "0 0 0 1 * * *") +config :archethic, Archethic.Reward.Scheduler, + # Every day at 02:00:00 + interval: System.get_env("ARCHETHIC_REWARD_SCHEDULER_INTERVAL", "0 0 2 * * * *") config :archethic, Archethic.Crypto.SharedSecretsKeystore, diff --git a/config/test.exs b/config/test.exs index 21f003ea1..d982fa2b2 100755 --- a/config/test.exs +++ b/config/test.exs @@ -115,8 +115,7 @@ config :archethic, Archethic.Mining.PendingTransactionValidation, validate_node_ config :archethic, Archethic.Metrics.Poller, enabled: false config :archethic, Archethic.Metrics.Collector, MockMetricsCollector -config :archethic, Archethic.Reward.NetworkPoolScheduler, enabled: false -config :archethic, Archethic.Reward.WithdrawScheduler, enabled: false +config :archethic, Archethic.Reward.Scheduler, enabled: false config :archethic, Archethic.SelfRepair.Scheduler, enabled: false, diff --git a/lib/archethic/db.ex b/lib/archethic/db.ex index b7cc745bf..599bb4bc8 100644 --- a/lib/archethic/db.ex +++ b/lib/archethic/db.ex @@ -36,8 +36,9 @@ defmodule Archethic.DB do @callback get_first_chain_address(binary()) :: binary() @callback get_first_public_key(Crypto.key()) :: binary() - @callback register_tps(DateTime.t(), float(), non_neg_integer()) :: :ok + @callback register_stats(DateTime.t(), float(), non_neg_integer(), non_neg_integer()) :: :ok @callback get_latest_tps() :: float() + @callback get_latest_burned_fees() :: non_neg_integer() @callback get_nb_transactions() :: non_neg_integer() @callback transaction_exists?(binary()) :: boolean() diff --git a/lib/archethic/db/embedded_impl.ex b/lib/archethic/db/embedded_impl.ex index 524aed150..71f1adb18 100644 --- a/lib/archethic/db/embedded_impl.ex +++ b/lib/archethic/db/embedded_impl.ex @@ -253,9 +253,16 @@ defmodule Archethic.DB.EmbeddedImpl do @doc """ Register the new stats from a self-repair cycle """ - @spec register_tps(time :: DateTime.t(), tps :: float(), nb_transactions :: non_neg_integer()) :: + @spec register_stats( + time :: DateTime.t(), + tps :: float(), + nb_transactions :: non_neg_integer(), + burned_fees :: non_neg_integer() + ) :: :ok - defdelegate register_tps(date, tps, nb_transactions), to: StatsInfo, as: :new_stats + defdelegate register_stats(date, tps, nb_transactions, burned_fees), + to: StatsInfo, + as: :new_stats @doc """ Return tps from the last self-repair cycle @@ -263,6 +270,12 @@ defmodule Archethic.DB.EmbeddedImpl do @spec get_latest_tps() :: float() defdelegate get_latest_tps, to: StatsInfo, as: :get_tps + @doc """ + Return burned_fees from the last self-repair cycle + """ + @spec get_latest_burned_fees() :: non_neg_integer() + defdelegate get_latest_burned_fees, to: StatsInfo, as: :get_burned_fees + @doc """ Return the last number of transaction in the network (from the previous self-repair cycles) """ diff --git a/lib/archethic/db/embedded_impl/stats_info.ex b/lib/archethic/db/embedded_impl/stats_info.ex index 775fbc75a..ac03f6fba 100644 --- a/lib/archethic/db/embedded_impl/stats_info.ex +++ b/lib/archethic/db/embedded_impl/stats_info.ex @@ -25,13 +25,22 @@ defmodule Archethic.DB.EmbeddedImpl.StatsInfo do GenServer.call(__MODULE__, :get_tps) end + @doc """ + Return burned fees from the last self-repair cycle + """ + @spec get_burned_fees() :: non_neg_integer() + def get_burned_fees do + GenServer.call(__MODULE__, :get_burned_fees) + end + @doc """ Register the new stats from a self-repair cycle """ - @spec new_stats(DateTime.t(), float(), non_neg_integer()) :: :ok - def new_stats(date = %DateTime{}, tps, nb_transactions) - when is_float(tps) and is_integer(nb_transactions) and nb_transactions >= 0 do - GenServer.cast(__MODULE__, {:new_stats, date, tps, nb_transactions}) + @spec new_stats(DateTime.t(), float(), non_neg_integer(), non_neg_integer()) :: :ok + def new_stats(date = %DateTime{}, tps, nb_transactions, burned_fees) + when is_float(tps) and is_integer(nb_transactions) and nb_transactions >= 0 and + is_integer(burned_fees) and burned_fees >= 0 do + GenServer.cast(__MODULE__, {:new_stats, date, tps, nb_transactions, burned_fees}) end def register_p2p_summaries(node_public_key, date, available?, avg_availability) @@ -60,18 +69,19 @@ defmodule Archethic.DB.EmbeddedImpl.StatsInfo do filepath = Path.join(db_path, "stats") fd = File.open!(filepath, [:binary, :read, :append]) - {:ok, %{fd: fd, filepath: filepath, tps: 0.0, nb_transactions: 0}, + {:ok, %{fd: fd, filepath: filepath, tps: 0.0, nb_transactions: 0, burned_fees: 0}, {:continue, :load_from_file}} end def handle_continue(:load_from_file, state = %{filepath: filepath, fd: fd}) do if File.exists?(filepath) do - {tps, nb_transactions} = load_from_file(fd) + {tps, nb_transactions, burned_fees} = load_from_file(fd) new_state = state |> Map.put(:tps, tps) |> Map.put(:nb_transactions, nb_transactions) + |> Map.put(:burned_fees, burned_fees) {:noreply, new_state} else @@ -79,12 +89,12 @@ defmodule Archethic.DB.EmbeddedImpl.StatsInfo do end end - defp load_from_file(fd, acc \\ {0.0, 0}) do - # Read each stats entry 16 bytes: 4(timestamp) + 8(tps) + 4(nb transactions) - case :file.read(fd, 16) do - {:ok, <<_timestamp::32, tps::float-64, nb_transactions::32>>} -> - {_, prev_nb_transactions} = acc - load_from_file(fd, {tps, prev_nb_transactions + nb_transactions}) + defp load_from_file(fd, acc \\ {0.0, 0, 0}) do + # Read each stats entry 20 bytes: 4(timestamp) + 8(tps) + 4(nb transactions) + 4(burned_fees) + case :file.read(fd, 20) do + {:ok, <<_timestamp::32, tps::float-64, nb_transactions::32, burned_fees::32>>} -> + {_, prev_nb_transactions, _} = acc + load_from_file(fd, {tps, prev_nb_transactions + nb_transactions, burned_fees}) :eof -> acc @@ -99,18 +109,26 @@ defmodule Archethic.DB.EmbeddedImpl.StatsInfo do {:reply, tps, state} end - def handle_cast({:new_stats, date, tps, nb_transactions}, state = %{fd: fd}) do - append_to_file(fd, date, tps, nb_transactions) + def handle_call(:get_burned_fees, _, state = %{burned_fees: burned_fees}) do + {:reply, burned_fees, state} + end + + def handle_cast({:new_stats, date, tps, nb_transactions, burned_fees}, state = %{fd: fd}) do + append_to_file(fd, date, tps, nb_transactions, burned_fees) new_state = state |> Map.put(:tps, tps) |> Map.update!(:nb_transactions, &(&1 + nb_transactions)) + |> Map.put(:burned_fees, burned_fees) {:noreply, new_state} end - defp append_to_file(fd, date, tps, nb_transactions) do - IO.binwrite(fd, <>) + defp append_to_file(fd, date, tps, nb_transactions, burned_fees) do + IO.binwrite( + fd, + <> + ) end end diff --git a/lib/archethic/mining/pending_transaction_validation.ex b/lib/archethic/mining/pending_transaction_validation.ex index cf89dcd0e..12fc2a97d 100644 --- a/lib/archethic/mining/pending_transaction_validation.ex +++ b/lib/archethic/mining/pending_transaction_validation.ex @@ -6,6 +6,10 @@ defmodule Archethic.Mining.PendingTransactionValidation do alias Archethic.Crypto + alias Archethic.DB + + alias Archethic.SharedSecrets + alias Archethic.Election alias Archethic.Governance @@ -304,36 +308,36 @@ defmodule Archethic.Mining.PendingTransactionValidation do end defp do_accept_transaction(%Transaction{ - type: type, + type: :token, data: %TransactionData{content: content} - }) - when type in [:token, :mint_rewards] do - schema = - :archethic - |> Application.app_dir("priv/json-schemas/token-core.json") - |> File.read!() - |> Jason.decode!() - |> ExJsonSchema.Schema.resolve() + }) do + verify_token_creation(content) + end - with {:ok, json_token} <- Jason.decode(content), - :ok <- ExJsonSchema.Validator.validate(schema, json_token), - %{"type" => "non-fungible", "supply" => supply, "properties" => properties} - when length(properties) == supply / 100_000_000 <- json_token do + # To accept mint_rewards transaction, we ensure that the supply correspond to the + # burned fees from the last summary and that there is no transaction since the last + # reward schedule + defp do_accept_transaction(%Transaction{ + type: :mint_rewards, + data: %TransactionData{content: content} + }) do + with :ok <- verify_token_creation(content), + {:ok, %{"supply" => supply}} <- Jason.decode(content), + true <- supply == DB.get_latest_burned_fees(), + network_pool_address <- SharedSecrets.get_network_pool_address(), + false <- + DB.get_last_chain_address(network_pool_address, Reward.last_scheduling_date()) != + network_pool_address do :ok else - {:error, reason} -> - Logger.debug("Invalid token token specification: #{inspect(reason)}") - {:error, "Invalid token transaction - Invalid specification"} - - %{"type" => "fungible", "properties" => properties} when length(properties) <= 1 -> - :ok + false -> + {:error, "The supply do not match burned fees from last summary"} - %{"type" => "fungible"} -> - {:error, "Invalid token transaction - Fungible should have only 1 set of properties"} + true -> + {:error, "There is already a mint rewards transaction since last schedule"} - %{"type" => "non-fungible"} -> - {:error, - "Invalid token transaction - Supply should match properties for non-fungible tokens"} + e -> + e end end @@ -371,6 +375,36 @@ defmodule Archethic.Mining.PendingTransactionValidation do defp do_accept_transaction(_), do: :ok + defp verify_token_creation(content) do + schema = + :archethic + |> Application.app_dir("priv/json-schemas/token-core.json") + |> File.read!() + |> Jason.decode!() + |> ExJsonSchema.Schema.resolve() + + with {:ok, json_token} <- Jason.decode(content), + :ok <- ExJsonSchema.Validator.validate(schema, json_token), + %{"type" => "non-fungible", "supply" => supply, "properties" => properties} + when length(properties) == supply / 100_000_000 <- json_token do + :ok + else + {:error, reason} -> + Logger.debug("Invalid token token specification: #{inspect(reason)}") + {:error, "Invalid token transaction - Invalid specification"} + + %{"type" => "fungible", "properties" => properties} when length(properties) > 1 -> + {:error, "Invalid token transaction - Fungible should have only 1 set of properties"} + + %{"type" => "fungible"} -> + :ok + + %{"type" => "non-fungible"} -> + {:error, + "Invalid token transaction - Supply should match properties for non-fungible tokens"} + end + end + defp get_allowed_node_key_origins do :archethic |> Application.get_env(__MODULE__, []) diff --git a/lib/archethic/reward.ex b/lib/archethic/reward.ex index 8b65d050d..fcd8d5999 100644 --- a/lib/archethic/reward.ex +++ b/lib/archethic/reward.ex @@ -5,7 +5,14 @@ defmodule Archethic.Reward do alias Archethic.OracleChain - alias __MODULE__.NetworkPoolScheduler + alias Archethic.Crypto + + alias Archethic.Election + + alias Archethic.P2P + alias Archethic.P2P.Node + + alias __MODULE__.Scheduler alias Archethic.TransactionChain.Transaction alias Archethic.TransactionChain.TransactionData @@ -28,6 +35,15 @@ defmodule Archethic.Reward do @doc """ Create a transaction for minting new rewards + + ## Examples + + iex> %{ + ...> type: :mint_rewards, + ...> data: %{ + ...> content: "{\\n \\"supply\\":2000000000,\\n \\"type\\":\\"fungible\\",\\n \\"name\\":\\"Mining UCO rewards\\",\\n \\"symbol\\":\\"MUCO\\"\\n}\\n" + ...> } + ...> } = Reward.new_rewards_mint(2_000_000_000) """ @spec new_rewards_mint(amount :: non_neg_integer()) :: Transaction.t() def new_rewards_mint(amount) do @@ -45,6 +61,25 @@ defmodule Archethic.Reward do Transaction.new(:mint_rewards, data) end + @doc """ + Determine if the local node is the initiator of the new rewards mint + """ + @spec initiator?() :: boolean() + def initiator?(index \\ 0) do + %Node{first_public_key: initiator_key} = + next_address() + |> Election.storage_nodes(P2P.authorized_and_available_nodes()) + |> Enum.at(index) + + initiator_key == Crypto.first_node_public_key() + end + + defp next_address do + key_index = Crypto.number_of_network_pool_keys() + next_public_key = Crypto.network_pool_public_key(key_index + 1) + Crypto.derive_address(next_public_key) + end + @doc """ Return the list of transfers to rewards the validation nodes for a specific date """ @@ -58,11 +93,11 @@ defmodule Archethic.Reward do Returns the last date of the rewards scheduling from the network pool """ @spec last_scheduling_date() :: DateTime.t() - defdelegate last_scheduling_date, to: NetworkPoolScheduler, as: :last_date + defdelegate last_scheduling_date, to: Scheduler, as: :last_date def config_change(changed_conf) do changed_conf - |> Keyword.get(NetworkPoolScheduler) - |> NetworkPoolScheduler.config_change() + |> Keyword.get(Scheduler) + |> Scheduler.config_change() end end diff --git a/lib/archethic/reward/network_pool_scheduler.ex b/lib/archethic/reward/scheduler.ex similarity index 60% rename from lib/archethic/reward/network_pool_scheduler.ex rename to lib/archethic/reward/scheduler.ex index 08d2d9e49..fa3e6fb58 100644 --- a/lib/archethic/reward/network_pool_scheduler.ex +++ b/lib/archethic/reward/scheduler.ex @@ -1,4 +1,4 @@ -defmodule Archethic.Reward.NetworkPoolScheduler do +defmodule Archethic.Reward.Scheduler do @moduledoc false use GenServer @@ -8,19 +8,16 @@ defmodule Archethic.Reward.NetworkPoolScheduler do alias Archethic.Crypto - alias Archethic.Election + alias Archethic.PubSub + + alias Archethic.DB - alias Archethic.P2P alias Archethic.P2P.Node alias Archethic.Reward - # alias Archethic.TransactionChain.Transaction - # alias Archethic.TransactionChain.TransactionData - # alias Archethic.TransactionChain.TransactionData.Ledger - # alias Archethic.TransactionChain.TransactionData.UCOLedger - alias Archethic.Utils + alias Archethic.Utils.DetectNodeResponsiveness require Logger @@ -28,10 +25,6 @@ defmodule Archethic.Reward.NetworkPoolScheduler do GenServer.start_link(__MODULE__, args, name: __MODULE__) end - def start_scheduling do - GenServer.cast(__MODULE__, :start_scheduling) - end - @doc """ Get the last node rewards scheduling date """ @@ -42,11 +35,13 @@ defmodule Archethic.Reward.NetworkPoolScheduler do def init(args) do interval = Keyword.fetch!(args, :interval) + PubSub.register_to_node_update() {:ok, %{interval: interval}, :hibernate} end def handle_info( - {:node_update, %Node{authorized?: true, first_public_key: first_public_key}}, + {:node_update, + %Node{authorized?: true, available?: true, first_public_key: first_public_key}}, state = %{interval: interval} ) do if Crypto.first_node_public_key() == first_public_key do @@ -76,16 +71,51 @@ defmodule Archethic.Reward.NetworkPoolScheduler do end end + def handle_info( + {:node_update, %Node{available?: false, first_public_key: first_public_key}}, + state = %{timer: timer} + ) do + if Crypto.first_node_public_key() == first_public_key do + Process.cancel_timer(timer) + {:noreply, Map.delete(state, :timer)} + else + {:noreply, state} + end + end + def handle_info({:node_update, _}, state), do: {:noreply, state} - def handle_info(:send_rewards, state = %{interval: interval}) do + def handle_info(:mint_rewards, state = %{interval: interval}) do timer = schedule(interval) - if sender?() do - interval - |> get_last_date - |> Reward.get_transfers() - |> send_rewards() + case DB.get_latest_burned_fees() do + 0 -> + Logger.info("No mint rewards transaction needed") + + amount -> + tx = Reward.new_rewards_mint(amount) + + if Reward.initiator?() do + Archethic.send_new_transaction(tx) + + Logger.info("New mint rewards transaction sent with #{amount} token", + transaction_address: Base.encode16(tx.address) + ) + else + DetectNodeResponsiveness.start_link(tx.address, fn count -> + if Reward.initiator?(count) do + Logger.debug("Mint secret creation...attempt #{count}", + transaction_address: Base.encode16(tx.address) + ) + + Logger.info("New mint rewards transaction sent with #{amount} token", + transaction_address: Base.encode16(tx.address) + ) + + Archethic.send_new_transaction(tx) + end + end) + end end {:noreply, Map.put(state, :timer, timer), :hibernate} @@ -122,46 +152,8 @@ defmodule Archethic.Reward.NetworkPoolScheduler do end end - defp sender? do - next_transaction_index = Crypto.number_of_network_pool_keys() + 1 - node_public_key = Crypto.last_node_public_key() - - with true <- P2P.authorized_node?(), - next_address <- - Crypto.node_shared_secrets_public_key(next_transaction_index) |> Crypto.hash(), - [%Node{last_public_key: ^node_public_key} | _] <- - Election.storage_nodes(next_address, P2P.authorized_and_available_nodes()) do - true - else - _ -> - false - end - end - - defp send_rewards([]), do: :ok - - # defp send_rewards(transfers) do - # Logger.debug("Sending node reward transaction") - - # Transaction.new(:node_rewards, %TransactionData{ - # code: """ - # condition inherit: [ - # # We need to ensure the transaction type keep consistent - # # So we can apply specific rules during the transaction verification - # type: node_rewards - # ] - # """, - # ledger: %Ledger{ - # uco: %UCOLedger{ - # transfers: transfers - # } - # } - # }) - # |> Archethic.send_new_transaction() - # end - defp schedule(interval) do - Process.send_after(self(), :send_rewards, Utils.time_offset(interval) * 1000) + Process.send_after(self(), :mint_rewards, Utils.time_offset(interval) * 1000) end def config_change(nil), do: :ok diff --git a/lib/archethic/reward/supervisor.ex b/lib/archethic/reward/supervisor.ex index cfbe003f0..f5567d888 100644 --- a/lib/archethic/reward/supervisor.ex +++ b/lib/archethic/reward/supervisor.ex @@ -3,7 +3,7 @@ defmodule Archethic.Reward.Supervisor do use Supervisor - alias Archethic.Reward.NetworkPoolScheduler + alias Archethic.Reward.Scheduler alias Archethic.Utils @@ -13,7 +13,7 @@ defmodule Archethic.Reward.Supervisor do def init(_) do children = [ - {NetworkPoolScheduler, Application.get_env(:archethic, NetworkPoolScheduler)} + {Scheduler, Application.get_env(:archethic, Scheduler)} ] Supervisor.init(Utils.configurable_children(children), strategy: :one_for_one) diff --git a/lib/archethic/self_repair/sync.ex b/lib/archethic/self_repair/sync.ex index 73b988e6e..77bbc9e1d 100644 --- a/lib/archethic/self_repair/sync.ex +++ b/lib/archethic/self_repair/sync.ex @@ -18,6 +18,8 @@ defmodule Archethic.SelfRepair.Sync do alias Archethic.TaskSupervisor alias Archethic.TransactionChain + alias Archethic.TransactionChain.TransactionSummary + alias Archethic.Utils require Logger @@ -123,7 +125,7 @@ defmodule Archethic.SelfRepair.Sync do The P2P view will also be updated if some node information are inside the beacon chain to determine the readiness or the availability of a node. - Also, the number of transaction received during the beacon summary interval will be stored. + Also, the number of transaction received and the fees burned during the beacon summary interval will be stored. """ @spec process_summary_aggregate(SummaryAggregate.t(), binary()) :: :ok def process_summary_aggregate( @@ -159,7 +161,7 @@ defmodule Archethic.SelfRepair.Sync do end) |> Enum.each(&update_availabilities/1) - update_statistics(summary_time, length(transaction_summaries)) + update_statistics(summary_time, transaction_summaries) end defp synchronize_transactions([], _node_patch), do: :ok @@ -229,9 +231,14 @@ defmodule Archethic.SelfRepair.Sync do P2P.set_node_average_availability(node_key, avg_availability) end - defp update_statistics(_date, 0), do: :ok + defp update_statistics(date, []) do + tps = DB.get_latest_tps() + DB.register_stats(date, tps, 0, 0) + end + + defp update_statistics(date, transaction_summaries) do + nb_transactions = length(transaction_summaries) - defp update_statistics(date, nb_transactions) do previous_summary_time = date |> Utils.truncate_datetime() @@ -240,12 +247,20 @@ defmodule Archethic.SelfRepair.Sync do nb_seconds = abs(DateTime.diff(previous_summary_time, date)) tps = nb_transactions / nb_seconds - DB.register_tps(date, tps, nb_transactions) + acc = 0 + + burned_fees = + transaction_summaries + |> Enum.reduce(acc, fn %TransactionSummary{fee: fee}, acc -> acc + fee end) + + DB.register_stats(date, tps, nb_transactions, burned_fees) Logger.info( "TPS #{tps} on #{Utils.time_to_string(date)} with #{nb_transactions} transactions" ) + Logger.info("Burned fees #{burned_fees} on #{Utils.time_to_string(date)}") + PubSub.notify_new_tps(tps, nb_transactions) end end diff --git a/test/archethic/bootstrap/network_init_test.exs b/test/archethic/bootstrap/network_init_test.exs index 370b6cf7a..efbd893ee 100644 --- a/test/archethic/bootstrap/network_init_test.exs +++ b/test/archethic/bootstrap/network_init_test.exs @@ -346,7 +346,7 @@ defmodule Archethic.Bootstrap.NetworkInitTest do network_address = SharedSecrets.get_network_pool_address() key = {network_address, 0} - assert %{token: %{^key => 3_400_000_000_000_000}} = Account.get_balance(network_address) + assert %{token: %{^key => 3_340_000_000_000_000}} = Account.get_balance(network_address) end test "init_software_origin_shared_secrets_chain/1 should create first origin shared secret transaction" do diff --git a/test/archethic/db/embedded_impl_test.exs b/test/archethic/db/embedded_impl_test.exs index abe618b4d..249404269 100644 --- a/test/archethic/db/embedded_impl_test.exs +++ b/test/archethic/db/embedded_impl_test.exs @@ -589,21 +589,36 @@ defmodule Archethic.DB.EmbeddedTest do test "should get the latest tps from the stats file" do date = DateTime.utc_now() - :ok = EmbeddedImpl.register_tps(date, 10.0, 10_000) + :ok = EmbeddedImpl.register_stats(date, 10.0, 10_000, 0) assert 10.0 == EmbeddedImpl.get_latest_tps() - :ok = EmbeddedImpl.register_tps(DateTime.add(date, 86_400), 5.0, 5_000) + :ok = EmbeddedImpl.register_stats(DateTime.add(date, 86_400), 5.0, 5_000, 0) assert 5.0 == EmbeddedImpl.get_latest_tps() end test "should get the latest nb of transactions" do - :ok = EmbeddedImpl.register_tps(DateTime.utc_now(), 10.0, 10_000) + :ok = EmbeddedImpl.register_stats(DateTime.utc_now(), 10.0, 10_000, 0) assert 10_000 = EmbeddedImpl.get_nb_transactions() - :ok = EmbeddedImpl.register_tps(DateTime.utc_now() |> DateTime.add(86_400), 5.0, 5_000) + :ok = EmbeddedImpl.register_stats(DateTime.utc_now() |> DateTime.add(86_400), 5.0, 5_000, 0) assert 15_000 = EmbeddedImpl.get_nb_transactions() end + + test "should get the latest burned fees amount" do + :ok = EmbeddedImpl.register_stats(DateTime.utc_now(), 10.0, 10_000, 15_000) + assert 15_000 = EmbeddedImpl.get_latest_burned_fees() + + :ok = + EmbeddedImpl.register_stats( + DateTime.utc_now() |> DateTime.add(86_400), + 5.0, + 5_000, + 20_000 + ) + + assert 20_000 = EmbeddedImpl.get_latest_burned_fees() + end end describe "Bootstrap info" do diff --git a/test/archethic/mining/pending_transaction_validation_test.exs b/test/archethic/mining/pending_transaction_validation_test.exs index a30edd95f..82317330b 100644 --- a/test/archethic/mining/pending_transaction_validation_test.exs +++ b/test/archethic/mining/pending_transaction_validation_test.exs @@ -7,6 +7,10 @@ defmodule Archethic.Mining.PendingTransactionValidationTest do alias Archethic.Mining.PendingTransactionValidation + alias Archethic.SharedSecrets.MemTables.NetworkLookup + + alias Archethic.Reward.Scheduler + alias Archethic.P2P alias Archethic.P2P.Message.FirstPublicKey alias Archethic.P2P.Message.GetFirstPublicKey @@ -347,5 +351,103 @@ defmodule Archethic.Mining.PendingTransactionValidationTest do assert :ok = PendingTransactionValidation.validate(tx) end + + test "should return :ok when a mint reward transaction passes all tests" do + tx_seed = :crypto.strong_rand_bytes(32) + {pub, _} = Crypto.derive_keypair(tx_seed, 1) + address = Crypto.derive_address(pub) + + NetworkLookup.set_network_pool_address(address) + + Scheduler.start_link(interval: "0 * * * * *") + + MockDB + |> stub(:get_latest_burned_fees, fn -> 300_000_000 end) + |> stub(:get_last_chain_address, fn _, _ -> address end) + + tx = + Transaction.new( + :mint_rewards, + %TransactionData{ + content: + Jason.encode!(%{ + supply: 300_000_000, + name: "MyToken", + type: "fungible", + symbol: "MTK" + }) + }, + tx_seed, + 0 + ) + + assert :ok = PendingTransactionValidation.validate(tx) + end + + test "should return :error when a mint reward transaction has != burned_fees" do + tx_seed = :crypto.strong_rand_bytes(32) + {pub, _} = Crypto.derive_keypair(tx_seed, 1) + address = Crypto.derive_address(pub) + + NetworkLookup.set_network_pool_address(address) + + Scheduler.start_link(interval: "0 * * * * *") + + MockDB + |> stub(:get_latest_burned_fees, fn -> 200_000_000 end) + |> stub(:get_last_chain_address, fn _, _ -> address end) + + tx = + Transaction.new( + :mint_rewards, + %TransactionData{ + content: + Jason.encode!(%{ + supply: 300_000_000, + name: "MyToken", + type: "fungible", + symbol: "MTK" + }) + }, + tx_seed, + 0 + ) + + assert {:error, "The supply do not match burned fees from last summary"} = + PendingTransactionValidation.validate(tx) + end + + test "should return :error when there is already a mint rewards transaction since last schedule" do + tx_seed = :crypto.strong_rand_bytes(32) + {pub, _} = Crypto.derive_keypair(tx_seed, 1) + address = Crypto.derive_address(pub) + + NetworkLookup.set_network_pool_address(:crypto.strong_rand_bytes(32)) + + Scheduler.start_link(interval: "0 * * * * *") + + MockDB + |> stub(:get_latest_burned_fees, fn -> 300_000_000 end) + |> stub(:get_last_chain_address, fn _, _ -> address end) + + tx = + Transaction.new( + :mint_rewards, + %TransactionData{ + content: + Jason.encode!(%{ + supply: 300_000_000, + name: "MyToken", + type: "fungible", + symbol: "MTK" + }) + }, + tx_seed, + 0 + ) + + assert {:error, "There is already a mint rewards transaction since last schedule"} = + PendingTransactionValidation.validate(tx) + end end end diff --git a/test/archethic/reward/scheduler_test.exs b/test/archethic/reward/scheduler_test.exs new file mode 100644 index 000000000..a469e78ee --- /dev/null +++ b/test/archethic/reward/scheduler_test.exs @@ -0,0 +1,100 @@ +defmodule Archethic.Reward.SchedulerTest do + use ArchethicCase, async: false + + alias Archethic.Crypto + + alias Archethic.P2P + alias Archethic.P2P.Node + alias Archethic.P2P.Message.StartMining + + alias Archethic.Reward.Scheduler + + import Mox + + setup do + P2P.add_and_connect_node(%Node{ + first_public_key: Crypto.last_node_public_key(), + last_public_key: Crypto.last_node_public_key(), + geo_patch: "AAA", + available?: true, + authorized?: true, + authorization_date: DateTime.utc_now(), + average_availability: 1.0 + }) + end + + test "should initiate the reward scheduler and trigger mint reward" do + MockDB + |> stub(:get_latest_burned_fees, fn -> 0 end) + + assert {:ok, pid} = Scheduler.start_link(interval: "*/1 * * * * *") + + assert %{interval: "*/1 * * * * *"} = :sys.get_state(pid) + + send( + pid, + {:node_update, + %Node{ + authorized?: true, + available?: true, + first_public_key: Crypto.first_node_public_key() + }} + ) + + :erlang.trace(pid, true, [:receive]) + + assert_receive {:trace, ^pid, :receive, :mint_rewards}, 3_000 + end + + test "should send transaction when burning fees > 0" do + MockDB + |> stub(:get_latest_burned_fees, fn -> 15_000 end) + + me = self() + + MockClient + |> stub(:send_message, fn _, %StartMining{transaction: %{type: type}}, _ -> + send(me, type) + end) + + assert {:ok, pid} = Scheduler.start_link(interval: "*/1 * * * * *") + + send( + pid, + {:node_update, + %Node{ + authorized?: true, + available?: true, + first_public_key: Crypto.first_node_public_key() + }} + ) + + assert_receive :mint_rewards, 1_500 + end + + test "should not send transaction when burning fees = 0" do + MockDB + |> stub(:get_latest_burned_fees, fn -> 0 end) + + me = self() + + MockClient + |> stub(:send_message, fn _, %StartMining{transaction: %{type: type}}, _ -> + send(me, type) + end) + + assert {:ok, pid} = Scheduler.start_link(interval: "*/1 * * * * *") + + send( + pid, + {:node_update, + %Node{ + authorized?: true, + available?: true, + first_public_key: Crypto.first_node_public_key() + }} + ) + + refute_receive :mint_rewards, 1_500 + end +end diff --git a/test/archethic/reward_test.exs b/test/archethic/reward_test.exs new file mode 100644 index 000000000..eeea9c1b0 --- /dev/null +++ b/test/archethic/reward_test.exs @@ -0,0 +1,8 @@ +defmodule Archethic.RewardTest do + use ArchethicCase + use ExUnitProperties + + alias Archethic.Reward + + doctest Reward +end diff --git a/test/archethic/self_repair/sync_test.exs b/test/archethic/self_repair/sync_test.exs index 9b699eced..25bee655a 100644 --- a/test/archethic/self_repair/sync_test.exs +++ b/test/archethic/self_repair/sync_test.exs @@ -259,7 +259,7 @@ defmodule Archethic.SelfRepair.SyncTest do end) MockDB - |> stub(:register_tps, fn _, _, _ -> :ok end) + |> stub(:register_stats, fn _, _, _, _ -> :ok end) assert :ok = Sync.load_missed_transactions( @@ -339,7 +339,7 @@ defmodule Archethic.SelfRepair.SyncTest do end) MockDB - |> stub(:register_tps, fn _, _, _ -> + |> stub(:register_stats, fn _, _, _, _ -> :ok end) @@ -351,7 +351,8 @@ defmodule Archethic.SelfRepair.SyncTest do %TransactionSummary{ address: tx_address, type: :transfer, - timestamp: DateTime.utc_now() + timestamp: DateTime.utc_now(), + fee: 0 } ] },