Skip to content
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
233 changes: 233 additions & 0 deletions neps/nep-0611.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
---
NEP: 611
Title: Pending Transaction Queue
Authors: Robin Cheng <[email protected]>
Status: Draft
DiscussionsTo: https://github.com/near/NEPs/pull/611
Type: Protocol
Version: 1.0.0
Created: 2025-05-28
LastUpdated: 2025-05-28
---

## Summary

Check failure on line 13 in neps/nep-0611.md

View workflow job for this annotation

GitHub Actions / markdown-lint

Headings should be surrounded by blank lines

neps/nep-0611.md:13 MD022/blanks-around-headings Headings should be surrounded by blank lines [Expected: 1; Actual: 0; Below] [Context: "## Summary"] https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md022.md
In the near future of the Near blockchain, we foresee that via the SPICE project, transaction and receipt
execution will become become decoupled from the blockchain itself; they will no longer run in lockstep.
Instead, transactions will be included in the blocks first, and then execution will follow later.

This inherently introduces a problem that we must accept transactions before we know whether they are
valid. Today, when a chunk producer produces a chunk containing a transaction, it can verify using the
current shard state that the transaction has a valid signature, has enough balance, and a valid nonce.
But as execution becomes asynchronous, we no longer have the current shard state to verify the
transactions against.

This NEP proposes a mechanism called the Pending Transaction Queue to solve this problem.

## Motivation

### Why is this worth solving?

A potential for DoS attacks exists whenever the blockchain allows anyone to submit work without paying.
Invalid transactions present such a vulnerability: if a transaction is included in a block (or more
precisely, a chunk of the block) but ends up being invalid because the sender does not have enough
balance, this transaction takes block space but cannot be charged against anyone.

A very easy-to-perform attack exists if we do nothing to mitigate the problem:

* The attacker creates two accounts, $A$ and $B$, with sufficiently many access keys each, and deploying
a specific contract for both accounts that provides a `send_near` function.
* The attacker deposits 10 NEAR in $A$.
* The attacker then performs the following, repeatedly:
* Submit a transaction to call A's `send_near` function, instructing it to send the account's remaining
balance to $B$.
* Right after, it floods the blockchain by signing and submitting many (arbitrary) transactions as $A$.
Because execution is asynchronous, the chunk producers think that there is still enough balance in
$A$'s account, so these transactions are accepted into chunks.
* Some blocks later, the execution catches up, and the `send_near` function drains $A$'s account.
* Subsequent executions of the following transactions all fail because $A$ has insufficient balance.
* After this is done, the attacker repeats but with $A$ and $B$ swapped.

This attack can be carried out with a very simple script and requires no cost other than a single contract
call every few blocks, but this ends up filling up the blockchain, denying legitimate transactions from
being included. This is also very hard to defend against, because the attacker can simply create more
accounts. Note that this attack pattern is not the only problematic one; instead of sending away the
balance, the attacker can also delete access keys or delete the whole account.

## Specification

Check failure on line 56 in neps/nep-0611.md

View workflow job for this annotation

GitHub Actions / markdown-lint

Headings should be surrounded by blank lines

neps/nep-0611.md:56 MD022/blanks-around-headings Headings should be surrounded by blank lines [Expected: 1; Actual: 0; Below] [Context: "## Specification"] https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md022.md
To solve this problem, we present two critical components which work together to ensure that all accepted
transactions are valid. At a high level, they are:
* **Access Key vs Gas Key**: In addition to Access Keys, we introduce a second kind of signing key called the Gas Key. A gas key can be funded with NEAR, and when issuing a transaction using a gas key, the transaction only consumes gas from the gas key, not from the account.

Check failure on line 59 in neps/nep-0611.md

View workflow job for this annotation

GitHub Actions / markdown-lint

Lists should be surrounded by blank lines

neps/nep-0611.md:59 MD032/blanks-around-lists Lists should be surrounded by blank lines [Context: "* **Access Key vs Gas Key**: I..."] https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md032.md
* **Pending Transaction Queue**: Chunk producers keep track of pending (accepted into a block, but not
executed) transactions, and ensures that transactions sent with access keys are limited in parallelism,
whereas transactions sent with gas keys are limited to the available gas in the gas key.

We will now specify how exactly they work.

### Definition of "Pending Transactions"

Check failure on line 66 in neps/nep-0611.md

View workflow job for this annotation

GitHub Actions / markdown-lint

Headings should be surrounded by blank lines

neps/nep-0611.md:66 MD022/blanks-around-headings Headings should be surrounded by blank lines [Expected: 1; Actual: 0; Below] [Context: "### Definition of "Pending Transactions""] https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md022.md
In a model where execution follows but lags behind consensus, there are transactions which are accepted
into consensus and thus committed to be executed in the near future, but are not yet executed. This set
of transactions is called the *pending transactions*. We always discuss this in the context of one shard.

Note that there are two slightly different ways to treat this definition, depending on how exactly the
"execution head" (how far the execution has caught up) is defined:
* It can be defined locally as the progress of execution at a node, but this will be different between

Check failure on line 73 in neps/nep-0611.md

View workflow job for this annotation

GitHub Actions / markdown-lint

Lists should be surrounded by blank lines

neps/nep-0611.md:73 MD032/blanks-around-lists Lists should be surrounded by blank lines [Context: "* It can be defined locally as..."] https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md032.md
nodes.
* It can be defined deterministically as the last block whose execution result is certified by consensus.

For the purpose of this NEP, we use the latter definition, so that the notion of pending transactions is
consistent across all nodes and the determinination of what transactions are eligible to be included by
a chunk producer can be verified -- even though we do not plan to implement this verification right now.

Another note is that the notion of pending transactions is anchored at a specific chunk that is being produced. In case of forks, we use the block that the chunk is being produced on top of to compute the

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"The notion of pending transactions is anchored at a specific chunk that is being produced." Agree.

