-
Notifications
You must be signed in to change notification settings - Fork 31
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
Why does Semux need the function PendingManager.processTransaction() ? #310
Comments
Yes @fenghaoming, you're right! The purpose of executing pending transactions in the pool is to exclude invalid transactions after the state change caused by importing a new block. There is definitely a performance cost associated with this process and there are two possible workarounds:
Will definitely consider these options. |
First solutionI think the first asynchronous solution is still unreliable because all functions in the PendingManager need to be synchronized. Second solutionI think the second lighter scheme is reasonable, and this is how Ethereum deals with it. An example is as follows: ExampleAssume that an account currently has a nonce of 1 and a balance of 1000 ETH, and send four transactions as follows quickly and continuously (Nonce is null when user sends) :
According to Semux's current treatment:The first three transactions were successfully added to the tx pool (1000 ETH is just enough for the GasUsed accumulated according to the pending transactions execution), As Ethereum does:All transactions are successfully added to the trading pool, and on subsequent block generation then:
After that, if the user initiates the following transaction:
ThereforeWe can see that the removal of the pending transactions execution in the PendingManager only results in the following two disadvantages:
These two disadvantages only affect the case of sending multiple transactions in rapid succession when the user has only a small balance. Are there any other shortcomings I haven't considered? Thank you very much! |
This is a thorough analysis! I agree with the first conclusion but have some doubt about the second scenario. The balance check should deduct the sender’s balance by the theoretical max cost and increase its nonce to the pending state after each added transaction. With a large number of transactions, they should be reorganized by the designated nonce and validated the same way. The transaction validation in pending pool may include false positives but should be sound. Out of curiosity though, are you able to make a PR to improve the pending manager? :) |
Also note that most of the account states should already be in cache and can be retrieved fairly fast. |
May I ask what is your suspicion about the second scenario? Could you give me an example? Do you want me to optimize the Pending Manager as I said above (remove mock execution, do not maintain a set of world states, just do simple "per transaction independent" balance checks)? |
Let me clarify it a bit - my point was that we should not remove transaction execution, but simplify it instead. When a new block is imported,
The above process does not trigger any VM execution and only requires account state (cached with high probability), so it should be fast.
Essentially, transactions from the same sender are validated sequentially, so "insufficient balance" error should be immediately available. I might have missed details, but feel free to add as you see fit. :) |
I agree with you on the whole, but I have a different opinion on one point Example:For example, there are 4 transactions from the same sender with a balance of 1000 ETH, each of which has a GasPrice of 1 ETH, a GasLimit of 500, but the actual execution consumes a GasUsed of 300 According to your approach:The first two transactions successfully joined the PendingPool, and the last two transactions reported an error of "insufficient balance" in real time. My view:All four transactions were successfully added to the PendingPool because "500 is less than 1000" (where each transaction is "independently" compared with the account balance by GasLimit), and finally the first three transactions successfully packaged into the block (use the "cumulative" GasUsed to compare when the block generating) and the last transaction is discarded. So:I think your approach requires the user not to set the GasLimit too high (much higher than GasUsed) when sending a transaction, otherwise subsequent transactions from that user could easily check for error "insufficient balance" unreasonably. |
Yes, your example is absolutely correct. The proposal is an optimistic approach and may falsely reject a transaction even if the sender has enough balance to cover the actual transaction cost. I think it only affects users who deliberately wants to spend all the available funds. In this case, they can potentially lower the |
I stand in the user's position to think about this problem.When I send a transaction, I don't know how much gas it actually consumes, so I usually try to set the gas as high as possible to avoid the error of outOfGas and wasting my ETH.This is just my personal opinion, do not know right? Another reason I don't want to continue to maintain user balances in "onBlockAdded()" is that it still needs to read every transaction in the trade pool to operate, even if it no longer penetrates into the VM layer, but the time complexity is still O(n). I think the trade pool should be maintained in both account and nonce dimensions.
So we're going to do it this way, the number of onBlockAdded transactions that need to be processed is related only to the number of transactions in the block, not to the number of transactions pending. |
We can simply sort the transactions based on the gasPrice and limit the number of transactions that are actively evluated for pending block. So the complexity can be limited to O(1). The execute time is not wasted as it will spend up the block creation process, given most of the account states are hot. That said, I do like the idea about grouping transactions by the sender's address. I will explore this a bit more. |
Aso, regarding:
I don't have data about different user behaviors so I'm going to guess that some people might prefer to set the gasLimit as high as possible. Let's say he/she uses |
If there 10,000 txs in txPool, but only 1000 txs that are actively evluated for pending block. You maintain user balances in txPool only with the 1000 txs. Here comes another new one tx. In my opinion, we should judge whether the balance of this transaction is sufficient by deducting the cost of those 10,000 transactions. Maintaining only those 1000 transactions makes no sense on its own.
I'm sorry, I don't understand how it will spend up the block creation process. Could you give an example? Thank you! |
Why does Semux need the function PendingManager.processTransaction() ?
src/main/java/org/semux/core/PendingManager.java
Because of the BlockGasLimit, it is possible for a block to fail to package all transactions in a tx pool.
Each time a block is added, let's assume that the block contains 300 transactions and the pool accumulates 1000 transactions due to receiving pressure.
The onBlockAdded() function triggers the 1000 transactions to be reexecuted, blocking block adding.
If the nodes are still receiving transactions at this point, the transactions will be packaged and consensused more slowly, and the tx pool will pile up transactions further, creating a vicious cycle.
So, what is the necessity of doing this?
Thx!
The text was updated successfully, but these errors were encountered: