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

Consume the MUCO correctly #1593

Merged
merged 7 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/archethic/bootstrap/network_init.ex
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,9 @@ defmodule Archethic.Bootstrap.NetworkInit do
%LedgerValidation{fee: fee}
|> LedgerValidation.filter_usable_inputs(unspent_outputs, nil)
|> LedgerValidation.mint_token_utxos(tx, timestamp, 1)
|> LedgerValidation.build_resolved_movements(movements, resolved_addresses, tx_type)
|> LedgerValidation.validate_sufficient_funds()
|> LedgerValidation.validate_sufficient_funds(movements)
|> LedgerValidation.consume_inputs(address, timestamp)
|> LedgerValidation.build_resolved_movements(resolved_addresses, tx_type)
|> LedgerValidation.to_ledger_operations()

validation_stamp =
Expand Down
130 changes: 101 additions & 29 deletions lib/archethic/mining/ledger_validation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ defmodule Archethic.Mining.LedgerValidation do

@unit_uco 100_000_000

defstruct transaction_movements: [],
defstruct state: :init,
transaction_movements: [],
unspent_outputs: [],
fee: 0,
consumed_inputs: [],
Expand All @@ -31,13 +32,33 @@ defmodule Archethic.Mining.LedgerValidation do

alias Archethic.TransactionChain.TransactionData

@typedoc """
LedgerValidation should execute functions in a specific order.
To avoid miss order, state is updated to ensure order is respected
State is updated following
- :init
- :filtered_inputs
- :utxos_minted
- :sufficient_funds_validated
- :inputs_consumed
- :movements_resolved
"""
@type state() ::
:init
| :filtered_inputs
| :utxos_minted
| :sufficient_funds_validated
| :inputs_consumed
| :movements_resolved

