From 1afa4deccebbfbeef283c935214bc018b462a5be Mon Sep 17 00:00:00 2001 From: "bl@ckode" Date: Wed, 30 Mar 2022 14:47:19 +0530 Subject: [PATCH 01/16] Add faucet rate limiter genserver --- config/config.exs | 6 ++ lib/archethic_web/faucet_rate_limiter.ex | 86 ++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 lib/archethic_web/faucet_rate_limiter.ex diff --git a/config/config.exs b/config/config.exs index f7b98c0e8..6a4deab38 100644 --- a/config/config.exs +++ b/config/config.exs @@ -38,6 +38,12 @@ config :logger, :console, ], colors: [enabled: true] +# Faucet rate limit in Number of transactions +config :archethic, :faucet_rate_limit, 3 + +# Faucet rate limit Expiry time in milliseconds +config :archethic, :faucet_rate_limit_expiry, 3_600_000 + # Use Jason for JSON parsing in Phoenix config :phoenix, :json_library, Jason diff --git a/lib/archethic_web/faucet_rate_limiter.ex b/lib/archethic_web/faucet_rate_limiter.ex new file mode 100644 index 000000000..73cf22f7f --- /dev/null +++ b/lib/archethic_web/faucet_rate_limiter.ex @@ -0,0 +1,86 @@ +defmodule ArchEthicWeb.FaucetRateLimiter do + @moduledoc false + + use GenServer + + @faucet_rate_limit Application.compile_env!(:archethic, :faucet_rate_limit) + @faucet_rate_limit_expiry Application.compile_env!(:archethic, :faucet_rate_limit_expiry) + @block_period_expiry @faucet_rate_limit_expiry + @clean_time @faucet_rate_limit_expiry + + def start_link(opts) do + GenServer.start_link(__MODULE__, opts, name: __MODULE__) + end + + @doc """ + Register a faucet transaction address to monitor + """ + @spec register(binary(), non_neg_integer()) :: :ok + + def register(tx_address, start_time) + when is_binary(tx_address) and is_integer(start_time) do + GenServer.cast(__MODULE__, {:register, tx_address, start_time}) + end + + def init(_) do + schedule_clean() + {:ok, %{}} + end + + def handle_cast({:register, tx_address, start_time}, state) do + transaction = Map.get(state, tx_address) + + transaction_info = + if transaction do + tx_count = transaction.tx_count + 1 + + if tx_count == @faucet_rate_limit do + GenServer.cast(__MODULE__, {:block, tx_address}) + Process.send_after(self(), {:clean, tx_address}, @block_period_expiry) + end + + transaction + |> Map.put(:last_time, start_time) + |> Map.put(:tx_count, tx_count) + else + %{ + start_time: start_time, + last_time: start_time, + transactions_count: 1, + blocked?: false + } + end + + {:noreply, Map.put(state, tx_address, transaction_info)} + end + + def handle_cast({:block, tx_address}, state) do + transaction = Map.get(state, tx_address) + transaction = %{transaction | blocked?: true} + new_state = Map.put(state, tx_address, transaction) + {:noreply, new_state} + end + + def handle_info({:clean, tx_address}, state) do + {:noreply, Map.delete(state, tx_address)} + end + + def handle_info(:clean, state) do + schedule_clean() + now = System.monotonic_time() + + new_state = + Enum.filter(state, fn + {_address, %{last_time: start_time}} -> + millisecond_elapsed = System.convert_time_unit(now - start_time, :native, :millisecond) + millisecond_elapsed <= @block_period_expiry + end) + |> Enum.into(%{}) + + {:noreply, new_state} + end + + defp schedule_clean() do + Process.send_after(self(), :clean, @clean_time) + end +end From 68eb9afc018a2886e4bf2ed0a865d1484b0fdfd5 Mon Sep 17 00:00:00 2001 From: "bl@ckode" Date: Wed, 30 Mar 2022 16:29:33 +0530 Subject: [PATCH 02/16] Utility function seconds to human readable done --- lib/archethic/utils.ex | 23 ++++++++ .../controllers/faucet_controller.ex | 7 +++ lib/archethic_web/faucet_rate_limiter.ex | 53 ++++++++++++------- 3 files changed, 65 insertions(+), 18 deletions(-) diff --git a/lib/archethic/utils.ex b/lib/archethic/utils.ex index 395af8b65..b3031459a 100644 --- a/lib/archethic/utils.ex +++ b/lib/archethic/utils.ex @@ -612,4 +612,27 @@ defmodule ArchEthic.Utils do {attestation, rest} = ReplicationAttestation.deserialize(rest) deserialize_transaction_attestations(rest, nb_attestations, [attestation | acc]) end + + @doc """ + Convert the seconds to human readable format + + ## Examples + + iex> ArchEthic.Utils.seconds_to_hh_mm_ss(3666) + "1 hour 01 minute 06 second" + """ + def seconds_to_hh_mm_ss(0), do: "00:00:00" + + def seconds_to_hh_mm_ss(seconds) do + units = [3600, 60, 1] + + [h | t] = + Enum.map_reduce(units, seconds, fn unit, val -> {div(val, unit), rem(val, unit)} end) + |> elem(0) + |> Enum.drop_while(&match?(0, &1)) + + {h, t} = if length(t) == 0, do: {0, [h]}, else: {h, t} + + "#{h} hour #{t |> Enum.map_join(" minute ", fn term -> term |> Integer.to_string() |> String.pad_leading(2, "0") end)} second" + end end diff --git a/lib/archethic_web/controllers/faucet_controller.ex b/lib/archethic_web/controllers/faucet_controller.ex index f6ff28e55..16557ea69 100644 --- a/lib/archethic_web/controllers/faucet_controller.ex +++ b/lib/archethic_web/controllers/faucet_controller.ex @@ -13,6 +13,7 @@ defmodule ArchEthicWeb.FaucetController do } alias ArchEthicWeb.TransactionSubscriber + alias ArchEthicWeb.FaucetRateLimiter @pool_seed Application.compile_env(:archethic, [__MODULE__, :seed]) @@ -42,6 +43,7 @@ defmodule ArchEthicWeb.FaucetController do def create_transfer(conn, %{"address" => address}) do with {:ok, recipient_address} <- Base.decode16(address, case: :mixed), true <- Crypto.valid_address?(recipient_address), + %{archived?: false} <- FaucetRateLimiter.get_address_archive_status(address), {:ok, tx_address} <- transfer(recipient_address) do TransactionSubscriber.register(tx_address, System.monotonic_time()) @@ -57,6 +59,11 @@ defmodule ArchEthicWeb.FaucetController do |> put_flash(:error, "Unable to send the transaction") |> render("index.html", address: address, link_address: "") + %{archived?: true} -> + conn + |> put_flash(:error, "Archived address") + |> render("index.html", address: address, link_address: "") + _ -> conn |> put_flash(:error, "Malformed address") diff --git a/lib/archethic_web/faucet_rate_limiter.ex b/lib/archethic_web/faucet_rate_limiter.ex index 73cf22f7f..6cc59a4d8 100644 --- a/lib/archethic_web/faucet_rate_limiter.ex +++ b/lib/archethic_web/faucet_rate_limiter.ex @@ -5,7 +5,7 @@ defmodule ArchEthicWeb.FaucetRateLimiter do @faucet_rate_limit Application.compile_env!(:archethic, :faucet_rate_limit) @faucet_rate_limit_expiry Application.compile_env!(:archethic, :faucet_rate_limit_expiry) - @block_period_expiry @faucet_rate_limit_expiry + @archive_period_expiry @faucet_rate_limit_expiry @clean_time @faucet_rate_limit_expiry def start_link(opts) do @@ -17,9 +17,14 @@ defmodule ArchEthicWeb.FaucetRateLimiter do """ @spec register(binary(), non_neg_integer()) :: :ok - def register(tx_address, start_time) - when is_binary(tx_address) and is_integer(start_time) do - GenServer.cast(__MODULE__, {:register, tx_address, start_time}) + def register(address, start_time) + when is_binary(address) and is_integer(start_time) do + GenServer.cast(__MODULE__, {:register, address, start_time}) + end + + def get_address_archive_status(address) + when is_binary(address) do + GenServer.call(__MODULE__, {:archive_status, address}) end def init(_) do @@ -27,16 +32,27 @@ defmodule ArchEthicWeb.FaucetRateLimiter do {:ok, %{}} end - def handle_cast({:register, tx_address, start_time}, state) do - transaction = Map.get(state, tx_address) + def handle_call({:archive_status, address}, _from, state) do + reply = + if address_state = Map.get(state, address) do + address_state + else + %{archived?: false} + end + + {:reply, reply, state} + end + + def handle_cast({:register, address, start_time}, state) do + transaction = Map.get(state, address) transaction_info = if transaction do tx_count = transaction.tx_count + 1 if tx_count == @faucet_rate_limit do - GenServer.cast(__MODULE__, {:block, tx_address}) - Process.send_after(self(), {:clean, tx_address}, @block_period_expiry) + GenServer.cast(__MODULE__, {:archive, address}) + Process.send_after(self(), {:clean, address}, @archive_period_expiry) end transaction @@ -46,23 +62,24 @@ defmodule ArchEthicWeb.FaucetRateLimiter do %{ start_time: start_time, last_time: start_time, - transactions_count: 1, - blocked?: false + tx_count: 1, + archived?: false, + archived_since: nil } end - {:noreply, Map.put(state, tx_address, transaction_info)} + {:noreply, Map.put(state, address, transaction_info)} end - def handle_cast({:block, tx_address}, state) do - transaction = Map.get(state, tx_address) - transaction = %{transaction | blocked?: true} - new_state = Map.put(state, tx_address, transaction) + def handle_cast({:archive, address}, state) do + transaction = Map.get(state, address) + transaction = %{transaction | archived?: true, archived_since: System.monotonic_time()} + new_state = Map.put(state, address, transaction) {:noreply, new_state} end - def handle_info({:clean, tx_address}, state) do - {:noreply, Map.delete(state, tx_address)} + def handle_info({:clean, address}, state) do + {:noreply, Map.delete(state, address)} end def handle_info(:clean, state) do @@ -73,7 +90,7 @@ defmodule ArchEthicWeb.FaucetRateLimiter do Enum.filter(state, fn {_address, %{last_time: start_time}} -> millisecond_elapsed = System.convert_time_unit(now - start_time, :native, :millisecond) - millisecond_elapsed <= @block_period_expiry + millisecond_elapsed <= @archive_period_expiry end) |> Enum.into(%{}) From ca48c5c9a8699f32368a72ab4295341e48b4a13f Mon Sep 17 00:00:00 2001 From: "bl@ckode" Date: Wed, 30 Mar 2022 16:40:53 +0530 Subject: [PATCH 03/16] Add logic to prevent faucet transaction --- lib/archethic_web/controllers/faucet_controller.ex | 12 ++++++++++-- lib/archethic_web/supervisor.ex | 5 +++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/archethic_web/controllers/faucet_controller.ex b/lib/archethic_web/controllers/faucet_controller.ex index 16557ea69..1eb96b5c2 100644 --- a/lib/archethic_web/controllers/faucet_controller.ex +++ b/lib/archethic_web/controllers/faucet_controller.ex @@ -41,6 +41,8 @@ defmodule ArchEthicWeb.FaucetController do end def create_transfer(conn, %{"address" => address}) do + FaucetRateLimiter.register(address, System.monotonic_time()) + with {:ok, recipient_address} <- Base.decode16(address, case: :mixed), true <- Crypto.valid_address?(recipient_address), %{archived?: false} <- FaucetRateLimiter.get_address_archive_status(address), @@ -59,9 +61,15 @@ defmodule ArchEthicWeb.FaucetController do |> put_flash(:error, "Unable to send the transaction") |> render("index.html", address: address, link_address: "") - %{archived?: true} -> + %{archived?: true, archived_since: archived_since} -> + now = System.monotonic_time() + archived_elapsed_time = System.convert_time_unit(now - archived_since, :native, :second) + conn - |> put_flash(:error, "Archived address") + |> put_flash( + :error, + "Archived address, Try after #{ArchEthic.Utils.seconds_to_hh_mm_ss(archived_elapsed_time)}" + ) |> render("index.html", address: address, link_address: "") _ -> diff --git a/lib/archethic_web/supervisor.ex b/lib/archethic_web/supervisor.ex index e63dfdf6d..fbaf746b1 100644 --- a/lib/archethic_web/supervisor.ex +++ b/lib/archethic_web/supervisor.ex @@ -6,7 +6,7 @@ defmodule ArchEthicWeb.Supervisor do alias ArchEthic.Networking alias ArchEthicWeb.Endpoint - alias ArchEthicWeb.TransactionSubscriber + alias ArchEthicWeb.{FaucetRateLimiter, TransactionSubscriber} require Logger @@ -25,7 +25,8 @@ defmodule ArchEthicWeb.Supervisor do # Start the endpoint when the application starts Endpoint, {Absinthe.Subscription, Endpoint}, - TransactionSubscriber + TransactionSubscriber, + FaucetRateLimiter ] opts = [strategy: :one_for_one] From 4c936d2f426e8e0416b6af9aaadf237ba2057753 Mon Sep 17 00:00:00 2001 From: "bl@ckode" Date: Wed, 30 Mar 2022 17:43:43 +0530 Subject: [PATCH 04/16] Update function name --- lib/archethic/utils.ex | 7 +-- .../controllers/faucet_controller.ex | 4 +- lib/archethic_web/faucet_rate_limiter.ex | 18 +++++++ .../controllers/faucet_controller_test.exs | 53 ++++++++++++++++++- 4 files changed, 77 insertions(+), 5 deletions(-) diff --git a/lib/archethic/utils.ex b/lib/archethic/utils.ex index b3031459a..2d1d6ff7a 100644 --- a/lib/archethic/utils.ex +++ b/lib/archethic/utils.ex @@ -618,12 +618,13 @@ defmodule ArchEthic.Utils do ## Examples - iex> ArchEthic.Utils.seconds_to_hh_mm_ss(3666) + iex> ArchEthic.Utils.seconds_to_human_readable(3666) "1 hour 01 minute 06 second" """ - def seconds_to_hh_mm_ss(0), do: "00:00:00" + def seconds_to_human_readable(0), do: "00:00:00" - def seconds_to_hh_mm_ss(seconds) do + def seconds_to_human_readable(seconds) do + seconds = round(seconds) units = [3600, 60, 1] [h | t] = diff --git a/lib/archethic_web/controllers/faucet_controller.ex b/lib/archethic_web/controllers/faucet_controller.ex index 1eb96b5c2..3af964ded 100644 --- a/lib/archethic_web/controllers/faucet_controller.ex +++ b/lib/archethic_web/controllers/faucet_controller.ex @@ -16,6 +16,7 @@ defmodule ArchEthicWeb.FaucetController do alias ArchEthicWeb.FaucetRateLimiter @pool_seed Application.compile_env(:archethic, [__MODULE__, :seed]) + @faucet_rate_limit_expiry Application.compile_env(:archethic, :faucet_rate_limit_expiry) plug(:enabled) @@ -64,11 +65,12 @@ defmodule ArchEthicWeb.FaucetController do %{archived?: true, archived_since: archived_since} -> now = System.monotonic_time() archived_elapsed_time = System.convert_time_unit(now - archived_since, :native, :second) + archived_elapsed_diff = div(@faucet_rate_limit_expiry, 1000) - archived_elapsed_time conn |> put_flash( :error, - "Archived address, Try after #{ArchEthic.Utils.seconds_to_hh_mm_ss(archived_elapsed_time)}" + "Archived address, Try after #{ArchEthic.Utils.seconds_to_human_readable(archived_elapsed_diff)}" ) |> render("index.html", address: address, link_address: "") diff --git a/lib/archethic_web/faucet_rate_limiter.ex b/lib/archethic_web/faucet_rate_limiter.ex index 6cc59a4d8..37d7c279f 100644 --- a/lib/archethic_web/faucet_rate_limiter.ex +++ b/lib/archethic_web/faucet_rate_limiter.ex @@ -17,21 +17,35 @@ defmodule ArchEthicWeb.FaucetRateLimiter do """ @spec register(binary(), non_neg_integer()) :: :ok + ## Client Call backs def register(address, start_time) when is_binary(address) and is_integer(start_time) do GenServer.cast(__MODULE__, {:register, address, start_time}) end + def reset() do + GenServer.call(__MODULE__, :reset) + end + + def clean_address(address) do + GenServer.call(__MODULE__, {:clean, address}) + end + def get_address_archive_status(address) when is_binary(address) do GenServer.call(__MODULE__, {:archive_status, address}) end + # Server Call backs def init(_) do schedule_clean() {:ok, %{}} end + def handle_call(:reset, _from, _state) do + {:reply, :ok, %{}} + end + def handle_call({:archive_status, address}, _from, state) do reply = if address_state = Map.get(state, address) do @@ -43,6 +57,10 @@ defmodule ArchEthicWeb.FaucetRateLimiter do {:reply, reply, state} end + def handle_call({:clean, address}, _from, state) do + {:reply, :ok, Map.delete(state, address)} + end + def handle_cast({:register, address, start_time}, state) do transaction = Map.get(state, address) diff --git a/test/archethic_web/controllers/faucet_controller_test.exs b/test/archethic_web/controllers/faucet_controller_test.exs index ec36373c1..5dbcb16cf 100644 --- a/test/archethic_web/controllers/faucet_controller_test.exs +++ b/test/archethic_web/controllers/faucet_controller_test.exs @@ -25,6 +25,8 @@ defmodule ArchEthicWeb.FaucetControllerTest do TransactionData.UCOLedger } + alias ArchEthicWeb.FaucetRateLimiter + import Mox @pool_seed Application.compile_env(:archethic, [ArchEthicWeb.FaucetController, :seed]) @@ -48,7 +50,7 @@ defmodule ArchEthicWeb.FaucetControllerTest do describe "create_transfer/2" do test "should show success flash with tx URL on valid transaction", %{conn: conn} do recipient_address = "000098fe10e8633bce19c59a40a089731c1f72b097c5a8f7dc71a37eb26913aa4f80" - + FaucetRateLimiter.clean_address(recipient_address) tx = Transaction.new( :transfer, @@ -95,5 +97,54 @@ defmodule ArchEthicWeb.FaucetControllerTest do assert html_response(conn, 200) =~ "Malformed address" end + + test "should show error flash on faucet rate limit is reached", %{conn: conn} do + faucet_rate_limit = Application.get_env(:archethic, :faucet_rate_limit) + + recipient_address = "000098fe10e8633bce19c59a40a089731c1f72b097c5a8f7dc71a37eb26913aa4f80" + + tx = + Transaction.new( + :transfer, + %TransactionData{ + ledger: %Ledger{ + uco: %UCOLedger{ + transfers: [ + %UCOLedger.Transfer{ + to: recipient_address, + amount: 10_000_000_000 + } + ] + } + } + }, + @pool_seed, + 0, + Crypto.default_curve() + ) + + MockClient + |> stub(:send_message, fn + _, %GetLastTransactionAddress{}, _ -> + {:ok, %LastTransactionAddress{address: "1234"}} + + _, %GetTransactionChainLength{}, _ -> + {:ok, %TransactionChainLength{length: 0}} + + _, %StartMining{}, _ -> + PubSub.notify_new_transaction(tx.address) + + {:ok, %Ok{}} + end) + + faucet_requests = + for _request_index <- 1..faucet_rate_limit+1 do + post(conn, Routes.faucet_path(conn, :create_transfer), address: recipient_address) + end + + conn = List.last(faucet_requests) + + assert html_response(conn, 200) =~ "Archived address" + end end end From c7612e2d0eed0f363af2372f255984f4aa290b62 Mon Sep 17 00:00:00 2001 From: "bl@ckode" Date: Wed, 30 Mar 2022 18:02:06 +0530 Subject: [PATCH 05/16] Add module attribute @impl for GenServer implementation --- lib/archethic_web/faucet_rate_limiter.ex | 8 ++++++++ test/archethic_web/controllers/faucet_controller_test.exs | 7 ++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/archethic_web/faucet_rate_limiter.ex b/lib/archethic_web/faucet_rate_limiter.ex index 37d7c279f..0ee42d29d 100644 --- a/lib/archethic_web/faucet_rate_limiter.ex +++ b/lib/archethic_web/faucet_rate_limiter.ex @@ -37,15 +37,18 @@ defmodule ArchEthicWeb.FaucetRateLimiter do end # Server Call backs + @impl GenServer def init(_) do schedule_clean() {:ok, %{}} end + @impl GenServer def handle_call(:reset, _from, _state) do {:reply, :ok, %{}} end + @impl GenServer def handle_call({:archive_status, address}, _from, state) do reply = if address_state = Map.get(state, address) do @@ -57,10 +60,12 @@ defmodule ArchEthicWeb.FaucetRateLimiter do {:reply, reply, state} end + @impl GenServer def handle_call({:clean, address}, _from, state) do {:reply, :ok, Map.delete(state, address)} end + @impl GenServer def handle_cast({:register, address, start_time}, state) do transaction = Map.get(state, address) @@ -89,6 +94,7 @@ defmodule ArchEthicWeb.FaucetRateLimiter do {:noreply, Map.put(state, address, transaction_info)} end + @impl GenServer def handle_cast({:archive, address}, state) do transaction = Map.get(state, address) transaction = %{transaction | archived?: true, archived_since: System.monotonic_time()} @@ -96,10 +102,12 @@ defmodule ArchEthicWeb.FaucetRateLimiter do {:noreply, new_state} end + @impl GenServer def handle_info({:clean, address}, state) do {:noreply, Map.delete(state, address)} end + @impl GenServer def handle_info(:clean, state) do schedule_clean() now = System.monotonic_time() diff --git a/test/archethic_web/controllers/faucet_controller_test.exs b/test/archethic_web/controllers/faucet_controller_test.exs index 5dbcb16cf..9770f14de 100644 --- a/test/archethic_web/controllers/faucet_controller_test.exs +++ b/test/archethic_web/controllers/faucet_controller_test.exs @@ -51,6 +51,7 @@ defmodule ArchEthicWeb.FaucetControllerTest do test "should show success flash with tx URL on valid transaction", %{conn: conn} do recipient_address = "000098fe10e8633bce19c59a40a089731c1f72b097c5a8f7dc71a37eb26913aa4f80" FaucetRateLimiter.clean_address(recipient_address) + tx = Transaction.new( :transfer, @@ -100,7 +101,7 @@ defmodule ArchEthicWeb.FaucetControllerTest do test "should show error flash on faucet rate limit is reached", %{conn: conn} do faucet_rate_limit = Application.get_env(:archethic, :faucet_rate_limit) - + recipient_address = "000098fe10e8633bce19c59a40a089731c1f72b097c5a8f7dc71a37eb26913aa4f80" tx = @@ -137,8 +138,8 @@ defmodule ArchEthicWeb.FaucetControllerTest do {:ok, %Ok{}} end) - faucet_requests = - for _request_index <- 1..faucet_rate_limit+1 do + faucet_requests = + for _request_index <- 1..(faucet_rate_limit + 1) do post(conn, Routes.faucet_path(conn, :create_transfer), address: recipient_address) end From ae5f5add673db6bd52b356cb15f49a088749761c Mon Sep 17 00:00:00 2001 From: "bl@ckode" Date: Wed, 30 Mar 2022 18:32:14 +0530 Subject: [PATCH 06/16] Update human readable format converting to seconds --- lib/archethic/utils.ex | 3 ++- lib/archethic_web/controllers/faucet_controller.ex | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/archethic/utils.ex b/lib/archethic/utils.ex index 2d1d6ff7a..72a82f8df 100644 --- a/lib/archethic/utils.ex +++ b/lib/archethic/utils.ex @@ -634,6 +634,7 @@ defmodule ArchEthic.Utils do {h, t} = if length(t) == 0, do: {0, [h]}, else: {h, t} - "#{h} hour #{t |> Enum.map_join(" minute ", fn term -> term |> Integer.to_string() |> String.pad_leading(2, "0") end)} second" + base_unit = if length(t) > 1, do: "hour", else: "minute" + "#{h} #{base_unit} #{t |> Enum.map_join(" minute ", fn term -> term |> Integer.to_string() |> String.pad_leading(2, "0") end)} second" end end diff --git a/lib/archethic_web/controllers/faucet_controller.ex b/lib/archethic_web/controllers/faucet_controller.ex index 3af964ded..545a4dd0c 100644 --- a/lib/archethic_web/controllers/faucet_controller.ex +++ b/lib/archethic_web/controllers/faucet_controller.ex @@ -42,6 +42,7 @@ defmodule ArchEthicWeb.FaucetController do end def create_transfer(conn, %{"address" => address}) do + FaucetRateLimiter.register(address, System.monotonic_time()) with {:ok, recipient_address} <- Base.decode16(address, case: :mixed), From 47b24ccf0de369efffb191d7a35fd46d980343a3 Mon Sep 17 00:00:00 2001 From: "bl@ckode" Date: Wed, 30 Mar 2022 21:54:45 +0530 Subject: [PATCH 07/16] Update logic of archiving using map update/4 --- lib/archethic/utils.ex | 1 + .../controllers/faucet_controller.ex | 1 - lib/archethic_web/faucet_rate_limiter.ex | 55 +++++++++---------- 3 files changed, 27 insertions(+), 30 deletions(-) diff --git a/lib/archethic/utils.ex b/lib/archethic/utils.ex index 72a82f8df..61b875ad9 100644 --- a/lib/archethic/utils.ex +++ b/lib/archethic/utils.ex @@ -635,6 +635,7 @@ defmodule ArchEthic.Utils do {h, t} = if length(t) == 0, do: {0, [h]}, else: {h, t} base_unit = if length(t) > 1, do: "hour", else: "minute" + "#{h} #{base_unit} #{t |> Enum.map_join(" minute ", fn term -> term |> Integer.to_string() |> String.pad_leading(2, "0") end)} second" end end diff --git a/lib/archethic_web/controllers/faucet_controller.ex b/lib/archethic_web/controllers/faucet_controller.ex index 545a4dd0c..3af964ded 100644 --- a/lib/archethic_web/controllers/faucet_controller.ex +++ b/lib/archethic_web/controllers/faucet_controller.ex @@ -42,7 +42,6 @@ defmodule ArchEthicWeb.FaucetController do end def create_transfer(conn, %{"address" => address}) do - FaucetRateLimiter.register(address, System.monotonic_time()) with {:ok, recipient_address} <- Base.decode16(address, case: :mixed), diff --git a/lib/archethic_web/faucet_rate_limiter.ex b/lib/archethic_web/faucet_rate_limiter.ex index 0ee42d29d..3d86cd4a6 100644 --- a/lib/archethic_web/faucet_rate_limiter.ex +++ b/lib/archethic_web/faucet_rate_limiter.ex @@ -67,39 +67,36 @@ defmodule ArchEthicWeb.FaucetRateLimiter do @impl GenServer def handle_cast({:register, address, start_time}, state) do - transaction = Map.get(state, address) + initial_tx_setup = %{ + start_time: start_time, + last_time: start_time, + tx_count: 1, + archived?: false, + archived_since: nil + } + + updated_state = + Map.update(state, address, initial_tx_setup, fn + %{tx_count: tx_count} = transaction when tx_count + 1 == @faucet_rate_limit -> + tx_count = transaction.tx_count + 1 - transaction_info = - if transaction do - tx_count = transaction.tx_count + 1 - - if tx_count == @faucet_rate_limit do - GenServer.cast(__MODULE__, {:archive, address}) Process.send_after(self(), {:clean, address}, @archive_period_expiry) - end - - transaction - |> Map.put(:last_time, start_time) - |> Map.put(:tx_count, tx_count) - else - %{ - start_time: start_time, - last_time: start_time, - tx_count: 1, - archived?: false, - archived_since: nil - } - end - {:noreply, Map.put(state, address, transaction_info)} - end + %{ + transaction + | archived?: true, + archived_since: System.monotonic_time(), + last_time: start_time, + tx_count: tx_count + 1 + } + + %{tx_count: tx_count} = transaction -> + transaction + |> Map.put(:last_time, start_time) + |> Map.put(:tx_count, tx_count + 1) + end) - @impl GenServer - def handle_cast({:archive, address}, state) do - transaction = Map.get(state, address) - transaction = %{transaction | archived?: true, archived_since: System.monotonic_time()} - new_state = Map.put(state, address, transaction) - {:noreply, new_state} + {:noreply, updated_state} end @impl GenServer From 3cbfc5c7f32322e5b85cea2841e838924488e92e Mon Sep 17 00:00:00 2001 From: "bl@ckode" Date: Wed, 30 Mar 2022 21:59:16 +0530 Subject: [PATCH 08/16] Update the flash message for blocked address --- lib/archethic_web/controllers/faucet_controller.ex | 2 +- test/archethic_web/controllers/faucet_controller_test.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/archethic_web/controllers/faucet_controller.ex b/lib/archethic_web/controllers/faucet_controller.ex index 3af964ded..335c99ec5 100644 --- a/lib/archethic_web/controllers/faucet_controller.ex +++ b/lib/archethic_web/controllers/faucet_controller.ex @@ -70,7 +70,7 @@ defmodule ArchEthicWeb.FaucetController do conn |> put_flash( :error, - "Archived address, Try after #{ArchEthic.Utils.seconds_to_human_readable(archived_elapsed_diff)}" + "Blocked address temporarily. Try after #{ArchEthic.Utils.seconds_to_human_readable(archived_elapsed_diff)}" ) |> render("index.html", address: address, link_address: "") diff --git a/test/archethic_web/controllers/faucet_controller_test.exs b/test/archethic_web/controllers/faucet_controller_test.exs index 9770f14de..3d6163e52 100644 --- a/test/archethic_web/controllers/faucet_controller_test.exs +++ b/test/archethic_web/controllers/faucet_controller_test.exs @@ -145,7 +145,7 @@ defmodule ArchEthicWeb.FaucetControllerTest do conn = List.last(faucet_requests) - assert html_response(conn, 200) =~ "Archived address" + assert html_response(conn, 200) =~ "Blocked address" end end end From f49953df498e757ddf0506f5701219b26602110d Mon Sep 17 00:00:00 2001 From: "bl@ckode" Date: Wed, 30 Mar 2022 22:13:15 +0530 Subject: [PATCH 09/16] Update the registering the transaction to monitor --- .../controllers/faucet_controller.ex | 13 +++++------ lib/archethic_web/faucet_rate_limiter.ex | 22 +++++++++---------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/lib/archethic_web/controllers/faucet_controller.ex b/lib/archethic_web/controllers/faucet_controller.ex index 335c99ec5..c025c25d5 100644 --- a/lib/archethic_web/controllers/faucet_controller.ex +++ b/lib/archethic_web/controllers/faucet_controller.ex @@ -42,11 +42,10 @@ defmodule ArchEthicWeb.FaucetController do end def create_transfer(conn, %{"address" => address}) do - FaucetRateLimiter.register(address, System.monotonic_time()) - with {:ok, recipient_address} <- Base.decode16(address, case: :mixed), true <- Crypto.valid_address?(recipient_address), - %{archived?: false} <- FaucetRateLimiter.get_address_archive_status(address), + %{blocked?: false} <- FaucetRateLimiter.get_address_block_status(address), + :ok <- FaucetRateLimiter.register(address, System.monotonic_time()), {:ok, tx_address} <- transfer(recipient_address) do TransactionSubscriber.register(tx_address, System.monotonic_time()) @@ -62,15 +61,15 @@ defmodule ArchEthicWeb.FaucetController do |> put_flash(:error, "Unable to send the transaction") |> render("index.html", address: address, link_address: "") - %{archived?: true, archived_since: archived_since} -> + %{blocked?: true, blocked_since: blocked_since} -> now = System.monotonic_time() - archived_elapsed_time = System.convert_time_unit(now - archived_since, :native, :second) - archived_elapsed_diff = div(@faucet_rate_limit_expiry, 1000) - archived_elapsed_time + blocked_elapsed_time = System.convert_time_unit(now - blocked_since, :native, :second) + blocked_elapsed_diff = div(@faucet_rate_limit_expiry, 1000) - blocked_elapsed_time conn |> put_flash( :error, - "Blocked address temporarily. Try after #{ArchEthic.Utils.seconds_to_human_readable(archived_elapsed_diff)}" + "Blocked address temporarily. Try after #{ArchEthic.Utils.seconds_to_human_readable(blocked_elapsed_diff)}" ) |> render("index.html", address: address, link_address: "") diff --git a/lib/archethic_web/faucet_rate_limiter.ex b/lib/archethic_web/faucet_rate_limiter.ex index 3d86cd4a6..4754cf0c1 100644 --- a/lib/archethic_web/faucet_rate_limiter.ex +++ b/lib/archethic_web/faucet_rate_limiter.ex @@ -5,7 +5,7 @@ defmodule ArchEthicWeb.FaucetRateLimiter do @faucet_rate_limit Application.compile_env!(:archethic, :faucet_rate_limit) @faucet_rate_limit_expiry Application.compile_env!(:archethic, :faucet_rate_limit_expiry) - @archive_period_expiry @faucet_rate_limit_expiry + @block_period_expiry @faucet_rate_limit_expiry @clean_time @faucet_rate_limit_expiry def start_link(opts) do @@ -31,9 +31,9 @@ defmodule ArchEthicWeb.FaucetRateLimiter do GenServer.call(__MODULE__, {:clean, address}) end - def get_address_archive_status(address) + def get_address_block_status(address) when is_binary(address) do - GenServer.call(__MODULE__, {:archive_status, address}) + GenServer.call(__MODULE__, {:block_status, address}) end # Server Call backs @@ -49,12 +49,12 @@ defmodule ArchEthicWeb.FaucetRateLimiter do end @impl GenServer - def handle_call({:archive_status, address}, _from, state) do + def handle_call({:block_status, address}, _from, state) do reply = if address_state = Map.get(state, address) do address_state else - %{archived?: false} + %{blocked?: false} end {:reply, reply, state} @@ -71,8 +71,8 @@ defmodule ArchEthicWeb.FaucetRateLimiter do start_time: start_time, last_time: start_time, tx_count: 1, - archived?: false, - archived_since: nil + blocked?: false, + blocked_since: nil } updated_state = @@ -80,12 +80,12 @@ defmodule ArchEthicWeb.FaucetRateLimiter do %{tx_count: tx_count} = transaction when tx_count + 1 == @faucet_rate_limit -> tx_count = transaction.tx_count + 1 - Process.send_after(self(), {:clean, address}, @archive_period_expiry) + Process.send_after(self(), {:clean, address}, @block_period_expiry) %{ transaction - | archived?: true, - archived_since: System.monotonic_time(), + | blocked?: true, + blocked_since: System.monotonic_time(), last_time: start_time, tx_count: tx_count + 1 } @@ -113,7 +113,7 @@ defmodule ArchEthicWeb.FaucetRateLimiter do Enum.filter(state, fn {_address, %{last_time: start_time}} -> millisecond_elapsed = System.convert_time_unit(now - start_time, :native, :millisecond) - millisecond_elapsed <= @archive_period_expiry + millisecond_elapsed <= @block_period_expiry end) |> Enum.into(%{}) From ad4b6b6af6a23950cbcd6cd4ae92889714c34f3c Mon Sep 17 00:00:00 2001 From: "bl@ckode" Date: Thu, 31 Mar 2022 10:09:03 +0530 Subject: [PATCH 10/16] Fix Credo errors --- lib/archethic/utils.ex | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/archethic/utils.ex b/lib/archethic/utils.ex index 61b875ad9..8534c8aee 100644 --- a/lib/archethic/utils.ex +++ b/lib/archethic/utils.ex @@ -620,6 +620,12 @@ defmodule ArchEthic.Utils do iex> ArchEthic.Utils.seconds_to_human_readable(3666) "1 hour 01 minute 06 second" + + iex> ArchEthic.Utils.seconds_to_human_readable(66) + "1 minute 06 second" + + iex> ArchEthic.Utils.seconds_to_human_readable(6) + "0 minute 06 second" """ def seconds_to_human_readable(0), do: "00:00:00" @@ -632,7 +638,7 @@ defmodule ArchEthic.Utils do |> elem(0) |> Enum.drop_while(&match?(0, &1)) - {h, t} = if length(t) == 0, do: {0, [h]}, else: {h, t} + {h, t} = if t == [], do: {0, [h]}, else: {h, t} base_unit = if length(t) > 1, do: "hour", else: "minute" From b84c177efae703866c253b90eb9f5eb859479a1a Mon Sep 17 00:00:00 2001 From: "bl@ckode" Date: Thu, 31 Mar 2022 12:18:31 +0530 Subject: [PATCH 11/16] Update note about rate limit in faucet page --- assets/css/app.scss | 37 ++++++++++++++++++- assets/css/variables.scss | 24 +++++++++++- .../templates/faucet/index.html.eex | 3 ++ lib/archethic_web/views/faucet_view.ex | 7 ++++ 4 files changed, 69 insertions(+), 2 deletions(-) diff --git a/assets/css/app.scss b/assets/css/app.scss index 3b0c77ec9..f04846fb9 100644 --- a/assets/css/app.scss +++ b/assets/css/app.scss @@ -52,4 +52,39 @@ 95% {opacity: 1;} 100% {opacity: 0;} } -} \ No newline at end of file +} + +.note{ + opacity: .9; +} +// colors +.text-primary{ + color: $primary; +} +.text-secondary{ + color: $gray; +} +.text-success{ + color: $success; +} +.text-danger{ + color: $danger; +} +.text-warning{ + color: $warning; +} +.text-info{ + color: $info; +} +.text-light{ + color: $light; +} +.text-dark{ + color: $dark; +} +.text-muted{ + color: $gray; +} +.text-white{ + color: $white; +} diff --git a/assets/css/variables.scss b/assets/css/variables.scss index 3e98277f7..c9547ed80 100644 --- a/assets/css/variables.scss +++ b/assets/css/variables.scss @@ -12,4 +12,26 @@ $navbar-item-hover-background-color: transparent; $card-header-background-color: #fff; -$modal-background-background-color: #0a0a0a61; \ No newline at end of file +$modal-background-background-color: #0a0a0a61; + +//color variables +$blue: #007bff; +$indigo: #6610f2; +$purple: #6f42c1; +$pink: #e83e8c; +$red: #dc3545; +$orange: #fd7e14; +$yellow: #ffc107; +$green: #28a745; +$teal: #20c997; +$cyan: #17a2b8; +$white: #fff; +$gray: #6c757d; +$gray-dark: #343a40; +$secondary: #6c757d; +$success: #28a745; +$info: #17a2b8; +$warning: #ffc107; +$danger: #dc3545; +$light: #f8f9fa; +$dark: #343a40; diff --git a/lib/archethic_web/templates/faucet/index.html.eex b/lib/archethic_web/templates/faucet/index.html.eex index cd7f83b18..a69a0d523 100644 --- a/lib/archethic_web/templates/faucet/index.html.eex +++ b/lib/archethic_web/templates/faucet/index.html.eex @@ -24,6 +24,9 @@ +
+ NOTE : <%= faucet_rate_limit_message() %> +

