Skip to content

Commit

Permalink
Adapt the fee for multi recipients transactions
Browse files Browse the repository at this point in the history
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
  • Loading branch information
samuelmanzanera committed Aug 30, 2023
1 parent d91aaa5 commit a91d2be
Show file tree
Hide file tree
Showing 8 changed files with 639 additions and 358 deletions.
13 changes: 10 additions & 3 deletions lib/archethic/mining.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ defmodule Archethic.Mining do

use Retry

@protocol_version 1
@protocol_version 2

def protocol_version, do: @protocol_version

Expand Down Expand Up @@ -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
38 changes: 24 additions & 14 deletions lib/archethic/mining/fee.ex
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,20 @@ 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{
address: address,
type: type
},
uco_price_in_usd,
timestamp
timestamp,
protocol_version
) do
cond do
address == Bootstrap.genesis_address() ->
Expand All @@ -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 +
Expand Down Expand Up @@ -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}
Expand All @@ -178,4 +171,21 @@ 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 > 1 do
base_fee = minimum_fee(uco_price_in_usd)
(:math.log10(nb_recipients) + 1) * base_fee
end

defp cost_per_recipients(_, _, _protocol_version), do: 0
end
36 changes: 23 additions & 13 deletions lib/archethic/mining/validation_context.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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]),
Expand All @@ -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()
Expand All @@ -777,7 +781,8 @@ defmodule Archethic.Mining.ValidationContext do
Fee.calculate(
tx,
previous_usd_price,
validation_time
validation_time,
protocol_version
)

resolved_movements =
Expand Down Expand Up @@ -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 =
Expand All @@ -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,
Expand All @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion test/archethic/mining/distributed_workflow_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Loading

0 comments on commit a91d2be

Please sign in to comment.