Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AEIP-19 token recipients #1214

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
161 changes: 124 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,49 +832,129 @@ 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
results =
[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 0 <- Enum.count(results, &(&1 == nil)),
bchamagne marked this conversation as resolved.
Show resolved Hide resolved
{: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
i when is_integer(i) ->
{: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) do
case json_token["recipients"] || [] do
bchamagne marked this conversation as resolved.
Show resolved Hide resolved
[] ->
:ok

recipients ->
with :ok <- validate_token_recipients_amounts(recipients, json_token),
:ok <- validate_token_recipients_total(recipients, json_token) do
validate_token_recipients_addresses(recipients)
end
end
end

defp validate_token_recipients_addresses(recipients) do
valid? =
recipients
|> Enum.map(& &1["to"])
bchamagne marked this conversation as resolved.
Show resolved Hide resolved
|> Enum.map(&Base.decode16!(&1, case: :mixed))
|> Enum.all?(&Crypto.valid_address?/1)

if valid? do
:ok
else
{:error, "Invalid token transaction - invalid recipients addresses"}
end
end

defp validate_token_recipients_total(recipients, json_token) do
total =
recipients
|> Enum.map(& &1["amount"])
bchamagne marked this conversation as resolved.
Show resolved Hide resolved
|> Enum.sum()

if total <= json_token["supply"] do
bchamagne marked this conversation as resolved.
Show resolved Hide resolved
:ok
else
{:error, "Invalid token transaction - sum of recipients' amounts is bigger than supply"}
end
end

defp validate_token_recipients_amounts(recipients, json_token) do
# resupply token transactions do not have a type, but is applied only to fungible tokens
fungible? = (json_token["type"] || "fungible") == "fungible"

valid? =
Enum.all?(recipients, fn %{"amount" => amount} ->
if fungible? do
amount > 0
else
amount == 1 * @unit_uco
end
end)

if valid? do
:ok
else
{:error, "Invalid token transaction - invalid amount in recipients"}
end
end

Expand Down
45 changes: 21 additions & 24 deletions lib/archethic/mining/validation_context.ex
Original file line number Diff line number Diff line change
Expand Up @@ -719,14 +719,13 @@ defmodule Archethic.Mining.ValidationContext do
context = %__MODULE__{
transaction: tx = %Transaction{data: %TransactionData{recipients: recipients}},
previous_transaction: prev_tx,
unspent_outputs: unspent_outputs,
valid_pending_transaction?: valid_pending_transaction?,
validation_time: validation_time,
resolved_addresses: resolved_addresses,
contract_context: contract_context
}
) do
ledger_operations = get_ledger_operations(context)
{sufficient_funds?, ledger_operations} = get_ledger_operations(context)

validation_stamp =
%ValidationStamp{
Expand All @@ -741,8 +740,7 @@ defmodule Archethic.Mining.ValidationContext do
get_validation_error(
prev_tx,
tx,
ledger_operations,
unspent_outputs,
sufficient_funds?,
valid_pending_transaction?,
resolved_addresses,
validation_time,
Expand Down Expand Up @@ -796,9 +794,9 @@ defmodule Archethic.Mining.ValidationContext do

%LedgerOperations{
fee: fee,
transaction_movements: resolved_movements
transaction_movements: resolved_movements,
tokens_to_mint: LedgerOperations.get_utxos_from_transaction(tx, validation_time)
}
|> LedgerOperations.from_transaction(tx, validation_time)
|> LedgerOperations.consume_inputs(
tx.address,
unspent_outputs,
Expand All @@ -809,18 +807,16 @@ defmodule Archethic.Mining.ValidationContext do
@spec get_validation_error(
previous_transaction :: nil | Transaction.t(),
pending_transaction :: Transaction.t(),
ledger_operations :: LedgerOperations.t(),
unspent_outputs :: list(UnspentOutput.t()),
valid_pending_transaction :: boolean(),
sufficient_funds? :: boolean(),
valid_pending_transaction? :: boolean(),
resolved_addresses :: list({binary(), binary()}),
validation_time :: DateTime.t(),
contract_context :: nil | Contract.Context.t()
) :: nil | ValidationStamp.error()
defp get_validation_error(
prev_tx,
tx,
ledger_operations,
unspent_outputs,
sufficient_funds?,
valid_pending_transaction?,
resolved_addresses,
validation_time,
Expand All @@ -830,6 +826,9 @@ defmodule Archethic.Mining.ValidationContext do
not valid_pending_transaction? ->
:invalid_pending_transaction

not sufficient_funds? ->
:insufficient_funds

not valid_inherit_condition?(prev_tx, tx, validation_time) ->
:invalid_inherit_constraints

Expand All @@ -839,9 +838,6 @@ defmodule Archethic.Mining.ValidationContext do
not valid_contract_recipients?(tx, resolved_addresses, validation_time) ->
:invalid_recipients_execution

has_insufficient_funds?(ledger_operations, unspent_outputs) ->
:insufficient_funds

true ->
nil
end
Expand All @@ -863,10 +859,6 @@ defmodule Archethic.Mining.ValidationContext do
|> SmartContractValidation.valid_contract_calls?(tx, validation_time)
end

defp has_insufficient_funds?(ledger_ops, inputs) do
not LedgerOperations.sufficient_funds?(ledger_ops, inputs)
end

####################
defp valid_inherit_condition?(
prev_tx = %Transaction{data: %TransactionData{code: code}},
Expand Down Expand Up @@ -1152,20 +1144,20 @@ defmodule Archethic.Mining.ValidationContext do
transaction: tx,
previous_transaction: prev_tx,
valid_pending_transaction?: valid_pending_transaction?,
unspent_outputs: unspent_outputs,
resolved_addresses: resolved_addresses,
validation_time: validation_time,
contract_context: contract_context
}
) do
validated_context = %{context | transaction: %{tx | validation_stamp: stamp}}

{sufficient_funds?, _} = get_ledger_operations(validated_context)

expected_error =
get_validation_error(
prev_tx,
tx,
get_ledger_operations(validated_context),
unspent_outputs,
sufficient_funds?,
valid_pending_transaction?,
resolved_addresses,
validation_time,
Expand Down Expand Up @@ -1231,10 +1223,15 @@ defmodule Archethic.Mining.ValidationContext do
%LedgerOperations{unspent_outputs: expected_unspent_outputs} =
%LedgerOperations{
fee: fee,
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, previous_unspent_outputs, timestamp)
|> LedgerOperations.consume_inputs(
tx.address,
previous_unspent_outputs,
timestamp
)
|> elem(1)

expected_unspent_outputs == next_unspent_outputs
end
Expand Down
Loading