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

feat: return blknum and transaction index on transaction submit #133

Merged
merged 1 commit into from
Oct 9, 2020

Conversation

pgebal
Copy link
Contributor

@pgebal pgebal commented Sep 22, 2020

Returns block number an index for a submitted transaction.

To determine transaction index on submission I had to lock transaction on currently forming block.
Changes:

  • transactions are associated with block and have index set on insert
  • position is assigned to output on insert
  • Engine.DB.Transaction.insert inserts a transaction and new block if current forming block exceeds transaction limit
  • Engine.DB.Block.form no longer inserts a block. It calculates merkle root hash and sets block state to pending_submission.
  • Engine.DB.Transaction.insert required that there is a forming block when inserting a transaction
  • refactor: separated transaction query, transaction changeset and block query modules
  • assumes that there is always a pending block in the database

@pgebal pgebal force-pushed the pgebal/draft-block-forming branch 3 times, most recently from 3fcb21d to 0b542b2 Compare September 23, 2020 14:31
apps/engine/lib/engine/callbacks/in_flight_exit_started.ex Outdated Show resolved Hide resolved
apps/engine/lib/engine/callbacks/piggyback.ex Outdated Show resolved Hide resolved
apps/engine/lib/engine/db/block.ex Show resolved Hide resolved
apps/engine/lib/engine/db/block.ex Outdated Show resolved Hide resolved
apps/engine/lib/engine/db/output.ex Outdated Show resolved Hide resolved
apps/engine/lib/engine/db/transaction.ex Outdated Show resolved Hide resolved
apps/engine/lib/engine/db/block.ex Outdated Show resolved Hide resolved
apps/engine/test/engine/db/transaction_test.exs Outdated Show resolved Hide resolved
@pgebal pgebal force-pushed the pgebal/draft-block-forming branch 2 times, most recently from a3ff177 to 3d9b6a8 Compare September 24, 2020 17:34
@pgebal pgebal force-pushed the pgebal/draft-block-forming branch 9 times, most recently from 41e08e1 to 620e3b0 Compare September 28, 2020 15:57
@pgebal pgebal changed the title WIP - calculating tx index on transaction insert feat: return blknum and transaction index on transaction submit Sep 28, 2020
@pgebal pgebal marked this pull request as ready for review September 28, 2020 16:01
|> Multi.run("new-block", &insert_block/2)
|> Multi.run("form-block", &attach_transactions_to_block/2)
|> Multi.run("hash-block", &generate_block_hash/2)
|> Multi.run("block", &get_or_insert_forming_block/2)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that has to be used carefully.
If there is no forming block, then each concurrent transaction could insert a block.
That should not be the case becausewe won't be probably forming empty blocks and after each inserting a transaction via Transaction.insert we'll always have a forming block in the db.

# basic version, checking transaction limit is postponed until we get transction fees implemented
case last_tx_index >= @max_transaction_in_block do
true ->
{:ok, _} = prepare_for_submission(repo, %{"block" => block})
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As @InoMurko pointed in discussion in the issue - this should not be done on insert, especially calculating block's hash.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah and you are missing fee transactions. This should just transition the block to a new state so it can be picked by an other service to process the finalization (add fee tx / calculate hash and stuff).

apps/api/lib/api/v1/controllers/transaction_controller.ex Outdated Show resolved Hide resolved
apps/api/lib/api/v1/controllers/transaction_controller.ex Outdated Show resolved Hide resolved
apps/api/lib/api/v1/controllers/transaction_controller.ex Outdated Show resolved Hide resolved
apps/api/test/api/v1/router_test.exs Show resolved Hide resolved
apps/engine/test/engine/db/transaction_test.exs Outdated Show resolved Hide resolved
apps/engine/test/engine/db/transaction_test.exs Outdated Show resolved Hide resolved
apps/engine/test/engine/db/transaction_test.exs Outdated Show resolved Hide resolved
assert block.hash == hash
end

test "correctly generates block hash for empty txs list" do
assert {:ok, %{"hash-block" => block}} = Block.form()
assert {:ok, %{block_for_submission: block}} = Block.form()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we disallow empty blocks? Empty blocks have the same hash.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes! we shouldn't form empty blocks, but I guess this can be done in your next PR when you tackle the block finalization?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right

assert tx1.tx_index + 1 == tx2.tx_index
end

test "does not create conflicts when inserting multiple transaction concurrently" do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought that only this test was causing an issue when using async: true because it was doing async stuff.
Other tests in this file could go to the other test file that runs with async: true, no?

Copy link
Contributor Author

