Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

/block.validate endpoint #1668

Merged
merged 53 commits into from
Aug 10, 2020
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
5c95a04
feat: init block.validate
Jul 21, 2020
00bab13
completes parameter validation tests
Jul 21, 2020
10bf92e
rename validation method with stronger typing
Jul 21, 2020
1528e91
feat: verify_transactions and and negative test
Jul 21, 2020
525be8f
add: positive test for verify_transactions
Jul 22, 2020
a1389d0
chore: remove single pipes
Jul 22, 2020
78d1a03
verify_transactions: add @spec and @doc
Jul 22, 2020
61d78b2
pending
Jul 22, 2020
64bab23
Merge branch 'ripzery/block.validate' of https://github.com/omisego/e…
Jul 22, 2020
a185216
test: Added tests for `verify_merkle_root/1`
Jul 22, 2020
8b14919
remove reverse operation
Jul 22, 2020
e8eb1fe
Merge branch 'ripzery/block.validate' of https://github.com/omisego/e…
Jul 22, 2020
cd23f9a
refactor: simplify tests
Jul 22, 2020
552fa7b
refactor: simplify verify_merkle_root/1 tests
Jul 22, 2020
8efd75f
refactor: merkle validation tests + entities as module attributes
Jul 22, 2020
a1477d1
add dialyzer spec and remove argument pattern matching
Jul 22, 2020
e2f7c45
move logic to dedicated validator module
Jul 23, 2020
5227dd5
test: added test for `block.validate` endpoint
Jul 24, 2020
f587d1c
refactor: make tests pass
Jul 24, 2020
26d1e53
feat: response for `validate_block` view
Jul 24, 2020
8d87423
feat: error for mismatched_merkle_root
Jul 24, 2020
ad8a1d7
simplify tests
Jul 24, 2020
53b41ec
rename error
Jul 24, 2020
85b9669
remove unused method
Jul 24, 2020
12f634d
fix: error name
Jul 24, 2020
7ac4143
fix: credo
Jul 24, 2020
7d68ef0
Merge branch 'master' of https://github.com/omisego/elixir-omg into r…
Jul 24, 2020
3457db6
merge issue: gitmodules
Jul 24, 2020
2a29284
merge master issue: gitmodules [2]
Jul 24, 2020
2b1519b
merge master issue: gitmodules [3]
Jul 24, 2020
fca2b63
Merge branch 'ripzery/block.validate' of https://github.com/omisego/e…
Jul 24, 2020
0958ae0
(re)delete specs
Jul 24, 2020
05b2886
return submodule
ayrat555 Jul 24, 2020
3999f62
Merge branch 'master' of https://github.com/omisego/elixir-omg into r…
Aug 3, 2020
7634d73
refactor: decouple verify_merkle_root from transaction recovery
Aug 3, 2020
6608d60
add: negative test for invalid transactions
Aug 3, 2020
caa9286
Merge branch 'ripzery/block.validate' of https://github.com/omisego/e…
Aug 3, 2020
2ae6174
refactor: improve performance of verify_transactions
Aug 3, 2020
b68d533
use pin operator instead of comparison
Aug 3, 2020
3509f77
refactor: remove use of & &1. for clearer syntax
Aug 4, 2020
26b021e
remove redundant comment
Aug 4, 2020
dc59963
Merge branch 'master' of https://github.com/omisego/elixir-omg into r…
Aug 5, 2020
9b7e2cb
move block validation logic into Watcher
Aug 6, 2020
2110584
move parameter validation logic into BlockConstraints
Aug 6, 2020
c0a2fc5
reflect file changes in controller
Aug 6, 2020
1581d5c
refactor: make verification functions private and shift tests to stat…
Aug 7, 2020
c6b1a49
fix potential false positive by computing Merkle root correctly.
Aug 7, 2020
c3362a5
add: swagger specs
Aug 7, 2020
a4d023f
refactor: endpoint to return boolean result
Aug 7, 2020
f07e407
add: nil error
Aug 7, 2020
89e473b
update documentation to reflect boolean result
Aug 7, 2020
037945d
Merge branch 'master' of https://github.com/omisego/elixir-omg into r…
Aug 7, 2020
4a03a84
remove error reason in response
Aug 7, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions apps/omg_watcher_rpc/lib/web/controllers/block.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ defmodule OMG.WatcherRPC.Web.Controller.Block do

use OMG.WatcherRPC.Web, :controller

alias OMG.Block
alias OMG.WatcherInfo.API.Block, as: InfoApiBlock
alias OMG.WatcherRPC.Web.Validator

Expand All @@ -43,4 +44,22 @@ 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.BlockValidator.parse_to_validate(params),
{:ok, _block} <- stateless_validate(block) do
api_response(block, conn, :validate_block)
end
end

@spec stateless_validate(Block.t()) :: any
defp stateless_validate(block) do
with {:ok, block} <- Validator.BlockValidator.verify_merkle_root(block),
{:ok, block} <- Validator.BlockValidator.verify_transactions(block.transactions) do
{:ok, block}
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
2 changes: 2 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,8 @@ defmodule OMG.WatcherRPC.Web.Validator.BlockConstraints do
@moduledoc """
Validates `/block.all` query parameters
"""

use OMG.WatcherRPC.Web, :controller
alias OMG.WatcherRPC.Web.Validator.Helpers

@doc """
Expand Down
72 changes: 72 additions & 0 deletions apps/omg_watcher_rpc/lib/web/validators/block_validator.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# 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.BlockValidator do
@moduledoc """
Operations related to block validation.
"""

use OMG.WatcherRPC.Web, :controller
alias OMG.Block
alias OMG.State.Transaction

@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

@doc """
Verifies that given Merkle root matches reconstructed Merkle root.
"""
@spec verify_merkle_root(Block.t()) :: {:ok, Block.t()} | {:error, :mismatched_merkle_root}
def verify_merkle_root(block) do
%{hash: reconstructed_merkle_root_hash} =
block.transactions
|> Enum.map(fn tx ->
{:ok, recovered_tx} = Transaction.Recovered.recover_from(tx)
recovered_tx
end)
|> OMG.Block.hashed_txs_at(block.number)

case block.hash == reconstructed_merkle_root_hash do
kalouo marked this conversation as resolved.
Show resolved Hide resolved
true -> {:ok, block}
_ -> {:error, :invalid_merkle_root}
end
end

@doc """
Verifies that transactions are correctly formed.
"""
@spec verify_transactions(transactions :: list(Transaction.Recovered.t())) ::
{:ok, list(Transaction.Recovered.t())}
| {:error, Transaction.Recovered.recover_tx_error()}
def verify_transactions(transactions) do
Enum.reduce_while(transactions, {:ok, []}, fn tx, {:ok, already_recovered} ->
case Transaction.Recovered.recover_from(tx) do
{:ok, recovered} -> {:cont, {:ok, already_recovered ++ [recovered]}}
kalouo marked this conversation as resolved.
Show resolved Hide resolved
error -> {:halt, error}
end
end)
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,26 @@ 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 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()

describe "get_block/2" do
@tag fixtures: [:initial_blocks]
test "/block.get returns correct block if existent" do
Expand All @@ -40,7 +55,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"
}

Expand All @@ -55,7 +72,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"
}

Expand Down Expand Up @@ -131,4 +150,89 @@ 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 == %{
"code" => "block.validate:invalid_merkle_root",
"description" => "Block hash does not match reconstructed Merkle root.",
"object" => "error"
}
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 == %{
"hash" => params.hash,
"number" => params.number,
"transactions" => params.transactions
}
end
end
end
Loading