From 6a574ad0ec4193794fbf1e465337f83a997812b5 Mon Sep 17 00:00:00 2001 From: Samuel Manzanera Date: Tue, 29 Aug 2023 15:10:00 +0200 Subject: [PATCH] Adapt the fee for multi recipients transactions The idea is to ensure a bulk transaction to be more cost efficient than multiple transactions. Indeed, the validation is done one time versus multiple times. The cost is then determined as logarithmic according to the number of recipients --- lib/archethic/mining.ex | 13 +- lib/archethic/mining/fee.ex | 41 +- lib/archethic/mining/validation_context.ex | 36 +- .../mining/distributed_workflow_test.exs | 2 +- test/archethic/mining/fee_test.exs | 882 +++++++++++------- .../mining/validation_context_test.exs | 12 +- test/archethic/replication_test.exs | 2 +- .../transaction_controller_test.exs | 2 +- test/support/transaction_factory.ex | 12 +- 9 files changed, 643 insertions(+), 359 deletions(-) diff --git a/lib/archethic/mining.ex b/lib/archethic/mining.ex index 046d12acc..a7043c5b3 100644 --- a/lib/archethic/mining.ex +++ b/lib/archethic/mining.ex @@ -27,7 +27,7 @@ defmodule Archethic.Mining do use Retry - @protocol_version 1 + @protocol_version 2 def protocol_version, do: @protocol_version @@ -245,6 +245,13 @@ defmodule Archethic.Mining do @doc """ Get the transaction fee """ - @spec get_transaction_fee(Transaction.t(), float(), DateTime.t()) :: non_neg_integer() - defdelegate get_transaction_fee(tx, uco_price_in_usd, timestamp), to: Fee, as: :calculate + @spec get_transaction_fee( + transaction :: Transaction.t(), + uco_price_in_usd :: float(), + timestamp :: DateTime.t(), + protocol_version :: pos_integer() + ) :: non_neg_integer() + def get_transaction_fee(tx, uco_price_in_usd, timestamp, proto_version \\ protocol_version()) do + Fee.calculate(tx, uco_price_in_usd, timestamp, proto_version) + end end diff --git a/lib/archethic/mining/fee.ex b/lib/archethic/mining/fee.ex index 22a885ac9..7ed109097 100644 --- a/lib/archethic/mining/fee.ex +++ b/lib/archethic/mining/fee.ex @@ -37,10 +37,11 @@ defmodule Archethic.Mining.Fee do @spec calculate( transaction :: Transaction.t(), uco_usd_price :: float(), - timestamp :: DateTime.t() + timestamp :: DateTime.t(), + protocol_version :: pos_integer() ) :: non_neg_integer() - def calculate(%Transaction{type: :keychain}, _, _), do: 0 - def calculate(%Transaction{type: :keychain_access}, _, _), do: 0 + def calculate(%Transaction{type: :keychain}, _, _, _), do: 0 + def calculate(%Transaction{type: :keychain_access}, _, _, _), do: 0 def calculate( tx = %Transaction{ @@ -48,7 +49,8 @@ defmodule Archethic.Mining.Fee do type: type }, uco_price_in_usd, - timestamp + timestamp, + protocol_version ) do cond do address == Bootstrap.genesis_address() -> @@ -71,7 +73,7 @@ defmodule Archethic.Mining.Fee do nb_storage_nodes ) - replication_cost = cost_per_recipients(nb_recipients, uco_price_in_usd) + replication_cost = cost_per_recipients(nb_recipients, uco_price_in_usd, protocol_version) fee = minimum_fee(uco_price_in_usd) + storage_cost + replication_cost + @@ -143,15 +145,6 @@ defmodule Archethic.Mining.Fee do price_per_storage_node * nb_storage_nodes end - # Send transaction to a single recipient does not include an additional cost - defp cost_per_recipients(1, _), do: 0 - - # Send transaction to multiple recipients (for bulk transfers) will generate an additional cost - # As more storage pools are required to send the transaction - 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} @@ -178,4 +171,24 @@ defmodule Archethic.Mining.Fee do defp get_token_recipients_from_json(%{"recipients" => recipients}), do: recipients defp get_token_recipients_from_json(_json), do: [] + + # Send transaction to a single recipient does not include an additional cost + defp cost_per_recipients(1, _, 1), do: 0 + + # Send transaction to multiple recipients (for bulk transfers) will generate an additional cost + # As more storage pools are required to send the transaction + defp cost_per_recipients(nb_recipients, uco_price_in_usd, 1) do + nb_recipients * (0.1 / uco_price_in_usd) + end + + defp cost_per_recipients(nb_recipients, uco_price_in_usd, _protocol_version) + when nb_recipients > 0 do + base_fee = minimum_fee(uco_price_in_usd) + # To ensure with a simple tx, the price doesn't beyond $0.01 + # We can assume the recipient cost to replicate transaction to be something about 1/3 of the load for a given transaction + # And we apply a logarithmic progression, as the cost of replication might be reduced by the overlap of storage node election + (:math.log10(nb_recipients) + 0.3) * base_fee + end + + defp cost_per_recipients(_, _, _protocol_version), do: 0 end diff --git a/lib/archethic/mining/validation_context.ex b/lib/archethic/mining/validation_context.ex index bc5cb53c2..8e99313f5 100644 --- a/lib/archethic/mining/validation_context.ex +++ b/lib/archethic/mining/validation_context.ex @@ -726,13 +726,14 @@ defmodule Archethic.Mining.ValidationContext do contract_context: contract_context } ) do - {sufficient_funds?, ledger_operations} = get_ledger_operations(context) + protocol_version = Mining.protocol_version() + {sufficient_funds?, ledger_operations} = get_ledger_operations(context, protocol_version) resolved_recipients = resolved_recipients(recipients, resolved_addresses) validation_stamp = %ValidationStamp{ - protocol_version: Mining.protocol_version(), + protocol_version: protocol_version, timestamp: validation_time, proof_of_work: do_proof_of_work(tx), proof_of_integrity: TransactionChain.proof_of_integrity([tx, prev_tx]), @@ -755,12 +756,15 @@ defmodule Archethic.Mining.ValidationContext do %{context | validation_stamp: validation_stamp} end - defp get_ledger_operations(%__MODULE__{ - transaction: tx, - unspent_outputs: unspent_outputs, - validation_time: validation_time, - resolved_addresses: resolved_addresses - }) do + defp get_ledger_operations( + %__MODULE__{ + transaction: tx, + unspent_outputs: unspent_outputs, + validation_time: validation_time, + resolved_addresses: resolved_addresses + }, + protocol_version + ) do previous_usd_price = validation_time |> OracleChain.get_last_scheduling_date() @@ -777,7 +781,8 @@ defmodule Archethic.Mining.ValidationContext do Fee.calculate( tx, previous_usd_price, - validation_time + validation_time, + protocol_version ) resolved_movements = @@ -1124,7 +1129,11 @@ defmodule Archethic.Mining.ValidationContext do do: poe == Election.validation_nodes_election_seed_sorting(tx, timestamp) defp valid_stamp_fee?( - %ValidationStamp{timestamp: timestamp, ledger_operations: %LedgerOperations{fee: fee}}, + %ValidationStamp{ + protocol_version: protocol_version, + timestamp: timestamp, + ledger_operations: %LedgerOperations{fee: fee} + }, %__MODULE__{transaction: tx} ) do previous_usd_price = @@ -1136,12 +1145,13 @@ defmodule Archethic.Mining.ValidationContext do Fee.calculate( tx, previous_usd_price, - timestamp + timestamp, + protocol_version ) == fee end defp valid_stamp_error?( - stamp = %ValidationStamp{error: error}, + stamp = %ValidationStamp{error: error, protocol_version: protocol_version}, context = %__MODULE__{ transaction: tx = %Transaction{data: %TransactionData{recipients: recipients}}, previous_transaction: prev_tx, @@ -1154,7 +1164,7 @@ defmodule Archethic.Mining.ValidationContext do validated_context = %{context | transaction: %{tx | validation_stamp: stamp}} resolved_recipients = resolved_recipients(recipients, resolved_addresses) - {sufficient_funds?, _} = get_ledger_operations(validated_context) + {sufficient_funds?, _} = get_ledger_operations(validated_context, protocol_version) expected_error = get_validation_error( diff --git a/test/archethic/mining/distributed_workflow_test.exs b/test/archethic/mining/distributed_workflow_test.exs index 89a052429..af5cdfcff 100644 --- a/test/archethic/mining/distributed_workflow_test.exs +++ b/test/archethic/mining/distributed_workflow_test.exs @@ -1274,7 +1274,7 @@ defmodule Archethic.Mining.DistributedWorkflowTest do proof_of_election: Election.validation_nodes_election_seed_sorting(tx, DateTime.utc_now()), ledger_operations: %LedgerOperations{ - fee: Fee.calculate(tx, 0.07, timestamp), + fee: Fee.calculate(tx, 0.07, timestamp, ArchethicCase.current_protocol_version()), transaction_movements: Transaction.get_movements(tx), tokens_to_mint: LedgerOperations.get_utxos_from_transaction(tx, timestamp) } diff --git a/test/archethic/mining/fee_test.exs b/test/archethic/mining/fee_test.exs index a37ac9272..41466a3fc 100644 --- a/test/archethic/mining/fee_test.exs +++ b/test/archethic/mining/fee_test.exs @@ -14,40 +14,22 @@ defmodule Archethic.Mining.FeeTest do alias Archethic.TransactionChain.TransactionData.UCOLedger alias Archethic.TransactionChain.TransactionData.UCOLedger.Transfer - describe "calculate/2 with 50 storage nodes" do + alias Archethic.TransactionFactory + + alias Archethic.Utils + + use ExUnitProperties + + describe "calculate/2" do setup do add_nodes(50) :ok end test "should return a fee less than amount to send for a single transfer" do - # 0.05014249 UCO for 1 UCO at $0.2 - assert 5_014_249 = - %Transaction{ - address: <<0::8, :crypto.strong_rand_bytes(32)::binary>>, - type: :transfer, - data: %TransactionData{ - ledger: %Ledger{ - uco: %UCOLedger{ - transfers: [ - %Transfer{ - amount: trunc(100_000_000), - to: <<0::8, :crypto.strong_rand_bytes(32)::binary>> - } - ] - } - } - }, - previous_public_key: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, - previous_signature: :crypto.strong_rand_bytes(32), - origin_signature: :crypto.strong_rand_bytes(32) - } - |> Fee.calculate(0.2, DateTime.utc_now()) - end + amount = 100_000_000 - test "should increase fee when the amount increases for single transfer " do - # 0.00501425 UCO for 1 UCO - assert 501_425 == + assert tx_fee = %Transaction{ address: <<0::8, :crypto.strong_rand_bytes(32)::binary>>, type: :transfer, @@ -56,7 +38,7 @@ defmodule Archethic.Mining.FeeTest do uco: %UCOLedger{ transfers: [ %Transfer{ - amount: 100_000_000, + amount: amount, to: <<0::8, :crypto.strong_rand_bytes(32)::binary>> } ] @@ -67,318 +49,590 @@ defmodule Archethic.Mining.FeeTest do previous_signature: :crypto.strong_rand_bytes(32), origin_signature: :crypto.strong_rand_bytes(32) } - |> Fee.calculate(2.0, DateTime.utc_now()) + |> Fee.calculate(0.2, DateTime.utc_now(), ArchethicCase.current_protocol_version()) - # 0.00501425 UCO for 60 UCO - assert 501_425 = - %Transaction{ - address: <<0::8, :crypto.strong_rand_bytes(32)::binary>>, - type: :transfer, - data: %TransactionData{ - ledger: %Ledger{ - uco: %UCOLedger{ - transfers: [ - %Transfer{ - amount: 6_000_000_000, - to: <<0::8, :crypto.strong_rand_bytes(32)::binary>> - } - ] - } - } - }, - previous_public_key: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, - previous_signature: :crypto.strong_rand_bytes(32), - origin_signature: :crypto.strong_rand_bytes(32) - } - |> Fee.calculate(2.0, DateTime.utc_now()) + assert tx_fee < amount end test "should take token unique recipients into account (token creation)" do address1 = random_address() - # 0.21 UCO for 4 recipients (3 unique in content + 1 in ledger) + 1 token at $2.0 - assert 21_016_950 == - %Transaction{ - address: <<0::8, :crypto.strong_rand_bytes(32)::binary>>, - type: :token, - data: %TransactionData{ - content: """ - { - "aeip": [2, 8, 19], - "supply": 300000000, - "type": "fungible", - "name": "My token", - "symbol": "MTK", - "properties": {}, - "recipients": [ - { - "to": "#{Base.encode16(address1)}", - "amount": 100000000 - }, - { - "to": "#{Base.encode16(address1)}", - "amount": 100000000 - }, - { - "to": "#{Base.encode16(random_address())}", - "amount": 100000000 - }, - { - "to": "#{Base.encode16(random_address())}", - "amount": 100000000 - } - ] - } - """, - ledger: %Ledger{ - uco: %UCOLedger{ - transfers: [ - %Transfer{ - amount: 100_000_000, - to: <<0::8, :crypto.strong_rand_bytes(32)::binary>> - } - ] - } - } - }, - previous_public_key: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, - previous_signature: :crypto.strong_rand_bytes(32), - origin_signature: :crypto.strong_rand_bytes(32) - } - |> Fee.calculate(2.0, DateTime.utc_now()) + + tx_distinct_recipients = %Transaction{ + address: <<0::8, :crypto.strong_rand_bytes(32)::binary>>, + type: :token, + data: %TransactionData{ + content: """ + { + "aeip": [2, 8, 19], + "supply": 300000000, + "type": "fungible", + "name": "My token", + "symbol": "MTK", + "properties": {}, + "recipients": [ + { + "to": "#{Base.encode16(address1)}", + "amount": 100000000 + }, + { + "to": "#{Base.encode16(address1)}", + "amount": 100000000 + }, + { + "to": "#{Base.encode16(random_address())}", + "amount": 100000000 + }, + { + "to": "#{Base.encode16(random_address())}", + "amount": 100000000 + } + ] + } + """, + ledger: %Ledger{ + uco: %UCOLedger{ + transfers: [ + %Transfer{ + amount: 100_000_000, + to: <<0::8, :crypto.strong_rand_bytes(32)::binary>> + } + ] + } + } + }, + previous_public_key: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, + previous_signature: :crypto.strong_rand_bytes(32), + origin_signature: :crypto.strong_rand_bytes(32) + } + + fee_tx_distinct_recipients = + Fee.calculate( + tx_distinct_recipients, + 2.0, + DateTime.utc_now(), + ArchethicCase.current_protocol_version() + ) + + tx_uniq_recipients = %Transaction{ + address: <<0::8, :crypto.strong_rand_bytes(32)::binary>>, + type: :token, + data: %TransactionData{ + content: """ + { + "aeip": [2, 8, 19], + "supply": 300000000, + "type": "fungible", + "name": "My token", + "symbol": "MTK", + "properties": {}, + "recipients": [ + { + "to": "#{Base.encode16(address1)}", + "amount": 100000000 + }, + { + "to": "#{Base.encode16(random_address())}", + "amount": 100000000 + }, + { + "to": "#{Base.encode16(random_address())}", + "amount": 100000000 + } + ] + } + """, + ledger: %Ledger{ + uco: %UCOLedger{ + transfers: [ + %Transfer{ + amount: 100_000_000, + to: <<0::8, :crypto.strong_rand_bytes(32)::binary>> + } + ] + } + } + }, + previous_public_key: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, + previous_signature: :crypto.strong_rand_bytes(32), + origin_signature: :crypto.strong_rand_bytes(32) + } + + fee_tx_uniq_recipients = + Fee.calculate( + tx_uniq_recipients, + 2.0, + DateTime.utc_now(), + ArchethicCase.current_protocol_version() + ) + + tx_uniq_recipients_size = + tx_uniq_recipients.data + |> TransactionData.serialize(tx_uniq_recipients.version) + |> byte_size() + + tx_distinct_recipients_size = + tx_distinct_recipients.data + |> TransactionData.serialize(tx_distinct_recipients.version) + |> byte_size() + + nb_storage_nodes = 50 + + diff_bytes = tx_distinct_recipients_size - tx_uniq_recipients_size + price_per_byte = 1.0e-8 / 2.0 + price_per_storage_node = price_per_byte * diff_bytes + diff_fee = price_per_storage_node * nb_storage_nodes + + assert fee_tx_distinct_recipients - fee_tx_uniq_recipients == diff_fee * 100_000_000 end test "should take token unique recipients into account (token resupply)" do - # 0.11 UCO for 2 recipients + 1 token at $2.0 - assert 11_010_100 == - %Transaction{ - address: <<0::8, :crypto.strong_rand_bytes(32)::binary>>, - type: :token, - data: %TransactionData{ - content: """ - { - "aeip": [8, 18], - "supply": 1000, - "token_reference": "0000C13373C96538B468CCDAB8F95FDC3744EBFA2CD36A81C3791B2A205705D9C3A2", - "recipients": [ - { - "to": "#{Base.encode16(random_address())}", - "amount": 100000000 - }, - { - "to": "#{Base.encode16(random_address())}", - "amount": 100000000 - } - ] - } - """ - }, - previous_public_key: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, - previous_signature: :crypto.strong_rand_bytes(32), - origin_signature: :crypto.strong_rand_bytes(32) - } - |> Fee.calculate(2.0, DateTime.utc_now()) + addr1 = random_address() + + tx_uniq_recipients = %Transaction{ + address: <<0::8, :crypto.strong_rand_bytes(32)::binary>>, + type: :token, + data: %TransactionData{ + content: """ + { + "aeip": [8, 18], + "supply": 1000, + "token_reference": "0000C13373C96538B468CCDAB8F95FDC3744EBFA2CD36A81C3791B2A205705D9C3A2", + "recipients": [ + { + "to": "#{Base.encode16(addr1)}", + "amount": 100000000 + } + ] + } + """ + }, + previous_public_key: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, + previous_signature: :crypto.strong_rand_bytes(32), + origin_signature: :crypto.strong_rand_bytes(32) + } + + fee_tx_uniq_recipients = + Fee.calculate( + tx_uniq_recipients, + 2.0, + DateTime.utc_now(), + ArchethicCase.current_protocol_version() + ) + + addr1 = random_address() + + tx_distinct_recipients = %Transaction{ + address: <<0::8, :crypto.strong_rand_bytes(32)::binary>>, + type: :token, + data: %TransactionData{ + content: """ + { + "aeip": [8, 18], + "supply": 1000, + "token_reference": "0000C13373C96538B468CCDAB8F95FDC3744EBFA2CD36A81C3791B2A205705D9C3A2", + "recipients": [ + { + "to": "#{Base.encode16(addr1)}", + "amount": 100000000 + }, + { + "to": "#{Base.encode16(addr1)}", + "amount": 100000000 + } + ] + } + """ + }, + previous_public_key: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, + previous_signature: :crypto.strong_rand_bytes(32), + origin_signature: :crypto.strong_rand_bytes(32) + } + + fee_tx_distinct_recipients = + Fee.calculate( + tx_distinct_recipients, + 2.0, + DateTime.utc_now(), + ArchethicCase.current_protocol_version() + ) + + tx_uniq_recipients_size = + tx_uniq_recipients.data + |> TransactionData.serialize(tx_uniq_recipients.version) + |> byte_size() + + tx_distinct_recipients_size = + tx_distinct_recipients.data + |> TransactionData.serialize(tx_distinct_recipients.version) + |> byte_size() + + nb_storage_nodes = 50 + + diff_bytes = tx_distinct_recipients_size - tx_uniq_recipients_size + price_per_byte = 1.0e-8 / 2.0 + price_per_storage_node = price_per_byte * diff_bytes + diff_fee = price_per_storage_node * nb_storage_nodes + + assert fee_tx_distinct_recipients - fee_tx_uniq_recipients == diff_fee * 100_000_000 end test "should pay additional fee for tokens without recipient" do - # 0.01 UCO for 0 transfer + 1 token at $2.0 - assert 1_003_524 == - %Transaction{ - address: <<0::8, :crypto.strong_rand_bytes(32)::binary>>, - type: :token, - data: %TransactionData{ - content: """ - { - "aeip": [2, 8, 19], - "supply": 300000000, - "type": "fungible", - "name": "My token", - "symbol": "MTK", - "properties": {} - } - """ - }, - previous_public_key: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, - previous_signature: :crypto.strong_rand_bytes(32), - origin_signature: :crypto.strong_rand_bytes(32) - } - |> Fee.calculate(2.0, DateTime.utc_now()) - end + tx = %Transaction{ + address: <<0::8, :crypto.strong_rand_bytes(32)::binary>>, + type: :token, + data: %TransactionData{ + content: """ + { + "aeip": [2, 8, 19], + "supply": 300000000, + "type": "fungible", + "name": "My token", + "symbol": "MTK", + "properties": {} + } + """ + }, + previous_public_key: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, + previous_signature: :crypto.strong_rand_bytes(32), + origin_signature: :crypto.strong_rand_bytes(32) + } - test "should decrease the fee when the amount stays the same but the price of UCO increases" do - # 0.00501425 UCO for 1 UCO at $ 2.0 - assert 501_425 = - %Transaction{ - address: <<0::8, :crypto.strong_rand_bytes(32)::binary>>, - type: :transfer, - data: %TransactionData{ - ledger: %Ledger{ - uco: %UCOLedger{ - transfers: [ - %Transfer{ - amount: 100_000_000, - to: <<0::8, :crypto.strong_rand_bytes(32)::binary>> - } - ] - } - } - }, - previous_public_key: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, - previous_signature: :crypto.strong_rand_bytes(32), - origin_signature: :crypto.strong_rand_bytes(32) - } - |> Fee.calculate(2.0, DateTime.utc_now()) + fee = Fee.calculate(tx, 2.0, DateTime.utc_now(), ArchethicCase.current_protocol_version()) - # 0.00100285 UCO for 1 UCO at $10.0 - assert 100_285 = - %Transaction{ - address: <<0::8, :crypto.strong_rand_bytes(32)::binary>>, - type: :transfer, - data: %TransactionData{ - ledger: %Ledger{ - uco: %UCOLedger{ - transfers: [ - %Transfer{ - amount: 100_000_000, - to: <<0::8, :crypto.strong_rand_bytes(32)::binary>> - } - ] - } - } - }, - previous_public_key: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, - previous_signature: :crypto.strong_rand_bytes(32), - origin_signature: :crypto.strong_rand_bytes(32) - } - |> Fee.calculate(10.0, DateTime.utc_now()) + nb_bytes = + tx.data + |> TransactionData.serialize(tx.version) + |> byte_size() + + nb_storage_nodes = 50 + price_per_byte = 1.0e-8 / 2.0 + price_per_storage_node = price_per_byte * nb_bytes + storage_cost = price_per_storage_node * nb_storage_nodes + + min_fee = 0.01 / 2.0 + additional_fee = min_fee + + assert Utils.to_bigint(min_fee + storage_cost + additional_fee) == fee end - test "sending multiple transfers should cost more than sending a single big transfer" do - # 0.05014249 UCO for 1_000 UCO - assert 5_014_249 = - %Transaction{ - address: <<0::8, :crypto.strong_rand_bytes(32)::binary>>, - type: :transfer, - data: %TransactionData{ - ledger: %Ledger{ - uco: %UCOLedger{ - transfers: [ - %Transfer{ - amount: 100_000_000_000, - to: <<0::8, :crypto.strong_rand_bytes(32)::binary>> - } - ] - } - } - }, - previous_public_key: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, - previous_signature: :crypto.strong_rand_bytes(32), - origin_signature: :crypto.strong_rand_bytes(32) - } - |> Fee.calculate(0.2, DateTime.utc_now()) + test "should decrease the fee when the amount stays the same but the price of UCO increases" do + fee1 = + %Transaction{ + address: <<0::8, :crypto.strong_rand_bytes(32)::binary>>, + type: :transfer, + data: %TransactionData{ + ledger: %Ledger{ + uco: %UCOLedger{ + transfers: [ + %Transfer{ + amount: 100_000_000, + to: <<0::8, :crypto.strong_rand_bytes(32)::binary>> + } + ] + } + } + }, + previous_public_key: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, + previous_signature: :crypto.strong_rand_bytes(32), + origin_signature: :crypto.strong_rand_bytes(32) + } + |> Fee.calculate(2.0, DateTime.utc_now(), ArchethicCase.current_protocol_version()) - # 500.1525425 UCO for 1000 transfer of 1 UCO - assert 50_015_254_250 = - %Transaction{ - address: <<0::8, :crypto.strong_rand_bytes(32)::binary>>, - type: :transfer, - data: %TransactionData{ - ledger: %Ledger{ - uco: %UCOLedger{ - transfers: - Enum.map(1..1000, fn _ -> - %Transfer{ - amount: 100_000_000, - to: <<0::8, :crypto.strong_rand_bytes(32)::binary>> - } - end) - } - } - }, - previous_public_key: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, - previous_signature: :crypto.strong_rand_bytes(32), - origin_signature: :crypto.strong_rand_bytes(32) - } - |> Fee.calculate(0.2, DateTime.utc_now()) + fee2 = + %Transaction{ + address: <<0::8, :crypto.strong_rand_bytes(32)::binary>>, + type: :transfer, + data: %TransactionData{ + ledger: %Ledger{ + uco: %UCOLedger{ + transfers: [ + %Transfer{ + amount: 100_000_000, + to: <<0::8, :crypto.strong_rand_bytes(32)::binary>> + } + ] + } + } + }, + previous_public_key: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, + previous_signature: :crypto.strong_rand_bytes(32), + origin_signature: :crypto.strong_rand_bytes(32) + } + |> Fee.calculate(10.0, DateTime.utc_now(), ArchethicCase.current_protocol_version()) + + assert fee2 < fee1 end test "should increase the fee when the transaction size increases" do - # 0.05254000 UCO to store 1KB - assert 5_254_000 = - %Transaction{ - address: <<0::8, :crypto.strong_rand_bytes(32)::binary>>, - type: :transfer, - data: %TransactionData{ - content: :crypto.strong_rand_bytes(1_000) - }, - previous_public_key: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, - previous_signature: :crypto.strong_rand_bytes(32), - origin_signature: :crypto.strong_rand_bytes(32) - } - |> Fee.calculate(0.2, DateTime.utc_now()) + fee_tx_small = + %Transaction{ + address: <<0::8, :crypto.strong_rand_bytes(32)::binary>>, + type: :transfer, + data: %TransactionData{ + content: :crypto.strong_rand_bytes(1_000) + }, + previous_public_key: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, + previous_signature: :crypto.strong_rand_bytes(32), + origin_signature: :crypto.strong_rand_bytes(32) + } + |> Fee.calculate(0.2, DateTime.utc_now(), ArchethicCase.current_protocol_version()) - # 25.05004 UCO to store 10MB - assert 2_505_004_000 = - %Transaction{ - address: <<0::8, :crypto.strong_rand_bytes(32)::binary>>, - type: :transfer, - data: %TransactionData{ - content: :crypto.strong_rand_bytes(10 * 1_000_000) - }, - previous_public_key: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, - previous_signature: :crypto.strong_rand_bytes(32), - origin_signature: :crypto.strong_rand_bytes(32) - } - |> Fee.calculate(0.2, DateTime.utc_now()) + fee_tx_big = + %Transaction{ + address: <<0::8, :crypto.strong_rand_bytes(32)::binary>>, + type: :transfer, + data: %TransactionData{ + content: :crypto.strong_rand_bytes(10 * 1_000_000) + }, + previous_public_key: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, + previous_signature: :crypto.strong_rand_bytes(32), + origin_signature: :crypto.strong_rand_bytes(32) + } + |> Fee.calculate(0.2, DateTime.utc_now(), ArchethicCase.current_protocol_version()) + + assert fee_tx_big > fee_tx_small end test "should cost more with more replication nodes" do - # 50 nodes: 0.00501425 UCO - assert 501_425 = - %Transaction{ - address: <<0::8, :crypto.strong_rand_bytes(32)::binary>>, - type: :transfer, - data: %TransactionData{ - ledger: %Ledger{ - uco: %UCOLedger{ - transfers: [ - %Transfer{ - amount: 100_000_000, - to: <<0::8, :crypto.strong_rand_bytes(32)::binary>> - } - ] - } - } - }, - previous_public_key: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, - previous_signature: :crypto.strong_rand_bytes(32), - origin_signature: :crypto.strong_rand_bytes(32) - } - |> Fee.calculate(2.0, DateTime.utc_now()) + tx_fee_50_nodes = + %Transaction{ + address: <<0::8, :crypto.strong_rand_bytes(32)::binary>>, + type: :transfer, + data: %TransactionData{ + ledger: %Ledger{ + uco: %UCOLedger{ + transfers: [ + %Transfer{ + amount: 100_000_000, + to: <<0::8, :crypto.strong_rand_bytes(32)::binary>> + } + ] + } + } + }, + previous_public_key: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, + previous_signature: :crypto.strong_rand_bytes(32), + origin_signature: :crypto.strong_rand_bytes(32) + } + |> Fee.calculate(2.0, DateTime.utc_now(), ArchethicCase.current_protocol_version()) add_nodes(100) - # 150 nodes: 0.00504275 UCO - assert 504_275 = - %Transaction{ - address: <<0::8, :crypto.strong_rand_bytes(32)::binary>>, - type: :transfer, - data: %TransactionData{ - ledger: %Ledger{ - uco: %UCOLedger{ - transfers: [ - %Transfer{ - amount: 100_000_000, - to: <<0::8, :crypto.strong_rand_bytes(32)::binary>> - } - ] - } - } - }, - previous_public_key: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, - previous_signature: :crypto.strong_rand_bytes(32), - origin_signature: :crypto.strong_rand_bytes(32) - } - |> Fee.calculate(2.0, DateTime.utc_now()) + tx_fee_100_nodes = + %Transaction{ + address: <<0::8, :crypto.strong_rand_bytes(32)::binary>>, + type: :transfer, + data: %TransactionData{ + ledger: %Ledger{ + uco: %UCOLedger{ + transfers: [ + %Transfer{ + amount: 100_000_000, + to: <<0::8, :crypto.strong_rand_bytes(32)::binary>> + } + ] + } + } + }, + previous_public_key: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, + previous_signature: :crypto.strong_rand_bytes(32), + origin_signature: :crypto.strong_rand_bytes(32) + } + |> Fee.calculate(2.0, DateTime.utc_now(), ArchethicCase.current_protocol_version()) + + assert tx_fee_50_nodes < tx_fee_100_nodes + end + + test "should cost more sending multiple transfers than sending a single big transfer" do + single_tx_fee = + %Transaction{ + address: <<0::8, :crypto.strong_rand_bytes(32)::binary>>, + type: :transfer, + data: %TransactionData{ + ledger: %Ledger{ + uco: %UCOLedger{ + transfers: [ + %Transfer{ + amount: 100_000_000_000, + to: <<0::8, :crypto.strong_rand_bytes(32)::binary>> + } + ] + } + } + }, + previous_public_key: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, + previous_signature: :crypto.strong_rand_bytes(32), + origin_signature: :crypto.strong_rand_bytes(32) + } + |> Fee.calculate(0.2, DateTime.utc_now(), ArchethicCase.current_protocol_version()) + + batched_tx_fee = + %Transaction{ + address: <<0::8, :crypto.strong_rand_bytes(32)::binary>>, + type: :transfer, + data: %TransactionData{ + ledger: %Ledger{ + uco: %UCOLedger{ + transfers: + Enum.map(1..1000, fn _ -> + %Transfer{ + amount: 100_000_000, + to: <<0::8, :crypto.strong_rand_bytes(32)::binary>> + } + end) + } + } + }, + previous_public_key: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, + previous_signature: :crypto.strong_rand_bytes(32), + origin_signature: :crypto.strong_rand_bytes(32) + } + |> Fee.calculate(0.2, DateTime.utc_now(), ArchethicCase.current_protocol_version()) + + assert batched_tx_fee > single_tx_fee + end + + test "should cost more when a token is created with multiple UTXO to create (collection)" do + fee1 = + %Transaction{ + address: <<0::8, :crypto.strong_rand_bytes(32)::binary>>, + type: :token, + data: %TransactionData{ + content: + Jason.encode!(%{ + type: "non-fungible", + collection: [ + %{image: "link"}, + %{image: "link"}, + %{image: "link"} + ] + }) + }, + previous_public_key: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, + previous_signature: :crypto.strong_rand_bytes(32), + origin_signature: :crypto.strong_rand_bytes(32) + } + |> Fee.calculate(2.0, DateTime.utc_now(), ArchethicCase.current_protocol_version()) + + fee2 = + %Transaction{ + address: <<0::8, :crypto.strong_rand_bytes(32)::binary>>, + type: :token, + data: %TransactionData{ + content: + Jason.encode!(%{ + type: "non-fungible", + collection: [ + %{image: "link"}, + %{image: "link"}, + %{image: "link"}, + %{image: "link"}, + %{image: "link"}, + %{image: "link"} + ] + }) + }, + previous_public_key: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, + previous_signature: :crypto.strong_rand_bytes(32), + origin_signature: :crypto.strong_rand_bytes(32) + } + |> Fee.calculate(2.0, DateTime.utc_now(), ArchethicCase.current_protocol_version()) + + assert fee2 > fee1 + end + + test "should cost more when a token is created with recipients" do + fee1 = + %Transaction{ + address: <<0::8, :crypto.strong_rand_bytes(32)::binary>>, + type: :token, + data: %TransactionData{ + content: + Jason.encode!(%{ + type: "fungible" + }) + }, + previous_public_key: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, + previous_signature: :crypto.strong_rand_bytes(32), + origin_signature: :crypto.strong_rand_bytes(32) + } + |> Fee.calculate(2.0, DateTime.utc_now(), ArchethicCase.current_protocol_version()) + + fee2 = + %Transaction{ + address: <<0::8, :crypto.strong_rand_bytes(32)::binary>>, + type: :token, + data: %TransactionData{ + content: + Jason.encode!(%{ + type: "fungible", + recipients: [ + %{to: "", amount: 1}, + %{to: "", amount: 1}, + %{to: "", amount: 1} + ] + }) + }, + previous_public_key: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, + previous_signature: :crypto.strong_rand_bytes(32), + origin_signature: :crypto.strong_rand_bytes(32) + } + |> Fee.calculate(2.0, DateTime.utc_now(), ArchethicCase.current_protocol_version()) + + assert fee2 > fee1 + end + + property "should cost more with multiple recipients but being more efficient than multiple transactions" do + check all(nb_recipients <- StreamData.integer(1..255)) do + batch_tx = + TransactionFactory.create_valid_transaction([], + type: :transfer, + ledger: %Ledger{ + uco: %UCOLedger{ + transfers: + Enum.map(1..nb_recipients, fn _ -> + %Transfer{ + to: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, + amount: 100_000_000 + } + end) + } + } + ) + + batch_tx_fee = + Fee.calculate( + batch_tx, + 2.0, + DateTime.utc_now(), + ArchethicCase.current_protocol_version() + ) + + single_tx = + TransactionFactory.create_valid_transaction([], + type: :transfer, + ledger: %Ledger{ + uco: %UCOLedger{ + transfers: [ + %Transfer{ + to: <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>>, + amount: 100_000_000 + } + ] + } + } + ) + + single_tx_fee = + Fee.calculate( + single_tx, + 2.0, + DateTime.utc_now(), + ArchethicCase.current_protocol_version() + ) + + assert batch_tx_fee < single_tx_fee * nb_recipients + end end end diff --git a/test/archethic/mining/validation_context_test.exs b/test/archethic/mining/validation_context_test.exs index 451f50f11..4012e8ceb 100644 --- a/test/archethic/mining/validation_context_test.exs +++ b/test/archethic/mining/validation_context_test.exs @@ -290,7 +290,7 @@ defmodule Archethic.Mining.ValidationContextTest do proof_of_election: Election.validation_nodes_election_seed_sorting(tx, DateTime.utc_now()), ledger_operations: %LedgerOperations{ - fee: Fee.calculate(tx, 0.07, timestamp), + fee: Fee.calculate(tx, 0.07, timestamp, ArchethicCase.current_protocol_version()), transaction_movements: Transaction.get_movements(tx), tokens_to_mint: LedgerOperations.get_utxos_from_transaction(tx, timestamp) } @@ -313,7 +313,7 @@ defmodule Archethic.Mining.ValidationContextTest do proof_of_election: Election.validation_nodes_election_seed_sorting(tx, DateTime.utc_now()), ledger_operations: %LedgerOperations{ - fee: Fee.calculate(tx, 0.07, timestamp), + fee: Fee.calculate(tx, 0.07, timestamp, ArchethicCase.current_protocol_version()), transaction_movements: Transaction.get_movements(tx), tokens_to_mint: LedgerOperations.get_utxos_from_transaction(tx, timestamp) } @@ -336,7 +336,7 @@ defmodule Archethic.Mining.ValidationContextTest do proof_of_election: Election.validation_nodes_election_seed_sorting(tx, DateTime.utc_now()), ledger_operations: %LedgerOperations{ - fee: Fee.calculate(tx, 0.07, timestamp), + fee: Fee.calculate(tx, 0.07, timestamp, ArchethicCase.current_protocol_version()), transaction_movements: Transaction.get_movements(tx), tokens_to_mint: LedgerOperations.get_utxos_from_transaction(tx, timestamp) } @@ -374,7 +374,7 @@ defmodule Archethic.Mining.ValidationContextTest do validation_time: timestamp, unspent_outputs: unspent_outputs }) do - fee = Fee.calculate(tx, 0.07, timestamp) + fee = Fee.calculate(tx, 0.07, timestamp, ArchethicCase.current_protocol_version()) %ValidationStamp{ timestamp: timestamp, @@ -414,7 +414,7 @@ defmodule Archethic.Mining.ValidationContextTest do proof_of_integrity: TransactionChain.proof_of_integrity([tx]), proof_of_election: Election.validation_nodes_election_seed_sorting(tx, DateTime.utc_now()), ledger_operations: %LedgerOperations{ - fee: Fee.calculate(tx, 0.07, timestamp), + fee: Fee.calculate(tx, 0.07, timestamp, ArchethicCase.current_protocol_version()), transaction_movements: Transaction.get_movements(tx), unspent_outputs: [ %UnspentOutput{ @@ -442,7 +442,7 @@ defmodule Archethic.Mining.ValidationContextTest do proof_of_election: Election.validation_nodes_election_seed_sorting(tx, DateTime.utc_now()), ledger_operations: %LedgerOperations{ - fee: Fee.calculate(tx, 0.07, timestamp), + fee: Fee.calculate(tx, 0.07, timestamp, ArchethicCase.current_protocol_version()), transaction_movements: Transaction.get_movements(tx) } |> LedgerOperations.consume_inputs(tx.address, unspent_outputs, timestamp) diff --git a/test/archethic/replication_test.exs b/test/archethic/replication_test.exs index 81826efe7..615b9c146 100644 --- a/test/archethic/replication_test.exs +++ b/test/archethic/replication_test.exs @@ -268,7 +268,7 @@ defmodule Archethic.ReplicationTest do ledger_operations = %LedgerOperations{ - fee: Fee.calculate(tx, 0.07, timestamp) + fee: Fee.calculate(tx, 0.07, timestamp, ArchethicCase.current_protocol_version()) } |> LedgerOperations.consume_inputs(tx.address, unspent_outputs, timestamp) |> elem(1) diff --git a/test/archethic_web/api/rest/controllers/transaction_controller_test.exs b/test/archethic_web/api/rest/controllers/transaction_controller_test.exs index 6bffc6e89..b06b561ba 100644 --- a/test/archethic_web/api/rest/controllers/transaction_controller_test.exs +++ b/test/archethic_web/api/rest/controllers/transaction_controller_test.exs @@ -68,7 +68,7 @@ defmodule ArchethicWeb.API.REST.TransactionControllerTest do }) assert %{ - "fee" => 5_000_290, + "fee" => 6_500_289, "rates" => %{ "eur" => 0.2, "usd" => 0.2 diff --git a/test/support/transaction_factory.ex b/test/support/transaction_factory.ex index 7b3a332fe..a87d3ee75 100644 --- a/test/support/transaction_factory.ex +++ b/test/support/transaction_factory.ex @@ -60,7 +60,7 @@ defmodule Archethic.TransactionFactory do ledger_operations = %LedgerOperations{ - fee: Fee.calculate(tx, 0.07, timestamp), + fee: Fee.calculate(tx, 0.07, timestamp, ArchethicCase.current_protocol_version()), transaction_movements: Transaction.get_movements(tx) } |> LedgerOperations.consume_inputs(tx.address, inputs, timestamp) @@ -101,7 +101,7 @@ defmodule Archethic.TransactionFactory do ledger_operations = %LedgerOperations{ - fee: Fee.calculate(tx, 0.07, timestamp) + fee: Fee.calculate(tx, 0.07, timestamp, ArchethicCase.current_protocol_version()) } |> LedgerOperations.consume_inputs(tx.address, inputs, timestamp) |> elem(1) @@ -136,7 +136,7 @@ defmodule Archethic.TransactionFactory do ledger_operations = %LedgerOperations{ - fee: Fee.calculate(tx, 0.07, timestamp) + fee: Fee.calculate(tx, 0.07, timestamp, ArchethicCase.current_protocol_version()) } |> LedgerOperations.consume_inputs(tx.address, inputs, timestamp) |> elem(1) @@ -172,7 +172,7 @@ defmodule Archethic.TransactionFactory do ledger_operations = %LedgerOperations{ - fee: Fee.calculate(tx, 0.07, timestamp) + fee: Fee.calculate(tx, 0.07, timestamp, ArchethicCase.current_protocol_version()) } |> LedgerOperations.consume_inputs(tx.address, inputs, timestamp) |> elem(1) @@ -234,7 +234,7 @@ defmodule Archethic.TransactionFactory do ledger_operations = %LedgerOperations{ - fee: Fee.calculate(tx, 0.07, timestamp), + fee: Fee.calculate(tx, 0.07, timestamp, ArchethicCase.current_protocol_version()), transaction_movements: [ %TransactionMovement{to: "@Bob4", amount: 30_330_000_000, type: :UCO} ] @@ -286,7 +286,7 @@ defmodule Archethic.TransactionFactory do ledger_operations = %LedgerOperations{ - fee: Fee.calculate(tx, 0.07, timestamp), + fee: Fee.calculate(tx, 0.07, timestamp, ArchethicCase.current_protocol_version()), transaction_movements: Transaction.get_movements(tx) } |> LedgerOperations.consume_inputs(tx.address, inputs, timestamp)