@pgebal pgebal Oct 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought that too, but there was 1 more insert test failing, so I decided to move them all here. I think we do not have to be concerned much about those tests running long, and there is a benefit of having all insert tests in one place.

assert block.hash == hash
end

test "correctly generates block hash for empty txs list" do
assert {:ok, %{"hash-block" => block}} = Block.form()
assert {:ok, %{block_for_submission: block}} = Block.form()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes! we shouldn't form empty blocks, but I guess this can be done in your next PR when you tackle the block finalization?

apps/api/test/api/v1/router_test.exs Outdated Show resolved Hide resolved
apps/engine/lib/engine/db/block.ex Show resolved Hide resolved
apps/engine/lib/engine/db/block.ex Outdated Show resolved Hide resolved
{:error, changeset}

error ->
_ = Logger.error("Error when inserting transaction #{inspect(error)}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this would be a good datadog counter

apps/engine/lib/engine/db/transaction.ex Outdated Show resolved Hide resolved
apps/engine/lib/engine/db/transaction/transaction_query.ex Outdated Show resolved Hide resolved
Comment on lines 124 to 125
|> Multi.run(:current_forming_block, &Block.get_forming_block_for_update/2)
|> Multi.run(:block_with_next_tx_index, &Block.get_block_and_tx_index_for_transaction/2)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems to me that perhaps these two reads could be just one.
get the block and it's max transaction index:

last_tx_index = repo.one(TransactionQuery.select_max_tx_index_for_block(block.id)) || -1

https://www.postgresqltutorial.com/postgresql-inner-join/#:~:text=To%20select%20data%20from%20both,data%20in%20the%20SELECT%20clause.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good idea to check if it improves performance. I'll dig into this and if it speeds things up, I'll create a separate PR for that.

apps/engine/test/support/db/factory.ex Outdated Show resolved Hide resolved
|> changeset(params)
|> repo.insert()
|> BlockChangeset.new_block_changeset(params)
|> repo.insert(on_conflict: :nothing)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we have a problem when this conflict happens? Last time I tried, the block returned didn't have an id so the transaction was failing. I could fix it by adding a

|> case do
     {:ok, %{id: nil} -> re-query pending forming block until we get iit.
  .....

But I don't know if it's a valid solution.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this would happen if a block is inserted by another transaction, right?

Copy link
Contributor Author

@pgebal pgebal Oct 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this method is called only when:

  • Block.formed -> we insert a new forming block, we can do nothing on conflict, forming block is in the database
  • Transaction.insert -> we insert a new block which we will associate a transaction with. In that case always Block.get_or_insert_forming_block is called, which selects a forming block from db after insert.

So it should be fine.

@@ -1,98 +1,102 @@
defmodule Engine.DB.TransactionTest do
use Engine.DB.DataCase, async: true
use Engine.DB.DataCase, async: false
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is done on purpose
I think all tests using Shared mode Sandbox should not be run sync.
https://hexdocs.pm/ecto_sql/Ecto.Adapters.SQL.Sandbox.html#module-shared-mode

Copy link
Contributor

@InoMurko InoMurko Oct 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_So Ecto.Adapters.SQL.Sandbox provides :shared mode, which a process can switch to after checkout/1. Ecto.Adapters.SQL.Sandbox.mode(Repo, {:shared, pid}) means “all processes running concurrently with pid should use the connection which pid owns.”

As the docs for SQL.Sandbox say, using :shared mode means that “tests can no longer run concurrently.” With the understanding we’ve built, this makes sense. If shared mode means “all concurrent processes share this connection,” we only want one one test process to be in that concurrent group. If any other test ran at the same time, the two tests would be working with the database within the same transaction and the changes that one test made would leak to the other test. This would destroy our illusion that each test has a database to itself.

So any test using :shared mode must be in a test module marked async: false. This means no tests from other modules will run concurrently with it. Since tests in the same module run serially, it will be the only test running until it completes and may share its database connection fearlessly with the processes whose behavior it tests._

yeah, seems correct!

Copy link
Contributor

@mederic-p mederic-p left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job! Let's start with this and see how we can improve it later.

@@ -51,7 +51,7 @@ defmodule Engine.DB.Transaction do
tx_hash: <<_::256>>,
tx_type: pos_integer(),
updated_at: DateTime.t(),
witnesses: DateTime.t()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😱

"""
def insert(tx_bytes) do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, I think it makes more sense like this!

@pgebal pgebal merged commit 9d8352b into master Oct 9, 2020
@pgebal pgebal deleted the pgebal/draft-block-forming branch October 9, 2020 15:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants