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

[discussion] Dynamic gas price during transaction execution #67

Closed
evgenykuzyakov opened this issue May 19, 2020 · 35 comments
Closed

[discussion] Dynamic gas price during transaction execution #67

evgenykuzyakov opened this issue May 19, 2020 · 35 comments
Assignees
Labels
bug Something isn't working T-economics About Protocol Economics WG-protocol Protocol Standards Work Group should be accountable

Comments

@evgenykuzyakov
Copy link
Contributor

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:

  • The issue is the receipt might be delayed and the gas price can grow more than 1% between 2 promise executions. In this case we have to charge more than the estimated amount. Need to think whether it can be addressed.
  • It's unclear the actual price change per transaction with the change.
  • It requires Runtime changes to charge gas at the current price and keep track of the remaining balance. When sending receipts the gas price estimation has to be done similar to initial transaction pricing.

Pros:

  • it doesn't require contract changes or transaction API changes.

It requires near/nearcore#2523 to be fixed, otherwise an access key allowance will be drained even faster.

@ilblackdragon
Copy link
Member

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)).

@bowenwang1996
Copy link
Collaborator

@evgenykuzyakov in solution 2, how many blocks are used to estimate the cost of a transaction?

@pmnoxx
Copy link
Contributor

pmnoxx commented May 20, 2020

@evgenykuzyakov in solution 2, you mentioned it's possible for someone to issue transaction using minimum gas and setting 10^16 budget.
The only solution I see is to charge people even for unused gas. That issue would be solved for example if there was some sort of penaulty:
Conditions:

  1. total gas assigned to block is more than 0.5e16
  2. total used gas is less than 1%

Action:

  1. Charge everyone on given block 0.0001% of unallocated gas. Everytime this rule gets triggered on a block, increase this percentage by 1%, with 1% max.
  2. only people with less gas usage than 1% of their allocated budges will get charged.

Pros:

  • This solves this type of attack
  • Under normal conditions penalty will not get applied, you can guarantee that you will not get the penaulty, as long as you estimate your gas usage within a factor of 100x

Cons:

  • This fee may sometimes add additional transaction cost.
  • This adds an extra variable per block making thing more complex

@SkidanovAlex
Copy link
Contributor

@evgenykuzyakov in solution 2, how many blocks are used to estimate the cost of a transaction?

The ratio of the prepaid gas and the minimum cost per promise.

@evgenykuzyakov
Copy link
Contributor Author

in solution 2, you mentioned it's possible for someone to issue transaction using minimum gas and setting 10^16 budget.

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.
The 2nd solution doesn't restrict the block limit to the prepaid only gas, it act as now by allowing large transactions that will block execution for the next 100 blocks. It just makes these transactions expensive (using increased gas prices). It's similar to keep the blocks filled with the new transactions without promises.

@MaksymZavershynskyi MaksymZavershynskyi added the bug Something isn't working label May 22, 2020
@MaksymZavershynskyi
Copy link
Contributor

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.

@bowenwang1996
Copy link
Collaborator

@nearmax @evgenykuzyakov I think for such a major change it is desirable to have a NEP first.

@MaksymZavershynskyi
Copy link
Contributor

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:

  • We want someone to perform operation on our blockchain, but while they do it they are given a great deal of flexibility that they can use to abuse the system. In case of validator staking validators can produce invalid blocks, in case of gas staking the users can arrange shard congestion by submitting tricky transactions;
  • We however are not able to prevent it, either because the computation is too complex (in case of validators we cannot run BFT consensus because it is too slow and expensive) or not possible (in case of transactions it is not possible to predict what receipts user's transaction will create);
  • So we solve it through staking -- we require the actors to loan the assets that we fully or partially return after they have completed their interaction;

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).

@MaksymZavershynskyi
Copy link
Contributor

