Add tests for OMG.Block and extract Merkle-related functions
MODIFY
apps/omg/lib/omg/block.ex
apps/omg/lib/omg/block.ex
@@ -18,9 +18,9 @@ defmodule OMG.Block do
"""
alias OMG.Crypto
+ alias OMG.Merkle
alias OMG.State.Transaction
- @transaction_merkle_tree_height 16
@type block_hash_t() :: <<_::256>>
defstruct [:transactions, :hash, :number]
@@ -48,7 +48,7 @@ defmodule OMG.Block do
|> Enum.map(&get_data_per_tx/1)
|> Enum.unzip()
- %__MODULE__{hash: merkle_hash(hashed_txs), transactions: txs_bytes, number: blknum}
+ %__MODULE__{hash: Merkle.hash(hashed_txs), transactions: txs_bytes, number: blknum}
end
@doc """
@@ -84,7 +84,7 @@ defmodule OMG.Block do
|> Enum.map(&get_data_per_tx/1)
|> Enum.unzip()
- create_tx_proof(hashed_txs, txindex)
+ Merkle.create_tx_proof(hashed_txs, txindex)
end
def inclusion_proof(%__MODULE__{transactions: transactions}, txindex), do: inclusion_proof(transactions, txindex)
@@ -96,30 +96,4 @@ defmodule OMG.Block do
end
defp to_recovered_tx(txbytes), do: Transaction.Recovered.recover_from!(txbytes)
-
- @default_leaf <<0>> |> List.duplicate(32) |> Enum.join() |> Crypto.hash()
- # Creates a Merkle proof that transaction under a given transaction index
- # is included in block consisting of hashed transactions
- @spec create_tx_proof(nonempty_list(binary()), non_neg_integer()) :: binary()
- defp create_tx_proof(hashed_txs, txindex),
- do:
- MerkleTree.build(hashed_txs,
- hash_function: &Crypto.hash/1,
- hash_leaves: false,
- height: @transaction_merkle_tree_height,
- default_data_block: @default_leaf
- )
- |> MerkleTree.Proof.prove(txindex)
- |> (& &1.hashes).()
- |> Enum.reverse()
- |> Enum.join()
-
- defp merkle_hash(hashed_txs),
- do:
- MerkleTree.fast_root(hashed_txs,
- hash_function: &Crypto.hash/1,
- hash_leaves: false,
- height: @transaction_merkle_tree_height,
- default_data_block: @default_leaf
- )
end

ADD
apps/omg/lib/omg/merkle.ex
+# Copyright 2019 OmiseGO Pte Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+defmodule OMG.Merkle do
+ @moduledoc """
+ Encapsulates all the interactions with the MerkleTree library.
+ """
+
+ alias OMG.Crypto
+
+ @transaction_merkle_tree_height 16
+ @default_leaf <<0>> |> List.duplicate(32) |> Enum.join() |> Crypto.hash()
+
+ # Creates a Merkle proof that transaction under a given transaction index
+ # is included in block consisting of hashed transactions
+ @spec create_tx_proof(nonempty_list(binary()), non_neg_integer()) :: binary()
+ def create_tx_proof(hashed_txs, txindex) do
+ build(hashed_txs)
+ |> prove(txindex)
+ |> (& &1.hashes).()
+ |> Enum.reverse()
+ |> Enum.join()
+ end
+
+ @spec hash(nonempty_list(binary())) :: binary()
+ def hash(hashed_txs) do
+ MerkleTree.fast_root(hashed_txs,
+ hash_function: &Crypto.hash/1,
+ hash_leaves: false,
+ height: @transaction_merkle_tree_height,
+ default_data_block: @default_leaf
+ )
+ end
+
+ defp build(hashed_txs) do
+ MerkleTree.build(hashed_txs,
+ hash_function: &Crypto.hash/1,
+ hash_leaves: false,
+ height: @transaction_merkle_tree_height,
+ default_data_block: @default_leaf
+ )
+ end
+
+ defp prove(hash, txindex) do
+ MerkleTree.Proof.prove(hash, txindex)
+ end
+end

