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

Filter inputs from calls in contract execution #1472

Merged
merged 3 commits into from
Mar 22, 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
82 changes: 81 additions & 1 deletion lib/archethic/contracts/contract/context.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ defmodule Archethic.Contracts.Contract.Context do
alias Archethic.TransactionChain.Transaction
alias Archethic.TransactionChain.TransactionData.Recipient

alias Archethic.TransactionChain.Transaction.ValidationStamp.LedgerOperations.UnspentOutput

alias Archethic.TransactionChain.Transaction.ValidationStamp.LedgerOperations.VersionedUnspentOutput

@enforce_keys [:status, :trigger, :timestamp]
Expand Down Expand Up @@ -229,12 +231,90 @@ defmodule Archethic.Contracts.Contract.Context do
"""
@spec valid_inputs?(t() | nil, list(VersionedUnspentOutput.t())) :: boolean()
def valid_inputs?(%__MODULE__{inputs: inputs = [_ | _]}, unspent_outputs = [_ | _]) do
filtered_unspent_outputs = filter_inputs(unspent_outputs)

Enum.all?(inputs, fn input ->
Enum.any?(unspent_outputs, &(&1 == input))
Enum.any?(filtered_unspent_outputs, &(&1 == input))
end)
end

def valid_inputs?(%__MODULE__{inputs: []}, _unspent_outputs = [_ | _]), do: false
def valid_inputs?(%__MODULE__{inputs: [_ | _]}, _unspent_outputs = []), do: false
def valid_inputs?(nil, _unspent_outputs), do: true

@doc """
Filter the contract's input to reject inputs related to the calls

## Examples

iex> [
...> %VersionedUnspentOutput{ unspent_output: %UnspentOutput{from: "@Charlie2", type: :state}},
...> %VersionedUnspentOutput{ unspent_output: %UnspentOutput{from: "@Bob3", type: :call}},
...> %VersionedUnspentOutput{ unspent_output: %UnspentOutput{from: "@Charlie2", type: :call}},
...> %VersionedUnspentOutput{ unspent_output: %UnspentOutput{from: "@Bob3", type: :UCO, amount: 100_000_000}},
...> %VersionedUnspentOutput{ unspent_output: %UnspentOutput{from: "@Tom5", type: :UCO, amount: 200_000_000}}
...> ]
...> |> Context.filter_inputs()
[
%VersionedUnspentOutput{ unspent_output: %UnspentOutput{from: "@Tom5", type: :UCO, amount: 200_000_000 }}
]
"""
@spec filter_inputs(list(VersionedUnspentOutput.t())) :: list(VersionedUnspentOutput.t())
def filter_inputs(inputs) do
calls =
Enum.reduce(inputs, MapSet.new(), fn
%VersionedUnspentOutput{
unspent_output: %UnspentOutput{type: :call, from: from}
},
acc ->
MapSet.put(acc, from)

_, acc ->
acc
end)

Enum.reject(inputs, &MapSet.member?(calls, &1.unspent_output.from))
end

@doc """
Returns the list of ledger inputs used in validation including the UTXOs sent by the trigger

## Examples

