---
id: TIP-1061
title: Configurable Accounts
description: Adds configurable accounts with weighted owners, stable addresses, nested owner signatures, and account keychain support.
authors: Jake Moxey (@jxom), Tanishk Goyal (@legion2002)
status: Draft
related: TIP-0001, TIP-1011, TIP-1020, TIP-1049, TIP-1053
protocolVersion: T11
---

# TIP-1061: Configurable Accounts

## Abstract

Configurable accounts can change who controls them and their configuration without changing their identity. They support multisig control, recovery, key rotation, and permissioned access (via access keys) while remaining the same Tempo account to applications and counterparties.

## Motivation

A team should be able to change its members or control policy without replacing the account, and a lost or compromised key should not require moving assets to a new address.

Today, these needs typically require a contract wallet or external account abstraction system. Configurable accounts make them part of Tempo's account keychain model, giving wallets and applications one consistent way to serve individuals, teams, treasuries, validators, and institutions.

---

# Specification

## Types and Limits

```rust
/// Tempo signature type byte for configurable account signatures.
pub const SIGNATURE_TYPE_CONFIGURABLE_ACCOUNT: u8 = 0x05;

/// Domain prefix for configurable account owner signatures.
pub const CONFIGURABLE_ACCOUNT_SIGNATURE_DOMAIN: &[u8] = b"tempo:configurable-account";

/// Maximum owner count that leaves room for fresh nonce creation and transaction overhead.
pub const MAX_CONFIGURABLE_ACCOUNT_OWNERS: usize = 58;

/// Maximum threshold for one account configuration.
pub const MAX_CONFIGURABLE_ACCOUNT_THRESHOLD: u8 = 8;

/// Maximum number of owner signatures in one configurable account signature.
pub const MAX_CONFIGURABLE_ACCOUNT_SIGNATURES: usize =
    MAX_CONFIGURABLE_ACCOUNT_THRESHOLD as usize;

/// Maximum number of configurable account signatures in one nested authorization path.
pub const MAX_CONFIGURABLE_ACCOUNT_NESTING_DEPTH: usize = 2;

/// Maximum encoded byte length of one primitive owner signature.
pub const MAX_CONFIGURABLE_ACCOUNT_PRIMITIVE_SIGNATURE_BYTES: usize =
    1 + MAX_WEBAUTHN_SIGNATURE_LENGTH;

/// Weighted owner in an account configuration.
pub struct ConfigurableAccountOwner {
    /// Address recovered from a primitive owner signature or named by a nested configurable account signature.
    pub owner: Address,

    /// Nonzero weight contributed by this owner.
    pub weight: u8,
}

/// Initial owner configuration used to derive and bootstrap an account.
pub struct ConfigurableAccountInit {
    /// Caller-chosen value that permits distinct accounts with otherwise identical configurations.
    pub salt: B256,

    /// Minimum total owner weight required for authorization.
    pub threshold: u8,

    /// Strictly ascending weighted owners.
    pub owners: Vec<ConfigurableAccountOwner>,
}

/// Signature payload for a configurable account.
pub enum ConfigurableAccountSignature {
    /// Carries the initial configuration that derives the account.
    Bootstrap {
        /// Initial owner configuration.
        init: ConfigurableAccountInit,

        /// Ordered primitive or nested configurable account owner signatures.
        signatures: Vec<TempoSignature>,
    },

    /// Carries the account directly.
    Initialized {
        /// Account authorized by the owner signatures.
        account: Address,

        /// Ordered primitive or nested configurable account owner signatures.
        signatures: Vec<TempoSignature>,
    },
}
```

An account may store up to 58 owners so a worst-case bootstrap fits within the transaction gas cap while leaving room for fresh nonce creation and transaction overhead. Each configurable account signature may contain at most 8 owner signatures, and primitive owner signatures have a byte limit. Nested configurable account owner signatures are limited to one level: the outer account is depth 1 and the nested owner account is depth 2. These limits bound transaction size and recursive signature-verification work.

Rules:

- Configurations MUST contain 1 to 58 unique, nonzero, address-sorted owners. Weights MUST be nonzero and total at most 255.
- Threshold MUST satisfy `1 <= threshold <= min(8, total_weight)`.
- Each owner signature MUST be a `TempoSignature::Primitive` or `TempoSignature::ConfigurableAccount`.
- Primitive owner signatures MUST NOT exceed `MAX_CONFIGURABLE_ACCOUNT_PRIMITIVE_SIGNATURE_BYTES`; nested owner signatures are bounded recursively by the signature-count and nesting-depth limits.

