From a4aa21e3b500434efe253fc2989a2edf36f2e338 Mon Sep 17 00:00:00 2001 From: Bastien CHAMAGNE Date: Fri, 9 Dec 2022 16:29:13 +0100 Subject: [PATCH 01/10] Transform chain route into a live view (for infinite scrolling) --- .../controllers/explorer_controller.ex | 123 ------------------ lib/archethic_web/explorer_router.ex | 3 +- .../live/chains/transaction_live.ex | 101 ++++++++++++++ .../templates/explorer/chain.html.heex | 22 +--- .../explorer/transaction_details.html.heex | 2 +- .../templates/layout/root.html.heex | 2 +- .../templates/node/details.html.heex | 2 +- 7 files changed, 109 insertions(+), 146 deletions(-) create mode 100644 lib/archethic_web/live/chains/transaction_live.ex diff --git a/lib/archethic_web/controllers/explorer_controller.ex b/lib/archethic_web/controllers/explorer_controller.ex index e90eccede..48f71969f 100644 --- a/lib/archethic_web/controllers/explorer_controller.ex +++ b/lib/archethic_web/controllers/explorer_controller.ex @@ -3,7 +3,6 @@ defmodule ArchethicWeb.ExplorerController do use ArchethicWeb, :controller - alias Archethic.OracleChain alias Archethic.Crypto alias Archethic.TransactionChain.Transaction @@ -23,126 +22,4 @@ defmodule ArchethicWeb.ExplorerController do render(conn, "404.html") end end - - def chain(conn, _params = %{"address" => address, "last" => "on"}) do - with {:ok, addr} <- Base.decode16(address, case: :mixed), - true <- Crypto.valid_address?(addr), - {:ok, %Transaction{address: last_address}} <- Archethic.get_last_transaction(addr), - {:ok, chain} <- Archethic.get_transaction_chain(last_address), - {:ok, %{uco: uco_balance}} <- Archethic.get_balance(addr), - uco_price <- DateTime.utc_now() |> OracleChain.get_uco_price() do - render(conn, "chain.html", - transaction_chain: List.flatten(chain), - chain_size: Enum.count(chain), - address: addr, - uco_balance: uco_balance, - last_checked?: true, - uco_price: uco_price - ) - else - :error -> - render(conn, "chain.html", - transaction_chain: [], - chain_size: 0, - address: "", - last_checked?: true, - error: :invalid_address, - uco_balance: 0, - uco_price: [eur: 0.05, usd: 0.07] - ) - - {:error, _} -> - render(conn, "chain.html", - transaction_chain: [], - chain_size: 0, - address: "", - last_checked?: true, - error: :network_issue, - uco_balance: 0, - uco_price: [eur: 0.05, usd: 0.07] - ) - - false -> - render(conn, "chain.html", - transaction_chain: [], - chain_size: 0, - address: "", - last_checked?: true, - error: :invalid_address, - uco_balance: 0, - uco_price: [eur: 0.05, usd: 0.07] - ) - - _ -> - render(conn, "chain.html", - transaction_chain: [], - chain_size: 0, - address: Base.decode16!(address, case: :mixed), - last_checked?: true, - uco_balance: 0, - uco_price: [eur: 0.05, usd: 0.07] - ) - end - end - - def chain(conn, _params = %{"address" => address}) do - with {:ok, addr} <- Base.decode16(address, case: :mixed), - true <- Crypto.valid_address?(addr), - {:ok, chain} <- Archethic.get_transaction_chain(addr), - {:ok, %{uco: uco_balance}} <- Archethic.get_balance(addr), - uco_price <- DateTime.utc_now() |> OracleChain.get_uco_price() do - render(conn, "chain.html", - transaction_chain: List.flatten(chain), - address: addr, - chain_size: Enum.count(chain), - uco_balance: uco_balance, - last_checked?: false, - uco_price: uco_price - ) - else - :error -> - render(conn, "chain.html", - transaction_chain: [], - address: "", - chain_size: 0, - uco_balance: 0, - last_checked?: false, - error: :invalid_address, - uco_price: [eur: 0.05, usd: 0.07] - ) - - false -> - render(conn, "chain.html", - transaction_chain: [], - address: "", - chain_size: 0, - uco_balance: 0, - last_checked?: false, - error: :invalid_address, - uco_price: [eur: 0.05, usd: 0.07] - ) - - {:error, _} -> - render(conn, "chain.html", - transaction_chain: [], - address: "", - chain_size: 0, - uco_balance: 0, - last_checked?: false, - error: :network_issue, - uco_price: [eur: 0.05, usd: 0.07] - ) - end - end - - def chain(conn, _params) do - render(conn, "chain.html", - transaction_chain: [], - address: "", - chain_size: 0, - last_checked?: false, - uco_balance: 0, - uco_price: [eur: 0.05, usd: 0.07] - ) - end end diff --git a/lib/archethic_web/explorer_router.ex b/lib/archethic_web/explorer_router.ex index befc56884..09baf637a 100644 --- a/lib/archethic_web/explorer_router.ex +++ b/lib/archethic_web/explorer_router.ex @@ -46,8 +46,7 @@ defmodule ArchethicWeb.ExplorerRouter do live("/transaction/:address", TransactionDetailsLive) - get("/chain", ExplorerController, :chain) - + live("/chain", TransactionChainLive) live("/chain/oracle", OracleChainLive) live("/chain/beacon", BeaconChainLive) live("/chain/rewards", RewardChainLive) diff --git a/lib/archethic_web/live/chains/transaction_live.ex b/lib/archethic_web/live/chains/transaction_live.ex new file mode 100644 index 000000000..3597ee6a2 --- /dev/null +++ b/lib/archethic_web/live/chains/transaction_live.ex @@ -0,0 +1,101 @@ +defmodule ArchethicWeb.TransactionChainLive do + @moduledoc false + + use ArchethicWeb, :live_view + + alias Archethic.Crypto + alias Archethic.OracleChain + alias Archethic.TransactionChain.Transaction + + alias ArchethicWeb.{ExplorerView} + + alias Phoenix.{View} + + @spec mount(map(), map(), Phoenix.LiveView.Socket.t()) :: + {:ok, Phoenix.LiveView.Socket.t()} + def mount(params, _session, socket) do + case params["address"] do + nil -> + {:ok, + assign(socket, %{ + transaction_chain: [], + address: "", + chain_size: 0, + uco_balance: 0, + uco_price: [eur: 0.05, usd: 0.07] + })} + + address -> + {:ok, assign(socket, get_paginated_transaction_chain(address))} + end + end + + @spec render(Phoenix.LiveView.Socket.assigns()) :: Phoenix.LiveView.Rendered.t() + def render(assigns) do + View.render(ExplorerView, "chain.html", assigns) + end + + defp get_paginated_transaction_chain(address) do + with {:ok, addr} <- Base.decode16(address, case: :mixed), + true <- Crypto.valid_address?(addr), + {:ok, %Transaction{address: last_address}} <- Archethic.get_last_transaction(addr), + {:ok, chain_length} <- Archethic.get_transaction_chain_length(last_address), + # ------------- + + # {:ok, chain} <- Archethic.get_transaction_chain(last_address), + {:ok, chain} <- Archethic.get_transaction_chain_by_paging_address(last_address, addr), + # ------------- + {:ok, %{uco: uco_balance}} <- Archethic.get_balance(addr), + uco_price <- DateTime.utc_now() |> OracleChain.get_uco_price() do + IO.inspect(chain_length, label: "chainlength") + + %{ + transaction_chain: List.flatten(chain), + address: addr, + chain_size: chain_length, + uco_balance: uco_balance, + uco_price: uco_price + } + else + :error -> + %{ + transaction_chain: [], + address: "", + chain_size: 0, + uco_balance: 0, + error: "Invalid address", + uco_price: [eur: 0.05, usd: 0.07] + } + + false -> + %{ + transaction_chain: [], + address: "", + chain_size: 0, + uco_balance: 0, + error: "Invalid address", + uco_price: [eur: 0.05, usd: 0.07] + } + + {:error, :transaction_not_exists} -> + %{ + transaction_chain: [], + address: "", + chain_size: 0, + uco_balance: 0, + error: "Transaction not found", + uco_price: [eur: 0.05, usd: 0.07] + } + + {:error, _} -> + %{ + transaction_chain: [], + address: "", + chain_size: 0, + uco_balance: 0, + error: "Network issue", + uco_price: [eur: 0.05, usd: 0.07] + } + end + end +end diff --git a/lib/archethic_web/templates/explorer/chain.html.heex b/lib/archethic_web/templates/explorer/chain.html.heex index ccf4859f1..fd871a1fa 100644 --- a/lib/archethic_web/templates/explorer/chain.html.heex +++ b/lib/archethic_web/templates/explorer/chain.html.heex @@ -6,7 +6,7 @@
-
+
@@ -16,26 +16,12 @@
- <%= if assigns[:error] != nil and @error == :invalid_address do %> -