@typedoc """
- Transaction movements: represents the pending transaction ledger movements
- Unspent outputs: represents the new unspent outputs
- fee: represents the transaction fee
- Consumed inputs: represents the list of inputs consumed to produce the unspent outputs
"""
@type t() :: %__MODULE__{
state: state(),
transaction_movements: list(TransactionMovement.t()),
unspent_outputs: list(UnspentOutput.t()),
fee: non_neg_integer(),
Expand All @@ -58,17 +79,20 @@ defmodule Archethic.Mining.LedgerValidation do
def burning_address, do: @burning_address

@doc """
Filter inputs that can be used in this transaction
Filter inputs that can be used in this transaction
"""
@spec filter_usable_inputs(
ops :: t(),
inputs :: list(VersionedUnspentOutput.t()),
contract_context :: ContractContext.t() | nil
) :: t()
def filter_usable_inputs(ops, inputs, nil), do: %__MODULE__{ops | inputs: inputs}
def filter_usable_inputs(ops = %__MODULE__{state: :init}, inputs, nil),
do: %__MODULE__{ops | inputs: inputs} |> next_state()

def filter_usable_inputs(ops, inputs, contract_context),
do: %__MODULE__{ops | inputs: ContractContext.ledger_inputs(contract_context, inputs)}
def filter_usable_inputs(ops = %__MODULE__{state: :init}, inputs, contract_context) do
%__MODULE__{ops | inputs: ContractContext.ledger_inputs(contract_context, inputs)}
|> next_state()
end

@doc """
Build some ledger operations from a specific transaction
Expand All @@ -80,27 +104,30 @@ defmodule Archethic.Mining.LedgerValidation do
protocol_version :: non_neg_integer()
) :: t()
def mint_token_utxos(
ops,
ops = %__MODULE__{state: :filtered_inputs},
%Transaction{address: address, type: type, data: %TransactionData{content: content}},
timestamp,
protocol_version
)
when type in [:token, :mint_rewards] and not is_nil(timestamp) do
case Jason.decode(content) do
{:ok, json} ->
minted_utxos =
json
|> create_token_utxos(address, timestamp)
|> VersionedUnspentOutput.wrap_unspent_outputs(protocol_version)

%__MODULE__{ops | minted_utxos: minted_utxos}
new_ops =
case Jason.decode(content) do
{:ok, json} ->
minted_utxos =
json
|> create_token_utxos(address, timestamp)
|> VersionedUnspentOutput.wrap_unspent_outputs(protocol_version)

%__MODULE__{ops | minted_utxos: minted_utxos}

_ ->
ops
end

_ ->
ops
end
next_state(new_ops)
end

def mint_token_utxos(ops, _, _, _), do: ops
def mint_token_utxos(ops = %__MODULE__{state: :filtered_inputs}, _, _, _), do: next_state(ops)

defp create_token_utxos(
%{"token_reference" => token_ref, "supply" => supply},
Expand Down Expand Up @@ -186,34 +213,43 @@ defmodule Archethic.Mining.LedgerValidation do
@doc """
Build the resolved view of the movement, with the resolved address
and convert MUCO movement to UCO movement

**MUST** be done after sufficient_funds? and consume_inputs
"""
@spec build_resolved_movements(
ops :: t(),
movements :: list(TransactionMovement.t()),
resolved_addresses :: %{Crypto.prepended_hash() => Crypto.prepended_hash()},
tx_type :: Transaction.transaction_type()
) :: t()
def build_resolved_movements(ops, movements, resolved_addresses, tx_type) do
def build_resolved_movements(
ops = %__MODULE__{
state: :inputs_consumed,
transaction_movements: movements
},
resolved_addresses,
tx_type
) do
resolved_movements =
movements
|> TransactionMovement.resolve_addresses(resolved_addresses)
|> Enum.map(&TransactionMovement.maybe_convert_reward(&1, tx_type))
|> TransactionMovement.aggregate()

%__MODULE__{ops | transaction_movements: resolved_movements}
%__MODULE__{ops | transaction_movements: resolved_movements} |> next_state()
end

@doc """
Determine if the transaction has enough funds for it's movements
"""
@spec validate_sufficient_funds(ops :: t()) :: t()
@spec validate_sufficient_funds(ops :: t(), list(TransactionMovement.t())) :: t()
def validate_sufficient_funds(
ops = %__MODULE__{
state: :utxos_minted,
fee: fee,
inputs: inputs,
minted_utxos: minted_utxos,
transaction_movements: movements
}
minted_utxos: minted_utxos
},
movements
) do
balances =
%{uco: uco_balance, token: tokens_balance} = ledger_balances(inputs ++ minted_utxos)
Expand All @@ -226,8 +262,10 @@ defmodule Archethic.Mining.LedgerValidation do
| sufficient_funds?:
sufficient_funds?(uco_balance, uco_to_spend, tokens_balance, tokens_to_spend),
balances: balances,
amount_to_spend: amount_to_spend
amount_to_spend: amount_to_spend,
transaction_movements: movements
}
|> next_state()
end

defp total_to_spend(fee, movements) do
Expand Down Expand Up @@ -287,6 +325,7 @@ defmodule Archethic.Mining.LedgerValidation do
"""
@spec to_ledger_operations(ops :: t()) :: LedgerOperations.t()
def to_ledger_operations(%__MODULE__{
state: :movements_resolved,
transaction_movements: movements,
unspent_outputs: utxos,
fee: fee,
Expand All @@ -312,19 +351,35 @@ defmodule Archethic.Mining.LedgerValidation do
encoded_state :: State.encoded() | nil,
contract_context :: ContractContext.t() | nil
) :: t()
def consumed_inputs(ops = %__MODULE__{sufficient_funds?: false}), do: ops
def consume_inputs(
ops,
change_address,
timestamp,
encoded_state \\ nil,
contract_context \\ nil
)

def consume_inputs(
ops = %__MODULE__{state: :sufficient_funds_validated, sufficient_funds?: false},
_,
_,
_,
_
),
do: next_state(ops)

def consume_inputs(
ops = %__MODULE__{
state: :sufficient_funds_validated,
inputs: inputs,
minted_utxos: minted_utxos,
balances: %{uco: uco_balance, token: tokens_balance},
amount_to_spend: %{uco: uco_to_spend, token: tokens_to_spend}
},
change_address,
timestamp = %DateTime{},
encoded_state \\ nil,
contract_context \\ nil
encoded_state,
contract_context
) do
# Since AEIP-19 we can consume from minted tokens
# Sort inputs, to have consistent results across all nodes
Expand Down Expand Up @@ -369,6 +424,7 @@ defmodule Archethic.Mining.LedgerValidation do
| unspent_outputs: new_unspent_outputs,
consumed_inputs: versioned_consumed_utxos
}
|> next_state()
end

defp tokens_utxos(
Expand Down Expand Up @@ -511,4 +567,20 @@ defmodule Archethic.Mining.LedgerValidation do

[new_utxo | utxos]
end

defp next_state(ops = %__MODULE__{state: :init}), do: %__MODULE__{ops | state: :filtered_inputs}

defp next_state(ops = %__MODULE__{state: :filtered_inputs}),
do: %__MODULE__{ops | state: :utxos_minted}

defp next_state(ops = %__MODULE__{state: :utxos_minted}),
do: %__MODULE__{ops | state: :sufficient_funds_validated}

defp next_state(ops = %__MODULE__{state: :sufficient_funds_validated}),
do: %__MODULE__{ops | state: :inputs_consumed}

defp next_state(ops = %__MODULE__{state: :inputs_consumed}),
do: %__MODULE__{ops | state: :movements_resolved}

defp next_state(ops = %__MODULE__{state: :movements_resolved}), do: ops
end
28 changes: 8 additions & 20 deletions lib/archethic/mining/validation_context.ex
Original file line number Diff line number Diff line change
Expand Up @@ -803,14 +803,14 @@ defmodule Archethic.Mining.ValidationContext do
%LedgerValidation{fee: fee}
|> LedgerValidation.filter_usable_inputs(unspent_outputs, contract_context)
|> LedgerValidation.mint_token_utxos(tx, validation_time, protocol_version)
|> LedgerValidation.build_resolved_movements(movements, resolved_addresses, tx_type)
|> LedgerValidation.validate_sufficient_funds()
|> LedgerValidation.validate_sufficient_funds(movements)
|> LedgerValidation.consume_inputs(
address,
validation_time,
encoded_state,
contract_context
)
|> LedgerValidation.build_resolved_movements(resolved_addresses, tx_type)

case ops do
%LedgerValidation{sufficient_funds?: false} ->
Expand Down Expand Up @@ -1139,7 +1139,9 @@ defmodule Archethic.Mining.ValidationContext do
proof_of_integrity: fn -> valid_stamp_proof_of_integrity?(stamp, context) end,
proof_of_election: fn -> valid_stamp_proof_of_election?(stamp, context) end,
transaction_fee: fn -> valid_stamp_fee?(stamp, fee) end,
transaction_movements: fn -> valid_stamp_transaction_movements?(stamp, context) end,
transaction_movements: fn ->
valid_stamp_transaction_movements?(stamp, ledger_operations)
end,
recipients: fn -> valid_stamp_recipients?(stamp, context) end,
consumed_inputs: fn -> valid_consumed_inputs?(stamp, ledger_operations) end,
unspent_outputs: fn -> valid_stamp_unspent_outputs?(stamp, ledger_operations) end,
Expand Down Expand Up @@ -1232,25 +1234,11 @@ defmodule Archethic.Mining.ValidationContext do

defp valid_stamp_transaction_movements?(
%ValidationStamp{
ledger_operations:
_ops = %LedgerOperations{
transaction_movements: transaction_movements
}
ledger_operations: %LedgerOperations{transaction_movements: stamp_movements}
},
%__MODULE__{
transaction: tx = %Transaction{type: tx_type},
resolved_addresses: resolved_addresses
}
%LedgerOperations{transaction_movements: expected_movements}
) do
movements = Transaction.get_movements(tx)

%LedgerOperations{transaction_movements: resolved_movements} =
%LedgerValidation{}
|> LedgerValidation.build_resolved_movements(movements, resolved_addresses, tx_type)
|> LedgerValidation.to_ledger_operations()

length(resolved_movements) == length(transaction_movements) and
Enum.all?(resolved_movements, &(&1 in transaction_movements))
expected_movements |> MapSet.new() |> MapSet.equal?(MapSet.new(stamp_movements))
end

defp valid_consumed_inputs?(
Expand Down
8 changes: 3 additions & 5 deletions lib/archethic/p2p/message/validate_smart_contract_call.ex
Original file line number Diff line number Diff line change
Expand Up @@ -270,20 +270,18 @@ defmodule Archethic.P2P.Message.ValidateSmartContractCall do
defp calculate_fee(_, _), do: 0

defp enough_funds_to_send?(
%ActionWithTransaction{next_tx: tx = %Transaction{type: tx_type}},
%ActionWithTransaction{next_tx: tx},
inputs,
timestamp
) do
movements = Transaction.get_movements(tx)
protocol_version = Mining.protocol_version()
resolved_addresses = Enum.map(movements, &{&1.to, &1.to}) |> Map.new()

%LedgerValidation{sufficient_funds?: sufficient_funds?} =
%LedgerValidation{fee: 0}
%LedgerValidation{}
|> LedgerValidation.filter_usable_inputs(inputs, nil)
|> LedgerValidation.mint_token_utxos(tx, timestamp, protocol_version)
|> LedgerValidation.build_resolved_movements(movements, resolved_addresses, tx_type)
|> LedgerValidation.validate_sufficient_funds()
|> LedgerValidation.validate_sufficient_funds(movements)

sufficient_funds?
end
Expand Down
4 changes: 2 additions & 2 deletions test/archethic/mining/distributed_workflow_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1360,9 +1360,9 @@ defmodule Archethic.Mining.DistributedWorkflowTest do
%LedgerValidation{fee: fee}
|> LedgerValidation.filter_usable_inputs(unspent_outputs, contract_context)
|> LedgerValidation.mint_token_utxos(tx, timestamp, protocol_version)
|> LedgerValidation.build_resolved_movements(movements, resolved_addresses, tx.type)
|> LedgerValidation.validate_sufficient_funds()
|> LedgerValidation.validate_sufficient_funds(movements)
|> LedgerValidation.consume_inputs(tx.address, timestamp, encoded_state, contract_context)
|> LedgerValidation.build_resolved_movements(resolved_addresses, tx.type)
|> LedgerValidation.to_ledger_operations()

%ValidationStamp{
Expand Down
Loading
Loading