Skip to content

Commit

Permalink
/block.validate endpoint (#1668)
Browse files Browse the repository at this point in the history
* feat: init block.validate

* completes parameter validation tests

* rename validation method with stronger typing

* feat: verify_transactions and and negative test

* add: positive test for verify_transactions

* chore: remove single pipes

* verify_transactions: add @SPEC and @doc

* pending

* test: Added tests for `verify_merkle_root/1`

* remove reverse operation

* refactor: simplify tests

* refactor: simplify verify_merkle_root/1 tests

* refactor: merkle validation tests + entities as module attributes

* add dialyzer spec and remove argument pattern matching

* move logic to dedicated validator module

* test: added test for `block.validate` endpoint

* refactor: make tests pass

* feat: response for `validate_block` view

* feat: error for mismatched_merkle_root

* simplify tests

* rename error

* remove unused method

* fix: error name

* fix: credo

* merge issue: gitmodules

* merge master issue: gitmodules [2]

* merge master issue: gitmodules [3]

* Merge branch 'ripzery/block.validate' of https://github.com/omisego/elixir-omg into ripzery/block.validate

* (re)delete specs

* return submodule

* refactor: decouple verify_merkle_root from transaction recovery

* add: negative test for invalid transactions

* refactor: improve performance of verify_transactions

* use pin operator instead of comparison

* refactor: remove use of & &1. for clearer syntax

* remove redundant comment

* move block validation logic into Watcher

* move parameter validation logic into BlockConstraints

* reflect file changes in controller

* refactor: make verification functions private and shift tests to stateless_validate/1

* fix potential false positive by computing Merkle root correctly.

* add: swagger specs

* refactor: endpoint to return boolean result

* add: nil error

* update documentation to reflect boolean result

* remove error reason in response

Co-authored-by: euro <[email protected]>
Co-authored-by: Ayrat Badykov <[email protected]>
  • Loading branch information
3 people authored Aug 10, 2020
1 parent 8ddf382 commit 6ba53bd
Show file tree
Hide file tree
Showing 18 changed files with 625 additions and 4 deletions.
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[submodule "priv/cabbage"]
path = priv/cabbage
url = [email protected]:omgnetwork/specs.git
branch = master
branch = master
67 changes: 67 additions & 0 deletions apps/omg_watcher/lib/omg_watcher/block_validator.ex
Original file line number Diff line number Diff line change
@@ -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
124 changes: 124 additions & 0 deletions apps/omg_watcher/test/omg_watcher/block_validator_test.exs
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions apps/omg_watcher_rpc/lib/web/controllers/block.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
4 changes: 4 additions & 0 deletions apps/omg_watcher_rpc/lib/web/controllers/fallback.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions apps/omg_watcher_rpc/lib/web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
20 changes: 20 additions & 0 deletions apps/omg_watcher_rpc/lib/web/validators/block_constraints.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 """
Expand All @@ -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
6 changes: 6 additions & 0 deletions apps/omg_watcher_rpc/lib/web/views/block.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading

0 comments on commit 6ba53bd

Please sign in to comment.