The tricky part is that the chunk producer is required to know about all the transactions included in the previous chunks all the way up to the chunk that it is producing. I.e., producing a chunk for height h requires the knowledge of all transactions associated with heights until h-1. (We're considering a single shard.)

In SPICE we count on decoupling (from consensus) not just the execution, but data availability (even though we are not implementing it in SPICE v1). That is, making transaction data available will also be asynchronous. A chunk producer with the right of producing a chunk associated with height h might actually have a window of a few blocks after h to make sure the data is available, during which the data owners will certify (on chain) having received their respective data parts. This means that the availability certificate for a chunk associated with height h might only occur in a block at height h+a (a being the number of blocks it takes to ensure the availability). The next chunk producer (producing a chunk for h+1), however, might not yet know the contents of the chunk associated with h, and thus not have a precise view of the pending transaction set. This is a more general problem that also has other implications (such the problem with transaction duplication), but an incomplete view of the pending transaction set is also an issue.

I could imagine a solution that requires the chunk producer for h to post at least a commitment to the chunk on chain as soon as possible, and the pending transaction set at h+1 would be defined through these commitments. The chunk producer for h+1 would then need to obtain the transaction data associated with h before proposing its own chunk. Ideally the chunk producer at h would directly (with high priority) send its chunk to the producer at h+1. If the former does not send the chunk, the latter would need to retrieve the chunk from the data owners, slowing down the chunk production.

set of pending transactions.

Finally, this NEP does *not* depend on the implementation of SPICE. In the context where SPICE is not
yet in effect, we consider the pending transactions queue to always be empty (despite technically being
one chunk worth of transactions due to execution lagging one block behind in the current implementation),
because we can always verify the validity of all transactions at the moment of inclusion.

### Access Key vs Gas Key

Check failure on line 89 in neps/nep-0611.md

View workflow job for this annotation

GitHub Actions / markdown-lint

Headings should be surrounded by blank lines

neps/nep-0611.md:89 MD022/blanks-around-headings Headings should be surrounded by blank lines [Expected: 1; Actual: 0; Below] [Context: "### Access Key vs Gas Key"] https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md022.md
We introduce a new transaction version, `TransactionV2`, which is able to either specify an access key
or a gas key. Any older transaction version is equivalent to a `TransactionV2` that specifies an access
key.

#### Access Key Parallelism Restriction

Check failure on line 94 in neps/nep-0611.md

View workflow job for this annotation

GitHub Actions / markdown-lint

Headings should be surrounded by blank lines

neps/nep-0611.md:94 MD022/blanks-around-headings Headings should be surrounded by blank lines [Expected: 1; Actual: 0; Below] [Context: "#### Access Key Parallelism Restriction"] https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md022.md
We now restrict the ability to send multiple parallel pending transactions with Access Keys.

Specifically, for any given account $A$ with any number of access keys, the total number of access key transactions in the pending transaction queue whose sender is $A$ cannot exceed $P_{\mathrm {max}}$, a
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this enough to prevent the attack described above? Instead of having one account they send many transactions to, it could be many accounts they send only a few transactions to each.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is that the amount of block space the attacker consumes vs amount of gas the attacker has to pay, i.e. the "waste amplification factor", is unbounded in the attack mentioned in the NEP, but only O(1) with a maximum parallelism. It's then still possible to attack, but the attack would cost a proportional amount of gas - just cheaper than it is today - which isn't a very great incentive to mount the attack (given that the attack is only a DoS).

constant determined by the epoch; we propose $P_{\mathrm {max}} = 4$.

In other words, with traditional access keys, one cannot send more than 4 transactions with the same
account before they are executed. If one wishes the send more transactions in parallel, they would need
to create a gas key.

From a UX perspective, we pick the $P_{\mathrm {max}}$ constant so that it is very unlikely that anyone
exceeding this parallelism is an end user with a wallet app. Those who need more parallelism than this
would be using a script to send transactions, and for those use cases we require them to use gas keys.

#### Gas Keys

Check failure on line 108 in neps/nep-0611.md

View workflow job for this annotation

GitHub Actions / markdown-lint

Headings should be surrounded by blank lines

neps/nep-0611.md:108 MD022/blanks-around-headings Headings should be surrounded by blank lines [Expected: 1; Actual: 0; Below] [Context: "#### Gas Keys"] https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md022.md
This NEP adds Gas Keys, **conceptually** defined as the following:
```rust

Check failure on line 110 in neps/nep-0611.md

View workflow job for this annotation

GitHub Actions / markdown-lint

Fenced code blocks should be surrounded by blank lines

neps/nep-0611.md:110 MD031/blanks-around-fences Fenced code blocks should be surrounded by blank lines [Context: "```rust"] https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md031.md
struct ConceptualGasKey {
public_key: PublicKey,
nonces: Vec<Nonce>,
balance: Balance,
permission: AccessKeyPermission,
}
Comment on lines +132 to +137
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I wrote in another comment to make refunds easier, can we consider another representation?

Specifically, if we can store gas keys the same way we store the two existing access key types, then this would let them share the same namespace. Which makes it possible to detect refunds that should go to a gas key by using the public key.

So I would suggest something more like this:

pub enum AccessKeyPermission {
    FunctionCall(FunctionCallPermission),

    /// Grants full access to the account.
    /// NOTE: It's used to replace account-level public keys.
    FullAccess,

+   // Same access as FullAccess but with separate gas accounting
+   ConceptualGasKey,
}

struct ConceptualGasKey {
-   public_key: PublicKey,
    nonces: Vec<Nonce>,
    balance: Balance,
-   permission: AccessKeyPermission,
}

This would be slightly less flexible, as it doesn't allow to limit methods callable by the gas key. We could add a FunctionCallGasKey as another variant if that's needed.

Of course, this has pretty large ramifications for technical code details. We might not even need AddGasKey and DeleteGasKey actions and instead just reuse AddAccessKey and RemoveAccessKey.

Maybe this is going too far off the intended design. But from my point of view, this seems to be an alternative that simplifies a bunch of things by staying closer to the existing concepts of access key types.

```

We add these operations to manipulate gas keys:
* `AddGasKey(PublicKey, AccessKeyPermission, usize)`: Creates a gas key with the given public key,

Check failure on line 120 in neps/nep-0611.md

View workflow job for this annotation

GitHub Actions / markdown-lint

Lists should be surrounded by blank lines

neps/nep-0611.md:120 MD032/blanks-around-lists Lists should be surrounded by blank lines [Context: "* `AddGasKey(PublicKey, Access..."] https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md032.md
permission, and number of nonces.
* `DeleteGasKey(PublicKey)`: Deletes a gas key.
* `FundGasKey(PublicKey, Balance)`: deducts balance from the account and gives it to the gas key.

#### Semantics of Gas Key Actions
`AddGasKey` is verified and executed as follows:
* Check that the key does not already exist.
* Check that if the permission is a `FunctionCallPermission`, the allowance is `None` (unlimited
allowance).
* A new `GasKey` entry is added to the trie, keyed by (gas key prefix, account ID, public key). The
gas key prefix is a new trie prefix.
* For each nonce ID (from 0 to the number of nonces minus 1), store the default nonce at the trie
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is mildly problematic for scaling/perf reasons. We're spending a humongous amount of time accessing the account + accesskey during transaction validation in tx runtime in some workloads already. This is adding another walk through a trie.

OTOH if we make nonces array be inline the GasKey struct, then every time we load/deserialize the GasKey we have to read out and deserialize all them nonces.


I see that this multiple-nonce mechanism is meant to enable something outside of this NEP's primary motivation. This is okay, but we should introduce proper motivation for adding this feature in the NEP text.

I also wouldn't lock in a specific implementation here for now. I suspect that with MAX_NONCES scaled down to a smaller number (e.g. 16) it might become feasible to keep GasKey as a monolithic type.

Another reason to keep MAX_NONCES low initially is that it is trivial to increase the limit in the future, but decreasing it would be effectively impossible.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using gas keys would add one additional trie access (to load the nonce from a separate key).

However I am not sure it is a major performance concern as gas keys are only needed by accounts with contracts that wish to have more than 4 in-flight transactions (or by users that issue parallel transactions using multiple nonces).

There is a proof size tradeoff as well: if vector approach is used, value of all nonces will be included in the proof vs. just the accessed nonce.

I agree about keeping MAX_NONCES low initially.

Copy link

@darioush darioush Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@robin-near can you please weigh in on the choice about making each nonce its own trie key?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it seems with some of the recent caching efforts we can cache fetching the "main part" of the GasKey (with balance, permissions etc). so an additional trie access only happens only for the first access of each GasKey (then accessing other nonce indexes of the same gas key become cheaper for the same chunk).

Copy link
Contributor

@nagisa nagisa Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then accessing other nonce indexes of the same gas key become cheaper for the same chunk

I'm not sure that holds true with memtries at least. Accesses to memtrie don't really retain any memory of the prior accesses and so any new access will start walking from the trie root until it gets to the value.

Conceptually, though, this could be possible to make optimal by changing the memtrie. It would just need a much more involved API that allowed you to pass in a node from previous lookup to start walking from and only use the tail of the key, rather than the entire key.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that #522 is also open and might have a different solution to parallelization. But I'm not sure if it is compatible with the exact limit we want to enforce here.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an interesting proposal, and indeed a valid approach to dealing with duplicate transactions.
I don't agree with the part about writing state into the trie (this seems very expensive, to account for transactions in recent blocks)

This means the nodes would have to re-compute the new "recently accepted transactions" data structure. In spice we are trying to avoid this type of dependency on past chunks / blocks.

prefix (gas key prefix, account ID, public key, nonce ID).
* The default nonce is block height * 1e6, the same as the default nonce calculation for access keys.
* The operation is charged according to the amount of trie modifications.

`FundGasKey` is verified and executed as follows:
* The gas key must exist under the account.
* The cost of this action is the amount to fund; it is then verified the same way as a `Transfer` action.
* See below for "gas key transactions": it is also possible to fund a gas key using a gas key; so this
amount is not necessarily deducted straight from the account.
* To apply this action, the balance on the gas key is increased by the same amount.

`DeleteGasKey` is verified as executed as follows:
* The gas key must exist under the account.
* To apply this action, all relevant trie nodes are deleted, and the cost accounted for from trie
modifications.
* The remaining balance left in the key is **burned**. (This prevent the key deletion attack.)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is a possible middle ground that does not make gas keys be this risky.

Indeed, if this were to transfer the balance out of the gas key, we'd have to prevent the issue described in the motivation section (with send_near replaced with gas key deletion.) Even in that context a middle ground is still achievable by e.g. treating this action specially and:

  1. Whenever we put a transaction containing this action into the pending transaction queue, reserving all the remaining balance inside of it;
  2. Not accepting any further transactions (cause the chunk producers see 0/negative remaining balance due to the reservation above.)
  3. (It might be necessary to make this action only valid as the only action of the transaction, i.e. not possible to construct via promises.)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a compounding effect here with deleting the account (which can be done programmatically).

If we do a refund on delete for GasKey then we need to do something like not allowing accounts with GasKey to be deleted.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, fair enough. It could be quite nasty if an account deletion with a boatload of gas keys required a bunch of book-keeping on the chunk producer side as well.

I think it would be a fair requirement to impose that if you want to delete an account you have to get rid of all gas keys first and use the access key to sign the action. We already have a limit on the amount of storage for the contract already, this wouldn't be an unusual requirement.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also agree with the alternative of forbidding a deletion of accounts with gas keys.

This is because if we try to add some protection from deleting high balances, then we have to check an unspecified number of gas keys to do 1 account delete, which makes charging for it difficult.

This makes it such that the user has to first delete each of the gas keys and pay for the nonce deletion accordingly, in a separate action.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I understand it, the requirement for burning the remaining balance stems from the fact that a gas key (or the whole account) can be deleted through an access key, i.e. not involving a transaction signed by the gas key. Is this the case? Even if we disallow deleting accounts with gas keys?

If yes, would it make sense (or even be possible) to enforce the same restrictions for gas key deletions as we do for transfers? Putting it differently, can we treat a gas key deletion the same way as we treat a withdrawal, with the exception that the gas key would be deleted afterwards? The NEP later says that " contract execution cannot create receipts that withdraw from gas keys; only gas key transactions can". Is there a fundamental reason to restrict this to withdrawals?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i.e. not involving a transaction signed by the gas key. Is this the case? Even if we disallow deleting accounts with gas keys?

This is correct. Notably, account deletion can also occur programmatically (as a result of contract execution).

If yes, would it make sense (or even be possible) to enforce the same restrictions for gas key deletions as we do for transfers?

We could force the same restriction, i.e., disallow programmatic deletion of gas keys. However, contracts may benefit from the flexibility of programmatically adding/deleting gas keys.

* For user's benefit we may consider failing this action if the balance exceeds a certain threshold.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to consider an additional "force" flag in the DeleteGasKey action to give the user the possibility to override this check?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a note for this as an implementation consideration.

* Note that this does not mean that all balance in a gas key is eventually burned; one can withdraw from
a gas key with a gas key transaction with a simple `Transfer` action.

#### Gas Key Transactions

`TransactionV2` reuses the fields of `TransactionV1` except replacing the `public_key` and `nonce` fields
with an enum:
```rust
enum TransactionKeyKind {
AccessKey {
key: PublicKey,
nonce: Nonce
},
GasKey {
key: PublicKey,
nonce_id: usize,
nonce: Nonce,
}
}
```

The semantics for gas key transactions, at the moment of execution (conversion to receipts), are:
* A gas key transaction is valid iff all of the following are true:
* The public key corresponds to a valid gas key;
* The gas key has enough balance to cover the total transaction cost (all costs included in config.rs
`tx_cost`; this includes amounts included in `Transfer` actions too);
* The nonce ID < total number of nonces for the gas key, and the nonce is a valid nonce for that nonce
ID (per the same nonce checks as access key);
* When converting the gas key transaction to a receipt, the same logic applies as for access key
transactions, except
* The transaction cost is deducted from the gas key instead of the account.
* The new nonce is written for the specific nonce ID under the gas key.

#### Gas Key Pending Transaction Constraints
Unlike access key transactions, gas key transactions are not limited in parallelism; they are only limited
by the amount of gas these transactions consume. Specifically, for a gas key $G$, the total cost of
the transactions signed with $G$ in the pending transactions must not exceed the balance of $G$
(according to the state that the pending transactions are calculated based on).

This constraint should be good enough to cover cases of adding, funding, removing, or withdrawing from gas
keys as well. For adding and funding, we do need the execution to catch up before the newly available
balance can be used for pending transactions, which is suboptimal but correct. For withdrawing, the
action of withdrawing itself goes into the cost of the transaction and therefore is accounted for (and
note that contract execution cannot create receipts that withdraw from gas keys; only gas key
transactions can). For deletion, balance in gas keys are not refunded, so although subsequent pending
transactions may end up failing, the gas committed to those transactions are already burnt, eliminating
the opportunity for the aforementioned attack.

### Pending Transaction Queue

We would now maintain a new data structure that stores the pending transactions, called the Pending
Transaction Queue. Although it's conceptually a queue, it is stored as a collection indexed by
(account ID, transaction type), and further indexed by the block hash. Furthermore, the pending
transaction queue is stored per block per shard, not as a single data structure. The contents of the queue
is exactly the pending transactions according to the definition above.

The constraints are enforced as described above; we reiterate it here:
* There cannot be more than 4 access key transactions under the same account in the queue;
* There cannot be more gas key transactions under the same gas key in the queue whose total transaction
cost exceed the gas key's balance.

The constraints are maintained at the time of chunk production: when producing a chunk, we only accept
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To check the constraints hold, it sounds like this means chunk producers need to include a state witness to validators along with the proposed chunk, just like they do today. Does this work against the idea of SPICE separating the consensus and execution? I suppose maybe if this state witness is very small relative to today then SPICE could still be effective.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, good point. We are only addressing the attack surface from (unauthenticated) users, not attacks from chunk producers. Chunk producers today (and in SPICE) are high-stake entities because they are also block producers, and mounting this attack as a chunk producer is not even very effective because you can only affect the chunks you produce. It is also already possible today to simply produce a chunk that is empty.

transactions that would maintain these constraints. For limiting gas key transactions, we always query the
balance of the gas key from the executed state (as opposed to storing it in the queue).

To compute the pending transaction queues (one for each tracked shard) for a new block,
* Start from the queue from the previous block;
* Subtract the transactions that are included in each newly certified block;
* Add new transactions included in this block.

### Impact to Existing Protocol without SPICE
As mentioned above, this NEP applies with or without SPICE. The impact to the existing protocol is purely
positive:
* Access keys are not restricted, as the set of pending transactions is always empty.
* Gas keys allow programmatic transaction senders to more easily manage multiple nonces. Rather than
requiring multiple access keys to be created, they just need to create a single gas key.

The implementation of gas keys before SPICE also allows programmatic users to migrate to using gas keys,
preparing them for when SPICE is launched.

## Alternatives
TODO

## TODO: Other things