@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:

  • We need to use gas in the user-facing APIs and contracts, since gas represents the computation cost they can reason about;
  • We need to deduct the tokens based on the actual gas cost, so that users do not lose tokens even if they do not abuse the congested shard. The users will however need to "stake" more tokens since we cannot predict their actions before executing it.
  • This also removes another angle of attack -- someone congesting a single shard to make everyone in the system pay significantly more for the execution.

@bowenwang1996
Copy link
Collaborator

@nearmax in this proposal, who pays for the "staked" tokens?

@MaksymZavershynskyi
Copy link
Contributor

MaksymZavershynskyi commented May 25, 2020

@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.

@bowenwang1996
Copy link
Collaborator

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?

@pmnoxx
Copy link
Contributor

pmnoxx commented May 26, 2020

@nearmax

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:

  • sender doesn't know the price at time of sending
  • if sender estimates the transaction is going to use x gas at y cost, now y increases. This can make the transaction fail.
  • For example if I expect transaction to cost 10000, sender sets budget of 10000, but now with new price, the transaction could cost 10010. This would cause transaction to fail, because you would ran out of budget, and you would still have to pay for it.

Correct me if I'm wrong, would it be feasible to do the following

Solution 3: Limit amount of gas used per block
Let say the maximum amount of gas that can be used per block is G, which is equivalent to 1s of work. Let's limit the maximum amount of gas used by transaction to G/2.
Assign an ordering to all transactions t_1... t_n. Keep processing all transactions in that order as long as the sum of gas used doesn't exceed G/2. Then reject all remaining transactions, and refund the cost.

Pros:

  • sender now knows how much transaction costs
  • you will causes where a transaction fails due to sudden price spike and you get charged for it

Cons:

  • Some transactions may be rejected, but you would get refunded for it. (that's still better than solution 2)

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.

@MaksymZavershynskyi
Copy link
Contributor

@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 example

Assume the simple case, suppose user creates a transaction that performs a cross-contract call.
Suppose user attaches gas enough to perform 100 cross contract calls in the worst case. In the worst case, there will be 100 sequential cross-contract calls and all 100 blocks will be congested causing 1% growth of gas cost with each block. Therefore, with both proposals we will deduct attached_gas*avg_worst_case_gas_price where avg_worst_case_gas_price = current_gas_price*(1-1.01^2)/(1-1.01)/100 = 1.7* current_gas_price.

Suppose now, that user's contract did not do cross contract calls. How are we going to refund their gas?

  • In the original proposal we will take unspent gas and refund the tokens using avg_worst_case_gas_price. This means if user did not do cross-contract calls they will spend burnt_gas*avg_worst_case_gas_price tokens! Which is 1.7 times more than if they had not attached gas for 100 cross-contract calls. But what if they have to attach this much gas, e.g. for storage operations?
  • In the modified proposal the user will spend burnt_gas*current_gas_price tokens.

Multi-shard system

Suppose 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 1.01^1000=20959 larger than on normal shards. So if someone submits a transaction to a normal shard that can potentially do at least one cross-contract call we will price it 20959 more than the one that cannot do a cross-call. With the original proposal, not only the user will have to deduct prepaid_gas * 20959*normal_gas_price from their account, but they will also need to pay burnt_gas*20959*normal_gas_price tokens. With the modified proposal they will have to deduct prepaid_gas * 20959*normal_gas_price, but they will only pay burnt_gas*normal_gas_price and get the rest refunded.

@MaksymZavershynskyi
Copy link
Contributor

MaksymZavershynskyi commented May 26, 2020

@pmnoxx

sender doesn't know the price at time of sending

The sender knows the price at the time of sending. The price is calculated as:

max_num_contract_calls := prepaid_gas / contract_call_fees
avg_worst_case_gas_cost := (1-1.01^max_num_contract_calls)/(1-1.01)/max_num_contract_calls
cost_in_tokens := avg_worst_case_gas_cost * prepaid_gas

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).

@bowenwang1996
Copy link
Collaborator

