Skip to content

Commit

Permalink
Mint mining rewards token on self repair (#402)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Neylix committed Jul 26, 2022
1 parent b6648b0 commit aaeeebd
Show file tree
Hide file tree
Showing 18 changed files with 460 additions and 131 deletions.
2 changes: 1 addition & 1 deletion config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 1 addition & 5 deletions config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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 * * * * * *"
Expand Down
6 changes: 3 additions & 3 deletions config/prod.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
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.Scheduler, 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
17 changes: 15 additions & 2 deletions lib/archethic/db/embedded_impl.ex
Original file line number Diff line number Diff line change
Expand Up @@ -253,16 +253,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 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
43 changes: 39 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__.Scheduler

alias Archethic.TransactionChain.Transaction
alias Archethic.TransactionChain.TransactionData
Expand All @@ -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
Expand All @@ -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
"""
Expand All @@ -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
Loading

0 comments on commit aaeeebd

Please sign in to comment.