feat(gas-keys): charge gas on failed deposit for gas key txs#15031
feat(gas-keys): charge gas on failed deposit for gas key txs#15031darioush wants to merge 7 commits into
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #15031 +/- ##
==========================================
+ Coverage 68.85% 68.89% +0.04%
==========================================
Files 920 920
Lines 201667 202330 +663
Branches 201667 202330 +663
==========================================
+ Hits 138864 139405 +541
- Misses 56823 56943 +120
- Partials 5980 5982 +2
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 hardens gas key transactions by ensuring gas is still charged even when execution fails due to insufficient account deposit balance, preventing free “deposit-fail” spam. It also refactors verifier APIs to return a TxVerdict/VerificationResult describing state changes instead of mutating state directly.
Changes:
- Charge gas key balance (and advance gas key nonce) on “deposit insufficient” failures via
TxVerdict::DepositFailed. - Refactor
verify_and_charge_tx_ephemeral/verify_and_charge_gas_key_tx_ephemeralto returnTxVerdict, and apply state changes viaVerificationResult::apply(). - Add
InvalidTxError::NotEnoughBalanceForDepositandDepositCostFailureReason, plus tests covering the new behavior.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| test-loop-tests/src/tests/gas_keys.rs | Adds a TODO to cover deposit-insufficient gas key tx behavior in test-loop. |
| runtime/runtime/src/verifier.rs | Refactors verifier functions to return TxVerdict; adds gas-key deposit-failure handling and unit tests. |
| runtime/runtime/src/tests/apply.rs | Refactors test setup and adds an apply-level regression test ensuring gas is charged on deposit failure. |
| runtime/runtime/src/lib.rs | Introduces TxVerdict, AccessKeyUpdate, and VerificationResult::apply(); updates runtime processing to handle deposit-failed charging. |
| runtime/runtime-params-estimator/src/estimator_context.rs | Updates estimator to handle TxVerdict and apply state changes explicitly. |
| core/primitives/src/transaction.rs | Adds ExecutionOutcomeWithId::failed_with_gas_burnt to record gas/tokens burnt for specific failure cases. |
| core/primitives/src/errors.rs | Adds DepositCostFailureReason and InvalidTxError::NotEnoughBalanceForDeposit with display formatting. |
| chain/chain/src/runtime/mod.rs | Updates validation/filtering paths to handle TxVerdict and apply state updates on success. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| TxVerdict::Failed(e) | TxVerdict::DepositFailed { error: e, .. } => return Err(e), | ||
| }; | ||
| result.apply(&mut signer, &mut access_key); | ||
| set_tx_state_changes(state_update, &validated_tx, &signer, &access_key); |
There was a problem hiding this comment.
validate_verify_and_charge_transaction applies VerificationResult to the in-memory signer/access_key and writes them back via set_tx_state_changes, but it never persists the gas key nonce update to the trie. For gas key transactions this means TrieKey::GasKeyNonce remains stale even on success. Persist result.gas_key_nonce_update() (when present) using set_gas_key_nonce(...) on the provided state_update.
| set_tx_state_changes(state_update, &validated_tx, &signer, &access_key); | |
| set_tx_state_changes(state_update, &validated_tx, &signer, &access_key); | |
| // Persist gas key nonce updates (if any) to the trie so TrieKey::GasKeyNonce stays in sync. | |
| if let Some(nonce_index) = tx.nonce().nonce_index() { | |
| if let Some(new_nonce) = result.gas_key_nonce_update() { | |
| set_gas_key_nonce( | |
| state_update, | |
| tx.signer_id(), | |
| tx.public_key(), | |
| nonce_index, | |
| new_nonce, | |
| ); | |
| } | |
| } |
There was a problem hiding this comment.
This is not needed imo. Actually I would say it is a bug that some tests rely on set_tx_state_changes being called here. I don't think it's good to have a complex test helper here that's different from the production code.
We already have an apply level test to cover this case.
| /// Returned by both `verify_and_charge_tx_ephemeral` and | ||
| /// `verify_and_charge_gas_key_tx_ephemeral`. Neither function mutates state; | ||
| /// callers apply changes based on the variant: | ||
| /// - `Success`: apply all state changes via `VerificationResult::apply`. | ||
| /// - `DepositFailed`: apply gas-only state changes via `VerificationResult::apply` | ||
| /// (only returned by gas key path). | ||
| /// - `Failed`: no state changes. |
There was a problem hiding this comment.
The TxVerdict doc comment says neither verify_and_charge_tx_ephemeral nor verify_and_charge_gas_key_tx_ephemeral mutates state, but verify_and_charge_tx_ephemeral can still mutate the access key allowance for legacy protocol versions (pre-FixAccessKeyAllowanceCharging). Please update this doc comment to reflect the legacy mutation so callers aren’t misled.
There was a problem hiding this comment.
I prefer to leave the comment since the Legacy behavior will be removed soon.
| let transactions_gas_limit = chunk_tx_gas_limit(runtime_config, &prev_block, shard_id); | ||
|
|
||
| let mut result = PreparedTransactions::new(); | ||
| let mut prepared_transactions = PreparedTransactions::new(); |
There was a problem hiding this comment.
renamed to re-use "result" name in a more local scope
| /// (only returned by gas key path). | ||
| /// - `Failed`: no state changes. | ||
| #[derive(Debug)] | ||
| pub enum TxVerdict { |
There was a problem hiding this comment.
I added this new enum (TxVerdict), one alternative that comes to mind is to rename it to VerificationResult and then rename VerificationResult to TxChanges or similar name.
I didn't include it to keep diff smaller, but can apply it if we think it's a good idea.
| TxVerdict::Failed(e) | TxVerdict::DepositFailed { error: e, .. } => return Err(e), | ||
| }; | ||
| result.apply(&mut signer, &mut access_key); | ||
| set_tx_state_changes(state_update, &validated_tx, &signer, &access_key); |
There was a problem hiding this comment.
This is not needed imo. Actually I would say it is a bug that some tests rely on set_tx_state_changes being called here. I don't think it's good to have a complex test helper here that's different from the production code.
We already have an apply level test to cover this case.
| /// Returned by both `verify_and_charge_tx_ephemeral` and | ||
| /// `verify_and_charge_gas_key_tx_ephemeral`. Neither function mutates state; | ||
| /// callers apply changes based on the variant: | ||
| /// - `Success`: apply all state changes via `VerificationResult::apply`. | ||
| /// - `DepositFailed`: apply gas-only state changes via `VerificationResult::apply` | ||
| /// (only returned by gas key path). | ||
| /// - `Failed`: no state changes. |
There was a problem hiding this comment.
I prefer to leave the comment since the Legacy behavior will be removed soon.
This is 1 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 - Extracts shared test setup for gas key apply tests into a GasKeyTestSetup struct and setup_gas_key_test() function - Rewrites test_apply_gas_key_transaction and test_gas_refund_to_gas_key to use the shared helper - No behavior changes; purely a test refactor to reduce duplication ahead of adding more gas key tests
This is 2 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 - Introduces `TxVerdict` enum (`Success`, `Failed`) replacing `Result<VerificationResult, InvalidTxError>` as the return type for transaction verification - Introduces `AccessKeyUpdate` enum (`Regular`, `GasKey`) to describe access key mutations declaratively Makes `verify_and_charge_tx_ephemeral` and `verify_and_charge_gas_key_tx_ephemeral` pure (except for Legacy bug): they no longer mutate `&mut Account` / `&mut AccessKey`; callers apply changes via `VerificationResult::apply()` - No behavior changes; this restructuring enables adding new verdict variants (e.g. `DepositFailed`) in follow-up work - Resolves #14965
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 - When a gas key transaction fails because the account can't cover the deposit, gas is still charged from the gas key balance. This prevents SPICE spam where an attacker drains their account between submission and execution to get free transactions. - Adds `TxVerdict::DepositFailed` variant: the verifier returns this when gas key balance is sufficient but the account can't cover the deposit - Adds `NotEnoughBalanceForDeposit` error variant - `test_gas_key_tx_deposit_insufficient_charges_gas` (apply-level), `test_gas_key_tx_deposit_failed_for_{account_balance, storage_stake}` (verifier-level) - Will fix protocol schema and openapi.json after review.
|
smaller PRs merged in instead |
verify_and_charge_tx_ephemeral,verify_and_charge_gas_key_tx_ephemeral) to return aTxVerdictenum instead of mutating account/access key directly (see Avoid Input/Output arguments as part ofverify_and_charge_tx_ephemeral#14965). Callers apply state changes viaVerificationResult::apply().NotEnoughBalanceForDepositerror variant.test_gas_key_tx_deposit_insufficient_charges_gas(apply-level),test_gas_key_tx_deposit_failed_for_{account_balance, storage_stake}(verifier-level)