Skip to content
This repository has been archived by the owner on Oct 28, 2021. It is now read-only.

Populate legacy block's state from database instead of executing transactions #5792

Merged
merged 6 commits into from
Nov 4, 2019

Conversation

halfalicious
Copy link
Contributor

@halfalicious halfalicious commented Oct 25, 2019

Fix #5034

Description

These changes optimize the function that the client calls to initialize a Block object with blockchain data (Block::populateFromChain). Note that Block::populateFromChain is called by numerous RPC functions e.g. eth_call, eth_getBalance, eth_sendTransaction.

For context, the Client calls Block::populateFromChain to create a new Block object that's initialized with data from a blockchain block with a specific hash - this initialized Block can then be queried for specific data (e.g. eth_getBalance) or used to execute a transaction (eth_call and eth_sendTransaction).

Prior to these changes, Block::populateFromChain populated a block by retrieving the target block's header and parent's block header and block bytes from the blockchain and initializing various block fields using this information (e.g. current block header Block::m_currentBlock, parent block header Block::m_previousBlock). It would also verify the target block's header and build the target block's state trie and generate the transaction receipts by setting the block being populated's state root to the parent's state and executing all of the transactions (retrieved from the target block's bytes). The verification step and tx execution steps are obviously unnecessary - rather, we can simply retrieve the state root from the state database and retrieve the transactions and receipts from the block bytes (which we get from the block database).

My changes retrieve the block headers and block bytes from the database as before - however, I don't verify the block (not necessary since it was already verified during import) and I don't execute the transactions..rather I read the tx receipts from the block bytes and read the state root from the state database.

Validation

I validated my changes by retrieving the genesis block and a block with several transactions (64999) via RPC (web3.eth.getBlock) and comparing the results against a "stock" Aleth and verifying the results were identical. Results:

Genesis Block
Without Changes

  author: "0x0000000000000000000000000000000000000000",
  boundary: "0x0000000040000000000000000000000000000000000000000000000000000000",
  difficulty: 17179869184,
  extraData: "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa",
  gasLimit: 5000,
  gasUsed: 0,
  hash: "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3",
  logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  miner: "0x0000000000000000000000000000000000000000",
  mixHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
  nonce: "0x0000000000000042",
  number: 0,
  parentHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
  receiptsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
  seedHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
  sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
  size: 76,
  stateRoot: "0xd7f8974fb5ac78d9ac099b9ad5018bedc2ce0a72dad1827a1709da30580f0544",
  timestamp: 0,
  totalDifficulty: 17179869184,
  transactions: [],
  transactionsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
  uncles: []

With Changes

  author: "0x0000000000000000000000000000000000000000",
  boundary: "0x0000000040000000000000000000000000000000000000000000000000000000",
  difficulty: 17179869184,
  extraData: "0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa",
  gasLimit: 5000,
  gasUsed: 0,
  hash: "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3",
  logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  miner: "0x0000000000000000000000000000000000000000",
  mixHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
  nonce: "0x0000000000000042",
  number: 0,
  parentHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
  receiptsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
  seedHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
  sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
  size: 76,
  stateRoot: "0xd7f8974fb5ac78d9ac099b9ad5018bedc2ce0a72dad1827a1709da30580f0544",
  timestamp: 0,
  totalDifficulty: 17179869184,
  transactions: [],
  transactionsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
  uncles: []

Block 64999
Without Changes

 author: "0xf927a40c8b7f6e07c5af7fa2155b4864a4112b13",
  boundary: "0x0000000000932bb9e4d20f331c0fd1eda100ae03dd36d33a9b2a6a0e9cd3915e",
  difficulty: 1912573463224,
  extraData: "",
  gasLimit: 3141592,
  gasUsed: 118496,
  hash: "0x650ec59d844817aff623efdc7a976e2f7005b17035e5ac5e7025747fbc4091ad",
  logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  miner: "0xf927a40c8b7f6e07c5af7fa2155b4864a4112b13",
  mixHash: "0xdfe2a94512f29e4363b48d0e8661a78f11d41e6fd36d9f2cd91e0c77ba0d8082",
  nonce: "0x5f7c213366edf1ef",
  number: 64999,
  parentHash: "0x23ee2f4a420e1ce4beb6d5902fafa232c0d5718146e53a31b179c86949191b85",
  receiptsRoot: "0x61b8587e6a557ecefb87f0091be5b894c197a5b43d3960093bd051589351bc32",
  seedHash: "0x510e4e770828ddbf7f7b00ab00a9f6adaf81c0dc9cc85f1f8249c256942d61d9",
  sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
  size: 0,
  stateRoot: "0x4db358e5d7eca7ead868e622e560b2c4d4588147aee681905ad7acc17e8fd8ec",
  timestamp: 1439229736,
  totalDifficulty: 73588533123492323,
  transactions: ["0xa047af96f2342ff7bc1a061b6d83a9f00af0c7986bb24924391bf0e83de22f85", "0x49be91905241a1bac6e9cfd2568be07aa4c2c13aec5c2ce4f63f27068a5836b1", "0x9acb4b6573444ed7ed12cd8920fdcded1beadacb1872228ce09b34ccfce8e008"],
  transactionsRoot: "0xac021c3d56771233aebe36ebb0442373812b0489c3b478309339991d6dd51a71",
  uncles: []

With Changes

  author: "0xf927a40c8b7f6e07c5af7fa2155b4864a4112b13",
  boundary: "0x0000000000932bb9e4d20f331c0fd1eda100ae03dd36d33a9b2a6a0e9cd3915e",
  difficulty: 1912573463224,
  extraData: "",
  gasLimit: 3141592,
  gasUsed: 118496,
  hash: "0x650ec59d844817aff623efdc7a976e2f7005b17035e5ac5e7025747fbc4091ad",
  logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  miner: "0xf927a40c8b7f6e07c5af7fa2155b4864a4112b13",
  mixHash: "0xdfe2a94512f29e4363b48d0e8661a78f11d41e6fd36d9f2cd91e0c77ba0d8082",
  nonce: "0x5f7c213366edf1ef",
  number: 64999,
  parentHash: "0x23ee2f4a420e1ce4beb6d5902fafa232c0d5718146e53a31b179c86949191b85",
  receiptsRoot: "0x61b8587e6a557ecefb87f0091be5b894c197a5b43d3960093bd051589351bc32",
  seedHash: "0x510e4e770828ddbf7f7b00ab00a9f6adaf81c0dc9cc85f1f8249c256942d61d9",
  sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
  size: 0,
  stateRoot: "0x4db358e5d7eca7ead868e622e560b2c4d4588147aee681905ad7acc17e8fd8ec",
  timestamp: 1439229736,
  totalDifficulty: 73588533123492323,
  transactions: ["0xa047af96f2342ff7bc1a061b6d83a9f00af0c7986bb24924391bf0e83de22f85", "0x49be91905241a1bac6e9cfd2568be07aa4c2c13aec5c2ce4f63f27068a5836b1", "0x9acb4b6573444ed7ed12cd8920fdcded1beadacb1872228ce09b34ccfce8e008"],
  transactionsRoot: "0xac021c3d56771233aebe36ebb0442373812b0489c3b478309339991d6dd51a71",
  uncles: []

@halfalicious halfalicious changed the title Don't re-execute each transaction when retrieving a Block in the Client Don't re-execute transactions when retrieving a Block in the Client Oct 25, 2019
Refactor Block::populateFromChain so that the function doesn't do any
unnecessary work (e.g. verify a block, execute transactions).
The reprocess admin RPC method is the only function which depends on the
old Block::populateFromChain functionality where it replays each tx in
the block. There's no need for the function so we can remove it.

Additionally, check for StateDB corruption in Block::populateFromChain
before setting the state root so that we can log a user-friendly error
message.
@halfalicious halfalicious changed the title Don't re-execute transactions when retrieving a Block in the Client Don't execute a block's transactions when retrieving the block in the Client Oct 25, 2019
Seal must be removed since Client::call executes a tx which can mutate
the block's state.
@codecov-io
Copy link

codecov-io commented Oct 27, 2019

Codecov Report

Merging #5792 into master will increase coverage by 0.05%.
The diff coverage is 89.28%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #5792      +/-   ##
==========================================
+ Coverage   64.02%   64.08%   +0.05%     
==========================================
  Files         359      359              
  Lines       30796    30761      -35     
  Branches     3417     3417              
==========================================
- Hits        19718    19712       -6     
+ Misses       9855     9825      -30     
- Partials     1223     1224       +1

@halfalicious
Copy link
Contributor Author

I also plan on doing some basic performance profiling to determine if we're seeing any noticeable performance deltas with these changes.

@halfalicious halfalicious requested review from gumb0 and chfast and removed request for gumb0 October 28, 2019 03:58
@halfalicious halfalicious marked this pull request as ready for review October 28, 2019 03:58
@halfalicious
Copy link
Contributor Author

Still plan to do some minor perf profiling but in the mean-time I think @gumb0 and @chfast can start their review.

@halfalicious halfalicious changed the title Don't execute a block's transactions when retrieving the block in the Client Populate legacy block's state from database instead of executing transactions Oct 29, 2019
@halfalicious
Copy link
Contributor Author

halfalicious commented Oct 29, 2019

I've performed some basic profiling to get some ballpark numbers of the perf impact of these changes.

For context, I performed all profiling on my laptop running private Aleth release builds. I added timer code to compute the time it took for the Block::populateFromChain call in Client::block to execute. I triggered the function to be called by calling web3.eth.getBalance with a valid address with a non-zero balance for each block number 10 times and then took the median of the results for each block. I chose blocks with an increasing number of transactions so I could see how the tx count affected the results. I ended up selecting 6 blocks for testing with a range of 0 -> 141 txs.

The results are pretty dramatic - for a block without any txs (genesis), the execution time drops by ~3.5% (presumably primarily due to not validating the block). Once there are any transactions in a block, the execution time drops by 90+% (and in some cases up to ~99%!). Results:

Block number Base (us) Test (us) Delta (us) Delta (%)
0 (genesis) 141 136 -5 -3.5%
795502 (9 txs) 7,528 455 -7,073 -93.96%
795500 (18 txs) 69,285 756 -68,529 -98.91%
795503 (37 txs) 33,975 1,349 -32,626 -96.03%
795508 (60 txs) 53,977 2,076 -51,901 -96.15%
795526 (141 txs) 140,059 4,693 -135,366 -96.65%

Copy link
Member

@gumb0 gumb0 left a comment

Choose a reason for hiding this comment

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

Would it be not enough to just call sync(_bc, _h, blockHeader)?

Ah no, I think it rather creates a block wich is supposed to be mined on top of requested one...

// We know there are no transactions, so just populate directly.
m_state = State(m_state.accountStartNonce(), m_state.db(), BaseState::Empty); // TODO: try with PreExisting.
sync(_bc, _h, bi);
m_transactions.push_back(Transaction{txRLP.data(), CheckTransaction::None});
Copy link
Member

Choose a reason for hiding this comment

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

Did it fill m_transactions, m_transactionsSet, m_currentTxs, m_currentUncles before?

I suspect these are also useful only for mining, and RPC methods get them separately from BlockChain

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good question 😀

  • m_transactions & m_transactionsSet: Yes
  • m_currentTxs & m_currentUncles: No

So you raise a good question - can we assume that this function will only be called via RPC? I populated everything in the block because populateFromChain is a public function so it can be called by anyone...unless these fields are only usable in very specific situations (e.g. mining), perhaps we should fork the populateFromChain into two functions - 1 which populates the fields which are useful in most cases and another which populates the rest?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah I think ideally these different cases should be handled somehow by different classes.

But let's not complicate it here, I think I'm fine with leaving like this.
But m_currentTxs really looks to be specific to only commitToSeal and sealBlock methods, and I think it would be fine to not copy it here. m_currentUncles probably too.

@halfalicious
Copy link
Contributor Author

Would it be not enough to just call sync(_bc, _h, blockHeader)?

Ah no, I think it rather creates a block wich is supposed to be mined on top of requested one...

Correct - the biggest problem I see with this (assuming we don't care about m_transactions, m_transactionsSet, m_currentTxs, m_currentUncles) is that the parent block header will be set to the same as the current block's header (m_previousBlock == m_currentBlock)

Don't save block bytes in block and log "block not found" as warning
rather than error
@halfalicious
Copy link
Contributor Author

cc @gumb0

Also don't populate RLP representation of txs and uncles in
Block::populateFromChain since they are only useful during mining.
@halfalicious halfalicious merged commit 57b8156 into master Nov 4, 2019
@halfalicious halfalicious deleted the rpc-perf branch November 4, 2019 17:09
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

client run a transaction again with each eth_get RPC request
3 participants