MODIFY
apps/omg/test/omg/block_test.exs
apps/omg/test/omg/block_test.exs
@@ -25,6 +25,198 @@ defmodule OMG.BlockTest do
defp eth, do: OMG.Eth.RootChain.eth_pseudo_address()
+ describe "hashed_txs_at/2" do
+ @tag fixtures: [:stable_alice, :stable_bob]
+ test "returns a block with the list of transactions and a computed merkle root hash", %{
+ stable_alice: alice,
+ stable_bob: bob
+ } do
+ tx_1 = TestHelper.create_recovered([{1, 0, 0, alice}], eth(), [{bob, 100}])
+ tx_2 = TestHelper.create_recovered([{1, 1, 1, alice}], eth(), [{bob, 100}])
+
+ transactions = [tx_1, tx_2]
+
+ assert Block.hashed_txs_at(transactions, 10) == %Block{
+ hash:
+ <<34, 146, 42, 183, 241, 237, 118, 74, 89, 216, 33, 45, 171, 175, 216, 24, 97, 154, 90, 81, 21, 199,
+ 155, 254, 64, 112, 96, 58, 205, 178, 162, 77>>,
+ number: 10,
+ transactions: [
+ tx_1.signed_tx_bytes,
+ tx_2.signed_tx_bytes
+ ]
+ }
+ end
+
+ test "handles an empty list of transactions" do
+ assert Block.hashed_txs_at([], 10) == %Block{
+ hash:
+ <<119, 106, 49, 219, 52, 161, 160, 167, 202, 175, 134, 44, 255, 223, 255, 23, 137, 41, 127, 250, 220,
+ 56, 11, 211, 211, 146, 129, 211, 64, 171, 211, 173>>,
+ number: 10,
+ transactions: []
+ }
+ end
+ end
+
+ describe "to_api_format/1" do
+ test "formats to map for API" do
+ block = %Block{
+ hash: "hash",
+ number: 10,
+ transactions: ["tx_1_bytes", "tx_2_bytes"]
+ }
+
+ assert Block.to_api_format(block) == %{
+ blknum: 10,
+ hash: "hash",
+ transactions: ["tx_1_bytes", "tx_2_bytes"]
+ }
+ end
+ end
+
+ describe "to_db_value/1" do
+ test "formats to DB format with valid inputs" do
+ block = %Block{
+ hash: "hash",
+ number: 10,
+ transactions: ["tx_1_bytes", "tx_2_bytes"]
+ }
+
+ assert Block.to_db_value(block) == %{
+ hash: "hash",
+ number: 10,
+ transactions: ["tx_1_bytes", "tx_2_bytes"]
+ }
+ end
+
+ test "fails to format when the list of transactions is not a list" do
+ # Not sure how we want to handle this, there is currently no
+ # fallback function in the Block module
+ assert_raise FunctionClauseError, fn ->
+ Block.to_db_value(%Block{
+ hash: "hash",
+ number: 10,
+ transactions: %{}
+ })
+ end
+ end
+
+ test "fails to format when the hash is not a binary" do
+ assert_raise FunctionClauseError, fn ->
+ Block.to_db_value(%Block{
+ hash: 1,
+ number: 10,
+ transactions: []
+ })
+ end
+ end
+
+ test "fails to format when the number is not an integer" do
+ assert_raise FunctionClauseError, fn ->
+ Block.to_db_value(%Block{
+ hash: 1,
+ number: "10",
+ transactions: []
+ })
+ end
+ end
+ end
+
+ describe "from_db_value/1" do
+ test "formats from DB format with valid inputs" do
+ block = %{
+ hash: "hash",
+ number: 10,
+ transactions: ["tx_1_bytes", "tx_2_bytes"]
+ }
+
+ assert Block.from_db_value(block) == %Block{
+ hash: "hash",
+ number: 10,
+ transactions: ["tx_1_bytes", "tx_2_bytes"]
+ }
+ end
+
+ test "fails to format when the list of transactions is not a list" do
+ # Not sure how we want to handle this, there is currently no
+ # fallback function in the Block module
+ assert_raise FunctionClauseError, fn ->
+ Block.from_db_value(%{
+ hash: "hash",
+ number: 10,
+ transactions: %{}
+ })
+ end
+ end
+
+ test "fails to format when the hash is not a binary" do
+ assert_raise FunctionClauseError, fn ->
+ Block.from_db_value(%{
+ hash: 1,
+ number: 10,
+ transactions: []
+ })
+ end
+ end
+
+ test "fails to format when the number is not an integer" do
+ assert_raise FunctionClauseError, fn ->
+ Block.from_db_value(%{
+ hash: 1,
+ number: "10",
+ transactions: []
+ })
+ end
+ end
+ end
+
+ describe "inclusion_proof/2" do
+ @tag fixtures: [:stable_alice]
+ test "calculates the inclusion proof when a list of transactions is given", %{
+ stable_alice: alice
+ } do
+ tx_1 = TestHelper.create_encoded([{1, 0, 0, alice}], eth(), [{alice, 7}])
+ tx_2 = TestHelper.create_encoded([{1, 1, 0, alice}], eth(), [{alice, 2}])
+
+ encoded_proof =
+ [tx_1, tx_2]
+ |> Block.inclusion_proof(1)
+ |> Base.encode16(case: :lower)
+
+ assert "491b9e07c4997c976c38a91f" <> _ = encoded_proof
+ end
+
+ @tag fixtures: [:stable_alice]
+ test "calculates the inclusion proof when a block is given", %{
+ stable_alice: alice
+ } do
+ tx_1 = TestHelper.create_encoded([{1, 0, 0, alice}], eth(), [{alice, 7}])
+ tx_2 = TestHelper.create_encoded([{1, 1, 0, alice}], eth(), [{alice, 2}])
+
+ encoded_proof =
+ %Block{transactions: [tx_1, tx_2]}
+ |> Block.inclusion_proof(1)
+ |> Base.encode16(case: :lower)
+
+ assert "491b9e07c4997c976c38a91f" <> _ = encoded_proof
+ end
+
+ test "calculates the inclusion proof when an empty list is given" do
+ encoded_proof =
+ []
+ |> Block.inclusion_proof(1)
+ |> Base.encode16(case: :lower)
+
+ assert "290decd9548b62a8d60345a9" <> _ = encoded_proof
+ end
+
+ test "raises an error when an invalid input is given (map)" do
+ assert_raise FunctionClauseError, fn ->
+ Block.inclusion_proof(%{}, 1)
+ end
+ end
+
@tag fixtures: [:stable_alice, :stable_bob]
test "Block merkle proof smoke test", %{
stable_alice: alice
@@ -50,12 +242,10 @@ defmodule OMG.BlockTest do
test "Block merkle proof smoke test for deposit transactions",
%{alice: alice} do
tx = TestHelper.create_encoded([], eth(), [{alice, 7}])
-
- proof =
- %Block{transactions: [tx]}
- |> Block.inclusion_proof(0)
+ proof = Block.inclusion_proof(%Block{transactions: [tx]}, 0)
assert is_binary(proof)
assert byte_size(proof) == 32 * 16
end
end
+end

ADD
apps/omg/test/omg/merkle_test.exs
+# Copyright 2019 OmiseGO Pte Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+defmodule OMG.MerkleTest do
+ @moduledoc false
+
+ use ExUnitFixtures
+ use ExUnit.Case, async: true
+
+ alias OMG.Merkle
+
+ describe "create_tx_proof/2" do
+ test "creates merkle proofs based on list of values and index" do
+ # We don't want to be testing the underlying library here,
+ # we just want to ensure that our code calling it always
+ # return the same result
+ values = ["abc", "def", "ghi"]
+
+ proof_1 =
+ values
+ |> Merkle.create_tx_proof(1)
+ |> Base.encode16(case: :lower)
+
+ proof_2 =
+ values
+ |> Merkle.create_tx_proof(2)
+ |> Base.encode16(case: :lower)
+
+ assert "6162639621ddbb2d2631190" <> _ = proof_1
+ assert proof_1 != proof_2
+ end
+ end
+
+ describe "hash/1" do
+ test "returns the merkle tree root for a list of transaction" do
+ values = ["abc", "def", "ghi"]
+
+ proof =
+ values
+ |> Merkle.hash()
+ |> Base.encode16(case: :lower)
+
+ assert proof == "5886d20993731f68ff6bc6b175f58aa2bddc09512eabe761aaf62ab57c086066"
+ end
+ end
+end

Add tests for OMG.Block and extract Merkle-related functions