iex> utxos = [
...> %VersionedUnspentOutput{ unspent_output: %UnspentOutput{from: "@Bob3", type: :call}},
...> %VersionedUnspentOutput{ unspent_output: %UnspentOutput{from: "@Charlie2", type: :call}},
...> %VersionedUnspentOutput{ unspent_output: %UnspentOutput{from: "@Bob3", type: :UCO, amount: 100_000_000}},
...> %VersionedUnspentOutput{ unspent_output: %UnspentOutput{from: "@Tom5", type: :UCO, amount: 200_000_000}},
...> %VersionedUnspentOutput{ unspent_output: %UnspentOutput{from: "@Alice5", type: :call}},
...> %VersionedUnspentOutput{ unspent_output: %UnspentOutput{from: "@Alice5", type: :call}},
...> ]
iex> %Context{
...> inputs: Context.filter_inputs(utxos),
...> trigger: {:transaction, "@Bob3", nil},
...> status: :ok,
...> timestamp: DateTime.utc_now()
...> }
...> |> Context.ledger_inputs(utxos)
[
%VersionedUnspentOutput{ unspent_output: %UnspentOutput{from: "@Bob3", type: :UCO, amount: 100_000_000}},
%VersionedUnspentOutput{ unspent_output: %UnspentOutput{from: "@Bob3", type: :call}},
%VersionedUnspentOutput{ unspent_output: %UnspentOutput{from: "@Tom5", type: :UCO, amount: 200_000_000}}
]
"""
@spec ledger_inputs(t(), list(VersionedUnspentOutput.t())) :: list(VersionedUnspentOutput.t())
def ledger_inputs(
%__MODULE__{trigger: {:transaction, address, _}, inputs: inputs},
chain_inputs
) do
Enum.reduce(chain_inputs, inputs, fn input, acc ->
if input.unspent_output.from == address do
[input | acc]
else
acc
end
end)
end

def ledger_inputs(%__MODULE__{inputs: inputs}, _inputs), do: inputs
end
1 change: 1 addition & 0 deletions lib/archethic/contracts/worker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,7 @@ defmodule Archethic.Contracts.Worker do

address
|> TransactionChain.fetch_unspent_outputs(nodes)
|> Contract.Context.filter_inputs()
|> Enum.to_list()
end
end
28 changes: 20 additions & 8 deletions lib/archethic/mining/smart_contract_validation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -98,21 +98,32 @@ defmodule Archethic.Mining.SmartContractValidation do
contract_context :: Contract.Context.t(),
prev_tx :: Transaction.t(),
genesis_address :: Crypto.prepended_hash(),
next_tx :: Transaction.t()
next_tx :: Transaction.t(),
chain_unspent_outputs :: list(VersionedUnspentOutput.t())
) :: {boolean(), State.encoded() | nil}
def valid_contract_execution?(
%Contract.Context{status: status, trigger: trigger, timestamp: timestamp, inputs: inputs},
%Contract.Context{
status: status,
trigger: trigger,
timestamp: timestamp,
inputs: contract_inputs
},
prev_tx,
genesis_address,
next_tx
next_tx,
chain_unspent_outputs
) do
trigger_type = trigger_to_trigger_type(trigger)
recipient = trigger_to_recipient(trigger)
opts = trigger_to_execute_opts(trigger)
inputs = VersionedUnspentOutput.unwrap_unspent_outputs(inputs)

with {:ok, maybe_trigger_tx} <-
validate_trigger(trigger, timestamp, genesis_address, inputs),
validate_trigger(
trigger,
timestamp,
genesis_address,
VersionedUnspentOutput.unwrap_unspent_outputs(chain_unspent_outputs)
),
{:ok, contract} <-
Contract.from_transaction(prev_tx),
{:ok, res} <-
Expand All @@ -121,7 +132,7 @@ defmodule Archethic.Mining.SmartContractValidation do
contract,
maybe_trigger_tx,
recipient,
inputs,
VersionedUnspentOutput.unwrap_unspent_outputs(contract_inputs),
opts
),
{:ok, encoded_state} <-
Expand All @@ -136,7 +147,8 @@ defmodule Archethic.Mining.SmartContractValidation do
_contract_context = nil,
prev_tx = %Transaction{data: %TransactionData{code: code}},
_genesis_address,
_next_tx = %Transaction{}
_next_tx = %Transaction{},
_chain_unspent_outputs
)
when code != "" do
# only contract without triggers (with only conditions) are allowed to NOT have a Contract.Context
Expand All @@ -145,7 +157,7 @@ defmodule Archethic.Mining.SmartContractValidation do
else: {true, nil}
end

def valid_contract_execution?(_, _, _, _), do: {true, nil}
def valid_contract_execution?(_, _, _, _, _), do: {true, nil}

defp validate_result(
%ActionWithTransaction{next_tx: expected_next_tx, encoded_state: encoded_state},
Expand Down
16 changes: 9 additions & 7 deletions lib/archethic/mining/validation_context.ex
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,8 @@ defmodule Archethic.Mining.ValidationContext do
contract_context,
prev_tx,
genesis_address,
tx
tx,
unspent_outputs
)

resolved_recipients = resolved_recipients(recipients, resolved_addresses)
Expand All @@ -687,7 +688,7 @@ defmodule Archethic.Mining.ValidationContext do
)

{sufficient_funds?, ledger_operations} =
get_ledger_operations(context, fee, unspent_outputs, validation_time, encoded_state)
get_ledger_operations(context, fee, validation_time, encoded_state)

validation_stamp =
%ValidationStamp{
Expand Down Expand Up @@ -756,10 +757,10 @@ defmodule Archethic.Mining.ValidationContext do
%__MODULE__{
transaction: tx = %Transaction{address: address, type: tx_type},
resolved_addresses: resolved_addresses,
contract_context: contract_context
contract_context: contract_context,
aggregated_utxos: unspent_outputs
},
fee,
inputs,
validation_time,
encoded_state
) do
Expand All @@ -771,7 +772,7 @@ defmodule Archethic.Mining.ValidationContext do
%LedgerOperations{fee: fee},
address,
validation_time |> DateTime.truncate(:millisecond),
inputs,
unspent_outputs,
movements,
LedgerOperations.get_utxos_from_transaction(tx, validation_time, protocol_version),
encoded_state,
Expand Down Expand Up @@ -1067,7 +1068,8 @@ defmodule Archethic.Mining.ValidationContext do
contract_context,
prev_tx,
genesis_address,
tx
tx,
aggregated_utxos
)

{valid_contract_recipients?, contract_recipients_fee} =
Expand All @@ -1083,7 +1085,7 @@ defmodule Archethic.Mining.ValidationContext do
)

{sufficient_funds?, ledger_operations} =
get_ledger_operations(context, stamp_fee, aggregated_utxos, validation_time, next_state)
get_ledger_operations(context, stamp_fee, validation_time, next_state)

subsets_verifications = [
aggregated_utxos: fn -> valid_aggregated_utxo?(aggregated_utxos, context) end,
Expand Down
3 changes: 2 additions & 1 deletion lib/archethic/replication/transaction_validator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@ defmodule Archethic.Replication.TransactionValidator do
contract_context,
prev_tx,
genesis_address,
tx
tx,
inputs
) do
{true, encoded_state} -> {:ok, encoded_state}
_ -> {:error, :invalid_contract_execution}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,16 +232,25 @@ defmodule Archethic.TransactionChain.Transaction.ValidationStamp.LedgerOperation
ops = %__MODULE__{fee: fee},
change_address,
timestamp = %DateTime{},
inputs \\ [],
chain_inputs \\ [],
movements \\ [],
tokens_to_mint \\ [],
encoded_state \\ nil,
contract_context \\ nil
) do
ledger_inputs =
case contract_context do
nil ->
chain_inputs

context = %ContractContext{} ->
ContractContext.ledger_inputs(context, chain_inputs)
end

# Since AEIP-19 we can consume from minted tokens
# Sort inputs, to have consistent results across all nodes
consolidated_inputs =
Enum.sort(tokens_to_mint ++ inputs, {:asc, VersionedUnspentOutput})
Enum.sort(tokens_to_mint ++ ledger_inputs, {:asc, VersionedUnspentOutput})
|> Enum.map(fn
utxo = %VersionedUnspentOutput{unspent_output: %UnspentOutput{from: ^change_address}} ->
# As the minted tokens are used internally during transaction's validation
Expand All @@ -254,6 +263,7 @@ defmodule Archethic.TransactionChain.Transaction.ValidationStamp.LedgerOperation
end)

%{uco: uco_balance, token: tokens_balance} = ledger_balances(consolidated_inputs)

%{uco: uco_to_spend, token: tokens_to_spend} = total_to_spend(fee, movements)

if sufficient_funds?(uco_balance, uco_to_spend, tokens_balance, tokens_to_spend) do
Expand Down Expand Up @@ -373,10 +383,12 @@ defmodule Archethic.TransactionChain.Transaction.ValidationStamp.LedgerOperation
defp get_token_to_consume(_, _, _, _), do: []

defp get_call_to_consume(inputs, %ContractContext{trigger: {:transaction, address, _}}) do
case Enum.find(inputs, &(&1.unspent_output.from == address)) do
inputs
|> Enum.find(&(&1.unspent_output.from == address))
|> then(fn
nil -> []
contract_call_input -> [contract_call_input]
end
end)
end

defp get_call_to_consume(_, _), do: []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ defmodule Archethic.Utils.Regression.Playbook.SmartContract.DeterministicBalance
def play(storage_nonce_pubkey, endpoint) do
contract_seed = SmartContract.random_seed()

nb_transactions = 1
nb_transactions = 100
triggers_seeds = Enum.map(1..nb_transactions, fn _ -> SmartContract.random_seed() end)

initial_funds =
Expand Down Expand Up @@ -43,21 +43,27 @@ defmodule Archethic.Utils.Regression.Playbook.SmartContract.DeterministicBalance
Enum.map(1..nb_transactions, fn i ->
Task.async(fn ->
SmartContract.trigger(Enum.at(triggers_seeds, i - 1), contract_address, endpoint,
await_timeout: 60_000
# uco_transfers: [%{to: contract_address, amount: Archethic.Utils.to_bigint(10)}]
await_timeout: 60_000,
uco_transfers: [%{to: contract_address, amount: Archethic.Utils.to_bigint(10)}]
)
end)
end)
|> Task.await_many(:infinity)

await_no_more_calls(genesis_address, endpoint)
balance = Api.get_uco_balance(contract_address, endpoint) |> Archethic.Utils.from_bigint()

if balance < 505 - 5 * nb_transactions do
Logger.info("Smart contract 'deterministic balance' has been decremented successfully")
%{"data" => %{"content" => logged_balance}} =
Api.get_last_transaction(contract_address, endpoint)

logged_balance = logged_balance |> String.to_float() |> Float.ceil()

expected_balance = 505.0 - 5 + (nb_transactions - 1) * (10 - 5)

if logged_balance == expected_balance do
Logger.info("Smart contract 'deterministic balance' has been updated successfully")
else
Logger.error(
"Smart contract 'deterministic balance' has not been decremented successfully: #{balance}"
"Smart contract 'deterministic balance' has not been updated successfully: #{logged_balance} - expected #{expected_balance}"
)
end
end
Expand All @@ -76,7 +82,7 @@ defmodule Archethic.Utils.Regression.Playbook.SmartContract.DeterministicBalance
calls ->
Logger.debug("Remaining calls #{length(calls)}")
Process.sleep(100)
# await_no_more_calls(contract_address, endpoint)
await_no_more_calls(contract_address, endpoint)
end
end

Expand All @@ -88,19 +94,30 @@ defmodule Archethic.Utils.Regression.Playbook.SmartContract.DeterministicBalance

condition inherit: [
content: (
previous_balance = String.to_number(previous.content)

diff = previous_balance - next.balance.uco
diff > 5.0 && diff < 6.0

log(previous.balance.uco)
log(next.balance.uco)
diff = ceil(previous.balance.uco) - ceil(next.balance.uco)
abs(diff) == 5.0
),
uco_transfers: ["00000000000000000000000000000000000000000000000000000000000000000000": 5]
]

fun ceil(number) do
number + (1 - Math.rem(number, 1))
end

fun abs(number) do
if number >= 0 do
number
else
number * -1
end
end

condition transaction: []
actions triggered_by: transaction do
Contract.add_uco_transfer to: 0x00000000000000000000000000000000000000000000000000000000000000000000, amount: 5
Contract.set_content(String.from_number(contract.balance.uco))
Contract.set_content(String.from_number(contract.balance.uco - 5.0))
end
"""
end
Expand Down
Loading
Loading