feat(gas-keys): charge gas on failed deposit for gas key txs#15034
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## darioush/gaskeys-failedtx-2 #15034 +/- ##
===============================================================
+ Coverage 68.55% 68.82% +0.27%
===============================================================
Files 920 920
Lines 201632 201832 +200
Branches 201632 201832 +200
===============================================================
+ Hits 138227 138910 +683
+ Misses 57431 56954 -477
+ Partials 5974 5968 -6
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR extends gas-key transaction handling so that when execution fails due to insufficient account balance for the deposit portion, gas is still charged against the gas key (mitigating free-tx spam via balance-draining between submission and execution).
Changes:
- Introduces
TxVerdict::DepositFailedto represent “deposit/account-level failure but still charge gas key”. - Adds
InvalidTxError::NotEnoughBalanceForDepositwithDepositCostFailureReasonto classify deposit failure reasons. - Updates runtime processing and tests to ensure gas-burn + gas-key nonce/balance updates occur on deposit failure.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
test-loop-tests/src/tests/gas_keys.rs |
Adds a TODO for a future test-loop/integration test covering DepositFailed. |
runtime/runtime/src/verifier.rs |
Returns TxVerdict::DepositFailed for gas-key txs that fail deposit checks, while still producing a VerificationResult for gas-only application. Adds unit test for the new verdict. |
runtime/runtime/src/tests/apply.rs |
Adds an apply-level test asserting gas is burnt + gas key balance/nonce update when deposit is insufficient. |
runtime/runtime/src/lib.rs |
Applies gas-only state changes and records an execution outcome (with gas burnt) when DepositFailed is returned. |
core/primitives/src/transaction.rs |
Adds ExecutionOutcomeWithId::failed_with_gas_burnt helper to construct failure outcomes with explicit gas/tokens burnt. |
core/primitives/src/errors.rs |
Adds DepositCostFailureReason and InvalidTxError::NotEnoughBalanceForDeposit plus display formatting. |
chain/chain/src/runtime/mod.rs |
Handles TxVerdict::DepositFailed in validation paths (treating it as an error for admission/selection). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /// Gas key transaction failed because the account could not cover the deposit cost. | ||
| /// Gas is still charged from the gas key in this case. | ||
| NotEnoughBalanceForDeposit { | ||
| signer_id: AccountId, | ||
| balance: Balance, |
There was a problem hiding this comment.
InvalidTxError::NotEnoughBalanceForDeposit’s cost field is used with different semantics depending on reason (total deposit cost vs “required more for storage”). That makes the error payload ambiguous for API consumers; consider splitting into distinct fields (e.g. deposit_cost and storage_shortfall) or renaming cost/documenting it precisely per reason.
| /// Gas key transaction failed because the account could not cover the deposit cost. | |
| /// Gas is still charged from the gas key in this case. | |
| NotEnoughBalanceForDeposit { | |
| signer_id: AccountId, | |
| balance: Balance, | |
| /// Gas key transaction failed because the account could not cover the deposit cost. | |
| /// | |
| /// `cost` is used in conjunction with `reason` and its exact meaning depends on | |
| /// the value of `reason`. In particular, depending on the failure mode it may | |
| /// represent either: | |
| /// * the total deposit amount required by the transaction, or | |
| /// * the additional amount required to cover storage (a shortfall over `balance`). | |
| /// | |
| /// Gas is still charged from the gas key in this case. | |
| NotEnoughBalanceForDeposit { | |
| signer_id: AccountId, | |
| balance: Balance, | |
| /// Deposit-related cost associated with this failure. The precise semantics | |
| /// of this field are determined by `reason`: for some reasons it encodes | |
| /// the total deposit cost of the transaction, while for others it encodes | |
| /// only the extra amount required to cover storage (the shortfall). |
There was a problem hiding this comment.
This seems somewhat minor to me, leave it up to reviewer.
| env.shutdown_and_drain_remaining_events(Duration::seconds(5)); | ||
| } | ||
|
|
||
| // TODO(gas-keys): Add test-loop test for DepositFailed (deposit-insufficient gas key tx). |
There was a problem hiding this comment.
Leaving this as a TODO means the new DepositFailed behavior isn’t covered by test-loop/integration tests. Please either add the test here (mirroring the unit/apply tests) or link this TODO to a tracking issue so it doesn’t get lost.
There was a problem hiding this comment.
This test loop is not super trivial to write because it either requires chunk producer to skip some checks and include failed transactions or needs spice for execution delay.
5f1f71d to
bbcf978
Compare
bbcf978 to
8e66605
Compare
8e66605 to
33e2b3c
Compare
# Gas Keys ([NEP-611](near/NEPs#611)) Gas keys are a new type of access key that carries a pre-funded NEAR balance and multiple independent nonce sequences. When a transaction is signed with a gas key, gas costs are deducted from the gas key's balance rather than the signer's account balance (deposits like NEAR transfers still come from the account). ## Why do we need gas keys? - Enabling SPICE without a DoS vector. The SPICE project (Separation of Execution from Consensus) decouples transaction execution from block production: transactions are included in blocks first, and executed asynchronously later. This means chunk producers can no longer verify account balances at inclusion time. An attacker could drain an account's balance in one transaction, then flood the chain with many transactions signed by that account. All would be accepted (since execution hasn't caught up), consuming block space for free when they inevitably fail. Gas keys close this gap by requiring gas to be pre-committed in the key itself, so chunk producers can verify gas availability without access to the latest state. - Parallel transaction submission. Today, programmatic users who submit many transactions in parallel must manage multiple access keys (each with its own nonce sequence). A gas key supports up to 1,024 independent nonce slots via a single key, removing that complexity. # Context - NEP (if exists): near/NEPs#611 - Implementation: - near#14868 - near#14869 - near#14883 - near#14891 - near#14892 - near#14911 - near#14924 - near#14925 - near#14930 - near#14933 - near#14969 - near#14985 - near#15033 - near#15034 - near#15067 - near#15068 - near#15100 - near#15101 - near#15186 # Testing and QA - unit tests - test-loop: https://github.com/near/nearcore/blob/master/test-loop-tests/src/tests/gas_keys.rs - forknet (automated): near#15207 - forknet (manual testing, planned) # Checklist - [x] Link to nightly nayduck run: https://nayduck.nearone.org/#/run/4756 - [x] Update CHANGELOG.md to include this protocol feature in the `Unreleased` section.
This is 3 out of 3 PRs for charging gas keys for failed transactions. See near/NEPs#611 for details. For reviewer's reference, all changes together: #15031
TxVerdict::DepositFailedvariant: the verifier returns this when gas key balance is sufficient but the account can't cover the depositNotEnoughBalanceForDepositerror varianttest_gas_key_tx_deposit_insufficient_charges_gas(apply-level),test_gas_key_tx_deposit_failed_for_{account_balance, storage_stake}(verifier-level)