## Signature Encoding

Signature type `0x05` has two forms:

- **Bootstrap.** Used to establish a configurable account's initial configuration during its first transaction.
- **Initialized.** Used after bootstrap, when authorization can use the account's stored configuration.

Rules:

- Type `0x05` MUST be rejected before T11.
- `signatures` MUST contain 1 to `MAX_CONFIGURABLE_ACCOUNT_SIGNATURES` owner signatures, each using the existing `TempoSignature` byte encoding. Decoders MUST reject an empty list before intrinsic gas is computed.
- Decoders MUST reject malformed encodings, trailing fields, limit violations, and excessive nesting.

### Bootstrap

The bootstrap form establishes a configurable account during its first transaction. It can authorize the transaction directly or authorize an access key that signs that transaction.

```text
0x05 || rlp([init, signatures])

init  = rlp([salt, threshold, owners])
owner = rlp([owner, weight])
```

Rules:

- A bootstrap signature MUST use `0x05 || rlp([init, signatures])` and derive its account from `init`.
- A top-level bootstrap MUST carry this encoding in either the outer signature or its key authorization signature, but not both.

### Initialized

The initialized form is used once the account configuration exists. It identifies the account so validation can authorize owner signatures against the current stored configuration.

```text
0x05 || rlp([account, signatures])
```

Rules:

- An initialized signature MUST use `0x05 || rlp([account, signatures])`.
- Nested owner signatures and account keychain signatures MUST use the initialized encoding.
- Key authorization signatures MUST use initialized encoding unless the key authorization supplies `init` for a top-level bootstrap.

## Account Identity

The initial owner configuration and salt establish an account identity that remains stable across later owner changes. The salt lets identical owner configurations derive distinct accounts:

```text
configurable_account_address = address(keccak256(
  "tempo:configurable-account" ||
  salt ||
  uint8(threshold) ||
  uint8(owners.len()) ||
  owners[0].owner || uint8(owners[0].weight) ||
  ...
)[12:32])
```

Rules:

- Derivation MUST use the formula exactly: ASCII domain, one-byte integers, raw concatenation, and no chain ID, RLP, or ABI encoding.
- The derived address MUST be nonzero and outside virtual, active-precompile, and TIP-20 namespaces.
- Configuration updates MUST replace the current threshold and owners without changing the account address.

## Authorization

Owners approve:

```text
configurable_account_digest = keccak256(
  "tempo:configurable-account" ||
  inner_digest ||
  account
)
```

The account binding prevents an owner signature from being reused for another configurable account or as an ordinary primitive signature.

Rules:

- `inner_digest` MUST be `tx.signature_hash()` when direct or the parent's configurable account digest when nested.
- Owner signatures MUST be owner-ordered, belong to the applicable configuration, and reach threshold on the final item.
- Validation MUST reject missing quorum, duplicate or unsorted owners, cycles, and any owner signature submitted after quorum is reached.
- Stateless recovery MUST validate the shape and return the claimed or derived account, without proving membership or quorum.
- Stateful validation MUST complete before the account is treated as an authorized sender.

## Transaction Execution

Bootstrap transactions establish account state before executing their call batch. Later transactions use the stored owner configuration and behave as ordinary Tempo transactions from that account.

### Bootstrap

A bootstrap transaction is the account's first transaction. It derives the account from its initial owner configuration and establishes that configuration before its calls execute, whether the quorum or a newly authorized access key signs the transaction.

The example assumes `alice_address < bob_address`, each owner has weight 1, and the threshold is 2.

```text
// Define the initial owner configuration.
// Ref: Types and Limits
init = configurable_account_init(
  salt = salt,
  threshold = 2,
  owners = [
    [alice_address, 1],
    [bob_address, 1]
  ]
)

// Ref: Account Identity formula
account = configurable_account_address

// Build the account's first transaction.
tx = transaction(
  from = account,
  calls = [...]
)

// Sign the account-bound transaction digest.
// Ref: Authorization formula
digest = configurable_account_digest(
  tx.signature_hash(),
  account
)

alice_signature = sign(alice_key, digest)
bob_signature   = sign(bob_key, digest)

// Attach the bootstrap signature containing the initial configuration.
tx.signature =
  0x05 || rlp([
    init,
    [alice_signature, bob_signature]
  ])
```

After validation, the account is available to the transaction's call batch. Initialization and nonce consumption are transaction-level effects rather than EVM call effects.

Rules:

- Bootstrap MUST target the account derived from `init`, without an existing account header.
- `init` MUST be carried by either the outer signature or `key_authorization.signature`.
- When `key_authorization.signature` carries `init`, the outer V2 account keychain signature MUST use the key authorized by that same key authorization.
- The target MUST have a zero protocol nonce.
- Prior 2D or expiring-nonce activity MUST NOT affect bootstrap eligibility.
- The target MAY already have a balance or storage.
- The target MUST have empty code and no EIP-7702 delegation.
- When bootstrap validation succeeds, the protocol MUST store the header and owner rows, emit `ConfigurableAccountInitialized(account)`, and consume the nonce. These effects MUST survive later call reverts.
- Failed bootstrap validation MUST write no account state and consume no nonce.
- `updateAccountConfig` MUST reject same-transaction bootstrap accounts.

### Initialized

An initialized transaction uses the account configuration at its block position to authorize the full call batch. Configuration updates are visible to later calls in the batch but do not change that transaction's authorization.

```text
// Build a transaction from the initialized account.
tx = transaction(
  from = account,
  calls = [...]
)

// Sign the account-bound transaction digest.
// Ref: Authorization formula
digest = configurable_account_digest(
  tx.signature_hash(),
  account
)

alice_signature = sign(alice_key, digest)
bob_signature   = sign(bob_key, digest)

// Attach the initialized signature containing the account address.
tx.signature =
  0x05 || rlp([
    account,
    [alice_signature, bob_signature]
  ])
```

Rules:

- Direct transactions MUST use initialized encoding and the block-position owner configuration.
- `tx.from`, `tx.origin`, and top-level `msg.sender` MUST equal the account; one authorization MUST cover the call batch.
- Configuration updates MUST be immediately visible to later calls in the batch. If the batch succeeds, its final update MUST be stored and affect authorization only for later transactions.
- Configurable accounts MUST have no EVM code or EIP-7702 delegation and MUST NOT be authorization-list authorities.
- Authorization lists MUST reject bare configurable account signatures.
- Sponsorship MUST use existing sponsored signing hashes.
- Pools MAY index by stateless recovery but MUST revalidate affected authorizations after owner updates.

## Storage

The configurable account precompile stores a compact header, ordered owner rows for enumeration, and direct owner-weight rows for authorization. `pad32` encodes an address or unsigned integer as a 32-byte, big-endian, left-zero-padded value. `mapping_slot` returns the Keccak-256 result as a big-endian `uint256`.

```text
mapping_slot(key, base) = keccak256(pad32(key) || pad32(base))

header_slot(account) = mapping_slot(account, 0)
owner_root(account)   = mapping_slot(account, 1)
owner_slot(account, index) = mapping_slot(uint32(index), owner_root(account))
weight_root(account)  = mapping_slot(account, 2)
weight_slot(account, owner) = mapping_slot(owner, weight_root(account))

storage[header_slot(account)] =
  uint256(threshold) |
  uint256(owner_count) << 8

storage[owner_slot(account, index)] =
  uint256(uint160(owner)) |
  uint256(weight) << 160

storage[weight_slot(account, owner)] = uint256(weight)
```

Rules:

- The header, ordered owners, and direct weights MUST use mapping base slots `0`, `1`, and `2`, respectively, with the key order above.
- Unused bits in every stored word MUST be zero. A zero header word MUST mean uninitialized; a nonzero header with a zero threshold, zero owner count, nonzero unused bits, or out-of-range value MUST be rejected as `InvalidConfig`.
- Ordered owner rows and direct weight rows MUST encode the same configuration.
- Replacing a configuration MUST clear stale ordered rows and direct owner-weight rows.

## Configurable Account Precompile

A dedicated precompile exposes account derivation, detection, configuration reads, and owner updates:

```solidity
interface IConfigurableAccount {
    struct ConfigurableAccountOwner {
        address owner;
        uint8 weight;
    }

    struct AccountConfig {
        uint8 threshold;
        ConfigurableAccountOwner[] owners;
    }

    /// Derives an account address from an initial configuration.
    /// Reverts when the configuration or derived address is invalid.
    function deriveAccount(
        bytes32 salt,
        uint8 threshold,
        ConfigurableAccountOwner[] calldata owners
    ) external pure returns (address account);

    /// Returns whether account has a complete configurable account header.
    /// Reverts with InvalidConfig for a partial header.
    function isConfigurableAccount(address account) external view returns (bool);

    /// Returns account's current configuration.
    /// Reverts with NotConfigurableAccount or InvalidConfig.
    function getAccountConfig(
        address account
    ) external view returns (AccountConfig memory);

    /// Replaces msg.sender's current configuration.
    /// Requires direct current-quorum authorization and a top-level batch call.
    function updateAccountConfig(
        uint8 threshold,
        ConfigurableAccountOwner[] calldata owners
    ) external;

    event ConfigurableAccountInitialized(address indexed account);
    event AccountConfigUpdated(
        address indexed account,
        uint8 threshold,
        ConfigurableAccountOwner[] owners
    );

    error NotConfigurableAccount();
    error InvalidAccount();
    error InvalidConfig();
    error InvalidThreshold();
    error InvalidOwner();
    error InvalidWeight();
    error TooManyOwners();
    error DuplicateOwner();
    error InvalidOwnerOrder();
    error AccountAlreadyInitialized();
    error UnauthorizedCaller();
    error SameTransactionUpdateNotAllowed();
}
```

Configuration updates are authorized by the account's current owner quorum.

Rules:

- The precompile MUST be deployed at `0xAACC000000000000000000000000000000000000` from T11.
- `deriveAccount` MUST use the account identity formula above and enforce the same configuration and address constraints as bootstrap, without reading account state.
- `isConfigurableAccount` MUST revert with `InvalidConfig` for partial headers.
- `getAccountConfig` MUST return the configuration or revert with `NotConfigurableAccount` or `InvalidConfig`.
- `updateAccountConfig` MUST be a direct top-level call with `msg.sender == tx.origin` and a zero keychain transaction key.
- It MUST require a complete header and valid configuration, and reject same-transaction bootstrap accounts.
- `updateAccountConfig` MUST NOT accept a separate authorization signature.
- Configuration validation MUST return the first applicable error in this order:
  - `InvalidOwner` for an empty owner list.
  - `TooManyOwners` above `MAX_CONFIGURABLE_ACCOUNT_OWNERS`.
  - `InvalidThreshold` when the threshold is zero or above `MAX_CONFIGURABLE_ACCOUNT_THRESHOLD`.
  - For each owner: `InvalidOwner` for the zero address, `InvalidWeight` for zero weight, `DuplicateOwner` when equal to the previous owner, or `InvalidOwnerOrder` when below it.
  - `InvalidWeight` when the weight sum overflows or exceeds `u8::MAX`.
  - `InvalidThreshold` when the threshold exceeds the weight sum.
- Address derivation and bootstrap MUST return `InvalidAccount` when the derived address is zero or reserved. Bootstrap MUST return `AccountAlreadyInitialized` when the account already has a complete header.
- `getAccountConfig` and `updateAccountConfig` MUST return `NotConfigurableAccount` for an absent header. `isConfigurableAccount`, `getAccountConfig`, and `updateAccountConfig` MUST return `InvalidConfig` for a partial header.
- `updateAccountConfig` MUST return `UnauthorizedCaller` for an indirect call, `msg.sender != tx.origin`, or a nonzero keychain transaction key, and `SameTransactionUpdateNotAllowed` for an account bootstrapped in the same transaction.

## Account Keychain

The account keychain recognizes configurable accounts as an additional signature type:

```solidity
enum SignatureType {
    Secp256k1, // 0
    P256,      // 1
    WebAuthn,  // 2
    ConfigurableAccount // 3
}
```

Rules:

- `SignatureType` MUST add `ConfigurableAccount = 3` without renumbering existing variants.
- Before T11, transaction key authorizations and every account keychain entrypoint MUST reject `ConfigurableAccount`; no type-3 key record may be stored.
- Primitive-signature positions MUST accept initialized configurable signatures, but not recursive keychain signatures.

### Access Keys on Configurable Accounts

A configurable account can delegate ordinary or admin authority through the existing account keychain. The owner quorum can register an access key directly or through transaction-level key authorization, including while bootstrapping the account.

The owner quorum authorizes a key authorization with this digest:

```text
configurable_account_digest(key_authorization.signature_hash(), account)
```

#### Signed Key Authorization Encoding

At T11, `SignedKeyAuthorization.signature` expands from `PrimitiveSignature` to `TempoSignature`. Its position and RLP representation remain unchanged, so existing primitive key authorizations remain byte-identical.

```text
signed_key_authorization = rlp([
  key_authorization,
  bytes(tempo_signature)
])
```

The second field is one RLP byte string containing the existing byte encoding of a `TempoSignature`.

Rules:

- Before T11, the field MUST decode as `PrimitiveSignature`; configurable and keychain signatures MUST be rejected.
- At and after T11, the field MUST decode as `TempoSignature::Primitive` or `TempoSignature::ConfigurableAccount`; `TempoSignature::Keychain` MUST be rejected.
- Configurable signatures MUST follow the bootstrap and initialized context rules above.
- Decoders MUST reject a list-valued signature field, malformed signature bytes, and trailing fields.

An initialized configurable account can also act as an admin access key and sign a key authorization for its parent account.

An active access key for a configurable account uses the existing V2 account keychain envelope:

```text
0x04 || user_address || access_key_signature
```

The access key signs the existing V2 account keychain digest, and the transaction executes as `user_address` under the key's stored restrictions.

Rules:

- Quorums MAY add ordinary or admin access keys directly or via `key_authorization`, including at bootstrap; existing key rules MUST apply.
- When a configurable account's owners authorize a key for that account, `key_authorization.account` MUST equal the configurable account address.
- The configurable signature MUST carry the same address in initialized encoding or derive it from `init` in bootstrap encoding, and MUST use the digest above.
- A bootstrap key authorization MAY carry `init`; validation MUST establish the account and register the key atomically.
- When the outer signature carries `init`, an accompanying key authorization signature MUST use initialized encoding and validate against that initial configuration.
- When an initialized configurable account registered as an admin access key authorizes a key for its parent, `key_authorization.account` MUST equal the parent address and `signature.account` MUST equal the admin access key address.
- The admin access key's owners MUST sign `configurable_account_digest(key_authorization.signature_hash(), signature.account)`.
- Authorize-and-use MAY occur in one transaction, including bootstrap, when the outer signature uses the new key.
- Access key transactions MUST use the V2 envelope, execute as the account under stored restrictions, and skip the parent quorum.

### Configurable Accounts as Access Keys

A configurable account can itself serve as an access key for any parent account, including another configurable account. Its owners authorize the V2 account keychain digest:

```text
keychain_digest = keccak256(0x04 || tx.signature_hash() || user_address)
configurable_account_digest(keychain_digest, key_id)
```

Validation first checks the access key account against its current owner configuration, then applies the parent account's stored access key restrictions.

Rules:

- Any account MAY register an initialized configurable account with `key_type = ConfigurableAccount` and its address as `key_id`.
- The inner signature MUST use initialized encoding without `init`; its owners MUST sign the digest above.
- Validation MUST use the access key's block-position configuration, then the parent's stored access key record.
- Configurable admin keys MAY perform TIP-1049 operations under the existing admin key rules.
- `verifyKeychain` and `verifyKeychainAdmin` MUST accept and statefully validate the initialized inner signature.
- Their calldata limits MUST accommodate the largest valid initialized configurable account signature; `recover` and `verify` MUST retain their existing limits.
- Stateless TIP-1020 `recover` and `verify` MUST reject bare configurable account signatures.
- Access keys MUST NOT call `updateAccountConfig` for their parent.
- Only a direct configurable account signature with a zero keychain transaction key MAY replace the parent's owner set.

## Gas

Configurable account authorization is charged as regular intrinsic gas. Costs scale with submitted owner signatures and nested account nodes:

```text
node(signature) =
    2,100
  + 2,100 * signature.signatures.len()
  + sum(full_primitive_cost(owner) or node(nested_owner))

full_primitive_cost(signature) =
  secp256k1: 3,000
  P256: 8,000
  WebAuthn: 8,000 + calldata_gas(webauthn_data)

direct_configurable_account_surcharge = node(outer_signature) - 3,000
```

The direct subtraction accounts for the secp256k1 transaction signature already included in ordinary intrinsic gas. A 1-of-1 secp256k1 configurable account therefore adds 4,200 gas; a 2-of-2 adds 9,300 gas.

Bootstrap also creates one packed header, one ordered row per owner, and one direct weight row per owner.

Rules:

- Intrinsic gas MUST use the formulas above.
- The 3,000 gas subtraction MUST apply once at the outer account node, never to nested nodes or key authorization sidecars.
- Keychain wrapping MUST charge 3,000 gas plus `node(inner) - 3,000`; direct authorization MUST NOT add the 900 gas buffer.
- Bootstrap MUST charge active SSTORE gas for `1 + 2 * owners.len()` slots, plus a 1,500 gas event buffer.
- Precompile reads and updates MUST use the active precompile storage schedule.

## Observability

Bootstrap transaction signatures and owner-update events expose enough data to reconstruct an account's configuration history.

Rules:

- Reconstructing the initial configuration MUST decode `init` from the transaction's outer or key authorization bootstrap signature.
- A successful owner update MUST emit `AccountConfigUpdated(account, threshold, owners)`.
- Account keychain paths MUST retain their existing events.

## Examples

These examples illustrate the canonical signing flows and main authorization edge cases. They add no normative rules.

### Bootstrap

A bootstrap transaction creates the configurable account and stores its initial owner configuration. Its signature carries `init` because no configuration exists in state yet.

```text
// Define and derive the account with alice_address < bob_address.
// Ref: Types and Limits
init = configurable_account_init(
  salt = salt,
  threshold = 2,
  owners = [
    [alice_address, 1],
    [bob_address, 1]
  ]
)

// Ref: Account Identity formula
account = configurable_account_address

// Build the account's first transaction.
tx = transaction(
  from = account,
  calls = [...]
)

// Authorize the transaction with the initial owners.
// Ref: Authorization formula
digest = configurable_account_digest(
  tx.signature_hash(),
  account
)

tx.signature =
  0x05 || rlp([
    init,
    [
      sign(alice_key, digest),
      sign(bob_key, digest)
    ]
  ])

execute(tx)
```

### Initialized

An initialized transaction uses the configuration already stored for the account. Its signature carries the account address instead of `init` so validation can load the current owners and threshold.

```text
// Build a later transaction from the initialized account.
tx = transaction(
  from = account,
  calls = [...]
)

// Authorize the transaction with the current owners.
// Ref: Authorization formula
digest = configurable_account_digest(
  tx.signature_hash(),
  account
)

tx.signature =
  0x05 || rlp([
    account,
    [
      sign(alice_key, digest),
      sign(bob_key, digest)
    ]
  ])

execute(tx)
```

### Nested Ownership

Each configurable account wraps the digest approved by its parent. This example uses an initialized configurable account as an owner of another initialized configurable account.

```text
// Build a transaction from the parent configurable account.
tx = transaction(
  from = parent_account,
  calls = [...]
)

// Bind the transaction to the parent configurable account.
// Ref: Authorization formula
parent_digest = configurable_account_digest(
  tx.signature_hash(),
  parent_account
)

// Bind the parent digest to the child configurable account.
child_digest = configurable_account_digest(
  parent_digest,
  child_account
)

alice_signature = sign(alice_key, child_digest)
bob_signature   = sign(bob_key, child_digest)

// Use the child's configurable account signature as a parent owner signature.
child_owner_signature =
  0x05 || rlp([
    child_account,
    [alice_signature, bob_signature]
  ])

// Attach the parent's configurable account signature.
tx.signature =
  0x05 || rlp([
    parent_account,
    [child_owner_signature]
  ])

execute(tx)
```

### Fee Sponsorship

Fee sponsorship supports either signing order because the owners and fee payer sign separate digests.

Owners sign first:

```text
// Mark the transaction as sponsored before the owners sign.
tx = transaction(
  from = account,
  calls = [...],
  fee_payer_signature = placeholder
)

// Sign the sender digest, which omits the fee token.
// Ref: Authorization formula
digest = configurable_account_digest(
  tx.signature_hash(),
  account
)

alice_signature = sign(alice_key, digest)
bob_signature   = sign(bob_key, digest)

tx.signature =
  0x05 || rlp([
    account,
    [alice_signature, bob_signature]
  ])

// Select the fee token and authorize payment for this account.
tx.fee_token = fee_token
fee_payer_digest = tx.fee_payer_signature_hash(account)
tx.fee_payer_signature = sign_secp256k1(fee_payer_key, fee_payer_digest)
```

The fee payer signs first:

```text
// Build the transaction with the fee token selected.
tx = transaction(
  from = account,
  calls = [...],
  fee_token = fee_token
)

// Authorize payment for the known configurable account.
fee_payer_digest = tx.fee_payer_signature_hash(account)
tx.fee_payer_signature = sign_secp256k1(fee_payer_key, fee_payer_digest)

// Sign the sender digest after sponsorship is attached.
// Ref: Authorization formula
digest = configurable_account_digest(
  tx.signature_hash(),
  account
)

alice_signature = sign(alice_key, digest)
bob_signature   = sign(bob_key, digest)

tx.signature =
  0x05 || rlp([
    account,
    [alice_signature, bob_signature]
  ])
```

### Weighted Quorum

Owner weights determine which ordered owner signatures satisfy the threshold and when an extra signature is invalid.

Assume `alice_address < bob_address < carol_address`:

```text
threshold = 3
owners = [
  [alice_address, 2],
  [bob_address, 1],
  [carol_address, 1]
]

// Ref: Authorization formula
digest = configurable_account_digest(tx.signature_hash(), account)

alice_signature = sign(alice_key, digest)
bob_signature   = sign(bob_key, digest)
carol_signature = sign(carol_key, digest)

// Valid: the final owner signature reaches the threshold.
signatures = [alice_signature, bob_signature]
signatures = [alice_signature, carol_signature]

// Invalid: the total weight is below the threshold.
signatures = [bob_signature, carol_signature]

// Invalid: Bob reaches the threshold before Carol's extra owner signature.
signatures = [alice_signature, bob_signature, carol_signature]
```

### Bootstrap and Immediate Access Key Use

This transaction initializes the configurable account and registers an access key at the same time. The owner quorum signs the key authorization, and the new access key signs the transaction.

```text
// Define and derive the new configurable account.
// Ref: Types and Limits
init = configurable_account_init(
  salt = salt,
  threshold = 2,
  owners = [
    [alice_address, 1],
    [bob_address, 1]
  ]
)

// Ref: Account Identity formula
account = configurable_account_address

// Authorize a primitive access key for the new account.
// Ref: Access Keys on Configurable Accounts
key_authorization = KeyAuthorization(
  chain_id = chain_id,
  account = account,
  key_type = Secp256k1,
  key_id = access_key_address
)

// Ref: Authorization formula
key_authorization_digest = configurable_account_digest(
  key_authorization.signature_hash(),
  account
)

key_authorization_signature =
  0x05 || rlp([
    init,
    [
      sign(alice_key, key_authorization_digest),
      sign(bob_key, key_authorization_digest)
    ]
  ])

// Include the signed key authorization in the bootstrap transaction.
tx = transaction(
  from = account,
  calls = [...],
  // Ref: Signed Key Authorization Encoding
  key_authorization = signed_key_authorization(
    key_authorization,
    key_authorization_signature
  )
)

// Authorize this transaction with the access key being registered.
// Ref: Account Keychain formula
access_key_digest = keccak256(
  0x04 ||
  tx.signature_hash() ||
  account
)

tx.signature =
  0x04 || account || sign(access_key, access_key_digest)

execute(tx)
```

Validation establishes the account from `init`, registers the access key, and executes the same transaction as `account` under the key's restrictions.

### Bootstrap and Subsequent Access Key Use

The owner quorum can initialize the account and register an access key without using that key for the bootstrap transaction. The access key can then authorize a subsequent transaction.

```text
// Define and derive the new configurable account.
// Ref: Types and Limits
init = configurable_account_init(
  salt = salt,
  threshold = 2,
  owners = [
    [alice_address, 1],
    [bob_address, 1]
  ]
)

// Ref: Account Identity formula
account = configurable_account_address

// Authorize a primitive access key for the new account.
// Ref: Access Keys on Configurable Accounts
key_authorization = KeyAuthorization(
  chain_id = chain_id,
  account = account,
  key_type = Secp256k1,
  key_id = access_key_address
)

// Ref: Authorization formula
key_authorization_digest = configurable_account_digest(
  key_authorization.signature_hash(),
  account
)

// The outer bootstrap supplies init, so this signature uses initialized encoding.
key_authorization_signature =
  0x05 || rlp([
    account,
    [
      sign(alice_key, key_authorization_digest),
      sign(bob_key, key_authorization_digest)
    ]
  ])

bootstrap_tx = transaction(
  from = account,
  calls = [...],
  // Ref: Signed Key Authorization Encoding
  key_authorization = signed_key_authorization(
    key_authorization,
    key_authorization_signature
  )
)

// The owner quorum authorizes the bootstrap transaction.
// Ref: Authorization formula
bootstrap_digest = configurable_account_digest(
  bootstrap_tx.signature_hash(),
  account
)

bootstrap_tx.signature =
  0x05 || rlp([
    init,
    [
      sign(alice_key, bootstrap_digest),
      sign(bob_key, bootstrap_digest)
    ]
  ])

execute(bootstrap_tx)

// The registered access key authorizes a later transaction.
access_key_tx = transaction(
  from = account,
  calls = [...]
)

// Ref: Account Keychain formula
access_key_digest = keccak256(
  0x04 ||
  access_key_tx.signature_hash() ||
  account
)

access_key_tx.signature =
  0x04 || account || sign(access_key, access_key_digest)

execute(access_key_tx)
```

