<!-- tempo-docs-context -->
> Source: [/docs/protocol/transactions/spec-tempo-transaction](https://tempo.xyz/developers/docs/protocol/transactions/spec-tempo-transaction.md)
> Section: [Tempo EVM](https://tempo.xyz/developers/docs/development.md). Build on Tempo EVM: wallets and keys, transactions and fees, TIP-20 Tokens, contracts, exchange, network details, nodes, protocol specifications, and changelog.
> [Documentation index](https://tempo.xyz/developers/llms.txt) · [Docs skill](https://tempo.xyz/developers/SKILL.md)

> Tempo MCP: Use `search`, `find_pages`, `read_page`, and `code` at `https://mcp.tempo.xyz` for Tempo and related documentation.
> If an indexed page is missing or reflects an older site, read this site’s [documentation index](https://tempo.xyz/developers/llms.txt) and Markdown pages directly.

[**Specification**](https://tempo.xyz/developers/docs/protocol)

# Tempo Transactions Specification

## Abstract

This spec introduces native protocol support for the following features, using Tempo Transactions:

* WebAuthn/P256 signature validation - enables passkey signing
* Parallelizable nonces - allows higher tx throughput for each account
* Gas sponsorship - allows apps to pay for their users' transactions
* Call Batching - allows users to multicall efficiently and atomically
* Validity windows - specify the earliest and latest times a transaction may be included
* Access Keys - allow a sender's key to provision scoped access keys with spending limits or admin access keys for key management

## Motivation

Current accounts are limited to secp256k1 signatures and sequential nonces, creating UX and scalability challenges.\
Users cannot leverage modern authentication methods like passkeys, applications face throughput limitations due to sequential nonces.

## Specification

### Transaction Type

A new EIP-2718 transaction type is introduced with type byte `0x76`:

```rust
pub struct TempoTransaction {
    // Standard EIP-1559 fields
    chain_id: ChainId,                          // EIP-155 replay protection
    max_priority_fee_per_gas: u128,
    max_fee_per_gas: u128,
    gas_limit: u64,
    calls: Vec<Call>,                           // Batch of calls to execute atomically
    access_list: AccessList,                    // EIP-2930 access list

    // nonce-related fields
    nonce_key: U256,                            // 2D nonce key (0 = protocol nonce, >0 = user nonces)
    nonce: u64,                                 // Current nonce value for the nonce key

    // Optional features
    fee_token: Option<Address>,                 // Optional fee token preference
    fee_payer_signature: Option<Signature>,     // Sponsored transactions (secp256k1 only)
    valid_before: Option<u64>,                  // Transaction expiration timestamp (seconds)
    valid_after: Option<u64>,                   // Transaction can only be included after this timestamp (seconds)
    key_authorization: Option<SignedKeyAuthorization>, // Access key authorization (optional)
    aa_authorization_list: Vec<TempoSignedAuthorization>, // EIP-7702 style authorizations with AA signatures
}

// Call structure for batching
pub struct Call {
    to: TxKind,      // Can be Address or Create
    value: U256,
    input: Bytes     // Calldata for the call
}

// Key authorization for provisioning access keys
// RLP encoding: [chain_id, key_type, key_id, expiry?, limits?, allowed_calls?, witness?, is_admin?, account?]
pub struct KeyAuthorization {
    chain_id: u64,                              // Chain ID for replay protection (0 = valid on any chain)
    key_type: SignatureType,                    // Type of key: Secp256k1 (0), P256 (1), or WebAuthn (2)
    key_id: Address,                            // Key identifier (address derived from public key)
    expiry: Option<u64>,                        // Unix timestamp when key expires (omit / None for never expires)
    limits: Option<Vec<TokenLimit>>,            // TIP20 spending limits (None = unlimited spending)
    allowed_calls: Option<Vec<CallScope>>,      // Call-scope allowlist (None = unrestricted; Some(empty) = scoped deny-all)
    witness: Option<B256>,                      // App-defined witness digest for replay protection
    is_admin: Option<bool>,                     // true to provision an admin access key
    account: Option<Address>,                   // Target account for admin-signed authorizations
}

// Signed key authorization (authorization + root/admin key signature)
pub struct SignedKeyAuthorization {
    authorization: KeyAuthorization,
    signature: PrimitiveSignature,              // Root/admin key's signature over keccak256(rlp(authorization))
}

// TIP20 spending limit for access keys
pub struct TokenLimit {
    token: Address,                             // TIP20 token address
    limit: U256,                                // Maximum spending amount for this token
    period: u64,                                // Recurring period in seconds (0 = one-time, non-zero = recurring)
}

// Call-scope allowlist entry: a target contract and its allowed selector rules
pub struct CallScope {
    target: Address,                            // Target contract address
    selector_rules: Vec<SelectorRule>,          // Allowed selectors on that target (empty = any selector allowed)
}

// Selector rule: a function selector and optional recipient allowlist (for recipient-bound TIP-20 selectors)
pub struct SelectorRule {
    selector: [u8; 4],                          // 4-byte function selector
    recipients: Vec<Address>,                   // Allowed recipients (empty = any recipient)
}
```

### Signature Types

Four signature schemes are supported. The signature type is determined by length and type identifier:

#### secp256k1 (65 bytes)

```rust
pub struct Signature {
    r: B256,        // 32 bytes
    s: B256,        // 32 bytes
    v: u8           // 1 byte (recovery id)
}
```

**Format**: No type identifier prefix (backward compatible). Total length: 65 bytes.
**Detection**: Exactly 65 bytes with no type identifier.

#### P256 (130 bytes)

```rust
pub struct P256SignatureWithPreHash {
    typeId: u8,         // 0x01
    r: B256,            // 32 bytes
    s: B256,            // 32 bytes
    pub_key_x: B256,    // 32 bytes
    pub_key_y: B256,    // 32 bytes
    pre_hash: bool      // 1 byte
}
```

**Format**: Type identifier `0x01` + 129 bytes of signature data. Total length: 130 bytes. The `typeId` is a wire format prefix (not a struct field) prepended during encoding.

Note: Some P256 implementers (like Web Crypto) require the digests to be pre-hashed before verification.
If `pre_hash` is set to `true`, then before verification: `digest = sha256(digest)`.

#### WebAuthn (Variable length, max 2KB)

```rust
pub struct WebAuthnSignature {
    typeId: u8,                 // 0x02
    webauthn_data: Bytes,       // Variable length (authenticatorData || clientDataJSON)
    r: B256,                    // 32 bytes
    s: B256,                    // 32 bytes
    pub_key_x: B256,            // 32 bytes
    pub_key_y: B256             // 32 bytes
}
```

**Format**: Type identifier `0x02` + variable webauthn\_data + 128 bytes (r, s, pub\_key\_x, pub\_key\_y). Total length: variable (minimum 129 bytes, maximum 2049 bytes). The `typeId` is a wire format prefix prepended during encoding. Parse by working backwards: last 128 bytes are r, s, pub\_key\_x, pub\_key\_y.

#### Keychain (Variable length)

```rust
pub struct KeychainSignature {
    typeId: u8,                     // 0x04 (Keychain V2)
    user_address: Address,          // 20 bytes - root account address
    signature: PrimitiveSignature   // Inner signature (Secp256k1, P256, or WebAuthn)
}
```

**Format**: Type identifier `0x04` + user\_address (20 bytes) + inner signature. The inner signature signs `keccak256(0x04 || transaction_signature_hash || user_address)`, binding it to the root account. The legacy `0x03` format is rejected from T1C onward. Use the SDK encoder rather than wrapping an ordinary transaction signature.
**Purpose**: Allows an access key to sign on behalf of a root account. The handler validates that `user_address` has authorized the access key in the AccountKeychain precompile.

### Address Derivation

#### secp256k1

```solidity
address(uint160(uint256(keccak256(abi.encode(x, y)))))
```

#### P256 and WebAuthn

```solidity
function deriveAddressFromP256(bytes32 pubKeyX, bytes32 pubKeyY) public pure returns (address) {    
    // Hash 
    bytes32 hash = keccak256(abi.encodePacked(
        pubKeyX,
        pubKeyY
    ));
    
    // Take last 20 bytes as address
    return address(uint160(uint256(hash)));
}
```

### Tempo Authorization List

The `aa_authorization_list` field enables EIP-7702 style delegation with support for all three AA signature types (secp256k1, P256, and WebAuthn), not just secp256k1.

#### Structure

```rust
pub struct TempoSignedAuthorization {
    inner: Authorization,      // Standard EIP-7702 authorization
    signature: TempoSignature,    // Can be Secp256k1, P256, or WebAuthn
}
```

Each authorization in the list:

* Delegates an account to a specified implementation contract
* Is signed by the account's authority using any supported signature type
* Follows EIP-7702 semantics for delegation and execution

#### Validation

* Cannot have `Create` calls when `aa_authorization_list` is non-empty (follows EIP-7702 semantics)
* Authority address is recovered from the signature and matched against the authorization

### Parallelizable Nonces

* **Protocol nonce (key 0)**: Existing account nonce, incremented for regular txs, 7702 authorization, or `CREATE`
* **User nonces (keys 1-N)**: Enable parallel execution with special gas schedule
* **Reserved sequence keys**: Nonce sequence keys with the most significant byte `0x5b` are reserved for protocol-managed validator sequencing.

#### Account State Changes

* `nonces: mapping(uint256 => uint64)` - 2D nonce tracking

**Implementation Note:** Nonces are stored in the storage of a designated precompile at address `0x4E4F4E4345000000000000000000000000000000` (ASCII hex for "NONCE"), as there is currently no clean way to extend account state in Reth.

**Storage Layout at 0x4E4F4E4345:**

* Storage key: `keccak256(abi.encode(account_address, nonce_key))`
* Storage value: `nonce` (uint64)

Note: Protocol Nonce key (0), is directly stored in the account state, just like normal transaction types.

#### Nonce Precompile

The nonce precompile implements the following interface for managing 2D nonces:

```solidity
/// @title INonce - Nonce Precompile Interface
/// @notice Interface for managing 2D nonces as per the Tempo Transaction spec
/// @dev This precompile manages user nonce keys (1-N) while protocol nonces (key 0)
///      are handled directly by account state. Each account can have multiple
///      independent nonce sequences identified by a nonce key.
interface INonce {
    /// @notice Emitted when a nonce is incremented for an account and nonce key
    /// @param account The account whose nonce was incremented
    /// @param nonceKey The nonce key that was incremented
    /// @param newNonce The new nonce value after incrementing
    event NonceIncremented(address indexed account, uint256 indexed nonceKey, uint64 newNonce);
    /// @notice Thrown when trying to access protocol nonce (key 0) through the precompile
    /// @dev Protocol nonce should be accessed through account state, not this precompile
    error ProtocolNonceNotSupported();
    /// @notice Thrown when an invalid nonce key is provided
    error InvalidNonceKey();
    /// @notice Thrown when a nonce value would overflow
    error NonceOverflow();
    /// @notice Get the current nonce for a specific account and nonce key
    /// @param account The account address
    /// @param nonceKey The nonce key (must be > 0, protocol nonce key 0 not supported)
    /// @return nonce The current nonce value
    function getNonce(address account, uint256 nonceKey) external view returns (uint64 nonce);
}
```

#### Precompile Implementation

The precompile contract maintains a single storage mapping:

```solidity
contract Nonce is INonce {
    /// @dev Mapping from account -> nonce key -> nonce value
    mapping(address => mapping(uint256 => uint64)) private nonces;
}
```

#### Gas Schedule

Existing nonce streams and new nonce streams have different costs. New streams create storage; protocol nonce zero can also incur first-account costs. See [nonce gas accounting](#nonce-key-gas-schedule), and use `eth_estimateGas` against the target network rather than assuming the pre-launch 22,100-gas storage cost.

### Transaction Validation

#### Signature Validation

1. Determine type from signature format:
   * 65 bytes (no type identifier) = secp256k1
   * First byte `0x01` + 129 bytes = P256 (total 130 bytes)
   * First byte `0x02` + variable data = WebAuthn (total 129-2049 bytes)
   * First byte `0x04` + 20 bytes + inner signature = Keychain V2 (legacy `0x03` is rejected from T1C)
   * Otherwise invalid
2. Apply appropriate verification:
   * secp256k1: Standard `ecrecover`
   * P256: P256 curve verification with provided public key (sha256 pre-hash if flag set)
   * WebAuthn: Parse clientDataJSON, verify challenge and type, then P256 verify
   * Keychain: Verify inner signature, then validate access key authorization via AccountKeychain precompile

#### Nonce Validation

1. Fetch sequence for given nonce key
2. Verify sequence matches transaction
3. Increment sequence

#### Fee Payer Validation (if present)

1. Verify fee payer signature (K1 only initially)
2. Recover payer address via `ecrecover`
3. Deduct fees from payer instead of sender

### Fee Payer Signature Details

The Tempo Transaction Type (0x76) supports **gas sponsorship** where a third party (fee payer) can pay transaction fees on behalf of the sender. This is achieved through dual signature domains—the sender signs with transaction type byte `0x76`, while the fee payer signs with magic byte `0x78` to ensure domain separation and prevent signature reuse attacks.

#### Signing Domains

##### Sender Signature

For computing the transaction hash that the sender signs:

* Fields are preceded by transaction type byte `0x76`
* Field 11 (`fee_token`) is encoded as empty string (`0x80`) **if and only if** `fee_payer_signature` is present. This allows the fee payer to specify the fee token.
* Field 12 (`fee_payer_signature`) is encoded as:
  * Single byte `0x00` if fee payer signature will be present (placeholder)
  * Empty string `0x80` if no fee payer

**Sender Signature Hash:**

```rust
// When fee_payer_signature is present:
sender_hash = keccak256(0x76 || rlp([
    chain_id,
    max_priority_fee_per_gas,
    max_fee_per_gas,
    gas_limit,
    calls,
    access_list,
    nonce_key,
    nonce,
    valid_before,
    valid_after,
    0x80,  // fee_token encoded as EMPTY (skipped)
    0x00,  // placeholder byte for fee_payer_signature
    aa_authorization_list,
    key_authorization?  // encode only if present
]))

// When no fee_payer_signature:
sender_hash = keccak256(0x76 || rlp([
    chain_id,
    max_priority_fee_per_gas,
    max_fee_per_gas,
    gas_limit,
    calls,
    access_list,
    nonce_key,
    nonce,
    valid_before,
    valid_after,
    fee_token,  // fee_token is INCLUDED
    0x80,       // empty for no fee_payer_signature
    aa_authorization_list,
    key_authorization?  // encode only if present
]))
```

##### Fee Payer Signature

Only included for sponsored transactions. For computing the fee payer's signature hash:

* Fields are preceded by **magic byte `0x78`** (different from transaction type `0x76`)
* Field 11 (`fee_token`) is **always included** (20-byte address or `0x80` for None)
* Field 12 is serialized as the **sender address** (20 bytes). This commits the fee payer to sponsoring a specific sender.

**Fee Payer Signature Hash:**

```rust
fee_payer_hash = keccak256(0x78 || rlp([  // Note: 0x78 magic byte
    chain_id,
    max_priority_fee_per_gas,
    max_fee_per_gas,
    gas_limit,
    calls,
    access_list,
    nonce_key,
    nonce,
    valid_before,
    valid_after,
    fee_token,      // fee_token ALWAYS included
    sender_address, // 20-byte sender address
    aa_authorization_list,
    key_authorization? // encode only if present
]))
```

#### Key Properties

1. **Sender Flexibility**: By omitting `fee_token` from sender signature when fee payer is present, the fee payer can specify which token to use for payment without invalidating the sender's signature
2. **Fee Payer Commitment**: Fee payer's signature includes `fee_token` and `sender_address`, ensuring they agree to:
   * Pay for the specific sender
   * Use the specific fee token
3. **Domain Separation**: Different magic bytes (`0x76` vs `0x78`) prevent signature reuse attacks between sender and fee payer roles
4. **Deterministic Fee Payer**: The fee payer address is statically recoverable from the transaction via secp256k1 signature recovery

#### Validation Rules

**Signature Requirements:**

* Sender signature MUST be valid (secp256k1, P256, or WebAuthn depending on signature length)
* If `fee_payer_signature` present:
  * MUST be recoverable via secp256k1 (only secp256k1 supported for fee payers)
  * Recovery MUST succeed, otherwise transaction is invalid
* If `fee_payer_signature` absent:
  * Fee payer defaults to sender address (self-paid transaction)

**Token Preference:**

* When `fee_token` is `Some(address)`, this overrides any account/validator-level preferences
* Validation ensures the token is a valid TIP-20 token with sufficient balance/liquidity
* Failures reject the transaction before execution (see Token Preferences spec)

**Fee Payer Resolution:**

* Fee payer signature present → recovered address via `ecrecover`
* Fee payer signature absent → sender address
* This address is used for all fee accounting (pre-charge, refund) via TIP Fee Manager precompile

#### Transaction Flow

1. **User prepares transaction**: Sets `fee_payer_signature` to placeholder (`Some(Signature::default())`)
2. **User signs**: Computes sender hash (with fee\_token skipped) and signs
3. **Fee payer receives** user-signed transaction
4. **Fee payer verifies** user signature is valid
5. **Fee payer signs**: Computes fee payer hash (with fee\_token and sender\_address) and signs
6. **Complete transaction**: Replace placeholder with actual fee payer signature
7. **Broadcast**: Transaction is sent to network with both signatures

#### Error Cases

* `fee_payer_signature` present but unrecoverable → invalid transaction
* Fee payer balance insufficient for `gas_limit * max_fee_per_gas` in fee token → invalid
* Any sender signature failure → invalid
* Malformed RLP → invalid

### RLP Encoding

The transaction is RLP encoded as follows:

**Signed Transaction Envelope:**

```
0x76 || rlp([
    chain_id,
    max_priority_fee_per_gas,
    max_fee_per_gas,
    gas_limit,
    calls,                   // RLP list of Call structs
    access_list,
    nonce_key,
    nonce,
    valid_before,            // 0x80 if None
    valid_after,             // 0x80 if None
    fee_token,               // 0x80 if None
    fee_payer_signature,     // 0x80 if None, RLP list [v, r, s] if Some
    aa_authorization_list,   // EIP-7702 style authorization list with AA signatures
    key_authorization?,      // Only encoded if present (backwards compatible)
    sender_signature         // TempoSignature bytes (secp256k1, P256, WebAuthn, or Keychain)
])
```

**Call Encoding:**

```
rlp([to, value, input])
```

**Key Authorization Encoding:**

Use the [signed key authorization format](#key-authorization-encoding), including the trailing optional restrictions and signature. The same section defines the unsigned digest and omission rules.

**Notes:**

* Optional fields encode as `0x80` (EMPTY\_STRING\_CODE) when `None`
* The `key_authorization` field is truly optional - when `None`, no bytes are encoded (backwards compatible)
* The `calls` field is a list that must contain at least one Call (empty calls list is invalid)
* The `sender_signature` field is the final field and contains the TempoSignature bytes (secp256k1, P256, WebAuthn, or Keychain)
* KeyAuthorization uses RLP trailing field semantics for optional `expiry`, `limits`, `allowed_calls`, `witness`, `is_admin`, and `account`

### WebAuthn Signature Verification

WebAuthn verification follows the [Daimo P256 verifier approach](https://github.com/daimo-eth/p256-verifier/blob/master/src/WebAuthn.sol).

#### Signature Format

```
signature = authenticatorData || clientDataJSON || r (32) || s (32) || pubKeyX (32) || pubKeyY (32)
```

Parse by working backwards:

* Last 32 bytes: `pubKeyY`
* Previous 32 bytes: `pubKeyX`
* Previous 32 bytes: `s`
* Previous 32 bytes: `r`
* Remaining bytes: `authenticatorData || clientDataJSON` (requires parsing to split)

#### Authenticator Data Structure (minimum 37 bytes)

```
Bytes 0-31:   rpIdHash (32 bytes)
Byte 32:      flags (1 byte)
              - Bit 0 (0x01): User Presence (UP)
              - Bit 2 (0x04): User Verification (UV)
              - At least one of UP or UV must be set; AT and ED must be clear
Bytes 33-36:  signCount (4 bytes)
```

#### Verification Steps

The verifier splits the payload after the 37-byte authenticator data, parses `clientDataJSON` as JSON, checks `type == "webauthn.get"` and the unpadded Base64URL challenge, then verifies P256 over `sha256(authenticatorData || sha256(clientDataJSON))`. It rejects invalid JSON and unsupported authenticator flags. See the [canonical verifier](https://github.com/tempoxyz/tempo/blob/main/crates/primitives/src/transaction/tt_signature.rs) for the exact checks.

#### What We Verify

* Authenticator data minimum length (37 bytes)
* At least one of User Presence (UP) or User Verification (UV) is set
* Attested credential data (AT) and extension data (ED) flags are clear
* `"type":"webauthn.get"` in clientDataJSON
* Challenge matches tx\_hash (Base64URL encoded)
* P256 signature validity

#### What We Skip

* Origin verification (not applicable to blockchain)
* RP ID hash validation (no central RP in decentralized context)
* Signature counter (anti-cloning left to application layer)
* Backup flags (account policy decision)

#### Parsing authenticatorData and clientDataJSON

Tempo accepts exactly 37 bytes of authenticator data for assertions. It rejects the AT and ED flags, so attested credential data and extensions are unsupported. The remaining bytes before `r`, `s`, and the public key are `clientDataJSON`.

### Access Keys

A sender can choose to authorize an Access Key to sign transactions on the sender's behalf. This is useful to enable
flows where a root key (e.g. a passkey) would provision a short-lived (scoped) Access Key to
be able to sign transactions on the sender's behalf without inducing another passkey prompt.

More information about Access Keys can be found in the [Account Keychain Specification](./AccountKeychain).

A sender can authorize a key by signing over a "key authorization" item that contains the following information:

* **Chain ID** for replay protection (0 = valid on any chain)
* **Key type** (Secp256k1, P256, or WebAuthn)
* **Key ID** (address derived from the public key)
* **Expiration** timestamp of when the key should expire (optional - None means never expires)
* TIP20 token **spending limits** for the key (optional - None means unlimited spending):
  * Each limit carries a `period` (0 = one-time, non-zero = recurring in seconds). Recurring limits roll over to `max` when `current_timestamp >= periodEnd`.
  * Root keys and active admin access keys can update limits via `updateSpendingLimit()` without revoking the key. Updates reset `remaining` and `max` to `newLimit` while preserving `period` and `periodEnd`.
  * Note: Spending limits only apply to TIP20 token transfers, not ETH or other asset transfers
* **Call scopes** for the key (optional - None means unrestricted):
  * Each entry pins a `target` contract and a list of allowed selector rules. An empty selector list on a target means any selector is allowed on that target.
  * Selector rules can additionally constrain TIP-20 recipient-bearing selectors to a recipient allowlist.
  * `Some([])` (an empty top-level allowlist) means scoped deny-all.

Access-key-signed transactions cannot include a transaction-level creation call (`to` omitted). An allowed factory call can still execute `CREATE` or `CREATE2` internally; top-level call scopes do not sandbox factory internals.

<span id="key-authorization-encoding" />

#### RLP Encoding

**Unsigned Format:**

The root key or an active admin access key signs over the keccak256 hash of the RLP encoded `KeyAuthorization`:

```
key_authorization_digest = keccak256(rlp([chain_id, key_type, key_id, expiry?, limits?, allowed_calls?, witness?, is_admin?, account?]))

chain_id = u64 (0 = valid on any chain)
key_type = 0 (Secp256k1) | 1 (P256) | 2 (WebAuthn)
key_id = Address (derived from the public key)
expiry = Option<u64> (unix timestamp, None = never expires; omitted expiry is translated to u64::MAX when the protocol calls the precompile)
limits = Option<Vec<[token, limit, period]>> (None = unlimited spending; period = 0 means one-time)
allowed_calls = Option<Vec<[target, [[selector, [recipient, ...]], ...]]>> (None = unrestricted; Some([]) = scoped deny-all)
witness = Option<bytes32> (app-defined witness digest)
is_admin = Option<bool> (true provisions an admin access key)
account = Option<Address> (required when an admin access key signs the authorization)
```

**Signed Format:**

The signed format (`SignedKeyAuthorization`) includes all fields with the `signature` appended:

```
signed_key_authorization = rlp([chain_id, key_type, key_id, expiry?, limits?, allowed_calls?, witness?, is_admin?, account?, signature])
```

The `signature` is a `PrimitiveSignature` (secp256k1, P256, or WebAuthn) signed by the root key or an active admin access key.

Note: `expiry`, `limits`, `allowed_calls`, `witness`, `is_admin`, and `account` use RLP trailing field semantics — they can be omitted entirely when None.

When `is_admin` is `true`, `expiry`, `limits`, and `allowed_calls` must be omitted or empty because admin keys are only for key management. When an admin access key signs the authorization, `account` must be present and must equal the account being modified.

:::warning[Expiry encoding]
For `key_authorization`, the canonical non-expiring encoding omits `expiry` (`None`).

There is one decoder nuance: because `KeyAuthorization` uses canonical trailing optional fields, an explicit empty `expiry` placeholder (`0x80`) is also interpreted as `None` when another trailing optional field follows it. But a final `expiry` encoded as zero/empty is rejected, and a literal `0x00` is invalid RLP for this field.

Do not hand-encode `expiry = 0` or rely on `Some(0)` as a sentinel. The supported encoding to target is omission, and the protocol translates omitted expiry to `u64::MAX` when materializing the `AccountKeychain.authorizeKey(...)` call.
:::

Intrinsic gas for `key_authorization` accounts for the storage written for periodic-limit state and call-scope entries. See the [specification](https://github.com/tempoxyz/tempo/blob/main/tips/tip-1011.md#intrinsic-gas-for-key-authorization) for slot-counting rules.

#### Keychain Precompile

The Account Keychain precompile (deployed at address `0xAAAAAAAA00000000000000000000000000000000`) manages authorized access keys for accounts. It enables root keys and admin access keys to provision scoped access keys with expiry timestamps and per-TIP20 token spending limits, or admin access keys for key management.

**See the [Account Keychain Specification](./AccountKeychain) for complete interface details, storage layout, and implementation.**

#### Protocol Behavior

The protocol enforces Access Key authorization and spending limits natively.

##### Transaction Validation

When a TempoTransaction is received, the protocol:

1. **Identifies the signing key** from the transaction signature
   * If signature is a `Keychain` variant: extracts the `keyId` (address) of the Access Key
   * Otherwise: treats it as the Root Key (keyId = address(0))

2. **Validates KeyAuthorization** (if present in transaction)
   * The `key_authorization` field in `TempoTransaction` provisions a NEW Access Key
   * The Root Key or an active admin access key MUST sign:
     * The `key_authorization` digest: `keccak256(rlp([chain_id, key_type, key_id, expiry?, limits?, allowed_calls?, witness?, is_admin?, account?]))`
   * If an admin access key signs the authorization, the `account` field must be present and must equal the account being modified
   * Access Key (being authorized) CAN sign the same tx which it is authorized in.
   * This enables "authorize and use" in a single transaction

3. **Sets transaction context**
   * Stores `transactionKey[account] = keyId` in protocol state
   * Used to enforce authorization hierarchy during execution, can
     also be used by DApps to see which key authorized the current tx.

4. **Validates Key Authorization** (for Access Keys)
   * Queries precompile: `getKey(account, keyId)` returns `KeyInfo`
   * Checks key is active (not revoked)
   * Checks expiry: `current_timestamp < expiry`; non-expiring keys are stored with `expiry = u64::MAX`
   * Rejects transaction if validation fails

##### Authorization Hierarchy Enforcement

The protocol enforces a strict three-role hierarchy:

**Root Key** (keyId = address(0)):

* The account's primary key (address matches account address)
* Can call ALL precompile functions
* No spending limits
* Can authorize, revoke, and update access keys

**Admin Access Keys** (keyId != address(0), `is_admin = true`):

* Secondary keys authorized by the Root Key or another active admin access key
* Can call key-management mutators (`authorizeKey`, `authorizeAdminKey`, `revokeKey`, `updateSpendingLimit`, `setAllowedCalls`, `removeAllowedCalls`)
* Cannot carry spending limits, call scopes, or expiry
* Cannot include a transaction-level creation call; internal factory creation remains possible

**Limited Access Keys** (keyId != address(0), `is_admin = false`):

* Secondary keys authorized by the Root Key or an active admin access key
* CANNOT call mutable precompile functions (`authorizeKey`, `authorizeAdminKey`, `revokeKey`, `updateSpendingLimit`, `setAllowedCalls`, `removeAllowedCalls`)
* Subject to per-TIP20 token spending limits and call-scope checks during execution
* Cannot include a transaction-level creation call; internal factory creation remains possible
* Can have expiry timestamps

When a limited Access Key attempts to call any mutable keychain function:

1. Transaction executes normally until the precompile call
2. Precompile checks `getTransactionKey()` and confirms the key is not the Root Key or an active admin access key
3. Call reverts with `UnauthorizedCaller` error
4. Entire transaction is reverted

##### Spending Limit Enforcement

The protocol tracks and enforces spending limits for TIP20 token transfers:

**Scope:** Direct TIP-20 transfers and approval increases are tracked, along with account-paid fees and token pulls by approved protocol precompiles. T12 additionally tracks `burnAt` when burning the transaction originator's tokens.

* Spending limits only apply when `msg.sender == tx.origin` (direct EOA calls)
* When a contract makes transfers on behalf of the user, spending limits do NOT apply (e.g., `transferFrom()`)
* Native value transfers are NOT limited
* NFT transfers are NOT limited
* Other asset types are NOT limited

**Tracking:** During transaction execution, when an Access Key's transaction directly calls TIP20 methods:

1. Protocol intercepts `transfer(to, amount)`, `transferWithMemo()`, and `approve(spender, amount)` calls
2. For `transfer`/`transferWithMemo`, the full `amount` is checked against the remaining limit
3. For `approve`, only **increases** in approval (new approval minus previous allowance) are checked and counted against the limit
4. Queries: `getRemainingLimitWithPeriod(account, keyId, token)`, which returns `(remaining, periodEnd)` and reflects any periodic rollover
5. Checks: relevant amount (transfer amount or approval increase) `<= remaining`
6. If check fails: reverts with `SpendingLimitExceeded`
7. If check passes: decrements the limit by the relevant amount
8. Updates are stored in precompile state

**Root Key Behavior:** Spending limit checks are skipped entirely (no limits apply)

**Recurring Limits:** When a `TokenLimit.period` is non-zero, the limit recurs. `remaining` rolls over to `max` once `current_timestamp >= periodEnd`, and `periodEnd` advances by `period`. Callers observe rollover state via `getRemainingLimitWithPeriod`.

**Limit Updates:**

* Limits deplete as tokens are spent
* Root Key or an active admin access key can call `updateSpendingLimit(keyId, token, newLimit)` to set new limits
* Setting a new limit REPLACES both `remaining` and `max` with `newLimit`. The configured `period` and current `periodEnd` are preserved.

##### Call Scope Enforcement

When an Access Key has stored call scopes (`allowed_calls` was set at authorization, or `setAllowedCalls(...)` was called later), the protocol enforces them on top-level calls signed by that Access Key:

1. For each call in the batch, look up the matching `(target, selector)` allowlist entry
2. If the target is not in the allowlist, or the selector is not allowed on that target, revert with `CallNotAllowed`
3. For recipient-bound TIP-20 selectors (`transfer`, `approve`, and `transferWithMemo`), additionally enforce that the call's recipient is in the rule's recipient allowlist (when non-empty)
4. Access keys with `allowed_calls = None` are unrestricted; `Some([])` is scoped deny-all

##### Contract Creation Restriction

Access-key-signed transactions cannot contain a transaction-level creation call. Calls to permitted factories can still create contracts internally with `CREATE` or `CREATE2`.

##### Creating and Using KeyAuthorization

The following is encoding pseudocode, not a runnable SDK example. Helper names stand for your encoder and signer. Use the [SDK integration guide](https://tempo.xyz/developers/docs/guide/tempo-transaction) for executable examples; populate gas and fee fields from current RPC estimates.

**First-Time Authorization Flow:**

1. **Generate Access Key**
   ```typescript
   // Generate a new P256 or secp256k1 key pair
   const accessKey = generateKeyPair("p256"); // or "secp256k1"
   const keyId = deriveAddress(accessKey.publicKey);
   ```

2. **Create Authorization Message**
   ```typescript
   // Define key parameters
   const keyAuth = {
     chain_id: 42431, // Moderato testnet
     key_type: SignatureType.P256,      // 1
     key_id: keyId,                     // address derived from public key
     expiry: timestamp + 86400,         // 24 hours from now; omit this field for a non-expiring key authorization
     limits: [
       // One-time limit (period = 0)
       { token: USDG_ADDRESS, limit: 1000000000, period: 0 },                  // 1000 USDG (6 decimals), one-time
       // Recurring weekly limit (period = 604800 seconds)
       { token: SECOND_TIP20_ADDRESS, limit: 500000000n, period: 604800 } // 500 TIP-20 tokens / week (6 decimals)
     ],
     // Optional call scopes — omit for an unrestricted key
     allowed_calls: [
       {
         target: USDG_ADDRESS,
         selector_rules: [
           // transfer(address,uint256) restricted to a single recipient
           { selector: "0xa9059cbb", recipients: [TRUSTED_RECIPIENT] }
         ]
       }
     ]
   };

   // Compute digest: keccak256(rlp([chain_id, key_type, key_id, expiry?, limits?, allowed_calls?, witness?, is_admin?, account?]))
   const authDigest = computeAuthorizationDigest(keyAuth);
   ```

3. **Root Key Signs Authorization**
   ```typescript
   // Sign with Root Key (e.g., passkey prompt)
   const rootSignature = await signWithRootKey(authDigest);
   ```

4. **Build TempoTransaction**
   ```typescript
   const tx = {
     chain_id: 42431, // Moderato testnet
     nonce: await getNonce(account),
     nonce_key: 0,
     calls: [{ to: recipient, value: 0, input: "0x" }],
     gas_limit: 200000,
     max_fee_per_gas: 1000000000,
     max_priority_fee_per_gas: 1000000000,
     key_authorization: {
       authorization: keyAuth,
       signature: rootSignature  // Root Key's signature on authDigest
     },
     // ... other fields
   };
   ```

5. **Access Key Signs Transaction**
   ```typescript
   // Sign transaction with the NEW Access Key being authorized
   const txHash = computeTxSignatureHash(tx);
   const accessKeyHash = keccak256(concat(['0x04', txHash, account]));
   const accessSignature = await signWithAccessKey(accessKeyHash, accessKey);

   // Wrap in Keychain signature
   const finalSignature = {
     Keychain: {
       version: 2, // wire prefix 0x04
       user_address: account,
       signature: { P256: accessSignature }  // or Secp256k1
     }
   };
   ```

6. **Submit Transaction**
   * Protocol validates Root Key signed the `key_authorization`
   * Protocol calls `authorizeKey()` on the precompile to store the key
   * Protocol validates Access Key signature on transaction
   * Transaction executes with spending limits enforced

**Subsequent Usage (Key Already Authorized):**

```typescript
// Access Key is already authorized, just sign transactions directly
const tx = {
  chain_id: 42431, // Moderato testnet
  nonce: await getNonce(account),
  calls: [{ to: recipient, value: 0, input: calldata }],
  key_authorization: null,  // No authorization needed
  // ... other fields
};

const txHash = computeTxSignatureHash(tx);
const accessKeyHash = keccak256(concat(['0x04', txHash, account]));
const accessSignature = await signWithAccessKey(accessKeyHash, accessKey);

const finalSignature = {
  Keychain: {
    version: 2, // wire prefix 0x04
    user_address: account,
    signature: { P256: accessSignature }
  }
};

// Submit - protocol validates key is authorized and not expired
```

##### Key Management Operations

**Revoking an Access Key:**

```typescript
// Must be signed by the Root Key or an active admin key
const tx = {
  chain_id: 42431, // Moderato testnet
  nonce: await getNonce(account),
  calls: [{
    to: ACCOUNT_KEYCHAIN_ADDRESS,
    value: 0,
    input: encodeCall("revokeKey", [keyId])
  }],
  // ... sign with the Root Key or an active admin key
};
```

**Updating Spending Limits:**

```typescript
// Must be signed by the Root Key or an active admin key
const tx = {
  chain_id: 42431, // Moderato testnet
  nonce: await getNonce(account),
  calls: [{
    to: ACCOUNT_KEYCHAIN_ADDRESS,
    value: 0,
    input: encodeCall("updateSpendingLimit", [
      keyId,
      USDG_ADDRESS,
      2000000000  // New limit: 2000 USDG
    ])
  }],
  // ... sign with the Root Key or an active admin key
};
```

**Note:** After updating, both `remaining` and `max` are set to `newLimit`. The configured `period` and current `periodEnd` are preserved.

##### Querying Key State

Applications can query key information and spending limits:

```typescript
// Check if key is authorized and get info
const keyInfo = await precompile.getKey(account, keyId);
// Returns: { signatureType, keyId, expiry, enforceLimits, isRevoked, isAdmin }

// Check remaining spending limit and current period end for a token
const { remaining, periodEnd } = await precompile.getRemainingLimitWithPeriod(
  account, keyId, USDG_ADDRESS
);
// Returns: (uint256 remaining, uint64 periodEnd). Reflects periodic rollover.

// Inspect call scopes
const { isScoped, scopes } = await precompile.getAllowedCalls(account, keyId);
// isScoped = false → key is unrestricted
// isScoped = true, scopes = [...] → key is scoped to those (target, selector, recipient) entries
// isScoped = true, scopes = [] → scoped deny-all (also returned for missing/revoked/expired keys)

// Get which key signed current transaction (callable from contracts)
const currentKey = await precompile.getTransactionKey();
// Returns: address (0x0 for Root Key, keyId for Access Key)
```

## Rationale

### Signature Type Detection by Length

Using signature length for type detection avoids adding explicit type fields while maintaining deterministic parsing. The chosen lengths (65, 129, variable) are naturally distinct.

### Linear Gas Scaling for Nonce Keys

Creating a nonce stream adds persistent storage. Its cost follows the active fork's storage schedule; updating an existing stream avoids creating that additional slot.

<span id="no-nonce-expiry" />

### Sequential and expiring nonces

Sequential nonce streams retain their counters across transactions. [Expiring nonces](https://tempo.xyz/developers/docs/protocol/tips/tip-1009) provide a separate mode for transactions with a bounded validity window. Choose the mode appropriate for the application's ordering and replay-protection requirements.

### Backwards Compatibility

This spec introduces a new transaction type and does not modify existing transaction processing. Legacy transactions continue to work unchanged. We special case `nonce key = 0` (also referred to as the protocol nonce key) to maintain compatibility with existing nonce behavior.

## Gas Costs

### Signature Verification Gas Schedule

Different signature types incur different base transaction costs to reflect their computational complexity:

| Signature Type | Base Gas Cost | Calculation | Rationale |
|----------------|---------------|-------------|-----------|
| **secp256k1** | 21,000 | Standard | Includes 3,000 gas for ecrecover precompile |
| **P256** | 26,000 | 21,000 + 5,000 | Base 21k + additional 5k for P256 verification |
| **WebAuthn** | 26,000 + variable data cost | 26,000 + (calldata gas for clientDataJSON) | Base P256 cost plus variable cost for clientDataJSON based on size |
| **Keychain** | Inner signature + 3,000 | primitive\_sig\_cost + 3,000 | Inner signature cost + key validation overhead (2,100 SLOAD + 900 buffer) |

**Rationale:**

* The base 21,000 gas for standard transactions already includes the cost of secp256k1 signature verification via ecrecover (3,000 gas)
* [EIP 7951](https://eips.ethereum.org/EIPS/eip-7951) sets P256 verification cost at 6,900 gas. We add 1,100 gas to account for the additional 65 bytes of signature size (129 bytes total vs 64 bytes for secp256k1), giving 8,000 gas total. Since the base 21k already includes 3,000 gas for ecrecover (which P256 doesn't use), the net additional cost is 8,000 - 3,000 = **5,000 gas**.
* WebAuthn signatures require additional computation to parse and validate the clientDataJSON structure. We cap the total signature size at 2kb. The signature is also  charged using the same gas schedule as calldata (16 gas per non-zero byte, 4 gas per zero byte) to prevent the use of this signature space from spam.
* Keychain signatures wrap a primitive signature and are used by access keys. They add 3,000 gas to cover key validation during transaction validation (cold SLOAD to verify key exists + processing overhead).
* Individual per-signature-type gas costs allow us to add more advanced verification methods in the future like multisigs, which could have dynamic gas pricing.

<span id="nonce-key-gas-schedule" />

<span id="case-1-protocol-nonce-key-0" />

<span id="case-2-existing-user-nonce-key-nonce--0" />

<span id="case-3-new-user-nonce-key-nonce--0" />

<span id="key-authorization-gas-schedule" />

<span id="gas-components" />

<span id="gas-formula" />

<span id="examples" />

<span id="rationale" />

<span id="reference-pseudocode" />

### Nonce and key authorization gas

The active fork's gas schedule determines nonce and access-key storage costs. The pre-T1B constants of 22,100 gas for a new nonce stream and 30,000 gas for a key authorization are historical and must not be used as current estimates.

* An existing nonce stream updates an existing slot. A new stream creates storage; first use of the protocol nonce can also create account state.
* Expiring nonces use the separate replay-protection path rather than creating a permanent nonce stream.
* Key authorization charges for its signature, the key record, two storage slots per token limit, call-scope storage, and applicable witness/admin events. Scope selectors and recipients add work and storage.
* T7 storage-credit rules change storage accounting. A complete estimate must also include calldata, the access list, extra batch targets, EIP-7702 authorizations, and the calls themselves.

Use `eth_estimateGas` with the complete transaction on the target network. Implementers can use the [canonical intrinsic-gas calculation](https://github.com/tempoxyz/tempo/blob/main/crates/revm/src/handler.rs) together with the [fork-dependent gas parameters](https://github.com/tempoxyz/tempo/blob/main/crates/revm/src/gas_params.rs); a fixed per-key formula omits state and fork-dependent charges.

## Security Considerations

### Mempool DOS Protection

Transaction pools perform pre-execution validation checks before accepting transactions. These checks are performed for free by the nodes, making them potential DOS vectors. The three primary validation checks are:

1. **Signature verification** - Must be valid
2. **Nonce verification** - Must match current account nonce
3. **Balance check** - Account must have sufficient balance to pay for transaction

This transaction type impacts all three areas:

#### Signature Verification Impact

* **P256 signatures**: Fixed computational cost similar to ecrecover.
* **WebAuthn signatures**: Variable cost due to clientDataJSON parsing, but **capped at 2KB total signature size** to prevent abuse
* **Mitigation**: All signature types have bounded computational costs that are in the same ballpark as standard ecrecover.

#### Nonce Verification Impact

* **2D nonce lookup**: Requires additional storage read from nonce precompile
* **Cost**: Equivalent to a cold SLOAD (~2,100 gas worth of free computation)
* **Mitigation**: Cost is bounded to a manageable value.

#### Fee Payer Impact

* **Additional account read**: When fee payer is specified, must fetch fee payer's account to verify balance
* **Cost**: Effectively doubles the free account access work for sponsored transactions
* **Mitigation**: Cost is still bounded to a single additional account read.

#### Comparison to Ethereum

The introduction of 7702 delegated accounts already created complex cross-transaction dependencies in the mempool, which prevents any static pool checks from being useful.
Because a single transaction can invalidate multiple others by spending balances of multiple accounts

**Assessment:** While this transaction type introduces additional pre-execution validation costs, all costs are bounded to reasonable limits. The mempool complexity issues around cross-transaction dependencies already exist in Ethereum due to 7702 and accounts with code, making static validation inherently difficult. So the incremental cost from this transaction type is acceptable given these existing constraints.
