TIP-1009: Expiring Nonces
Protocol Version: T1
Status: Mainnet
Authors: Tempo Team
Related: TIP-20, Transactions
Status: Mainnet
Authors: Tempo Team
Related: TIP-20, Transactions
Abstract
TIP-1009 introduces expiring nonces, an alternative replay protection mechanism where transactions are valid only within a specified time window. Instead of tracking sequential nonces, the protocol uses transaction hashes with expiry timestamps to prevent replay attacks. This enables use cases like gasless transactions, meta-transactions, and simplified UX where users don’t need to manage nonce ordering.Motivation
Traditional sequential nonces require careful ordering—if transaction N fails or is delayed, all subsequent transactions (N+1, N+2, …) are blocked. This creates friction for:- Gasless/Meta-transactions: Relayers need complex nonce management across multiple users
- Parallel submission: Users cannot submit multiple independent transactions simultaneously
- Recovery from failures: Stuck transactions require explicit cancellation with the same nonce
validBefore timestamp.
Specification
Nonce Key
Expiring nonce transactions use a reserved nonce key:nonceKey = uint256.max, the protocol treats it as an expiring nonce transaction.
Transaction Fields
Expiring nonce transactions require:| Field | Type | Description |
|---|---|---|
nonceKey | uint256 | Must be uint256.max to indicate expiring nonce mode |
nonce | uint64 | Must be 0 (unused, validated for consistency) |
validBefore | uint64 | Unix timestamp (seconds) after which the transaction is invalid |
Validity Window
ThevalidBefore timestamp must satisfy:
nowis the current block timestampMAX_EXPIRY_SECS = 30seconds
validBefore in the past or more than 30 seconds in the future are rejected.
Replay Protection
Replay protection uses a circular buffer data structure in the Nonce precompile:Storage Layout
Circular Buffer Design
The circular buffer has a fixed capacity:Algorithm
When processing an expiring nonce transaction:-
Validate expiry window: Reject if
validBefore <= noworvalidBefore > now + 30 -
Replay check: Read
expiringNonceSeen[txHash]- If entry exists and
expiry > now, reject as replay
- If entry exists and
-
Get buffer position: Read
expiringNonceRingPtr, computeidx = ptr % CAPACITY -
Read existing entry: Read
expiringNonceRing[idx]to getoldHash -
Eviction check (safety): If
oldHash != 0:- Read
expiringNonceSeen[oldHash] - If
expiry > now, reject (buffer full of valid entries) - Clear
expiringNonceSeen[oldHash] = 0
- Read
-
Insert new entry:
- Write
expiringNonceRing[idx] = txHash - Write
expiringNonceSeen[txHash] = validBefore
- Write
-
Advance pointer: Write
expiringNonceRingPtr = ptr + 1
Gas Costs
The intrinsic gas cost for expiring nonce transactions includes:- 2 cold SLOADs:
seen[txHash],ring[idx](unique slots per tx) - 1 warm SLOAD:
seen[oldHash](warm because we just readring[idx]which points to it) - 3 SSTOREs at RESET price:
seen[oldHash]=0,ring[idx],seen[txHash]
Why SSTORE_RESET (2,900) instead of SSTORE_SET (20,000) for
seen[txHash]:SSTORE_SET cost exists to penalize permanent state growth. Expiring nonce data is ephemeral: evicted within 30 seconds, fixed-size buffer (300k entries). No permanent state growth, so the 20k penalty doesn’t apply.Interaction with Other Features
2D Nonces
Expiring nonces and 2D nonces are mutually exclusive:nonceKey = 0: Protocol nonce (standard sequential)nonceKey = 1..uint256.max-1: 2D nonce keysnonceKey = uint256.max: Expiring nonce mode
Access Keys (Keychain)
Expiring nonces work with access key signatures. ThevalidBefore provides an additional security boundary—even if an access key is compromised, transactions signed with it become invalid after the expiry window.
Fee Tokens
Expiring nonce transactions pay fees in TIP-20 fee tokens like any other Tempo transaction.Invariants
Must Hold
| ID | Invariant | Description |
|---|---|---|
| E1 | No replay | A transaction hash can never be executed twice (changing validBefore produces a different hash) |
| E2 | Expiry enforcement | Transactions with validBefore <= now must be rejected |
| E3 | Window bounds | Transactions with validBefore > now + MAX_EXPIRY_SECS must be rejected |
| E4 | Nonce must be zero | Expiring nonce transactions must have nonce == 0 |
| E5 | Valid before required | Expiring nonce transactions must have validBefore set |
| E6 | No nonce mutation | Expiring nonce txs do not increment protocol nonce or any 2D nonce |
| E7 | Concurrent independence | Multiple expiring nonce txs from same sender can execute in same block |
Benchmark Results
Benchmarks were run to measure state savings from expiring nonces compared to 2D nonces.Key Findings
| Metric | Value |
|---|---|
| Per-transaction state savings | ~100 bytes |
| Circular buffer capacity | 300,000 entries |
| Buffer fills at 5k TPS | ~60 seconds |
Controlled Benchmark (100k transactions at 5k TPS)
| Nonce Type | Final DB Size | Transactions |
|---|---|---|
| 2D Nonces | 4,342.85 MB | 100,000 |
| Expiring Nonces | 4,332.18 MB | 100,000 |
| Difference | -10.67 MB | - |
Scaling Projections
| TPS | Daily Transactions | Daily State Savings |
|---|---|---|
| 5,000 | 432M | 43.2 GB |
| 10,000 | 864M | 86.4 GB |