Don't have a wallet address? Learn how to create a new one!
diff --git a/lib/archethic_web/views/faucet_view.ex b/lib/archethic_web/views/faucet_view.ex index db6097111..72c09a505 100644 --- a/lib/archethic_web/views/faucet_view.ex +++ b/lib/archethic_web/views/faucet_view.ex @@ -1,3 +1,10 @@ defmodule ArchEthicWeb.FaucetView do use ArchEthicWeb, :view + + def faucet_rate_limit_message() do + rate_limit = Application.get_env(:archethic, :faucet_rate_limit) + expiry = Application.get_env(:archethic, :faucet_rate_limit_expiry, 0) + + "Allowed only #{rate_limit} transactions for the period of #{ArchEthic.Utils.seconds_to_human_readable(expiry / 1000)}" + end end From 8715b6a5080727a180d5ce0d779ed119fc07d15b Mon Sep 17 00:00:00 2001 From: "bl@ckode" Date: Thu, 31 Mar 2022 13:36:33 +0530 Subject: [PATCH 12/16] Remove css color variables in favor of bulma classes --- assets/css/app.scss | 35 ------------------- assets/css/variables.scss | 22 ------------ .../templates/faucet/index.html.eex | 2 +- 3 files changed, 1 insertion(+), 58 deletions(-) diff --git a/assets/css/app.scss b/assets/css/app.scss index f04846fb9..92cd352c6 100644 --- a/assets/css/app.scss +++ b/assets/css/app.scss @@ -53,38 +53,3 @@ 100% {opacity: 0;} } } - -.note{ - opacity: .9; -} -// colors -.text-primary{ - color: $primary; -} -.text-secondary{ - color: $gray; -} -.text-success{ - color: $success; -} -.text-danger{ - color: $danger; -} -.text-warning{ - color: $warning; -} -.text-info{ - color: $info; -} -.text-light{ - color: $light; -} -.text-dark{ - color: $dark; -} -.text-muted{ - color: $gray; -} -.text-white{ - color: $white; -} diff --git a/assets/css/variables.scss b/assets/css/variables.scss index c9547ed80..e7f7bf9c4 100644 --- a/assets/css/variables.scss +++ b/assets/css/variables.scss @@ -13,25 +13,3 @@ $navbar-item-hover-background-color: transparent; $card-header-background-color: #fff; $modal-background-background-color: #0a0a0a61; - -//color variables -$blue: #007bff; -$indigo: #6610f2; -$purple: #6f42c1; -$pink: #e83e8c; -$red: #dc3545; -$orange: #fd7e14; -$yellow: #ffc107; -$green: #28a745; -$teal: #20c997; -$cyan: #17a2b8; -$white: #fff; -$gray: #6c757d; -$gray-dark: #343a40; -$secondary: #6c757d; -$success: #28a745; -$info: #17a2b8; -$warning: #ffc107; -$danger: #dc3545; -$light: #f8f9fa; -$dark: #343a40; diff --git a/lib/archethic_web/templates/faucet/index.html.eex b/lib/archethic_web/templates/faucet/index.html.eex index a69a0d523..9e77f3d13 100644 --- a/lib/archethic_web/templates/faucet/index.html.eex +++ b/lib/archethic_web/templates/faucet/index.html.eex @@ -25,7 +25,7 @@
- NOTE : <%= faucet_rate_limit_message() %> + NOTE : <%= faucet_rate_limit_message() %>