@nearmax Thanks for the explanation. Maybe I misunderstood the original proposal but according to

Then we can charge each receipt with the real current gas price instead of the initial prepaid gas price.

It seems that we are charging at current gas price when receipts are processed, instead of charging avg_worst_case_gas_cost

@bowenwang1996
Copy link
Collaborator

@pmnoxx

Let say the maximum amount of gas that can be used per block is G, which is equivalent to 1s of work. Let's limit the maximum amount of gas used by transaction to G/2.
Assign an ordering to all transactions t_1... t_n. Keep processing all transactions in that order as long as the sum of gas used doesn't exceed G/2. Then reject all remaining transactions, and refund the cost.

This is exactly what we do today.

@MaksymZavershynskyi
Copy link
Contributor

MaksymZavershynskyi commented May 26, 2020

@nearmax Thanks for the explanation. Maybe I misunderstood the original proposal but according to

Then we can charge each receipt with the real current gas price instead of the initial prepaid gas price.

It seems that we are charging at current gas price when receipts are processed, instead of charging avg_worst_case_gas_cost

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?

@vgrichina
Copy link

@nearmax Do I understand correctly that from developer/user perspective it's going to look like following:

  • when I send transaction – I get charged NEAR for worst case scenario (attached gas * max gas price)
  • as transaction get processed gas which is not passed to nested calls / spent on compute gets refunded

Is there anything else important I'm missing?

@bowenwang1996
Copy link
Collaborator

@nearmax

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?

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

@MaksymZavershynskyi
Copy link
Contributor

@nearmax Do I understand correctly that from developer/user perspective it's going to look like following:

  • when I send transaction – I get charged NEAR for worst case scenario (attached gas * max gas price)
  • as transaction get processed gas which is not passed to nested calls / spent on compute gets refunded

Is there anything else important I'm missing?

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.

@MaksymZavershynskyi
Copy link
Contributor

MaksymZavershynskyi commented May 26, 2020

@bowenwang1996 Here is how the code that you linked works right now.

How it works right now

  • Alice creates transaction T that has X prepaid_gas, and sends it to shard S0;
  • S0 runtime receives T and converts it to the receipt R1 that has also X prepaid_gas. While doing so it burns V gas. It also deducts (X+V)*gas_cost from Alice's account, and records gas_cost in R1. It then sends it to shard S1;
  • S1 runtime receives receipt R1, executes it while burning Y gas. Suppose R1 was a contract call performing another cross-contract call to shard S2 and attaching Z gas to it. Then S1 runtime will create another receipt R2 with Z gas, it also records gas_cost in R2. Then it will create a refund with (X-Y-Z)*gas_cost and send it to shard S0;
  • S2 runtime receives receipt R2, executes it while burning W gas. Suppose there are no cross-contract calls produced while executing R2. Then S2 runtime will create a refund with (Z-W)*gas_cost and send it to shard S0.

In these computations gas_cost is the cost of the gas that was recorded when T was converted to R0. In a single-sharded system (where S0, S1, S2 are the same shards) this price is global for the entire system. In a multi-sharded system this is the price is different for each shard, and it this case, it is the price on shard S0.

At the end:

  • Alice were temporarily deducted X*gas_cost tokens;
  • Alice later were refunded (X-Y-Z)*gas_cost + (Z-W)*gas_cost = (X-Y-W)*gas_cost tokens;
  • It costed Alice (V+Y+W)*gas_cost tokens to execute this transaction.

Original proposal

In @evgenykuzyakov 's proposal, runtimes use avg_worst_case_gas_cost instead of gas_cost that is computed like this:

max_num_contract_calls := X / contract_call_fees
avg_worst_case_gas_cost := gas_cost_on_the_most_expensive_shard*(1-1.01^max_num_contract_calls)/(1-1.01)/max_num_contract_calls

Which means it will cost Alice (V+Y+W)*avg_worst_case_gas_cost to execute her transaction.
It presents problems both for a single-sharded and for multi-sharded system.