The bootstrap transaction initializes the account and registers the access key. The later transaction executes as `account` under the key's stored restrictions without another owner quorum.

### Configuration Rotation

The current quorum authorizes an owner update, which preserves the account address and applies to later transactions.

Assume Alice and Bob are both required. They replace themselves with Carol:

```text
// Build a top-level owner update.
rotate_tx = transaction(
  from = account,
  calls = [
    // Ref: Configurable Account Precompile
    updateAccountConfig(
      threshold = 1,
      owners = [[carol_address, 1]]
    )
  ]
)

// The current owners authorize the update.
// Ref: Authorization formula
rotate_digest = configurable_account_digest(
  rotate_tx.signature_hash(),
  account
)

rotate_tx.signature =
  0x05 || rlp([
    account,
    [
      sign(alice_key, rotate_digest),
      sign(bob_key, rotate_digest)
    ]
  ])

execute(rotate_tx)

// The next transaction uses Carol and keeps the same account address.
next_tx = transaction(
  from = account,
  calls = [...]
)

// Ref: Authorization formula
next_digest = configurable_account_digest(
  next_tx.signature_hash(),
  account
)

next_tx.signature =
  0x05 || rlp([
    account,
    [sign(carol_key, next_digest)]
  ])
```

## Backwards Compatibility

This change is additive for transactions and accounts that do not use configurable account signatures. Applications can move assets or state from an existing EOA through their current flows.

Rules:

- This TIP MUST NOT add or reorder transaction fields.
- Transactions that do not use configurable accounts MUST remain byte-identical.
- An address with a nonzero protocol nonce, EVM code, or EIP-7702 delegation MUST NOT bootstrap.

## Invariants

Rules:

- **Stable identity.** Bootstrap MUST satisfy `tx.from == configurable_account_address`. `updateAccountConfig` MUST NOT change the address.
- **Bootstrap exclusivity.** Before a header exists, the transaction MUST carry exactly one bootstrap signature as its outer or key authorization signature. After a header exists, bootstrap signatures MUST be rejected.
- **Bootstrap eligibility.** Bootstrap MUST require no header, a zero protocol nonce, empty code, and no delegation. Balance and storage MUST NOT block it.
- **Configuration validity.** Configurations MUST satisfy all owner, weight, total-weight, and threshold limits above.
- **Marker consistency.** An initialized header MUST have complete matching ordered owner and direct weight rows.
- **No account code.** A configurable account MUST NOT have EVM bytecode or EIP-7702 delegation code.
- **Owner signatures.** Owner signatures MUST be primitive over the current digest or nested with the parent digest as `inner_digest`, without `init`. Keychain signatures MUST NOT be accepted as owner signatures.
- **Owner set membership.** Recovered or nested owner addresses MUST be sorted and belong to the applicable owner configuration.
- **Threshold enforcement.** Current owner weights MUST reach threshold on the final owner signature, not before.
- **Current-state authorization.** Initialized authorization MUST use the owner configuration at its block position.
- **Configurable account sender.** Direct quorum transactions MUST use `TempoSignature::ConfigurableAccount` as the outer signature.
- **Signature contexts.** Configurable signatures MUST be outer, nested owner, account keychain inner, or key authorization signatures. Bootstrap MUST be outer or authorize the outer access key.
- **Key authorization.** Transactions MAY carry `key_authorization`, including at bootstrap. Its signature MUST use bootstrap encoding only when it supplies `init`; otherwise it MUST use initialized encoding.
- **Account keychain access.** Configurable accounts MAY own or act as access keys. Access keys MUST NOT replace parent owner sets.
- **Fee payer.** `fee_payer_signature` MUST be secp256k1. Its recovered fee payer MUST NOT be a configurable account.
- **Config update frame.** `updateAccountConfig` MUST run in a protocol-created top-level frame for one `tx.calls` entry.
- **Config update identity.** `updateAccountConfig` MUST require a complete configurable account header for `msg.sender`.
- **Config update bootstrap exclusion.** `updateAccountConfig` MUST reject accounts initialized earlier in the same transaction.
- **No config update signature.** `updateAccountConfig` MUST NOT accept an additional signature parameter.