Don't have a wallet address? Learn how to create a new one! From 79b7ad004502f203ea96a180a8f9409686463792 Mon Sep 17 00:00:00 2001 From: "bl@ckode" Date: Thu, 31 Mar 2022 13:57:23 +0530 Subject: [PATCH 13/16] Update staring faucet rate limit genserve only if enabled from config --- lib/archethic_web/supervisor.ex | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/lib/archethic_web/supervisor.ex b/lib/archethic_web/supervisor.ex index fbaf746b1..d1a29a39e 100644 --- a/lib/archethic_web/supervisor.ex +++ b/lib/archethic_web/supervisor.ex @@ -20,14 +20,15 @@ defmodule ArchEthicWeb.Supervisor do try_open_port(Keyword.get(endpoint_conf, :http)) - children = [ - {Phoenix.PubSub, [name: ArchEthicWeb.PubSub, adapter: Phoenix.PubSub.PG2]}, - # Start the endpoint when the application starts - Endpoint, - {Absinthe.Subscription, Endpoint}, - TransactionSubscriber, - FaucetRateLimiter - ] + children = + [ + {Phoenix.PubSub, [name: ArchEthicWeb.PubSub, adapter: Phoenix.PubSub.PG2]}, + # Start the endpoint when the application starts + Endpoint, + {Absinthe.Subscription, Endpoint}, + TransactionSubscriber + ] + |> add_facucet_rate_limit_child() opts = [strategy: :one_for_one] Supervisor.init(children, opts) @@ -39,4 +40,14 @@ defmodule ArchEthicWeb.Supervisor do port = Keyword.get(conf, :port) Networking.try_open_port(port, false) end + + defp add_facucet_rate_limit_child(children) do + faucet_config = Application.get_env(:archethic, ArchEthicWeb.FaucetController, []) + + if faucet_config[:enabled] do + children ++ [FaucetRateLimiter] + else + children + end + end end From 9efb8c03aad775bb6ed39a668ad97c291b89306e Mon Sep 17 00:00:00 2001 From: "bl@ckode" Date: Thu, 31 Mar 2022 14:04:10 +0530 Subject: [PATCH 14/16] Update flash message for faucet rate limit exceeded --- lib/archethic_web/controllers/faucet_controller.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/archethic_web/controllers/faucet_controller.ex b/lib/archethic_web/controllers/faucet_controller.ex index c025c25d5..e080109d5 100644 --- a/lib/archethic_web/controllers/faucet_controller.ex +++ b/lib/archethic_web/controllers/faucet_controller.ex @@ -69,7 +69,7 @@ defmodule ArchEthicWeb.FaucetController do conn |> put_flash( :error, - "Blocked address temporarily. Try after #{ArchEthic.Utils.seconds_to_human_readable(blocked_elapsed_diff)}" + "Blocked address as you exceeded usage limit of UCO temporarily. Try after #{ArchEthic.Utils.seconds_to_human_readable(blocked_elapsed_diff)}" ) |> render("index.html", address: address, link_address: "") From 3a5c30e81534f4d5e8d51f9f0d0fd89de6e99499 Mon Sep 17 00:00:00 2001 From: "bl@ckode" Date: Thu, 31 Mar 2022 16:28:02 +0530 Subject: [PATCH 15/16] Remove unused functions --- lib/archethic_web/faucet_rate_limiter.ex | 26 ++++++++++++++---------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/lib/archethic_web/faucet_rate_limiter.ex b/lib/archethic_web/faucet_rate_limiter.ex index 4754cf0c1..0be1e6c13 100644 --- a/lib/archethic_web/faucet_rate_limiter.ex +++ b/lib/archethic_web/faucet_rate_limiter.ex @@ -8,29 +8,38 @@ defmodule ArchEthicWeb.FaucetRateLimiter do @block_period_expiry @faucet_rate_limit_expiry @clean_time @faucet_rate_limit_expiry + @type address_status :: %{ + start_time: non_neg_integer(), + last_time: non_neg_integer(), + tx_count: non_neg_integer(), + blocked?: boolean(), + blocked_since: non_neg_integer() + } + def start_link(opts) do GenServer.start_link(__MODULE__, opts, name: __MODULE__) end + ## Client Call backs + @doc """ Register a faucet transaction address to monitor """ @spec register(binary(), non_neg_integer()) :: :ok - - ## Client Call backs def register(address, start_time) when is_binary(address) and is_integer(start_time) do GenServer.cast(__MODULE__, {:register, address, start_time}) end - def reset() do - GenServer.call(__MODULE__, :reset) - end - + @doc """ + Cleans particular address. + """ + @spec clean_address(binary()) :: :ok def clean_address(address) do GenServer.call(__MODULE__, {:clean, address}) end + @spec get_address_block_status(binary()) :: address_status() def get_address_block_status(address) when is_binary(address) do GenServer.call(__MODULE__, {:block_status, address}) @@ -43,11 +52,6 @@ defmodule ArchEthicWeb.FaucetRateLimiter do {:ok, %{}} end - @impl GenServer - def handle_call(:reset, _from, _state) do - {:reply, :ok, %{}} - end - @impl GenServer def handle_call({:block_status, address}, _from, state) do reply = From af87a1d394e44ade728a5fae5186a898627105f1 Mon Sep 17 00:00:00 2001 From: "bl@ckode" Date: Thu, 31 Mar 2022 17:45:11 +0530 Subject: [PATCH 16/16] Remove doc for clean_address function --- lib/archethic_web/faucet_rate_limiter.ex | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/archethic_web/faucet_rate_limiter.ex b/lib/archethic_web/faucet_rate_limiter.ex index 0be1e6c13..9e78b991f 100644 --- a/lib/archethic_web/faucet_rate_limiter.ex +++ b/lib/archethic_web/faucet_rate_limiter.ex @@ -31,9 +31,7 @@ defmodule ArchEthicWeb.FaucetRateLimiter do GenServer.cast(__MODULE__, {:register, address, start_time}) end - @doc """ - Cleans particular address. - """ + @doc false @spec clean_address(binary()) :: :ok def clean_address(address) do GenServer.call(__MODULE__, {:clean, address})