Invalid address

+ <%= if assigns[:error] != nil do %> +

<%= @error %>

<% end %>
-
-
- -
-
-
-
- -
-
-
-
@@ -97,7 +83,7 @@ <%= for tx <- Enum.reverse(@transaction_chain) do %>
- <%= link to: Routes.live_path(@conn, ArchethicWeb.TransactionDetailsLive, Base.encode16(tx.address)) do%> + <%= link to: Routes.live_path(@socket, ArchethicWeb.TransactionDetailsLive, Base.encode16(tx.address)) do%> <%= Base.encode16(tx.address) %> <%= Base.encode16(:binary.part(tx.address, 0, 13)) %>... <% end %> diff --git a/lib/archethic_web/templates/explorer/transaction_details.html.heex b/lib/archethic_web/templates/explorer/transaction_details.html.heex index a90f2b7f7..88ba32244 100644 --- a/lib/archethic_web/templates/explorer/transaction_details.html.heex +++ b/lib/archethic_web/templates/explorer/transaction_details.html.heex @@ -23,7 +23,7 @@ <% end %>
- <%= link class: "button is-primary is-outlined is-fullwidth", to: Routes.explorer_path(@socket, :chain, address: Base.encode16(@address)) do%> + <%= link class: "button is-primary is-outlined is-fullwidth", to: Routes.live_path(@socket, ArchethicWeb.TransactionChainLive, address: Base.encode16(@address)) do%> Explore chain <% end %>
diff --git a/lib/archethic_web/templates/layout/root.html.heex b/lib/archethic_web/templates/layout/root.html.heex index 068707dee..7407c80a1 100644 --- a/lib/archethic_web/templates/layout/root.html.heex +++ b/lib/archethic_web/templates/layout/root.html.heex @@ -39,7 +39,7 @@ <% end %> +<% end %> From 64f4a7d8e53565e14dc0cc1c5fb1c12576480928 Mon Sep 17 00:00:00 2001 From: Bastien CHAMAGNE Date: Tue, 13 Dec 2022 11:08:15 +0100 Subject: [PATCH 03/10] sequential read 1 --- .../db/embedded_impl/chain_reader.ex | 71 ++++++++++--------- .../live/chains/transaction_live.ex | 4 +- 2 files changed, 40 insertions(+), 35 deletions(-) diff --git a/lib/archethic/db/embedded_impl/chain_reader.ex b/lib/archethic/db/embedded_impl/chain_reader.ex index 288bf47e3..b365dbaa5 100644 --- a/lib/archethic/db/embedded_impl/chain_reader.ex +++ b/lib/archethic/db/embedded_impl/chain_reader.ex @@ -150,10 +150,6 @@ defmodule Archethic.DB.EmbeddedImpl.ChainReader do def get_transaction_chain_desc(address, fields, opts, db_path) do start = System.monotonic_time() - # Always return transaction address - fields = if Enum.empty?(fields), do: fields, else: Enum.uniq([:address | fields]) - column_names = fields_to_column_names(fields) - case ChainIndex.get_tx_entry(address, db_path) do {:error, :not_exists} -> {[], false, ""} @@ -162,47 +158,54 @@ defmodule Archethic.DB.EmbeddedImpl.ChainReader do filepath = ChainWriter.chain_path(db_path, genesis_address) fd = File.open!(filepath, [:binary, :read]) - all_addresses = ChainIndex.list_chain_addresses(genesis_address, db_path) - - next_addresses = + all_addresses_desc = + ChainIndex.list_chain_addresses(genesis_address, db_path) + |> Enum.map(&elem(&1, 0)) + |> Enum.to_list() + |> Enum.reverse() + + # we could read from the file by moving cursor and reading bytes for every addresses + # but that would be slower than to sequentially read + # here we determine the limit and offset for the sequential read + # then we reverse the order of transactions + {limit_address, paging_state} = case Keyword.get(opts, :paging_state) do nil -> - all_addresses - |> Enum.to_list() - |> Enum.reverse() + {nil, + if length(all_addresses_desc) <= @page_size do + nil + else + all_addresses_desc + |> Enum.take(@page_size + 1) + |> List.last(nil) + end} paging_state -> - all_addresses - |> Enum.to_list() - |> Enum.reverse() - |> Enum.drop_while(fn {addr, _} -> addr != paging_state end) - |> Enum.drop(1) + next_addresses = + all_addresses_desc + |> Enum.drop_while(&(&1 != paging_state)) + |> Enum.drop(1) + + limit_address = List.first(next_addresses, nil) + + {limit_address, + if length(next_addresses) <= @page_size do + nil + else + next_addresses + |> Enum.take(@page_size + 1) + |> List.last(nil) + end} end - next_addresses_limited = - Enum.take(next_addresses, Keyword.get(opts, :transactions_per_page, 10)) - - more? = length(next_addresses_limited) < length(next_addresses) - paging_state = List.last(next_addresses_limited, "") - - transactions = - next_addresses_limited - |> Enum.map(fn {addr, _timestamp} -> - {:ok, %{offset: offset}} = ChainIndex.get_tx_entry(addr, db_path) - - :file.position(fd, offset) - {:ok, <>} = :file.read(fd, 8) - - fd - |> read_transaction(column_names, size, 0) - |> decode_transaction_columns(version) - end) + {transactions, more?, paging_state} = + process_get_chain(fd, limit_address, fields, [paging_state: paging_state], db_path) :telemetry.execute([:archethic, :db], %{duration: System.monotonic_time() - start}, %{ query: "get_transaction_chain_desc" }) - {transactions, more?, paging_state} + {Enum.reverse(transactions), more?, paging_state} end end diff --git a/lib/archethic_web/live/chains/transaction_live.ex b/lib/archethic_web/live/chains/transaction_live.ex index 417d4a36f..871a6a92f 100644 --- a/lib/archethic_web/live/chains/transaction_live.ex +++ b/lib/archethic_web/live/chains/transaction_live.ex @@ -54,10 +54,12 @@ defmodule ArchethicWeb.TransactionChainLive do %{ transaction_chain: transaction_chain, last_address: last_address, + chain_size: size, page: page } = socket.assigns - with %Transaction{address: paging_address} <- List.last(transaction_chain), + with false <- length(transaction_chain) == size, + %Transaction{address: paging_address} <- List.last(transaction_chain), {:ok, last_address} <- Base.decode16(last_address, case: :mixed), {:ok, next_transactions} <- paginate_chain(last_address, paging_address) do {:noreply, From ce81bcde83f7bee18bf2ed9644151d6272c5d28e Mon Sep 17 00:00:00 2001 From: Bastien CHAMAGNE Date: Tue, 13 Dec 2022 12:10:57 +0100 Subject: [PATCH 04/10] new algo, no need to reverse --- .../db/embedded_impl/chain_reader.ex | 51 ++++++++----------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/lib/archethic/db/embedded_impl/chain_reader.ex b/lib/archethic/db/embedded_impl/chain_reader.ex index b365dbaa5..7a8d82be8 100644 --- a/lib/archethic/db/embedded_impl/chain_reader.ex +++ b/lib/archethic/db/embedded_impl/chain_reader.ex @@ -158,44 +158,35 @@ defmodule Archethic.DB.EmbeddedImpl.ChainReader do filepath = ChainWriter.chain_path(db_path, genesis_address) fd = File.open!(filepath, [:binary, :read]) - all_addresses_desc = + all_addresses_asc = ChainIndex.list_chain_addresses(genesis_address, db_path) |> Enum.map(&elem(&1, 0)) - |> Enum.to_list() - |> Enum.reverse() - # we could read from the file by moving cursor and reading bytes for every addresses - # but that would be slower than to sequentially read - # here we determine the limit and offset for the sequential read - # then we reverse the order of transactions + # in order to read the file sequentially in DESC (faster than random access) + # we have to determine the good paging_state and limit_address {limit_address, paging_state} = case Keyword.get(opts, :paging_state) do nil -> - {nil, - if length(all_addresses_desc) <= @page_size do - nil - else - all_addresses_desc - |> Enum.take(@page_size + 1) - |> List.last(nil) - end} + chain_length = Enum.count(all_addresses_asc) + + if chain_length <= @page_size do + {nil, nil} + else + {nil, all_addresses_asc |> Enum.at(chain_length - 1 - @page_size)} + end paging_state -> - next_addresses = - all_addresses_desc - |> Enum.drop_while(&(&1 != paging_state)) - |> Enum.drop(1) - - limit_address = List.first(next_addresses, nil) - - {limit_address, - if length(next_addresses) <= @page_size do - nil - else - next_addresses - |> Enum.take(@page_size + 1) - |> List.last(nil) - end} + paging_state_idx = + all_addresses_asc + |> Enum.find_index(&(&1 == paging_state)) + + limit_address = Enum.at(all_addresses_asc, paging_state_idx - 1) + + if paging_state_idx < @page_size do + {limit_address, nil} + else + {limit_address, all_addresses_asc |> Enum.at(paging_state_idx - @page_size)} + end end {transactions, more?, paging_state} = From 463dff577777d9a74bb3e081d1abf96c92c1a165 Mon Sep 17 00:00:00 2001 From: Bastien CHAMAGNE Date: Tue, 13 Dec 2022 15:28:50 +0100 Subject: [PATCH 05/10] Add tests and fix the return values of the function (more?, paging_state) --- .../db/embedded_impl/chain_reader.ex | 28 ++++++--- test/archethic/db/embedded_impl_test.exs | 59 +++++++++++++++++++ 2 files changed, 80 insertions(+), 7 deletions(-) diff --git a/lib/archethic/db/embedded_impl/chain_reader.ex b/lib/archethic/db/embedded_impl/chain_reader.ex index 7a8d82be8..8fd832aca 100644 --- a/lib/archethic/db/embedded_impl/chain_reader.ex +++ b/lib/archethic/db/embedded_impl/chain_reader.ex @@ -158,21 +158,22 @@ defmodule Archethic.DB.EmbeddedImpl.ChainReader do filepath = ChainWriter.chain_path(db_path, genesis_address) fd = File.open!(filepath, [:binary, :read]) + # TODO: MAYBE THIS ALL SHOULD LIVE IN THE PROCESS_GET_CHAIN WITH A ORDER PARAMETER all_addresses_asc = ChainIndex.list_chain_addresses(genesis_address, db_path) |> Enum.map(&elem(&1, 0)) # in order to read the file sequentially in DESC (faster than random access) # we have to determine the good paging_state and limit_address - {limit_address, paging_state} = + {limit_address, paging_state, more?} = case Keyword.get(opts, :paging_state) do nil -> chain_length = Enum.count(all_addresses_asc) if chain_length <= @page_size do - {nil, nil} + {nil, nil, false} else - {nil, all_addresses_asc |> Enum.at(chain_length - 1 - @page_size)} + {nil, all_addresses_asc |> Enum.at(chain_length - 1 - @page_size), true} end paging_state -> @@ -183,20 +184,33 @@ defmodule Archethic.DB.EmbeddedImpl.ChainReader do limit_address = Enum.at(all_addresses_asc, paging_state_idx - 1) if paging_state_idx < @page_size do - {limit_address, nil} + {limit_address, nil, false} else - {limit_address, all_addresses_asc |> Enum.at(paging_state_idx - @page_size)} + {limit_address, all_addresses_asc |> Enum.at(paging_state_idx - 1 - @page_size), + true} end end - {transactions, more?, paging_state} = + # we cannot use the more? and paging_state from here because they only work + # with ASC order + {transactions, _more?, _paging_state} = process_get_chain(fd, limit_address, fields, [paging_state: paging_state], db_path) :telemetry.execute([:archethic, :db], %{duration: System.monotonic_time() - start}, %{ query: "get_transaction_chain_desc" }) - {Enum.reverse(transactions), more?, paging_state} + new_paging_state = + if more? do + case List.first(transactions) do + nil -> "" + tx -> tx.address + end + else + "" + end + + {Enum.reverse(transactions), more?, new_paging_state} end end diff --git a/test/archethic/db/embedded_impl_test.exs b/test/archethic/db/embedded_impl_test.exs index 362fefd2a..cec979d21 100644 --- a/test/archethic/db/embedded_impl_test.exs +++ b/test/archethic/db/embedded_impl_test.exs @@ -362,6 +362,65 @@ defmodule Archethic.DB.EmbeddedTest do end end + describe "get_transaction_chain_desc/4" do + test "should return empty when there is no transactions" do + {pub_key, _} = Crypto.generate_deterministic_keypair("SEED") + address = Crypto.derive_address(pub_key) + + assert {[], false, ""} == EmbeddedImpl.get_transaction_chain_desc(address) + end + + test "should return all transactions if there are less than one page (10)" do + transactions = + Enum.map(1..9, fn i -> + TransactionFactory.create_valid_transaction([], + index: i, + timestamp: DateTime.utc_now() |> DateTime.add(i * 60) + ) + end) + + EmbeddedImpl.write_transaction_chain(transactions) + + {page, false, ""} = EmbeddedImpl.get_transaction_chain_desc(List.last(transactions).address) + assert length(page) == 9 + assert page == Enum.reverse(transactions) + end + + test "should return transactions paginated if there are more than one page (10)" do + transactions = + Enum.map(1..28, fn i -> + TransactionFactory.create_valid_transaction([], + index: i, + timestamp: DateTime.utc_now() |> DateTime.add(i * 60) + ) + end) + + EmbeddedImpl.write_transaction_chain(transactions) + + {page1, true, paging_state1} = + EmbeddedImpl.get_transaction_chain_desc(List.last(transactions).address) + + assert length(page1) == 10 + assert paging_state1 == List.last(page1).address + + {page2, true, paging_state2} = + EmbeddedImpl.get_transaction_chain_desc(List.last(transactions).address, [], + paging_state: paging_state1 + ) + + assert length(page2) == 10 + assert paging_state2 == List.last(page2).address + + {page3, false, ""} = + EmbeddedImpl.get_transaction_chain_desc(List.last(transactions).address, [], + paging_state: paging_state2 + ) + + assert length(page3) == 8 + assert page1 ++ page2 ++ page3 == Enum.reverse(transactions) + end + end + describe "scan_chain/4" do test "should return the list of all the transactions until the one given" do transactions = From b66c02bfa54d25cbea9db455f7481ef0a2aa3abf Mon Sep 17 00:00:00 2001 From: Bastien CHAMAGNE Date: Tue, 13 Dec 2022 15:57:49 +0100 Subject: [PATCH 06/10] move the logic in a private func --- .../db/embedded_impl/chain_reader.ex | 96 +++++++++---------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/lib/archethic/db/embedded_impl/chain_reader.ex b/lib/archethic/db/embedded_impl/chain_reader.ex index 8fd832aca..e811dfef7 100644 --- a/lib/archethic/db/embedded_impl/chain_reader.ex +++ b/lib/archethic/db/embedded_impl/chain_reader.ex @@ -158,59 +158,14 @@ defmodule Archethic.DB.EmbeddedImpl.ChainReader do filepath = ChainWriter.chain_path(db_path, genesis_address) fd = File.open!(filepath, [:binary, :read]) - # TODO: MAYBE THIS ALL SHOULD LIVE IN THE PROCESS_GET_CHAIN WITH A ORDER PARAMETER - all_addresses_asc = - ChainIndex.list_chain_addresses(genesis_address, db_path) - |> Enum.map(&elem(&1, 0)) - - # in order to read the file sequentially in DESC (faster than random access) - # we have to determine the good paging_state and limit_address - {limit_address, paging_state, more?} = - case Keyword.get(opts, :paging_state) do - nil -> - chain_length = Enum.count(all_addresses_asc) - - if chain_length <= @page_size do - {nil, nil, false} - else - {nil, all_addresses_asc |> Enum.at(chain_length - 1 - @page_size), true} - end - - paging_state -> - paging_state_idx = - all_addresses_asc - |> Enum.find_index(&(&1 == paging_state)) - - limit_address = Enum.at(all_addresses_asc, paging_state_idx - 1) - - if paging_state_idx < @page_size do - {limit_address, nil, false} - else - {limit_address, all_addresses_asc |> Enum.at(paging_state_idx - 1 - @page_size), - true} - end - end - - # we cannot use the more? and paging_state from here because they only work - # with ASC order - {transactions, _more?, _paging_state} = - process_get_chain(fd, limit_address, fields, [paging_state: paging_state], db_path) + {transactions, more?, paging_state} = + process_get_chain_desc(fd, genesis_address, fields, opts, db_path) :telemetry.execute([:archethic, :db], %{duration: System.monotonic_time() - start}, %{ query: "get_transaction_chain_desc" }) - new_paging_state = - if more? do - case List.first(transactions) do - nil -> "" - tx -> tx.address - end - else - "" - end - - {Enum.reverse(transactions), more?, new_paging_state} + {transactions, more?, paging_state} end end @@ -314,6 +269,51 @@ defmodule Archethic.DB.EmbeddedImpl.ChainReader do {transactions, more?, paging_state} end + # in order to read the file sequentially in DESC (faster than random access) + # we have to determined the correct paging_state and limit_address + # then we can use the process_get_chain that does the ASC read + defp process_get_chain_desc(fd, genesis_address, fields, opts, db_path) do + all_addresses_asc = + ChainIndex.list_chain_addresses(genesis_address, db_path) + |> Enum.map(&elem(&1, 0)) + + {limit_address, paging_state, more?, new_paging_state} = + case Keyword.get(opts, :paging_state) do + nil -> + chain_length = Enum.count(all_addresses_asc) + + if chain_length <= @page_size do + {nil, nil, false, ""} + else + idx = chain_length - 1 - @page_size + + {nil, all_addresses_asc |> Enum.at(idx), true, all_addresses_asc |> Enum.at(idx + 1)} + end + + paging_state -> + paging_state_idx = + all_addresses_asc + |> Enum.find_index(&(&1 == paging_state)) + + limit_address = Enum.at(all_addresses_asc, paging_state_idx - 1) + + if paging_state_idx < @page_size do + {limit_address, nil, false, ""} + else + idx = paging_state_idx - 1 - @page_size + + {limit_address, all_addresses_asc |> Enum.at(idx), true, + all_addresses_asc |> Enum.at(idx + 1)} + end + end + + # call the ASC function and ignore the more? and paging_state + {transactions, _more?, _paging_state} = + process_get_chain(fd, limit_address, fields, [paging_state: paging_state], db_path) + + {Enum.reverse(transactions), more?, new_paging_state} + end + defp read_transaction(fd, fields, limit, position, acc \\ %{}) defp read_transaction(_fd, _fields, limit, position, acc) when limit == position, do: acc From 627a01d6686e8c9790bdcce2b6d60b67126465f3 Mon Sep 17 00:00:00 2001 From: Bastien CHAMAGNE Date: Mon, 19 Dec 2022 11:24:32 +0100 Subject: [PATCH 07/10] refactor by passing a order flag instead of many _desc func --- lib/archethic/db.ex | 5 -- lib/archethic/db/embedded_impl.ex | 13 ----- .../db/embedded_impl/chain_reader.ex | 51 ++++++++----------- lib/archethic/p2p/message.ex | 11 +--- lib/archethic/transaction_chain.ex | 8 --- test/archethic/db/embedded_impl_test.exs | 20 +++++--- 6 files changed, 34 insertions(+), 74 deletions(-) diff --git a/lib/archethic/db.ex b/lib/archethic/db.ex index 6e6fe1009..8c3869409 100644 --- a/lib/archethic/db.ex +++ b/lib/archethic/db.ex @@ -27,11 +27,6 @@ defmodule Archethic.DB do fields :: list(), opts :: [paging_state: nil | binary(), after: DateTime.t()] ) :: Enumerable.t() - @callback get_transaction_chain_desc( - binary(), - fields :: list(), - opts :: [paging_state: nil | binary(), after: DateTime.t()] - ) :: Enumerable.t() @callback write_transaction(Transaction.t(), storage_type()) :: :ok @callback write_beacon_summary(Summary.t()) :: :ok @callback clear_beacon_summaries() :: :ok diff --git a/lib/archethic/db/embedded_impl.ex b/lib/archethic/db/embedded_impl.ex index 05c873ff7..cc235ce3f 100644 --- a/lib/archethic/db/embedded_impl.ex +++ b/lib/archethic/db/embedded_impl.ex @@ -200,19 +200,6 @@ defmodule Archethic.DB.EmbeddedImpl do ChainReader.get_transaction_chain(address, fields, opts, db_path()) end - @doc """ - Get a transaction chain - anti-chronological order - - The returned values will be splitted according to the pagination state presents in the options - """ - @spec get_transaction_chain_desc(address :: binary(), fields :: list(), opts :: list()) :: - {transactions_by_page :: list(Transaction.t()), more? :: boolean(), - paging_state :: nil | binary()} - def get_transaction_chain_desc(address, fields \\ [], opts \\ []) - when is_binary(address) and is_list(fields) and is_list(opts) do - ChainReader.get_transaction_chain_desc(address, fields, opts, db_path()) - end - @doc """ Return the size of a transaction chain """ diff --git a/lib/archethic/db/embedded_impl/chain_reader.ex b/lib/archethic/db/embedded_impl/chain_reader.ex index e811dfef7..1aa4cc16b 100644 --- a/lib/archethic/db/embedded_impl/chain_reader.ex +++ b/lib/archethic/db/embedded_impl/chain_reader.ex @@ -109,6 +109,14 @@ defmodule Archethic.DB.EmbeddedImpl.ChainReader do end end + @doc """ + Return a transaction chain. + By default, order is chronological (ASC) + + Opts: + paging_state :: binary() + order :: :asc | :desc + """ @spec get_transaction_chain( address :: binary(), fields :: list(), @@ -129,40 +137,21 @@ defmodule Archethic.DB.EmbeddedImpl.ChainReader do fd = File.open!(filepath, [:binary, :read]) {transactions, more?, paging_state} = - process_get_chain(fd, address, fields, opts, db_path) - - :telemetry.execute([:archethic, :db], %{duration: System.monotonic_time() - start}, %{ - query: "get_transaction_chain" - }) + case Keyword.get(opts, :order, :asc) do + :asc -> + process_get_chain(fd, address, fields, opts, db_path) - {transactions, more?, paging_state} - end - end - - @spec get_transaction_chain_desc( - address :: binary(), - fields :: list(), - opts :: list(), - db_path :: String.t() - ) :: - {transactions_by_page :: list(Transaction.t()), more? :: boolean(), - paging_state :: nil | binary()} - def get_transaction_chain_desc(address, fields, opts, db_path) do - start = System.monotonic_time() - - case ChainIndex.get_tx_entry(address, db_path) do - {:error, :not_exists} -> - {[], false, ""} - - {:ok, %{genesis_address: genesis_address}} -> - filepath = ChainWriter.chain_path(db_path, genesis_address) - fd = File.open!(filepath, [:binary, :read]) - - {transactions, more?, paging_state} = - process_get_chain_desc(fd, genesis_address, fields, opts, db_path) + :desc -> + process_get_chain_desc(fd, genesis_address, fields, opts, db_path) + end + # we want different metrics for ASC and DESC :telemetry.execute([:archethic, :db], %{duration: System.monotonic_time() - start}, %{ - query: "get_transaction_chain_desc" + query: + case Keyword.get(opts, :order, :asc) do + :asc -> "get_transaction_chain" + :desc -> "get_transaction_chain_reverse" + end }) {transactions, more?, paging_state} diff --git a/lib/archethic/p2p/message.ex b/lib/archethic/p2p/message.ex index 4d5c27d53..0764eab0d 100644 --- a/lib/archethic/p2p/message.ex +++ b/lib/archethic/p2p/message.ex @@ -1366,15 +1366,8 @@ defmodule Archethic.P2P.Message do _ ) do {chain, more?, paging_state} = - case order do - :asc -> - tx_address - |> TransactionChain.get([], paging_state: paging_state) - - :desc -> - tx_address - |> TransactionChain.get_desc([], paging_state: paging_state) - end + tx_address + |> TransactionChain.get([], paging_state: paging_state, order: order) # empty list for fields/cols to be processed # new_page_state contains binary offset for the next page diff --git a/lib/archethic/transaction_chain.ex b/lib/archethic/transaction_chain.ex index 8d42242eb..fa4d4d2cb 100644 --- a/lib/archethic/transaction_chain.ex +++ b/lib/archethic/transaction_chain.ex @@ -175,14 +175,6 @@ defmodule Archethic.TransactionChain do Enumerable.t() | {list(Transaction.t()), boolean(), binary()} defdelegate get(address, fields \\ [], opts \\ []), to: DB, as: :get_transaction_chain - @doc """ - Retrieve an entire chain from the last transaction - The returned list is ordered anti-chronologically. - """ - @spec get_desc(binary(), list()) :: - Enumerable.t() | {list(Transaction.t()), boolean(), binary()} - defdelegate get_desc(address, fields \\ [], opts \\ []), to: DB, as: :get_transaction_chain_desc - @doc """ Persist only one transaction """ diff --git a/test/archethic/db/embedded_impl_test.exs b/test/archethic/db/embedded_impl_test.exs index cec979d21..84b73864b 100644 --- a/test/archethic/db/embedded_impl_test.exs +++ b/test/archethic/db/embedded_impl_test.exs @@ -362,12 +362,12 @@ defmodule Archethic.DB.EmbeddedTest do end end - describe "get_transaction_chain_desc/4" do + describe "get_transaction_chain/4 order: :desc" do test "should return empty when there is no transactions" do {pub_key, _} = Crypto.generate_deterministic_keypair("SEED") address = Crypto.derive_address(pub_key) - assert {[], false, ""} == EmbeddedImpl.get_transaction_chain_desc(address) + assert {[], false, ""} == EmbeddedImpl.get_transaction_chain(address, [], order: :desc) end test "should return all transactions if there are less than one page (10)" do @@ -381,7 +381,9 @@ defmodule Archethic.DB.EmbeddedTest do EmbeddedImpl.write_transaction_chain(transactions) - {page, false, ""} = EmbeddedImpl.get_transaction_chain_desc(List.last(transactions).address) + {page, false, ""} = + EmbeddedImpl.get_transaction_chain(List.last(transactions).address, [], order: :desc) + assert length(page) == 9 assert page == Enum.reverse(transactions) end @@ -398,22 +400,24 @@ defmodule Archethic.DB.EmbeddedTest do EmbeddedImpl.write_transaction_chain(transactions) {page1, true, paging_state1} = - EmbeddedImpl.get_transaction_chain_desc(List.last(transactions).address) + EmbeddedImpl.get_transaction_chain(List.last(transactions).address, [], order: :desc) assert length(page1) == 10 assert paging_state1 == List.last(page1).address {page2, true, paging_state2} = - EmbeddedImpl.get_transaction_chain_desc(List.last(transactions).address, [], - paging_state: paging_state1 + EmbeddedImpl.get_transaction_chain(List.last(transactions).address, [], + paging_state: paging_state1, + order: :desc ) assert length(page2) == 10 assert paging_state2 == List.last(page2).address {page3, false, ""} = - EmbeddedImpl.get_transaction_chain_desc(List.last(transactions).address, [], - paging_state: paging_state2 + EmbeddedImpl.get_transaction_chain(List.last(transactions).address, [], + paging_state: paging_state2, + order: :desc ) assert length(page3) == 8 From 18e7bb883570c7b313db31e3a2d15853d5f4b834 Mon Sep 17 00:00:00 2001 From: Bastien CHAMAGNE Date: Mon, 19 Dec 2022 11:42:11 +0100 Subject: [PATCH 08/10] also pass a order flag instead of _desc fn --- lib/archethic/db/embedded_impl.ex | 2 +- lib/archethic/db/embedded_impl/chain_reader.ex | 2 +- lib/archethic/transaction_chain.ex | 18 +++--------------- .../live/chains/transaction_live.ex | 2 +- 4 files changed, 6 insertions(+), 18 deletions(-) diff --git a/lib/archethic/db/embedded_impl.ex b/lib/archethic/db/embedded_impl.ex index cc235ce3f..6288e3ec0 100644 --- a/lib/archethic/db/embedded_impl.ex +++ b/lib/archethic/db/embedded_impl.ex @@ -188,7 +188,7 @@ defmodule Archethic.DB.EmbeddedImpl do end @doc """ - Get a transaction chain - chronological order + Get a transaction chain The returned values will be splitted according to the pagination state presents in the options """ diff --git a/lib/archethic/db/embedded_impl/chain_reader.ex b/lib/archethic/db/embedded_impl/chain_reader.ex index 1aa4cc16b..238f2bcc4 100644 --- a/lib/archethic/db/embedded_impl/chain_reader.ex +++ b/lib/archethic/db/embedded_impl/chain_reader.ex @@ -259,7 +259,7 @@ defmodule Archethic.DB.EmbeddedImpl.ChainReader do end # in order to read the file sequentially in DESC (faster than random access) - # we have to determined the correct paging_state and limit_address + # we have to determine the correct paging_state and limit_address # then we can use the process_get_chain that does the ASC read defp process_get_chain_desc(fd, genesis_address, fields, opts, db_path) do all_addresses_asc = diff --git a/lib/archethic/transaction_chain.ex b/lib/archethic/transaction_chain.ex index fa4d4d2cb..cd20e8e3f 100644 --- a/lib/archethic/transaction_chain.ex +++ b/lib/archethic/transaction_chain.ex @@ -763,24 +763,12 @@ defmodule Archethic.TransactionChain do end @doc """ - Get 10 transactions in a chain after a paging address - chronological order + Get 10 transactions in a chain after a paging address """ @spec fetch_transaction_chain(list(Node.t()), binary(), binary()) :: {:ok, list(Transaction.t())} | {:error, :network_issue} - def fetch_transaction_chain(nodes, address, paging_address) do - case do_fetch_transaction_chain(nodes, address, paging_address) do - {transactions, _more?, _paging_state} -> {:ok, transactions} - error -> error - end - end - - @doc """ - Get 10 transactions in a chain before a paging address - anti-chronological order - """ - @spec fetch_transaction_chain_desc(list(Node.t()), binary(), nil | binary()) :: - {:ok, list(Transaction.t())} | {:error, :network_issue} - def fetch_transaction_chain_desc(nodes, address, paging_address) do - case do_fetch_transaction_chain(nodes, address, paging_address, order: :desc) do + def fetch_transaction_chain(nodes, address, paging_address, opts \\ []) do + case do_fetch_transaction_chain(nodes, address, paging_address, opts) do {transactions, _more?, _paging_state} -> {:ok, transactions} error -> error end diff --git a/lib/archethic_web/live/chains/transaction_live.ex b/lib/archethic_web/live/chains/transaction_live.ex index 871a6a92f..3fa3aa087 100644 --- a/lib/archethic_web/live/chains/transaction_live.ex +++ b/lib/archethic_web/live/chains/transaction_live.ex @@ -144,6 +144,6 @@ defmodule ArchethicWeb.TransactionChainLive do # DESC pagination defp paginate_chain(address, paging_address) do nodes = Election.chain_storage_nodes(address, P2P.authorized_and_available_nodes()) - TransactionChain.fetch_transaction_chain_desc(nodes, address, paging_address) + TransactionChain.fetch_transaction_chain(nodes, address, paging_address, order: :desc) end end From ef530bec7d2f65a178b4752638a3baeea452e30d Mon Sep 17 00:00:00 2001 From: Bastien CHAMAGNE Date: Tue, 20 Dec 2022 09:58:57 +0100 Subject: [PATCH 09/10] update serialization/deserialization to fix issue with multiple nodes --- lib/archethic/p2p/message.ex | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/lib/archethic/p2p/message.ex b/lib/archethic/p2p/message.ex index 0764eab0d..f3438e8f1 100644 --- a/lib/archethic/p2p/message.ex +++ b/lib/archethic/p2p/message.ex @@ -253,12 +253,24 @@ defmodule Archethic.P2P.Message do <<3::8, tx_address::binary>> end - def encode(%GetTransactionChain{address: tx_address, paging_state: nil}) do - <<4::8, tx_address::binary, 0::8>> + def encode(%GetTransactionChain{address: tx_address, paging_state: nil, order: order}) do + order_bit = + case order do + :asc -> 0 + :desc -> 1 + end + + <<4::8, tx_address::binary, order_bit::1, 0::8>> end - def encode(%GetTransactionChain{address: tx_address, paging_state: paging_state}) do - <<4::8, tx_address::binary, byte_size(paging_state)::8, paging_state::binary>> + def encode(%GetTransactionChain{address: tx_address, paging_state: paging_state, order: order}) do + order_bit = + case order do + :asc -> 0 + :desc -> 1 + end + + <<4::8, tx_address::binary, order_bit::1, byte_size(paging_state)::8, paging_state::binary>> end def encode(%GetUnspentOutputs{address: tx_address, offset: offset}) do @@ -690,8 +702,8 @@ defmodule Archethic.P2P.Message do # def decode(<<4::8, rest::bitstring>>) do {address, - <>} = - Utils.deserialize_address(rest) + <>} = Utils.deserialize_address(rest) paging_state = case paging_state do @@ -702,8 +714,14 @@ defmodule Archethic.P2P.Message do paging_state end + order = + case order_bit do + 0 -> :asc + 1 -> :desc + end + { - %GetTransactionChain{address: address, paging_state: paging_state}, + %GetTransactionChain{address: address, paging_state: paging_state, order: order}, rest } end From ad1b43181ed35ba501041fcab2f02edf18e24c24 Mon Sep 17 00:00:00 2001 From: Bastien CHAMAGNE Date: Tue, 20 Dec 2022 18:27:26 +0100 Subject: [PATCH 10/10] add serialization unit tests --- test/archethic/p2p/messages_test.exs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/archethic/p2p/messages_test.exs b/test/archethic/p2p/messages_test.exs index 5e9fd8536..7c4967e8f 100644 --- a/test/archethic/p2p/messages_test.exs +++ b/test/archethic/p2p/messages_test.exs @@ -105,12 +105,31 @@ defmodule Archethic.P2P.MessageTest do test "GetTransactionChain message" do address = <<0::8>> <> <<0::8>> <> :crypto.strong_rand_bytes(32) + paging_state = <<0::8>> <> <<0::8>> <> :crypto.strong_rand_bytes(32) assert %GetTransactionChain{address: address} == %GetTransactionChain{address: address} |> Message.encode() |> Message.decode() |> elem(0) + + assert %GetTransactionChain{address: address, paging_state: paging_state} == + %GetTransactionChain{address: address, paging_state: paging_state} + |> Message.encode() + |> Message.decode() + |> elem(0) + + assert %GetTransactionChain{address: address, order: :desc} == + %GetTransactionChain{address: address, order: :desc} + |> Message.encode() + |> Message.decode() + |> elem(0) + + assert %GetTransactionChain{address: address, order: :asc} == + %GetTransactionChain{address: address, order: :asc} + |> Message.encode() + |> Message.decode() + |> elem(0) end test "GetUnspentOutputs message" do