diff --git a/.gitmodules b/.gitmodules index b04de0c9db..a343bb6c6e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "priv/cabbage"] path = priv/cabbage url = git@github.com:omgnetwork/specs.git - branch = master + branch = master \ No newline at end of file diff --git a/apps/omg_watcher/lib/omg_watcher/block_validator.ex b/apps/omg_watcher/lib/omg_watcher/block_validator.ex new file mode 100644 index 0000000000..8aae938ef1 --- /dev/null +++ b/apps/omg_watcher/lib/omg_watcher/block_validator.ex @@ -0,0 +1,67 @@ +# Copyright 2019-2020 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.Watcher.BlockValidator do + @moduledoc """ + Operations related to block validation. + """ + + alias OMG.Block + alias OMG.Merkle + alias OMG.State.Transaction + + @doc """ + Executes stateless validation of a submitted block: + - Verifies that transactions are correctly formed. + - Verifies that given Merkle root matches reconstructed Merkle root. + """ + @spec stateless_validate(Block.t()) :: {:ok, boolean()} | {:error, atom()} + def stateless_validate(submitted_block) do + with {:ok, recovered_transactions} <- verify_transactions(submitted_block.transactions), + {:ok, _block} <- verify_merkle_root(submitted_block, recovered_transactions) do + {:ok, true} + end + end + + @spec verify_merkle_root(Block.t(), list(Transaction.Recovered.t())) :: + {:ok, Block.t()} | {:error, :mismatched_merkle_root} + defp verify_merkle_root(block, transactions) do + reconstructed_merkle_hash = + transactions + |> Enum.map(&Transaction.raw_txbytes/1) + |> Merkle.hash() + + case block.hash do + ^reconstructed_merkle_hash -> {:ok, block} + _ -> {:error, :invalid_merkle_root} + end + end + + @spec verify_transactions(transactions :: list(Transaction.Recovered.t())) :: + {:ok, list(Transaction.Recovered.t())} + | {:error, Transaction.Recovered.recover_tx_error()} + defp verify_transactions(transactions) do + transactions + |> Enum.reverse() + |> Enum.reduce_while({:ok, []}, fn tx, {:ok, already_recovered} -> + case Transaction.Recovered.recover_from(tx) do + {:ok, recovered} -> + {:cont, {:ok, [recovered | already_recovered]}} + + error -> + {:halt, error} + end + end) + end +end diff --git a/apps/omg_watcher/test/omg_watcher/block_validator_test.exs b/apps/omg_watcher/test/omg_watcher/block_validator_test.exs new file mode 100644 index 0000000000..baa367f656 --- /dev/null +++ b/apps/omg_watcher/test/omg_watcher/block_validator_test.exs @@ -0,0 +1,124 @@ +# Copyright 2019-2020 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.WatcherRPC.Web.Validator.BlockValidatorTest do + use ExUnit.Case, async: true + use OMG.WatcherRPC.Web, :controller + + alias OMG.Merkle + alias OMG.State.Transaction + alias OMG.TestHelper + alias OMG.Watcher.BlockValidator + + @alice TestHelper.generate_entity() + @bob TestHelper.generate_entity() + @eth OMG.Eth.zero_address() + @payment_tx_type OMG.WireFormatTypes.tx_type_for(:tx_payment_v1) + + describe "stateless_validate/1" do + test "returns error if a transaction is not correctly formed (e.g. duplicate inputs)" do + input_1 = {1, 0, 0, @alice} + input_2 = {2, 0, 0, @alice} + input_3 = {3, 0, 0, @alice} + + signed_valid_tx = TestHelper.create_signed([input_1, input_2], @eth, [{@bob, 10}]) + signed_invalid_tx = TestHelper.create_signed([input_3, input_3], @eth, [{@bob, 10}]) + + %{sigs: sigs_valid} = signed_valid_tx + %{sigs: sigs_invalid} = signed_invalid_tx + + txbytes_valid = Transaction.raw_txbytes(signed_valid_tx) + txbytes_invalid = Transaction.raw_txbytes(signed_invalid_tx) + + [_, inputs_valid, outputs_valid, _, _] = ExRLP.decode(txbytes_valid) + [_, inputs_invalid, outputs_invalid, _, _] = ExRLP.decode(txbytes_invalid) + + hash_valid = ExRLP.encode([sigs_valid, @payment_tx_type, inputs_valid, outputs_valid, 0, <<0::256>>]) + + hash_invalid = + ExRLP.encode([ + sigs_invalid, + @payment_tx_type, + inputs_invalid, + outputs_invalid, + 0, + <<0::256>> + ]) + + block = %{ + hash: Merkle.hash([txbytes_valid, txbytes_invalid]), + number: 1000, + transactions: [hash_invalid, hash_valid] + } + + assert {:error, :duplicate_inputs} == BlockValidator.stateless_validate(block) + end + + test "accepts correctly formed transactions" do + recovered_tx_1 = TestHelper.create_recovered([{1, 0, 0, @alice}, {2, 0, 0, @alice}], @eth, [{@bob, 10}]) + recovered_tx_2 = TestHelper.create_recovered([{3, 0, 0, @alice}, {4, 0, 0, @alice}], @eth, [{@bob, 10}]) + + signed_txbytes_1 = recovered_tx_1.signed_tx_bytes + signed_txbytes_2 = recovered_tx_2.signed_tx_bytes + + merkle_root = + [recovered_tx_1, recovered_tx_2] + |> Enum.map(&Transaction.raw_txbytes/1) + |> Merkle.hash() + + block = %{ + hash: merkle_root, + number: 1000, + transactions: [signed_txbytes_1, signed_txbytes_2] + } + + assert {:ok, true} == BlockValidator.stateless_validate(block) + end + + test "returns error for non-matching Merkle root hash" do + recovered_tx_1 = TestHelper.create_recovered([{1, 0, 0, @alice}], @eth, [{@bob, 100}]) + recovered_tx_2 = TestHelper.create_recovered([{2, 0, 0, @alice}], @eth, [{@bob, 100}]) + + signed_txbytes = Enum.map([recovered_tx_1, recovered_tx_2], fn tx -> tx.signed_tx_bytes end) + + block = %{ + hash: "0x0", + number: 1000, + transactions: signed_txbytes + } + + assert {:error, :invalid_merkle_root} == BlockValidator.stateless_validate(block) + end + + test "accepts matching Merkle root hash" do + recovered_tx_1 = TestHelper.create_recovered([{1, 0, 0, @alice}], @eth, [{@bob, 100}]) + recovered_tx_2 = TestHelper.create_recovered([{2, 0, 0, @alice}], @eth, [{@bob, 100}]) + + signed_txbytes = Enum.map([recovered_tx_1, recovered_tx_2], fn tx -> tx.signed_tx_bytes end) + + valid_merkle_root = + [recovered_tx_1, recovered_tx_2] + |> Enum.map(&Transaction.raw_txbytes/1) + |> Merkle.hash() + + block = %{ + hash: valid_merkle_root, + number: 1000, + transactions: signed_txbytes + } + + assert {:ok, true} = BlockValidator.stateless_validate(block) + end + end +end diff --git a/apps/omg_watcher_rpc/lib/web/controllers/block.ex b/apps/omg_watcher_rpc/lib/web/controllers/block.ex index 59b09f514c..2a860f3594 100644 --- a/apps/omg_watcher_rpc/lib/web/controllers/block.ex +++ b/apps/omg_watcher_rpc/lib/web/controllers/block.ex @@ -16,9 +16,11 @@ defmodule OMG.WatcherRPC.Web.Controller.Block do @moduledoc """ Operations related to block. """ + require Logger use OMG.WatcherRPC.Web, :controller + alias OMG.Watcher alias OMG.WatcherInfo.API.Block, as: InfoApiBlock alias OMG.WatcherRPC.Web.Validator @@ -43,4 +45,20 @@ defmodule OMG.WatcherRPC.Web.Controller.Block do |> api_response(conn, :blocks) end end + + @doc """ + Executes stateful and stateless validation of a block. + """ + def validate_block(conn, params) do + with {:ok, block} <- Validator.BlockConstraints.parse_to_validate(params) do + case Watcher.BlockValidator.stateless_validate(block) do + {:ok, true} -> + api_response(%{valid: true}, conn, :validate_block) + + {:error, reason} -> + Logger.info("Block #{block.number} is invalid due to #{reason}") + api_response(%{valid: false}, conn, :validate_block) + end + end + end end diff --git a/apps/omg_watcher_rpc/lib/web/controllers/fallback.ex b/apps/omg_watcher_rpc/lib/web/controllers/fallback.ex index 74fb9c47b0..7d16750e1b 100644 --- a/apps/omg_watcher_rpc/lib/web/controllers/fallback.ex +++ b/apps/omg_watcher_rpc/lib/web/controllers/fallback.ex @@ -69,6 +69,10 @@ defmodule OMG.WatcherRPC.Web.Controller.Fallback do code: "transaction.create:self_transaction_not_supported", description: "This endpoint cannot be used to create merge or split transactions." }, + invalid_merkle_root: %{ + code: "block.validate:invalid_merkle_root", + description: "Block hash does not match reconstructed Merkle root." + }, missing_signature: %{ code: "submit_typed:missing_signature", description: diff --git a/apps/omg_watcher_rpc/lib/web/router.ex b/apps/omg_watcher_rpc/lib/web/router.ex index 7d25841775..6abf69557c 100644 --- a/apps/omg_watcher_rpc/lib/web/router.ex +++ b/apps/omg_watcher_rpc/lib/web/router.ex @@ -52,6 +52,8 @@ defmodule OMG.WatcherRPC.Web.Router do post("/account.get_exitable_utxos", Controller.Account, :get_exitable_utxos) + post("/block.validate", Controller.Block, :validate_block) + post("/utxo.get_exit_data", Controller.Utxo, :get_utxo_exit) post("/utxo.get_challenge_data", Controller.Challenge, :get_utxo_challenge) diff --git a/apps/omg_watcher_rpc/lib/web/validators/block_constraints.ex b/apps/omg_watcher_rpc/lib/web/validators/block_constraints.ex index fd6a11460d..2943aae4d7 100644 --- a/apps/omg_watcher_rpc/lib/web/validators/block_constraints.ex +++ b/apps/omg_watcher_rpc/lib/web/validators/block_constraints.ex @@ -16,6 +16,10 @@ defmodule OMG.WatcherRPC.Web.Validator.BlockConstraints do @moduledoc """ Validates `/block.all` query parameters """ + + use OMG.WatcherRPC.Web, :controller + + alias OMG.Block alias OMG.WatcherRPC.Web.Validator.Helpers @doc """ @@ -30,4 +34,20 @@ defmodule OMG.WatcherRPC.Web.Validator.BlockConstraints do Helpers.validate_constraints(params, constraints) end + + @doc """ + Validates that a block submitted for validation is correctly formed. + """ + @spec parse_to_validate(Block.t()) :: + {:error, {:validation_error, binary, any}} | {:ok, Block.t()} + def parse_to_validate(block) do + with {:ok, hash} <- expect(block, "hash", :hash), + {:ok, transactions} <- expect(block, "transactions", list: &is_hex/1), + {:ok, number} <- expect(block, "number", :pos_integer), + do: {:ok, %Block{hash: hash, transactions: transactions, number: number}} + end + + defp is_hex(original) do + expect(%{"hash" => original}, "hash", :hex) + end end diff --git a/apps/omg_watcher_rpc/lib/web/views/block.ex b/apps/omg_watcher_rpc/lib/web/views/block.ex index cd27523abe..8bb88dcf83 100644 --- a/apps/omg_watcher_rpc/lib/web/views/block.ex +++ b/apps/omg_watcher_rpc/lib/web/views/block.ex @@ -34,4 +34,10 @@ defmodule OMG.WatcherRPC.Web.View.Block do |> Response.serialize_page(data_paging) |> WatcherRPCResponse.add_app_infos() end + + def render("validate_block.json", %{response: block}) do + block + |> Response.serialize() + |> WatcherRPCResponse.add_app_infos() + end end diff --git a/apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs.yaml b/apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs.yaml index f10e7e17bc..2e6b50b966 100644 --- a/apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs.yaml +++ b/apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs.yaml @@ -22,6 +22,8 @@ tags: url: 'https://github.com/omisego/elixir-omg/blob/master/docs/api_specs/status_events_specs.md#byzantine-events' - name: Account description: Account related API. + - name: Block + description: Block-related API - name: UTXO description: UTXO related API. - name: Transaction @@ -907,6 +909,136 @@ paths: description: Something went wrong on the server messages: error_key: error_reason + /block.validate: + post: + tags: + - Block + summary: Verifies the stateless validity of a block. + description: | + Validates that the given transactions are correctly formed, and that the given hash corresponds to the reconstructed Merkle root hash. + operationId: validate + requestBody: + description: 'Block object with a hash, number and array of hexadecimal transaction bytes.' + required: true + content: + application/json: + schema: + title: BlockValidateBodySchema + type: object + properties: + hash: + type: string + transactions: + type: array + items: + type: string + number: + type: integer + required: + - hash + - transactions + - number + example: + number: 1000 + hash: 0xf8d083015ba98080808080940000... + transactions: + - 0xf8c0f843b841fc6dbf49a4baa783ec576291f6083be5ea... + - 0xf852c003eeed02eb94916f3753bd53e124d6d565ef1701... + responses: + '200': + description: Successful response to calling /block.validate + content: + application/json: + schema: + allOf: + - description: The response schema for a successful operation + type: object + properties: + version: + type: string + success: + type: boolean + data: + type: object + service_name: + type: string + required: + - service_name + - version + - success + - data + example: + service_name: watcher + version: 1.0.0+abcdefa + success: true + data: {} + - type: object + properties: + data: + type: object + properties: + valid: + type: boolean + example: + data: + valid: false + '500': + description: Returns an internal server error + content: + application/json: + schema: + description: The response schema for an error + allOf: + - description: The response schema for a successful operation + type: object + properties: + version: + type: string + success: + type: boolean + data: + type: object + service_name: + type: string + required: + - service_name + - version + - success + - data + example: + service_name: watcher + version: 1.0.0+abcdefa + success: true + data: {} + - type: object + properties: + data: + description: The object schema for an error + type: object + properties: + object: + type: string + code: + type: string + description: + type: string + messages: + type: object + required: + - object + - code + - description + - messages + required: + - data + example: + success: false + data: + object: error + code: 'server:internal_server_error' + description: Something went wrong on the server + messages: + error_key: error_reason /utxo.get_challenge_data: post: tags: diff --git a/apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/block/paths.yaml b/apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/block/paths.yaml new file mode 100644 index 0000000000..25b25a76b9 --- /dev/null +++ b/apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/block/paths.yaml @@ -0,0 +1,15 @@ +block.validate: + post: + tags: + - Block + summary: Verifies the stateless validity of a block. + description: > + Validates that the given transactions are correctly formed, and that the given hash corresponds to the reconstructed Merkle root hash. + operationId: validate + requestBody: + $ref: 'request_bodies.yaml#/BlockValidateBodySchema' + responses: + 200: + $ref: 'responses.yaml#/BlockValidateResponse' + 500: + $ref: '../responses.yaml#/InternalServerError' diff --git a/apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/block/request_bodies.yaml b/apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/block/request_bodies.yaml new file mode 100644 index 0000000000..0854b928df --- /dev/null +++ b/apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/block/request_bodies.yaml @@ -0,0 +1,25 @@ +BlockValidateBodySchema: + description: Block object with a hash, number and array of hexadecimal transaction bytes. + required: true + content: + application/json: + schema: + title: 'BlockValidateBodySchema' + type: object + properties: + hash: + type: string + transactions: + type: array + items: + type: string + number: + type: integer + required: + - hash + - transactions + - number + example: + number: 1000 + hash: '0xf8d083015ba98080808080940000...' + transactions: ["0xf8c0f843b841fc6dbf49a4baa783ec576291f6083be5ea...", "0xf852c003eeed02eb94916f3753bd53e124d6d565ef1701..." ] diff --git a/apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/block/response_schemas.yaml b/apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/block/response_schemas.yaml new file mode 100644 index 0000000000..b95fc00272 --- /dev/null +++ b/apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/block/response_schemas.yaml @@ -0,0 +1,11 @@ +BlockValidateResponseSchema: + allOf: + - $ref: '../response_schemas.yaml#/WatcherBaseResponseSchema' + - type: object + properties: + data: + type: object + $ref: 'schemas.yaml#/BlockValidateSchema' + example: + data: + valid: false diff --git a/apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/block/responses.yaml b/apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/block/responses.yaml new file mode 100644 index 0000000000..1a9a739797 --- /dev/null +++ b/apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/block/responses.yaml @@ -0,0 +1,6 @@ +BlockValidateResponse: + description: Successful response to calling /block.validate + content: + application/json: + schema: + $ref: 'response_schemas.yaml#/BlockValidateResponseSchema' diff --git a/apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/block/schemas.yaml b/apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/block/schemas.yaml new file mode 100644 index 0000000000..7cdca8787a --- /dev/null +++ b/apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/block/schemas.yaml @@ -0,0 +1,5 @@ +BlockValidateSchema: + type: object + properties: + valid: + type: boolean diff --git a/apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/swagger.yaml b/apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/swagger.yaml index e5ca09fed7..d16507831a 100644 --- a/apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/swagger.yaml +++ b/apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/swagger.yaml @@ -25,6 +25,8 @@ tags: url: "https://github.com/omisego/elixir-omg/blob/master/docs/api_specs/status_events_specs.md#byzantine-events" - name: Account description: Account related API. + - name: Block + description: Block-related API - name: UTXO description: UTXO related API. - name: Transaction @@ -41,6 +43,8 @@ paths: $ref: 'status/paths.yaml#/status.get' /account.get_exitable_utxos: $ref: 'account/paths.yaml#/account.get_exitable_utxos' + /block.validate: + $ref: 'block/paths.yaml#/block.validate' /utxo.get_challenge_data: $ref: 'utxo/paths.yaml#/utxo.get_challenge_data' /utxo.get_exit_data: diff --git a/apps/omg_watcher_rpc/test/omg_watcher_rpc/web/controllers/block_test.exs b/apps/omg_watcher_rpc/test/omg_watcher_rpc/web/controllers/block_test.exs index 9291e7c57a..1a1099c335 100644 --- a/apps/omg_watcher_rpc/test/omg_watcher_rpc/web/controllers/block_test.exs +++ b/apps/omg_watcher_rpc/test/omg_watcher_rpc/web/controllers/block_test.exs @@ -16,11 +16,28 @@ defmodule OMG.WatcherRPC.Web.Controller.BlockTest do use ExUnitFixtures use ExUnit.Case, async: false use OMG.WatcherInfo.Fixtures + use OMG.WatcherRPC.Web, :controller import OMG.WatcherInfo.Factory + alias OMG.Eth.Encoding + alias OMG.Merkle + alias OMG.State.Transaction + alias OMG.TestHelper + alias OMG.WireFormatTypes + alias Support.WatcherHelper + @valid_block %{ + hash: "0x" <> String.duplicate("00", 32), + number: 1000, + transactions: ["0x00"] + } + @eth OMG.Eth.zero_address() + @alice OMG.TestHelper.generate_entity() + @bob OMG.TestHelper.generate_entity() + @payment_tx_type WireFormatTypes.tx_type_for(:tx_payment_v1) + describe "get_block/2" do @tag fixtures: [:initial_blocks] test "/block.get returns correct block if existent" do @@ -40,7 +57,9 @@ defmodule OMG.WatcherRPC.Web.Controller.BlockTest do expected = %{ "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", - "messages" => %{"validation_error" => %{"parameter" => "blknum", "validator" => ":integer"}}, + "messages" => %{ + "validation_error" => %{"parameter" => "blknum", "validator" => ":integer"} + }, "object" => "error" } @@ -55,7 +74,9 @@ defmodule OMG.WatcherRPC.Web.Controller.BlockTest do expected = %{ "code" => "operation:bad_request", "description" => "Parameters required by this operation are missing or incorrect.", - "messages" => %{"validation_error" => %{"parameter" => "blknum", "validator" => ":integer"}}, + "messages" => %{ + "validation_error" => %{"parameter" => "blknum", "validator" => ":integer"} + }, "object" => "error" } @@ -131,4 +152,125 @@ defmodule OMG.WatcherRPC.Web.Controller.BlockTest do } = response end end + + describe "validate_block/2" do + @tag fixtures: [:phoenix_ecto_sandbox] + test "returns the error API response if a parameter is incorectly formed" do + invalid_hash = "0x1234" + invalid_params = Map.replace!(@valid_block, :hash, invalid_hash) + + %{"data" => data} = WatcherHelper.rpc_call("block.validate", invalid_params, 200) + + expected = %{ + "code" => "operation:bad_request", + "description" => "Parameters required by this operation are missing or incorrect.", + "messages" => %{ + "validation_error" => %{ + "parameter" => "hash", + "validator" => "{:length, 32}" + } + }, + "object" => "error" + } + + assert expected == data + end + + @tag fixtures: [:phoenix_ecto_sandbox] + test "returns the expected error if the block hash does not match the reconstructed Merkle root" do + recovered_tx_1 = TestHelper.create_recovered([{1, 0, 0, @alice}], @eth, [{@bob, 100}]) + recovered_tx_2 = TestHelper.create_recovered([{2, 0, 0, @alice}], @eth, [{@bob, 100}]) + + signed_txbytes = + [recovered_tx_1, recovered_tx_2] + |> Enum.map(fn tx -> tx.signed_tx_bytes end) + |> Enum.map(&Encoding.to_hex/1) + + invalid_merkle_root = "0x" <> String.duplicate("00", 32) + + params = %{ + hash: invalid_merkle_root, + number: 1000, + transactions: signed_txbytes + } + + %{"data" => data} = WatcherHelper.rpc_call("block.validate", params, 200) + + assert data == %{"valid" => false} + end + + @tag fixtures: [:phoenix_ecto_sandbox] + test "returns the expected error if the transactions are incorrectly formed" do + input_1 = {1, 0, 0, @alice} + input_2 = {2, 0, 0, @alice} + input_3 = {3, 0, 0, @alice} + + signed_valid_tx = TestHelper.create_signed([input_1, input_2], @eth, [{@bob, 10}]) + signed_invalid_tx = TestHelper.create_signed([input_3, input_3], @eth, [{@bob, 10}]) + + %{sigs: sigs_valid} = signed_valid_tx + %{sigs: sigs_invalid} = signed_invalid_tx + + txbytes_valid = Transaction.raw_txbytes(signed_valid_tx) + txbytes_invalid = Transaction.raw_txbytes(signed_invalid_tx) + + [_, inputs_valid, outputs_valid, _, _] = ExRLP.decode(txbytes_valid) + [_, inputs_invalid, outputs_invalid, _, _] = ExRLP.decode(txbytes_invalid) + + hash_valid = + [sigs_valid, @payment_tx_type, inputs_valid, outputs_valid, 0, <<0::256>>] + |> ExRLP.encode() + |> Encoding.to_hex() + + hash_invalid = + [sigs_invalid, @payment_tx_type, inputs_invalid, outputs_invalid, 0, <<0::256>>] + |> ExRLP.encode() + |> Encoding.to_hex() + + merkle_root = [txbytes_invalid, txbytes_valid] |> Merkle.hash() |> Encoding.to_hex() + + params = %{ + hash: merkle_root, + transactions: [hash_invalid, hash_valid], + number: 1000 + } + + # Sanity check + assert {:ok, Encoding.from_hex(merkle_root)} == expect(%{hash: merkle_root}, :hash, :hash) + + %{"data" => data} = WatcherHelper.rpc_call("block.validate", params, 200) + + assert data == %{"valid" => false} + end + + @tag fixtures: [:phoenix_ecto_sandbox] + test "returns the block if it is valid" do + recovered_tx_1 = TestHelper.create_recovered([{1, 0, 0, @alice}], @eth, [{@bob, 100}]) + recovered_tx_2 = TestHelper.create_recovered([{2, 0, 0, @alice}], @eth, [{@bob, 100}]) + + signed_txbytes = + [recovered_tx_1, recovered_tx_2] + |> Enum.map(fn tx -> tx.signed_tx_bytes end) + |> Enum.map(&Encoding.to_hex/1) + + valid_merkle_root = + [recovered_tx_1, recovered_tx_2] + |> Enum.map(&Transaction.raw_txbytes/1) + |> Merkle.hash() + |> Encoding.to_hex() + + # Sanity check + assert {:ok, Encoding.from_hex(valid_merkle_root)} == expect(%{hash: valid_merkle_root}, :hash, :hash) + + params = %{ + hash: valid_merkle_root, + number: 1000, + transactions: signed_txbytes + } + + %{"data" => data} = WatcherHelper.rpc_call("block.validate", params, 200) + + assert data == %{"valid" => true} + end + end end diff --git a/apps/omg_watcher_rpc/test/omg_watcher_rpc/web/validators/block_constraints_test.exs b/apps/omg_watcher_rpc/test/omg_watcher_rpc/web/validators/block_constraints_test.exs index c694936743..c82158ff2e 100644 --- a/apps/omg_watcher_rpc/test/omg_watcher_rpc/web/validators/block_constraints_test.exs +++ b/apps/omg_watcher_rpc/test/omg_watcher_rpc/web/validators/block_constraints_test.exs @@ -17,6 +17,12 @@ defmodule OMG.WatcherRPC.Web.Validator.BlockConstraintsTest do alias OMG.WatcherRPC.Web.Validator.BlockConstraints + @valid_block %{ + "hash" => "0x" <> String.duplicate("00", 32), + "number" => 1000, + "transactions" => ["0x00"] + } + describe "parse/1" do test "returns page and limit constraints when given page and limit params" do request_data = %{"page" => 1, "limit" => 100} @@ -58,4 +64,38 @@ defmodule OMG.WatcherRPC.Web.Validator.BlockConstraintsTest do assert BlockConstraints.parse(%{"page" => "abcd"}) == {:error, {:validation_error, "page", :integer}} end end + + describe "parse_to_validate/1" do + test "rejects invalid Merkle root hash" do + invalid_hash = "0x1234" + invalid_block = Map.replace!(@valid_block, "hash", invalid_hash) + + assert {:error, {:validation_error, "hash", {:length, 32}}} == + BlockConstraints.parse_to_validate(invalid_block) + end + + test "rejects non-list transactions parameter" do + invalid_transactions_param = "0x1234" + invalid_block = Map.replace!(@valid_block, "transactions", invalid_transactions_param) + + assert {:error, {:validation_error, "transactions", :list}} == + BlockConstraints.parse_to_validate(invalid_block) + end + + test "rejects non-hex elements in transactions list" do + invalid_tx_rlp = "0xZ" + invalid_block = Map.replace!(@valid_block, "transactions", [invalid_tx_rlp]) + + assert {:error, {:validation_error, "transactions.hash", :hex}} == + BlockConstraints.parse_to_validate(invalid_block) + end + + test "rejects invalid block number parameter" do + invalid_blknum = "ONE THOUSAND" + invalid_block = Map.replace!(@valid_block, "number", invalid_blknum) + + assert {:error, {:validation_error, "number", :integer}} == + BlockConstraints.parse_to_validate(invalid_block) + end + end end diff --git a/priv/cabbage b/priv/cabbage index cd262a3b62..96d67f4e06 160000 --- a/priv/cabbage +++ b/priv/cabbage @@ -1 +1 @@ -Subproject commit cd262a3b624a347b05208b154e0c2d7f85dd92a9 +Subproject commit 96d67f4e065aad020dfde111419e95e397f443e8