Skip to content

Commit

Permalink
AEIP-19 token recipients (#1214)
Browse files Browse the repository at this point in the history
* add recipients to token creation schema

* add recipients to resupply schema

* verify token recipients address & amount

* wip - inconsisentencies tx movment

* wip - error insufficient funds

* wip

* refactor consume_inputs

* lint: add ledgerops.tokens_to_mint

* unit test ledger operations

* unit test transaction.get_movements/1

* add more unit tests

* credo

* Fetch in parallel

* lint after feedback

* Fees take token recipients into account

* Lint + add token_id verification

---------

Co-authored-by: Neylix <julien.leclerc05@protonmail.com>
  • Loading branch information
bchamagne and Neylix committed Aug 22, 2023
1 parent 16a127f commit 75b7d5a
Show file tree
Hide file tree
Showing 17 changed files with 1,694 additions and 407 deletions.
11 changes: 8 additions & 3 deletions lib/archethic/bootstrap/network_init.ex
Original file line number Diff line number Diff line change
Expand Up @@ -185,10 +185,15 @@ defmodule Archethic.Bootstrap.NetworkInit do
operations =
%LedgerOperations{
fee: Mining.get_transaction_fee(tx, 0.07, timestamp),
transaction_movements: Transaction.get_movements(tx)
transaction_movements: Transaction.get_movements(tx),
tokens_to_mint: LedgerOperations.get_utxos_from_transaction(tx, timestamp)
}
|> LedgerOperations.from_transaction(tx, timestamp)
|> LedgerOperations.consume_inputs(tx.address, unspent_outputs, timestamp)
|> LedgerOperations.consume_inputs(
tx.address,
unspent_outputs,
timestamp
)
|> elem(1)

validation_stamp =
%ValidationStamp{
Expand Down
60 changes: 52 additions & 8 deletions lib/archethic/mining/fee.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ defmodule Archethic.Mining.Fee do
alias Archethic.TransactionChain.TransactionData.UCOLedger

@unit_uco 100_000_000
@token_creation_schema :archethic
|> Application.app_dir("priv/json-schemas/token-core.json")
|> File.read!()
|> Jason.decode!()
|> ExJsonSchema.Schema.resolve()

@token_resupply_schema :archethic
|> Application.app_dir("priv/json-schemas/token-resupply.json")
|> File.read!()
|> Jason.decode!()
|> ExJsonSchema.Schema.resolve()

@doc """
Determine the fee to paid for the given transaction
Expand Down Expand Up @@ -97,16 +108,22 @@ defmodule Archethic.Mining.Fee do
|> byte_size()
end

defp get_number_recipients(%Transaction{
data: %TransactionData{
ledger: %Ledger{
uco: %UCOLedger{transfers: uco_transfers},
token: %TokenLedger{transfers: token_transfers}
defp get_number_recipients(
tx = %Transaction{
data: %TransactionData{
ledger: %Ledger{
uco: %UCOLedger{transfers: uco_transfers},
token: %TokenLedger{transfers: token_transfers}
}
}
}
}) do
(uco_transfers ++ token_transfers)
|> Enum.uniq_by(& &1.to)
) do
uco_transfers_addresses = uco_transfers |> Enum.map(& &1.to)
token_transfers_addresses = token_transfers |> Enum.map(& &1.to)
token_recipients_addresses = get_token_recipients(tx) |> Enum.map(& &1["to"])

(uco_transfers_addresses ++ token_transfers_addresses ++ token_recipients_addresses)
|> Enum.uniq()
|> length()
end

Expand Down Expand Up @@ -134,4 +151,31 @@ defmodule Archethic.Mining.Fee do
defp cost_per_recipients(nb_recipients, uco_price_in_usd) do
nb_recipients * (0.1 / uco_price_in_usd)
end

defp get_token_recipients(%Transaction{
type: :token,
data: %TransactionData{content: content}
}) do
case Jason.decode(content) do
{:ok, json} ->
cond do
ExJsonSchema.Validator.valid?(@token_creation_schema, json) ->
get_token_recipients_from_json(json)

ExJsonSchema.Validator.valid?(@token_resupply_schema, json) ->
get_token_recipients_from_json(json)

true ->
[]
end

{:error, _} ->
[]
end
end

defp get_token_recipients(_tx), do: []

defp get_token_recipients_from_json(%{"recipients" => recipients}), do: recipients
defp get_token_recipients_from_json(_json), do: []
end
162 changes: 125 additions & 37 deletions lib/archethic/mining/pending_transaction_validation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ defmodule Archethic.Mining.PendingTransactionValidation do
alias Archethic.SharedSecrets
alias Archethic.SharedSecrets.NodeRenewal

alias Archethic.TaskSupervisor
alias Archethic.TransactionChain
alias Archethic.TransactionChain.Transaction
alias Archethic.TransactionChain.TransactionData
Expand Down Expand Up @@ -54,11 +55,11 @@ defmodule Archethic.Mining.PendingTransactionValidation do
|> Jason.decode!()
|> ExJsonSchema.Schema.resolve()

@token_schema :archethic
|> Application.app_dir("priv/json-schemas/token-core.json")
|> File.read!()
|> Jason.decode!()
|> ExJsonSchema.Schema.resolve()
@token_creation_schema :archethic
|> Application.app_dir("priv/json-schemas/token-core.json")
|> File.read!()
|> Jason.decode!()
|> ExJsonSchema.Schema.resolve()

@token_resupply_schema :archethic
|> Application.app_dir("priv/json-schemas/token-resupply.json")
Expand Down Expand Up @@ -768,22 +769,28 @@ defmodule Archethic.Mining.PendingTransactionValidation do
end

defp verify_token_transaction(tx = %Transaction{data: %TransactionData{content: content}}) do
case Jason.decode(content) do
{:error, _} ->
with {:ok, json_token} <- Jason.decode(content),
:ok <- verify_token_creation(tx, json_token) do
verify_token_recipients(json_token)
else
{:error, %Jason.DecodeError{}} ->
{:error, "Invalid token transaction - invalid JSON"}

{:ok, json_token} ->
case {ExJsonSchema.Validator.validate(@token_schema, json_token),
ExJsonSchema.Validator.validate(@token_resupply_schema, json_token)} do
{:ok, _} ->
verify_token_creation(json_token)
{:error, reason} ->
{:error, reason}
end
end

{_, :ok} ->
verify_token_resupply(tx, json_token)
defp verify_token_creation(tx, json_token) do
cond do
ExJsonSchema.Validator.valid?(@token_creation_schema, json_token) ->
verify_token_creation(json_token)

_ ->
{:error, "Invalid token transaction - neither a token creation nor a token resupply"}
end
ExJsonSchema.Validator.valid?(@token_resupply_schema, json_token) ->
verify_token_resupply(tx, json_token)

true ->
{:error, "Invalid token transaction - neither a token creation nor a token resupply"}
end
end

Expand Down Expand Up @@ -825,52 +832,133 @@ defmodule Archethic.Mining.PendingTransactionValidation do
end

defp verify_token_resupply(tx, %{"token_reference" => token_ref}) do
with {:ok, token_address} <- Base.decode16(token_ref, case: :mixed),
# verify same chain
{:ok, genesis_address} <- fetch_previous_tx_genesis_address(tx),
storage_nodes <-
Election.chain_storage_nodes(token_address, P2P.authorized_and_available_nodes()),
{:ok, ^genesis_address} <-
TransactionChain.fetch_genesis_address(token_address, storage_nodes),
# verify token_reference
{:ok,
%Transaction{
data: %TransactionData{
content: content
}
}} <- TransactionChain.fetch_transaction(token_address, storage_nodes),
# strict because there was a json schema validation before
token_address = Base.decode16!(token_ref, case: :mixed)

storage_nodes =
Election.chain_storage_nodes(token_address, P2P.authorized_and_available_nodes())

# fetch in parallel the data we need
tasks = [
Task.Supervisor.async_nolink(TaskSupervisor, fn ->
fetch_previous_tx_genesis_address(tx)
end),
Task.Supervisor.async_nolink(TaskSupervisor, fn ->
TransactionChain.fetch_genesis_address(token_address, storage_nodes)
end),
Task.Supervisor.async_nolink(TaskSupervisor, fn ->
TransactionChain.fetch_transaction(token_address, storage_nodes)
end)
]

# Shut down the tasks that did not reply nor exit
[tx_genesis_result, ref_genesis_result, ref_tx_result] =
Task.yield_many(tasks)
|> Enum.map(fn {task, res} ->
res || Task.shutdown(task, :brutal_kill)
end)

with {:ok, {:ok, genesis_address}} <- tx_genesis_result,
{:ok, {:ok, ^genesis_address}} <- ref_genesis_result,
{:ok, {:ok, %Transaction{data: %TransactionData{content: content}}}} <- ref_tx_result,
{:ok, reference_json_token} <- Jason.decode(content),
%{"type" => "fungible", "allow_mint" => true} <- reference_json_token do
:ok
else
nil ->
{:error, "Timeout when fetching the reference token or the genesis address"}

{:exit, _} ->
{:error, "Error when fetching the reference token or the genesis address"}

%{"type" => "non-fungible"} ->
{:error, "Invalid token transaction - token_reference must be fungible"}

%{"type" => "fungible"} ->
{:error, "Invalid token transaction - token_reference does not have allow_mint: true"}

{:ok, _} ->
{:ok, {:ok, _}} ->
{:error,
"Invalid token transaction - token_reference is not in the same transaction chain"}

{:error, :transaction_not_exists} ->
{:ok, {:error, :transaction_not_exists}} ->
{:error, "Invalid token transaction - token_reference not found"}

{:error, :invalid_transaction} ->
{:ok, {:error, :invalid_transaction}} ->
{:error, "Invalid token transaction - token_reference is invalid"}

{:error, :network_issue} ->
{:ok, {:error, :network_issue}} ->
{:error, "A network issue was raised, please retry later"}

{:error, %Jason.DecodeError{}} ->
{:error,
"Invalid token transaction - token_reference exists but does not contain a valid JSON"}
end
end

:error ->
{:error, "Invalid token transaction - token_reference is not an hexadecimal"}
defp verify_token_recipients(json_token = %{"recipients" => recipients, "supply" => supply})
when is_list(recipients) do
# resupply token transactions do not have a type, but is applied only to fungible tokens
fungible? = Map.get(json_token, "type", "fungible") == "fungible"

%{res: res} =
Enum.reduce_while(
recipients,
%{sum: 0, token_ids: MapSet.new(), res: :ok},
fn recipient = %{"amount" => amount}, acc = %{sum: sum, token_ids: token_ids} ->
with :ok <- validate_token_recipient_amount(amount, fungible?),
:ok <- validate_token_recipient_total(amount, sum, supply),
:ok <- validate_token_recipient_token_id(recipient, fungible?, token_ids) do
token_id = Map.get(recipient, "token_id", 0)

new_acc =
acc
|> Map.update!(:sum, &(&1 + amount))
|> Map.update!(:token_ids, &MapSet.put(&1, token_id))

{:cont, new_acc}
else
error -> {:halt, Map.put(acc, :res, error)}
end
end
)

res
end

defp verify_token_recipients(_), do: :ok

defp validate_token_recipient_amount(_, true), do: :ok
defp validate_token_recipient_amount(amount, false) when amount == @unit_uco, do: :ok

defp validate_token_recipient_amount(_, false),
do: {:error, "Invalid token transaction - invalid amount in recipients"}

defp validate_token_recipient_total(amount, sum, supply) when sum + amount <= supply, do: :ok

defp validate_token_recipient_total(_, _, _),
do: {:error, "Invalid token transaction - sum of recipients' amounts is bigger than supply"}

defp validate_token_recipient_token_id(%{"token_id" => _}, true, _),
do:
{:error,
"Invalid token transaction - recipient with token_id is now allowed on fungible token"}

defp validate_token_recipient_token_id(_recipient, true, _), do: :ok

defp validate_token_recipient_token_id(%{"token_id" => token_id}, false, token_ids) do
if MapSet.member?(token_ids, token_id) do
{:error,
"Invalid token transaction - recipient must have unique token_id for non fungible token"}
else
:ok
end
end

defp validate_token_recipient_token_id(_, false, _),
do:
{:error, "Invalid token transaction - recipient must have token_id for non fungible token"}

defp valid_collection_id?(collection) do
# If an id is specified in an item of the collection,
# all items must have a different specified id
Expand Down
Loading

0 comments on commit 75b7d5a

Please sign in to comment.