Single-sharded system

For 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 (V+Y+W)*avg_worst_case_gas_cost even if she only did a couple of cross-contract calls. It is actually a very realistic scenario, since Alice might need to attach a lot of gas, e.g. to work with the storage.

Multi-sharded system

In 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 S3, she will have to pay (V+Y+W)*avg_worst_case_gas_cost tokens, which will be 20k times larger than she actually costed to the blockchain. Basically, if there is one congested shard in the system everyone will pay an extreme number of tokens even for cheap operations.

Modified proposal

The modified proposal is the following:

  • Alice creates transaction T that has X prepaid_gas, and sends it to shard S0;
  • S0 runtime receives T and converts it to the receipt R1 that has also X prepaid_gas, while burning V gas. It also deducts X*avg_worst_case_gas_cost +V*gas_cost(S0) from Alice's account, and attaches X*avg_worst_case_gas_cost tokens to R1 . It then sends it to shard S1;
  • S1 runtime receives receipt R1, executes it while burning Y gas. Suppose R1 was a contract call performing another cross-contract call to shard S2 and attaching Z gas to it. Then S1 runtime will create another receipt R2 with Z*avg_worst_case_gas_cost tokens attached to it. Then it will create a refund with X*avg_worst_case_gas_cost-Z*avg_worst_case_gas_cost-Y*gas_cost(S1) and send it to shard S0;
  • S2 runtime receives receipt R2, executes it while burning W gas. Suppose there are no cross-contract calls produced while executing R2. Then S2 runtime will create a refund with Z*avg_worst_case_gas_cost-W*gas_cost(S2) and send it to shard S0.

At the end:

  • Alice were temporarily deducted X*avg_worst_case_gas_cost;
  • Alice later were refunded X*avg_worst_case_gas_cost-Z*avg_worst_case_gas_cost-Y*gas_cost(S1) + Z*avg_worst_case_gas_cost-W*gas_cost(S2) = X*avg_worst_case_gas_cost - Y*gas_cost(S1) - W*gas_cost(S2) tokens.
  • It costed Alice V*gas_cost(S0)+Y*gas_cost(S1)+W*gas_cost(S2) tokens to execute this transaction.

Summary

Both with original and the modified proposal Alice will get temporarily deducted X*avg_worst_case_gas_cost tokens. However, after all refunds are received:

  • In the original proposal, Alice will lose (V+Y+W)*avg_worst_case_gas_cost tokens;
  • In the modified proposal, Alice will lose V*gas_cost(S0)+Y*gas_cost(S1)+W*gas_cost(S2) tokens.

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.

@evgenykuzyakov
Copy link
Contributor Author

Issue 1: Delayed receipts

There 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 S0, how do we know for how long the S0 going to process receipts. The execution of the transaction may start in 1000 blocks when the gas will be 1.01^1000 times more expensive, but unless we know precisely how much we need to wait, we can't calculate the gas price of the worst case scenario

Helper

I 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 1.01^32 instead of 1000s.

It doesn't address the delayed receipts problem, but it helps to not overcharge the prepaid gas amount.

@MaksymZavershynskyi
Copy link
Contributor

@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.

I 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 1.01^32 instead of 1000s.

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.

@vgrichina
Copy link

@nearmax @evgenykuzyakov let's also make sure this issue is covered as part of the proposal:
near/nearcore#2523

@bowenwang1996
Copy link
Collaborator

@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 avg_worst_cast_gas_price when the transaction was initiated, we can use that for gas_price that we have today in ActionReceipt, and, when the receipt is executed, we also know the current gas price, so we can refund X * avg_worst_cast_gas_price - Z * avg_worst_cast_gas_price - Y * current_gas_price (btw this is how I think the original works).

@evgenykuzyakov
Copy link
Contributor Author

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.

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.

@evgenykuzyakov
Copy link
Contributor Author

evgenykuzyakov commented May 27, 2020

