Skip to content

Commit

Permalink
Mint mining reward token on self repair
Browse files Browse the repository at this point in the history
  • Loading branch information
Neylix committed Jun 24, 2022
2 parents 3a2c9a5 + 98d2507 commit 0e6d42d
Show file tree
Hide file tree
Showing 17 changed files with 426 additions and 130 deletions.
6 changes: 1 addition & 5 deletions config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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 * * * * * *"
Expand Down
6 changes: 3 additions & 3 deletions config/prod.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 1 addition & 2 deletions config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion lib/archethic/db.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
21 changes: 17 additions & 4 deletions lib/archethic/db/embedded_impl.ex
Original file line number Diff line number Diff line change
@@ -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
"""

Expand Down Expand Up @@ -218,16 +218,29 @@ 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
"""
@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)
"""
Expand All @@ -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()) => {
Expand Down
50 changes: 34 additions & 16 deletions lib/archethic/db/embedded_impl/stats_info.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -60,31 +69,32 @@ 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
{:noreply, state}
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
Expand All @@ -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, <<DateTime.to_unix(date)::32, tps::float-64, nb_transactions::32>>)
defp append_to_file(fd, date, tps, nb_transactions, burned_fees) do
IO.binwrite(
fd,
<<DateTime.to_unix(date)::32, tps::float-64, nb_transactions::32, burned_fees::32>>
)
end
end
82 changes: 58 additions & 24 deletions lib/archethic/mining/pending_transaction_validation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ defmodule Archethic.Mining.PendingTransactionValidation do

alias Archethic.Crypto

alias Archethic.DB

alias Archethic.SharedSecrets

alias Archethic.Election

alias Archethic.Governance
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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__, [])
Expand Down
49 changes: 45 additions & 4 deletions lib/archethic/reward.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
"""
Expand All @@ -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
Loading

0 comments on commit 0e6d42d

Please sign in to comment.