From e595f0b91868a9c3738626a959ecb8c30adcc59d Mon Sep 17 00:00:00 2001 From: Rogerio Pontual <44991200+jyeshe@users.noreply.github.com> Date: Wed, 4 Jan 2023 16:48:40 +0000 Subject: [PATCH] feat: query channel reserve at a hash (#1106) --- lib/ae_mdw/channels.ex | 95 +++-- lib/ae_mdw/node/db.ex | 1 + .../controllers/channel_controller.ex | 19 +- .../controllers/channel_controller_test.exs | 330 ++++++++++++++++-- 4 files changed, 379 insertions(+), 66 deletions(-) diff --git a/lib/ae_mdw/channels.ex b/lib/ae_mdw/channels.ex index 32ad13d86..aced6fd01 100644 --- a/lib/ae_mdw/channels.ex +++ b/lib/ae_mdw/channels.ex @@ -7,8 +7,10 @@ defmodule AeMdw.Channels do alias AeMdw.Collection alias AeMdw.Db.Model alias AeMdw.Db.State + alias AeMdw.Db.Util, as: DbUtil alias AeMdw.Error alias AeMdw.Error.Input, as: ErrInput + alias AeMdw.Node.Db alias AeMdw.Txs alias AeMdw.Util alias AeMdw.Validate @@ -18,7 +20,8 @@ defmodule AeMdw.Channels do require Model @typep state() :: State.t() - @typep pubkey() :: AeMdw.Node.Db.pubkey() + @typep pubkey() :: Db.pubkey() + @typep type_block_hash() :: {Db.hash_type(), Db.hash()} @typep pagination() :: Collection.direction_limit() @typep range() :: {:gen, Range.t()} | nil @typep cursor() :: Collection.pagination_cursor() @@ -63,15 +66,13 @@ defmodule AeMdw.Channels do type_count(state, :channel_settle_tx, from_txi, next_txi) end - @spec fetch_channel(state(), pubkey()) :: {:ok, channel()} | {:error, Error.t()} - def fetch_channel(state, channel_pk) do + @spec fetch_channel(state(), pubkey(), type_block_hash() | nil) :: + {:ok, channel()} | {:error, Error.t()} + def fetch_channel(state, channel_pk, type_block_hash \\ nil) do case locate(state, channel_pk) do - {:ok, channel, source} -> - {:ok, - render_channel(state, channel, - is_active?: source == Model.ActiveChannel, - node_details?: true - )} + {:ok, m_channel, source} -> + is_active? = source == Model.ActiveChannel + {:ok, render_channel(state, m_channel, is_active?, type_block_hash)} :not_found -> {:error, ErrInput.NotFound.exception(value: encode(:channel, channel_pk))} @@ -103,10 +104,8 @@ defmodule AeMdw.Channels do defp render_active_channels(state, keys) do Enum.map(keys, fn {_exp, channel_pk} -> - render_channel(state, State.fetch!(state, @table_active, channel_pk), - is_active?: true, - node_details?: false - ) + m_channel = State.fetch!(state, @table_active, channel_pk) + render_channel(state, m_channel, true) end) end @@ -118,12 +117,14 @@ defmodule AeMdw.Channels do responder: responder_pk, state_hash: state_hash, amount: amount, - updates: [{{last_updated_height, _mbi}, last_updated_txi} | _rest] = updates + updates: + [{{last_updated_height, _mbi} = update_block_index, last_updated_txi} | _rest] = + updates ), - is_active?: is_active?, - node_details?: node_details? + is_active?, + type_block_hash \\ nil ) do - %{"block_hash" => block_hash, "hash" => tx_hash, "tx" => %{"type" => tx_type}} = + %{"block_hash" => update_block_hash, "hash" => tx_hash, "tx" => %{"type" => tx_type}} = Txs.fetch!(state, last_updated_txi) channel = %{ @@ -138,17 +139,24 @@ defmodule AeMdw.Channels do active: is_active? } - with true <- node_details?, - {:ok, node_channel} <- - :aec_chain.get_channel_at_hash(channel_pk, Validate.id!(block_hash)) do - node_details = :aesc_channels.serialize_for_client(node_channel) + block_hash = + get_oldest_block_hash( + state, + type_block_hash, + Validate.id!(update_block_hash), + update_block_index + ) + + case :aec_chain.get_channel_at_hash(channel_pk, block_hash) do + {:ok, node_channel} -> + node_details = :aesc_channels.serialize_for_client(node_channel) - channel - |> Map.merge(node_details) - |> Map.drop(~w(id initiator_id responder_id channel_amount)) - |> Map.put(:amount, node_details["channel_amount"]) - else - _no_details -> + channel + |> Map.merge(node_details) + |> Map.drop(~w(id initiator_id responder_id channel_amount)) + |> Map.put(:amount, node_details["channel_amount"]) + + {:error, _reason} -> Map.put(channel, :amount, amount) end end @@ -175,4 +183,37 @@ defmodule AeMdw.Channels do end defp deserialize_scope(_nil_or_txis_scope), do: nil + + # Gets from the oldest block state tree since some channels might be absent from newer blocks + defp get_oldest_block_hash(_state, nil, update_block_hash, _update_block_index), + do: update_block_hash + + defp get_oldest_block_hash( + state, + {block_type, block_hash}, + update_block_hash, + update_block_index + ) do + case get_block_index(state, block_type, block_hash) do + {:ok, height, mbi} -> + if update_block_index < {height, mbi} do + update_block_hash + else + block_hash + end + + {:error, _reason} -> + update_block_hash + end + end + + defp get_block_index(state, :key, block_hash) do + with {:ok, height} <- DbUtil.key_block_height(state, block_hash) do + {:ok, height, -1} + end + end + + defp get_block_index(state, :micro, block_hash) do + DbUtil.micro_block_height_index(state, block_hash) + end end diff --git a/lib/ae_mdw/node/db.ex b/lib/ae_mdw/node/db.ex index a1b151095..8dff03f02 100644 --- a/lib/ae_mdw/node/db.ex +++ b/lib/ae_mdw/node/db.ex @@ -14,6 +14,7 @@ defmodule AeMdw.Node.Db do @type pubkey() :: <<_::256>> @type hash_type() :: nil | :key | :micro + @type hash() :: <<_::256>> @type height_hash() :: {hash_type(), pos_integer(), binary()} @type balances_map() :: %{{:address, pubkey()} => integer()} @type account_balance() :: {integer() | nil, height_hash()} diff --git a/lib/ae_mdw_web/controllers/channel_controller.ex b/lib/ae_mdw_web/controllers/channel_controller.ex index 15cc51623..989ea41a7 100644 --- a/lib/ae_mdw_web/controllers/channel_controller.ex +++ b/lib/ae_mdw_web/controllers/channel_controller.ex @@ -22,10 +22,25 @@ defmodule AeMdwWeb.ChannelController do end @spec channel(Conn.t(), map()) :: Conn.t() - def channel(%Conn{assigns: %{state: state}} = conn, %{"id" => id}) do + def channel(%Conn{assigns: %{state: state}} = conn, %{"id" => id} = params) do + block_hash = params["block_hash"] + with {:ok, channel_pk} <- Validate.id(id, [:channel]), - {:ok, channel} <- Channels.fetch_channel(state, channel_pk) do + {:ok, type_block_hash} <- valid_optional_block_hash?(block_hash), + {:ok, channel} <- Channels.fetch_channel(state, channel_pk, type_block_hash) do json(conn, channel) end end + + defp valid_optional_block_hash?(nil), do: {:ok, nil} + + defp valid_optional_block_hash?(block_hash) do + with {:ok, hash} <- Validate.id(block_hash) do + if String.starts_with?(block_hash, "kh") do + {:ok, {:key, hash}} + else + {:ok, {:micro, hash}} + end + end + end end diff --git a/test/ae_mdw_web/controllers/channel_controller_test.exs b/test/ae_mdw_web/controllers/channel_controller_test.exs index edccb7417..a9f0131ad 100644 --- a/test/ae_mdw_web/controllers/channel_controller_test.exs +++ b/test/ae_mdw_web/controllers/channel_controller_test.exs @@ -13,7 +13,13 @@ defmodule AeMdwWeb.ChannelControllerTest do describe "channels" do test "it returns active channels", %{conn: conn, store: store} do - channel_pk = TS.channel_pk(0) + channel_pk1 = TS.channel_pk(0) + channel_pk2 = TS.channel_pk(1) + initiator_pk = TS.address(0) + responder_pk = TS.address(1) + tx_hash = encode(:tx_hash, <>) + tx_type1 = "ChannelWithdrawTx" + tx_type2 = "ChannelCloseMutualTx" {:ok, state_hash} = :aeser_api_encoder.safe_decode( @@ -21,33 +27,108 @@ defmodule AeMdwWeb.ChannelControllerTest do "st_Wwxms0IVM7PPCHpeOXWeeZZm8h5p/SuqZL7IHIbr3CqtlCL+" ) - channel = + m_channel1 = Model.channel( - index: channel_pk, + index: channel_pk1, active: 1, - initiator: TS.address(0), - responder: TS.address(0), + initiator: initiator_pk, + responder: responder_pk, state_hash: state_hash, - amount: 5, - updates: [{{1, 1}, 1}] + updates: [{{500_000, 1}, 1_000}] ) + m_channel2 = + Model.channel(m_channel1, index: channel_pk2, active: 2, updates: [{{500_000, 1}, 2_000}]) + + block_hash = <> + with_mocks [ - {Txs, [], - fetch!: fn _state, 1 -> - %{"block_hash" => "", "hash" => "", "tx" => %{"type" => "ChannelWithdrawTx"}} + {Txs, [:passthrough], + fetch!: fn _state, txi when txi in [1_000, 2_000] -> + tx = + if txi == 1_000 do + %{"type" => tx_type1} + else + %{"type" => tx_type2} + end + + %{ + "block_hash" => encode(:micro_block_hash, block_hash), + "hash" => tx_hash, + "tx" => tx + } + end}, + {:aec_chain, [:passthrough], + get_channel_at_hash: fn pubkey, ^block_hash -> + amount = + if pubkey == channel_pk1, + do: 9_000_000, + else: 8_000_000 + + {:ok, + {:channel, {:id, :channel, pubkey}, {:id, :account, initiator_pk}, + {:id, :account, responder_pk}, %{initiator: [], responder: []}, amount, 3_400_000, + 3_600_000, 500_000, :basic, :basic, + <<13, 54, 141, 196, 223, 107, 172, 150, 198, 45, 62, 102, 159, 21, 123, 151, 241, + 235, 20, 175, 223, 198, 242, 127, 137, 194, 129, 204, 227, 139, 197, 132>>, 1, 2, + 3, 500_003, 3}} end} ] do store = store - |> Store.put(Model.ActiveChannel, channel) - |> Store.put(Model.ActiveChannelActivation, Model.activation(index: {1, channel_pk})) - |> Store.put(Model.Tx, Model.tx(index: 1)) + |> Store.put(Model.ActiveChannel, m_channel1) + |> Store.put(Model.ActiveChannel, m_channel2) + |> Store.put(Model.ActiveChannelActivation, Model.activation(index: {1, channel_pk1})) + |> Store.put(Model.ActiveChannelActivation, Model.activation(index: {2, channel_pk2})) - assert %{"data" => [channel]} = - conn |> with_store(store) |> get("/v2/channels", limit: 1) |> json_response(200) + assert %{"data" => [channel2, channel1]} = + conn |> with_store(store) |> get("/v2/channels") |> json_response(200) - assert %{"amount" => 5, "updates_count" => 1} = channel + channel_id1 = encode(:channel, channel_pk1) + channel_id2 = encode(:channel, channel_pk2) + initiator = encode_account(initiator_pk) + responder = encode_account(responder_pk) + state_hash = encode(:state, state_hash) + + assert %{ + "channel" => ^channel_id2, + "active" => true, + "amount" => 8_000_000, + "last_updated_height" => 500_000, + "last_updated_tx_hash" => ^tx_hash, + "last_updated_tx_type" => ^tx_type2, + "updates_count" => 1, + "responder" => ^responder, + "initiator" => ^initiator, + "channel_reserve" => 500_000, + "initiator_amount" => 3_400_000, + "responder_amount" => 3_600_000, + "round" => 1, + "solo_round" => 2, + "lock_period" => 3, + "locked_until" => 500_003, + "state_hash" => ^state_hash + } = channel2 + + assert %{ + "channel" => ^channel_id1, + "active" => true, + "amount" => 9_000_000, + "last_updated_height" => 500_000, + "last_updated_tx_hash" => ^tx_hash, + "last_updated_tx_type" => ^tx_type1, + "updates_count" => 1, + "responder" => ^responder, + "initiator" => ^initiator, + "channel_reserve" => 500_000, + "initiator_amount" => 3_400_000, + "responder_amount" => 3_600_000, + "round" => 1, + "solo_round" => 2, + "lock_period" => 3, + "locked_until" => 500_003, + "state_hash" => ^state_hash + } = channel1 end end @@ -66,21 +147,16 @@ defmodule AeMdwWeb.ChannelControllerTest do end describe "channel" do - test "it returns an active/inactive channel", %{conn: conn, store: store} do + test "returns an active/inactive channel on latest update state", %{conn: conn, store: store} do active_channel_pk = TS.channel_pk(0) inactive_channel_pk = TS.channel_pk(1) - active_channel_id = encode(:channel, active_channel_pk) - inactive_channel_id = encode(:channel, inactive_channel_pk) initiator_pk = TS.address(0) - initiator = encode_account(initiator_pk) responder_pk = TS.address(1) - responder = encode_account(responder_pk) - - {:ok, state_hash} = - :aeser_api_encoder.safe_decode( - :state, - "st_Wwxms0IVM7PPCHpeOXWeeZZm8h5p/SuqZL7IHIbr3CqtlCL+" - ) + tx_hash = encode(:tx_hash, <>) + tx_type1 = "ChannelWithdrawTx" + tx_type2 = "ChannelCloseMutualTx" + state_hash1 = <<1::256>> + state_hash2 = <<2::256>> active_channel = Model.channel( @@ -88,20 +164,33 @@ defmodule AeMdwWeb.ChannelControllerTest do active: 1, initiator: initiator_pk, responder: responder_pk, - state_hash: state_hash, - updates: [{{600_000, 1}, 1}] + state_hash: state_hash1, + updates: [{{600_000, 1}, 1_000}] + ) + + inactive_channel = + Model.channel(active_channel, + index: inactive_channel_pk, + state_hash: state_hash2, + updates: [{{600_000, 1}, 2_000}] ) - inactive_channel = Model.channel(active_channel, index: inactive_channel_pk) block_hash = <> with_mocks [ - {Txs, [], - fetch!: fn _state, 1 -> + {Txs, [:passthrough], + fetch!: fn _state, txi when txi in [1_000, 2_000] -> + tx = + if txi == 1_000 do + %{"type" => tx_type1} + else + %{"type" => tx_type2} + end + %{ "block_hash" => encode(:micro_block_hash, block_hash), - "hash" => "", - "tx" => %{"type" => "ChannelWithdrawTx"} + "hash" => tx_hash, + "tx" => tx } end}, {:aec_chain, [:passthrough], @@ -124,13 +213,20 @@ defmodule AeMdwWeb.ChannelControllerTest do store |> Store.put(Model.ActiveChannel, active_channel) |> Store.put(Model.InactiveChannel, inactive_channel) - |> Store.put(Model.Tx, Model.tx(index: 1)) + + active_channel_id = encode(:channel, active_channel_pk) + inactive_channel_id = encode(:channel, inactive_channel_pk) + initiator = encode_account(initiator_pk) + responder = encode_account(responder_pk) + state_hash = encode(:state, state_hash1) assert %{ "channel" => ^active_channel_id, "active" => true, "amount" => 9_000_000, "last_updated_height" => 600_000, + "last_updated_tx_hash" => ^tx_hash, + "last_updated_tx_type" => ^tx_type1, "updates_count" => 1, "responder" => ^responder, "initiator" => ^initiator, @@ -140,18 +236,23 @@ defmodule AeMdwWeb.ChannelControllerTest do "round" => 1, "solo_round" => 2, "lock_period" => 3, - "locked_until" => 600_003 + "locked_until" => 600_003, + "state_hash" => ^state_hash } = conn |> with_store(store) |> get("/v2/channels/#{active_channel_id}") |> json_response(200) + state_hash = encode(:state, state_hash2) + assert %{ "channel" => ^inactive_channel_id, "active" => false, "amount" => 8_000_000, "last_updated_height" => 600_000, + "last_updated_tx_hash" => ^tx_hash, + "last_updated_tx_type" => ^tx_type2, "updates_count" => 1, "responder" => ^responder, "initiator" => ^initiator, @@ -161,7 +262,8 @@ defmodule AeMdwWeb.ChannelControllerTest do "round" => 1, "solo_round" => 2, "lock_period" => 3, - "locked_until" => 600_003 + "locked_until" => 600_003, + "state_hash" => ^state_hash } = conn |> with_store(store) @@ -169,5 +271,159 @@ defmodule AeMdwWeb.ChannelControllerTest do |> json_response(200) end end + + test "returns an active/inactive channel on micro block state", %{conn: conn, store: store} do + active_channel_pk = TS.channel_pk(0) + inactive_channel_pk = TS.channel_pk(1) + initiator_pk = TS.address(0) + responder_pk = TS.address(1) + tx_hash = encode(:tx_hash, <>) + tx_type1 = "ChannelWithdrawTx" + tx_type2 = "ChannelCloseMutualTx" + state_hash1 = <<1::256>> + state_hash2 = <<2::256>> + + active_channel = + Model.channel( + index: active_channel_pk, + active: 1, + initiator: initiator_pk, + responder: responder_pk, + state_hash: state_hash1, + updates: [{{600_000, 1}, 1_000}] + ) + + inactive_channel = + Model.channel(active_channel, + index: inactive_channel_pk, + state_hash: state_hash2, + updates: [{{600_000, 1}, 2_000}] + ) + + update_block_hash = :crypto.strong_rand_bytes(32) + micro_block_hash = :crypto.strong_rand_bytes(32) + + with_mocks [ + {Txs, [:passthrough], + fetch!: fn _state, txi when txi in [1_000, 2_000] -> + tx = + if txi == 1_000 do + %{"type" => tx_type1} + else + %{"type" => tx_type2} + end + + %{ + "block_hash" => encode(:micro_block_hash, update_block_hash), + "hash" => tx_hash, + "tx" => tx + } + end}, + {:aec_chain, [:passthrough], + get_channel_at_hash: fn pubkey, ^micro_block_hash -> + amount = + if pubkey == active_channel_pk, + do: 9_000_000, + else: 8_000_000 + + {:ok, + {:channel, {:id, :channel, pubkey}, {:id, :account, initiator_pk}, + {:id, :account, responder_pk}, %{initiator: [], responder: []}, amount, 4_400_000, + 4_600_000, 500_000, :basic, :basic, + <<13, 54, 141, 196, 223, 107, 172, 150, 198, 45, 62, 102, 159, 21, 123, 151, 241, + 235, 20, 175, 223, 198, 242, 127, 137, 194, 129, 204, 227, 139, 197, 132>>, 1, 2, + 3, 600_003, 3}} + end}, + {AeMdw.Db.Util, [:passthrough], + micro_block_height_index: fn _state, ^micro_block_hash -> {:ok, 599_999, 1} end} + ] do + store = + store + |> Store.put(Model.ActiveChannel, active_channel) + |> Store.put(Model.InactiveChannel, inactive_channel) + + active_channel_id = encode(:channel, active_channel_pk) + inactive_channel_id = encode(:channel, inactive_channel_pk) + initiator = encode_account(initiator_pk) + responder = encode_account(responder_pk) + state_hash = encode(:state, state_hash1) + + assert %{ + "channel" => ^active_channel_id, + "active" => true, + "amount" => 9_000_000, + "last_updated_height" => 600_000, + "last_updated_tx_hash" => ^tx_hash, + "last_updated_tx_type" => ^tx_type1, + "updates_count" => 1, + "responder" => ^responder, + "initiator" => ^initiator, + "channel_reserve" => 500_000, + "initiator_amount" => 4_400_000, + "responder_amount" => 4_600_000, + "round" => 1, + "solo_round" => 2, + "lock_period" => 3, + "locked_until" => 600_003, + "state_hash" => ^state_hash + } = + conn + |> with_store(store) + |> get("/v2/channels/#{active_channel_id}", + block_hash: encode(:micro_block_hash, micro_block_hash) + ) + |> json_response(200) + + state_hash = encode(:state, state_hash2) + + assert %{ + "channel" => ^inactive_channel_id, + "active" => false, + "amount" => 8_000_000, + "last_updated_height" => 600_000, + "last_updated_tx_hash" => ^tx_hash, + "last_updated_tx_type" => ^tx_type2, + "updates_count" => 1, + "responder" => ^responder, + "initiator" => ^initiator, + "channel_reserve" => 500_000, + "initiator_amount" => 4_400_000, + "responder_amount" => 4_600_000, + "round" => 1, + "solo_round" => 2, + "lock_period" => 3, + "locked_until" => 600_003, + "state_hash" => ^state_hash + } = + conn + |> with_store(store) + |> get("/v2/channels/#{inactive_channel_id}", + block_hash: encode(:micro_block_hash, micro_block_hash) + ) + |> json_response(200) + end + end + + test "returns error when channel does not exist", %{conn: conn, store: store} do + channel_id = encode(:channel, :crypto.strong_rand_bytes(32)) + msg = "not found: #{channel_id}" + + assert %{"error" => ^msg} = + conn + |> with_store(store) + |> get("/v2/channels/#{channel_id}") + |> json_response(404) + end + + test "returns error when block is invalid", %{conn: conn, store: store} do + channel_pk = :crypto.strong_rand_bytes(32) + store = Store.put(store, Model.ActiveChannel, Model.channel(index: channel_pk)) + + assert %{"error" => "invalid id: kh_123"} = + conn + |> with_store(store) + |> get("/v2/channels/#{encode(:channel, channel_pk)}", block_hash: "kh_123") + |> json_response(400) + end end end