Solution

Locking the fee for the transaction

When the transaction is to a receipt, instead of using the current gas_price of the block, we use the maximum gas_price from all shards, then multiply it by inflation for the number of blocks equal to the maximum number of promises that can be done with the amount of prepaid gas. It's similar to what @nearmax explained above:

receipt_gas_price = max_gas_price_across_shards * (inflation_coef ^ (gas_amount // promise_cost))
  • max_gas_price_across_shards the maximum gas price across all shards.
  • inflation_coef the inflation coefficient. It can either be the current one 1.01 or we can bump it more to account for the potential delayed receipts, e.g. 1.05.
  • gas_amount is the sum of prepaid_gas from all function calls.
  • promise_cost the amount of gas required to issue a cross-contract promise (another function call). We'll need to bump this fee, to avoid creation of more than N calls, e.g. 32

Then the account and access key are charged with the amount of receipt_gas_price * gas_amount + tx_fees

Execution

When 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:

  • burnt_gas - the amount of gas burnt for the execution of the receipt
  • current_gas_price - the current gas price of the shard.
  • receipt_gas_price - the gas price at which the gas was pre-purchased originally.
  • unused_gas - the amount of gas that remained after execution, that was not burnt or passed to new receipts.

The refund in tokens is calculated the following way:

price_diff_refund = max(0, receipt_gas_price - current_gas_price) * burnt_gas
unused_gas_refund = receipt_gas_price * unused_gas
refund = price_diff_refund + unused_gas_refund

NOTE: in case of shard congestion due to a large number of delayed receipts, the current_gas_price may exceed the receipt_gas_price, which means the account will pay less than it actually costs. There was a proposal to delay execution of such receipts until the current gas price drops under the price of the receipt_gas_price, but this option may introduce additional issues such as delaying some receipts indefinitely by an attacker.

Side issue

With 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.

Conclusion

It 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:

@bowenwang1996
Copy link
Collaborator

@evgenykuzyakov in your proposal

price_diff_refund = max(0, receipt_gas_price - current_gas_price) * burnt_gas

means that the refund is at least unused_gas * receipt_gas_price, which means that even though the person can pay the full amount current_gas_price * burnt_gas, they somehow end up paying less. Checkout my proposal here #67 (comment).

@evgenykuzyakov
Copy link
Contributor Author

@bowenwang1996 It can be updated to be the following:

price_diff_refund = (receipt_gas_price - current_gas_price) * burnt_gas
unused_gas_refund = receipt_gas_price * unused_gas
refund = max(0, price_diff_refund + unused_gas_refund)

Assuming temporary values can go negative

@MaksymZavershynskyi
Copy link
Contributor

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.

@evgenykuzyakov
Copy link
Contributor Author

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 3% to account for delayed blocks. The maximum depth is 64. So the purchase gas price will be about 6.63 times higher than the actual current gas price for the maximum prepaid gas amount.

To limit the maximum depth to 64 without changing fees, the max prepaid gas should be around 310 * 10^12. So I set the limit to 300 * 10^12

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.

It doesn't make sense to manually tweak the estimated fees, because they depend on each other. Current max burn gas limit is 200 * 10^12 which is smaller than the max prepaid gas limit. It shouldn't affect bridge or EVM in any way.

@evgenykuzyakov
Copy link
Contributor Author

There are might be a few issues for devx:

  • 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.

evgenykuzyakov pushed a commit to near/nearcore that referenced this issue Jun 17, 2020
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
@bowenwang1996
Copy link
Collaborator

@evgenykuzyakov should this be closed?

@MaksymZavershynskyi
Copy link
Contributor

Closing it since it is implemented AFAIU.

@frol frol added the WG-protocol Protocol Standards Work Group should be accountable label Aug 31, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working T-economics About Protocol Economics WG-protocol Protocol Standards Work Group should be accountable
Projects
None yet
Development

No branches or pull requests

8 participants