diff --git a/config/dev.exs b/config/dev.exs index dbb1f432cd..7e7ae6e1e6 100755 --- a/config/dev.exs +++ b/config/dev.exs @@ -100,14 +100,10 @@ config :archethic, Archethic.Networking.Scheduler, interval: "0 * * * * * *" # -----end-of-Networking-dev-configs----- -config :archethic, Archethic.Reward.NetworkPoolScheduler, +config :archethic, Archethic.Reward.RewardScheduler, # 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 4f24bc6a3c..e3b2d44036 100755 --- a/config/prod.exs +++ b/config/prod.exs @@ -186,9 +186,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.RewardScheduler, + # 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 61eb1c90d2..39d4abbeca 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.RewardScheduler, enabled: false config :archethic, Archethic.SelfRepair.Scheduler, enabled: false, diff --git a/lib/archethic/db.ex b/lib/archethic/db.ex index b7cc745bfd..599bb4bc83 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 c080ff15e0..4d7c60518a 100644 --- a/lib/archethic/db/embedded_impl.ex +++ b/lib/archethic/db/embedded_impl.ex @@ -1,6 +1,6 @@ defmodule Archethic.DB.EmbeddedImpl do @moduledoc """ - Custom database implementation for Archethic storage layer using File for transaction chain storages and index backup + Custom database implementation for Archethic storage layer using File for transaction chain storages and index backup while using a key value in memory for fast lookup """ @@ -218,9 +218,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 @@ -228,6 +235,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) """ @@ -253,7 +266,7 @@ defmodule Archethic.DB.EmbeddedImpl do as: :add_node_view @doc """ - Register a new node view from the last self-repair cycle + Register a new node view from the last self-repair cycle """ @spec get_last_p2p_summaries() :: %{ (node_public_key :: Crypto.key()) => { diff --git a/lib/archethic/db/embedded_impl/stats_info.ex b/lib/archethic/db/embedded_impl/stats_info.ex index 775fbc75ac..ac03f6fba8 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 cf89dcd0e1..12fc2a97da 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 8b65d050d9..31c65a8d2a 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__.RewardScheduler alias Archethic.TransactionChain.Transaction alias Archethic.TransactionChain.TransactionData @@ -28,6 +35,21 @@ defmodule Archethic.Reward do @doc """ Create a transaction for minting new rewards + + ## Examples + + iex> case Reward.new_rewards_mint(2_000_000_000) do + ...> %{ + ...> type: :mint_rewards, + ...> data: %{ + ...> content: "{\\n \\"supply\\":2000000000,\\n \\"type\\":\\"fungible\\",\\n \\"name\\":\\"Mining UCO rewards\\",\\n \\"symbol\\":\\"MUCO\\"\\n}\\n" + ...> } + ...> } -> + ...> :ok + ...> _ -> + ...> :error + ...> end + :ok """ @spec new_rewards_mint(amount :: non_neg_integer()) :: Transaction.t() def new_rewards_mint(amount) do @@ -45,6 +67,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? do + %Node{first_public_key: initiator_key} = + next_address() + |> Election.storage_nodes(P2P.authorized_and_available_nodes()) + |> List.first() + + 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 +99,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: RewardScheduler, as: :last_date def config_change(changed_conf) do changed_conf - |> Keyword.get(NetworkPoolScheduler) - |> NetworkPoolScheduler.config_change() + |> Keyword.get(RewardScheduler) + |> RewardScheduler.config_change() end end diff --git a/lib/archethic/reward/network_pool_scheduler.ex b/lib/archethic/reward/reward_scheduler.ex similarity index 61% rename from lib/archethic/reward/network_pool_scheduler.ex rename to lib/archethic/reward/reward_scheduler.ex index 08d2d9e492..7cbc7991da 100644 --- a/lib/archethic/reward/network_pool_scheduler.ex +++ b/lib/archethic/reward/reward_scheduler.ex @@ -1,4 +1,4 @@ -defmodule Archethic.Reward.NetworkPoolScheduler do +defmodule Archethic.Reward.RewardScheduler do @moduledoc false use GenServer @@ -8,18 +8,14 @@ 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 require Logger @@ -28,10 +24,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,6 +34,7 @@ defmodule Archethic.Reward.NetworkPoolScheduler do def init(args) do interval = Keyword.fetch!(args, :interval) + PubSub.register_to_node_update() {:ok, %{interval: interval}, :hibernate} end @@ -78,14 +71,20 @@ defmodule Archethic.Reward.NetworkPoolScheduler do 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() + if Reward.initiator?() do + case DB.get_latest_burned_fees() do + 0 -> + Logger.info("No mint rewards transaction needed") + + amount -> + Reward.new_rewards_mint(amount) + |> Archethic.send_new_transaction() + + Logger.info("New mint rewards transaction sent with #{amount} token") + end end {:noreply, Map.put(state, :timer, timer), :hibernate} @@ -122,46 +121,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 cfbe003f0e..f0c0918aba 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.RewardScheduler alias Archethic.Utils @@ -13,7 +13,7 @@ defmodule Archethic.Reward.Supervisor do def init(_) do children = [ - {NetworkPoolScheduler, Application.get_env(:archethic, NetworkPoolScheduler)} + {RewardScheduler, Application.get_env(:archethic, RewardScheduler)} ] Supervisor.init(Utils.configurable_children(children), strategy: :one_for_one) diff --git a/lib/archethic/self_repair/sync/beacon_summary_handler.ex b/lib/archethic/self_repair/sync/beacon_summary_handler.ex index 4fc32bc035..c7467206f4 100644 --- a/lib/archethic/self_repair/sync/beacon_summary_handler.ex +++ b/lib/archethic/self_repair/sync/beacon_summary_handler.ex @@ -23,6 +23,7 @@ defmodule Archethic.SelfRepair.Sync.BeaconSummaryHandler do alias Archethic.TaskSupervisor alias Archethic.TransactionChain + alias Archethic.TransactionChain.TransactionSummary alias Archethic.Utils @@ -193,7 +194,7 @@ defmodule Archethic.SelfRepair.Sync.BeaconSummaryHandler 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(BeaconSummaryAggregate.t(), binary()) :: :ok def process_summary_aggregate( @@ -229,7 +230,7 @@ defmodule Archethic.SelfRepair.Sync.BeaconSummaryHandler 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 @@ -298,9 +299,14 @@ defmodule Archethic.SelfRepair.Sync.BeaconSummaryHandler 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() @@ -309,12 +315,20 @@ defmodule Archethic.SelfRepair.Sync.BeaconSummaryHandler 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/db/embedded_impl_test.exs b/test/archethic/db/embedded_impl_test.exs index e2b2fec086..ce0b084250 100644 --- a/test/archethic/db/embedded_impl_test.exs +++ b/test/archethic/db/embedded_impl_test.exs @@ -565,21 +565,29 @@ 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, 15_000) 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, 15_000) 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, 15_000) 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, 15_000) assert 15_000 = EmbeddedImpl.get_nb_transactions() end + + test "should get the latest burned fees" 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 a30edd95f8..86bff1bc61 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.RewardScheduler + 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) + + RewardScheduler.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) + + RewardScheduler.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)) + + RewardScheduler.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/reward_scheduler_test.exs b/test/archethic/reward/reward_scheduler_test.exs new file mode 100644 index 0000000000..446631522e --- /dev/null +++ b/test/archethic/reward/reward_scheduler_test.exs @@ -0,0 +1,100 @@ +defmodule Archethic.Reward.RewardSchedulerTest do + use ArchethicCase, async: false + + alias Archethic.Crypto + + alias Archethic.P2P + alias Archethic.P2P.Node + alias Archethic.P2P.Message.StartMining + + alias Archethic.Reward.RewardScheduler + + 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} = RewardScheduler.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 -> 15000 end) + + me = self() + + MockClient + |> stub(:send_message, fn _, %StartMining{transaction: %{type: type}}, _ -> + send(me, type) + end) + + assert {:ok, pid} = RewardScheduler.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} = RewardScheduler.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 0000000000..eeea9c1b09 --- /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/beacon_summary_handler_test.exs b/test/archethic/self_repair/sync/beacon_summary_handler_test.exs index c1bc96fb03..8aa7fd0fe5 100644 --- a/test/archethic/self_repair/sync/beacon_summary_handler_test.exs +++ b/test/archethic/self_repair/sync/beacon_summary_handler_test.exs @@ -417,7 +417,7 @@ defmodule Archethic.SelfRepair.Sync.BeaconSummaryHandlerTest do end) MockDB - |> stub(:register_tps, fn _, _, _ -> + |> stub(:register_stats, fn _, _, _, _ -> :ok end) @@ -429,7 +429,8 @@ defmodule Archethic.SelfRepair.Sync.BeaconSummaryHandlerTest do %TransactionSummary{ address: tx_address, type: :transfer, - timestamp: DateTime.utc_now() + timestamp: DateTime.utc_now(), + fee: 0 } ] }, diff --git a/test/archethic/self_repair/sync_test.exs b/test/archethic/self_repair/sync_test.exs index 9557f6bb91..e6157360da 100644 --- a/test/archethic/self_repair/sync_test.exs +++ b/test/archethic/self_repair/sync_test.exs @@ -247,7 +247,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(