-
Notifications
You must be signed in to change notification settings - Fork 137
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
[discussion] Dynamic gas price during transaction execution #67
Comments
Overall paying for gas at the correct price seems like the correct direction. With still expected low prices on gas, we can see for normal use cases (where people overestimate 10x) it will require bigger deposit but funds will get returned and spent will be the same. In cases when system at load users can expect to pay higher price as it's observed that blocks are full for last N blocks (unless it one time spike - for which we can figure out something around validity of tx #56 (comment)). |
@evgenykuzyakov in solution 2, how many blocks are used to estimate the cost of a transaction? |
@evgenykuzyakov in solution 2, you mentioned it's possible for someone to issue transaction using minimum gas and setting 10^16 budget.
Action:
Pros:
Cons:
|
The ratio of the prepaid gas and the minimum cost per promise. |
Yes, but this will be an expensive transaction. They are not going to be charged and we'll still include this transaction into the block, so it doesn't affect the runtime. |
Setting this as a Phase 1 issue, assigning @evgenykuzyakov and setting estimate to 5 (for the implementation). @evgenykuzyakov please adjust it if you think it is incorrect. This issue is a blocker for Phase 1 launch but it has lower priority than the contracts. |
@nearmax @evgenykuzyakov I think for such a major change it is desirable to have a NEP first. |
It just occurred to me that our prepaid gas can be fully viewed as a staking mechanism. Maybe, this is something DevX team can find useful for the documentation. CC @potatodepaulo , @mikedotexe , @chadoh . The general idea behind staking is the following:
What is new however, is that we previously though of stake slashing as a binary situation that only happens when actor misbehaves and it can be directly proven. With prepaid gas, the misbehavior is not only non-discreet but also it cannot be attributed to malice, so we "take away" part of the stake (if we could prove malice we would've taken all stake). |
@fckt has the following proposal that modifies @evgenykuzyakov 's proposal, which I think will solve cross-shard congestion problem better than the original proposal. TLDR: When one shard is congested the users should not be losing more tokens, they should be "staking" more tokens. (see my explanation above of how prepaid gas can be viewed as staking). @fckt suggested to taken into account upon each execution of the receipt what is the actual gas price in that given moment. This however, will require to change the receipt so that we attach tokens rather than gas. Then upon each execution part of the attached tokens are burnt and at the end the remainder is refunded. This does not require changes to the APIs. When creating a transaction the user will still indicate in gas how much gas they think it will take, the runtime will however pre-buy attached tokens based on the worst possible scenario in the most congested shard (the gas costs of the shards can be broadcasted through the block headers). Similarly, we keep gas in the contract API, so that when contract creates a cross-contract call it indicates in gas, how much gas should be attached. The overall motivation is the following:
|
@nearmax in this proposal, who pays for the "staked" tokens? |
The user who created the transaction, the same way it currently is. When transaction is converted to the receipt we estimate the worst case gas price and use it to deduct the necessary amount of tokens from the user's account, based on how much prepaid_gas is in the transaction created by the user. We then attach these tokens instead of gas to the receipt. |
I don't quite understand the difference to the original proposal. In both proposals we do some estimation to deduct tokens from the sender account when processing the transaction. It seems that in the new proposal the estimation is also done once -- when the transaction gets processed. If so, what's the point of attaching tokens to receipts? Also what is the difference to the original proposal? Is it that we estimate the cost differently? |
My understanding is that the goal is to prevent from one block from exceeding it's capacity. If I understand correctly the solution 2 has the following implications:
Correct me if I'm wrong, would it be feasible to do the following Solution 3: Limit amount of gas used per block Pros:
Cons:
Some consistent ordering would need to be defined. (I'm sure we can think of something). If I understand the problem correctly, this would be strictly better than solution 2. |
@bowenwang1996 in both proposals when runtime processes incoming transaction it uses the worst case scenario to compute the gas cost and deduct the corresponding amount of tokens from the account. However the difference is the following. Single shard exampleAssume the simple case, suppose user creates a transaction that performs a cross-contract call. Suppose now, that user's contract did not do cross contract calls. How are we going to refund their gas?
Multi-shard systemSuppose we have a system with large number of shards (e.g. 200), how different the gas cost of the least congested shard could be from the most congested shard? We won't be able to do dynamic resharding several times a day I presume, so it is possible to have one shard congested for 43k blocks while other shards operate normally. The gas price on congested shard after 1000 blocks is going to be |
The sender knows the price at the time of sending. The price is calculated as:
Re your proposal. We already have gas limit per block. Your proposal does not take into account async contracts and cross-contract calls. In Ethereum the contract calls are processed as block is filled with transactions, because Ethereum is not a sharded system the contract calls are blocking and so all artifacts of a single contract call are immediately computed -- if contract causes more cross-contract calls than can fit into the block then it is not included into the block. Our system is sharded, meaning we cannot do blocking calls between the contracts so we do asynchronous calls. When we execute contract on shard A it can request execution on shard B, shard C, etc, so overall computation caused by a transaction on shard A cannot be limited by a single limit on a single chunk (that's what we call blocks within a single shard). |
@nearmax Thanks for the explanation. Maybe I misunderstood the original proposal but according to
It seems that we are charging at current gas price when receipts are processed, instead of charging |
This is exactly what we do today. |
I don't see how it is achievable with the original proposal, we need to clarify that quote and how it will be implemented according to the original proposal. @evgenykuzyakov , could you clarify? |
@nearmax Do I understand correctly that from developer/user perspective it's going to look like following:
Is there anything else important I'm missing? |
I could be wrong, but it seems that we have all the instrumentations we need https://github.com/nearprotocol/nearcore/blob/72c49964338a85f7ae6f92b50828ffbe0f3614ca/runtime/runtime/src/lib.rs#L656 |
One minor detail is that we are technically currently (and in the future) refunding tokens and not gas. Also, depending on what modification of the proposal we are going to go with we might have the following quirk: the transaction gets X gas attached, but then inside the smart contract the amount of attached gas is seen as Y, because it will be sort of rescaled when moved between the shards according to their gas prices. |
@bowenwang1996 Here is how the code that you linked works right now. How it works right now
In these computations At the end:
Original proposalIn @evgenykuzyakov 's proposal, runtimes use
Which means it will cost Alice Single-sharded systemFor a single-sharded system it is largely a bad DevX. If Alice attached at lot of gas X she might pay a large price in tokens Multi-sharded systemIn multisharded system this can make our entire blockchain unusable. In sharded system each shard will have an independent gas price for each shard. With 200 shards it is quite possible that there will be one shard S3 that will be congested for say 1000 blocks (since 1000 blocks is less than one epoch we won't be able to dynamically reshard it to uncongest it). In 1000 blocks the gas price on that shard will be 20k times larger than on an average shard (that was congested 50% of the time and uncongested 50% of the time). This means that even though Alice did not touch shard Modified proposalThe modified proposal is the following:
At the end:
SummaryBoth with original and the modified proposal Alice will get temporarily deducted
For single-sharded system this is a minor improvements towards better DevX since Alice pays for what she actually used, not the worst case scenario. For multi-sharded system this is a critical improvement, since without it a single shard might make everyone in the system pay >>20k more than what they actually use. |
Issue 1: Delayed receiptsThere are still an issue with the delayed receipts. The shard can be congested for 1000 blocks, but when the transaction is accepted, we don't know the amount of block for the congestion. I currently, don't see a solution to the issue of estimation of the worst case scenario for the delayed receipts. Example: An attacker buys ton of gas to attack a shard HelperI think we can limit the depth of the promises to 32 (or 64) blocks. This way if the shard is not congested, the execution of the transaction should complete in most 32 blocks. So the max gas price without congestion has to be It doesn't address the delayed receipts problem, but it helps to not overcharge the prepaid gas amount. |
@evgenykuzyakov , I don't see it as an issue. Whoever congested the shard has payed for it already. When Alice comes to shard S0 and submits a computation that will be delayed for 1000 blocks she shouldn't pay more herself -- someone else did it for her.
We can do it by increasing the gas fee for creating a function call receipt. We should also decrease the maximum prepaid gas to be equal to the max gas a transaction can burn. Currently these values allow 4k contract calls per transaction. |
@nearmax @evgenykuzyakov let's also make sure this issue is covered as part of the proposal: |
@nearmax thanks for the explanation. I agree that modified proposal is better. However, I don't understand why we need to attach tokens to receipt. It seems that this is done solely for accounting. If that is the case, the following should also work: when we execute a receipt, we know that it has X prepaid gas and burns Y gas and have Z gas for some cross contract call. Since we know |
But we don't know when the first receipt of Alice is going to be executed. The real gas price when Alice's receipt starts executing will be determined based on the current delayed queue. We can't determine the length of the delayed queue in blocks (we can only look at the receipts). So we can't determine the price at which Alice's receipt should start executing. |
SolutionLocking the fee for the transactionWhen the transaction is to a receipt, instead of using the current
Then the account and access key are charged with the amount of ExecutionWhen the receipts are executed the runtime calculates the amount of burnt gas by the execution. Then instead of just charging the gas at the prepaid price, we charge gas at the current price of this shard. Let's say:
The refund in tokens is calculated the following way:
NOTE: in case of shard congestion due to a large number of delayed receipts, the Side issueWith overcharging the access key allowance, it makes more critical to refund the amount back to the access key allowance. So near/nearcore#2523 has to be fixed before the change. ConclusionIt doesn't address the delayed queue congestion completely, but it makes it more expensive to execute the congested shard attack, especially if we boost the inflation coefficient. The following action items are:
|
@evgenykuzyakov in your proposal
means that the refund is at least |
@bowenwang1996 It can be updated to be the following:
Assuming temporary values can go negative |
We should not be limiting the total prepaid gas, but instead we should increase the fee for the contract call. The proposed gas limit might be too low for some heavy contract like the bridge or EVM. While we can always reduce the cross contract call fee later. |
We need to change the amount of gas we attach to each transaction (e.g. in nearlib). The higher the amount of gas the more you need to have on the account and allowance to pre-purchase the gas at the inflated price. The current pessimistic inflation is set to To limit the maximum depth to 64 without changing fees, the max prepaid gas should be around
It doesn't make sense to manually tweak the estimated fees, because they depend on each other. Current max burn gas limit is |
There are might be a few issues for devx:
|
Implement dynamic gas price charging. See NEP for discussion: near/NEPs#67 List of changes: - Introduce a new config `pessimistic_gas_price_inflation_ratio`. Pessimistic inflation ratio, by default `3%` comparing to full block inflation of about `1%` gas price inflation. It's higher to account for potential delayed receipts. - Change `max_total_prepaid_gas` to `300 * 10^12`. It's higher than max burn gas per call, so it shouldn't affect existing contract much. - Receipts gas price is still the gas price at which the gas was purchased, but the actual gas price is used to burn gas. The remaining balance amount of gas is refunded back to the account and to the access key. - If the purchased gas price is lower than the current gas price, we try to use the unused gas amount to compensate for difference. This might happen due to really long delayed queues of receipts. If the difference is not possible to compensate, the amount is added to the `ApplyStats::gas_deficit_amount`. This amount reflect the balance that was not able to charge from the account and is needed for the balance checker. - The actual gas price is used to calculate burnt amount to reward the execution contract and validators. Even if the originator/signer account bought gas at a cheaper price (due to long queues). - Now even non-function call actions may generate refunds. E.g. a transfer from `alice` to `bob` will generate gas refund back to `alice`, to account for the potential increase in gas price. This increases the amount of refunds flying cross shard and likely will decrease our TPS for transfer transactions. Doesn't affect function calls much, because they almost always generate refunds. Fixes most in the near/NEPs#67 This change will introduce the devx issue: - ExecutionOutcome doesn't contain the actual gas_price or the block_height at which it was executed. It's available on the node side, when it pulls ExecutionOutcome for each block, so it can be included later for reporting. Without actual gas price, it's impossible to calculate the actual transaction cost. Depends on near/near-api-js#340
@evgenykuzyakov should this be closed? |
Closing it since it is implemented AFAIU. |
There are a few attacks that are possible with the ability to buy a lot of prepaid gas at the current fixed cheap price. Some of them are described here:
The challenge is the validator node and the runtime doesn't know the amount of gas that is actually going to be used when a transaction is accepted to a chunk. So a validator fills the block based on the burnt gas (the gas to convert a transaction into a receipt), instead of the total amount of the prepaid gas per transaction. This allows an attacker to issue a lot of transactions that have a lot of prepaid gas in each and pay at the current gas price. The gas price will grow in the next block, but the transactions are already have a lot of prepaid gas at a cheaper price. This attack allows to stall the shard for a long time without paying a lot of fees.
Solution 1: Prepaid gas -> Burnt gas
The first proposal is to change the prepaid gas to a burnt gas, so the amount of gas you prepay will be completely burnt. This allows to limit the chunk size based on the prepaid gas, instead of burnt gas, which prevents attacker from issuing a lot of cheap transactions.
The issue, is the contract developers will need to carefully estimate gas usage and might get stuck into inconsistent state if they underestimate the gas usage. It might also lead to unexpected results by users and lost funds due to overcharged gas amount.
Solution 2: Charge burnt gas at current price
The alternative is to change the way the prepaid gas is charged. Instead of buying all prepaid gas at the current price, assume that the transaction will issue the cheapest possible promise every block with NOOP compute. If the blocks are filled completely, the current gas price will grow at 1% (see economics) per block. We can estimate the maximum amount of tokens needed per block to issue such transaction (ignoring the delayed receipts). Then we can charge each receipt with the real current gas price instead of the initial prepaid gas price.
Cons:
Pros:
It requires near/nearcore#2523 to be fixed, otherwise an access key allowance will be drained even faster.
The text was updated successfully, but these errors were encountered: