---
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: T12
---

# 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.

This TIP implements configurable accounts as counterfactual native multisig accounts. Protocol identifiers, signatures, and ABI names use the term `multisig`.

## 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.

## Assumptions

- T12 activates consensus acceptance and validation of multisig signatures, account-leaf configuration hashes, and the signed key authorization changes together.
- The existing Tempo transaction, key authorization, and sponsorship digests bind the fields documented by their respective TIPs. Multisig authorization inherits those digest guarantees and adds the account and configuration version bindings defined below.
- Only successful transactions directly authorized by the applicable owner quorum can modify an account's multisig configuration hash.
- Account keychain validation continues to enforce access-key expiry, scope, spending limits, and admin permissions. Multisig authorization does not weaken those restrictions.
- Cross-chain recovery is an asset-recovery mechanism controlled by the initial owner configuration. It does not synchronize later Tempo configuration updates or make the address a general-purpose multisig identity on other chains.

## Threat Model

- Transaction submitters are untrusted and may supply malformed, oversized, deeply nested, cyclic, duplicate, stale, or insufficient owner data. Decoding and validation limits MUST bound work before rejecting these transactions.
- Owners may be unavailable, compromised, or malicious. Only the current configuration's threshold is trusted; no individual owner receives authority beyond its configured weight.
- Nested multisig owners are untrusted authorization principals. Nesting depth, ordering, membership, cycle, and per-node threshold checks prevent them from bypassing the parent configuration.
- Access keys and fee payers are untrusted delegates, not owners. Access keys remain subject to account keychain restrictions and cannot update the owner configuration; fee sponsorship grants no account authority.
- Attackers may choose salts, owner sets, and primitive keys while searching for address collisions. Reserved namespace outputs are rejected. Finding an EOA key and multisig input with the same 160-bit address, with generic work of approximately 2^80, is outside this TIP's security scope.
- Initial owners may later be removed or compromised. They permanently retain baseline recovery authority on non-Tempo chains, so applications MUST NOT treat the recovery wallet as proof of the account's current Tempo owners or allow it to stand in for current Tempo governance.
- RPC simulators and transaction pools may use stale or incomplete configuration data. Their results are advisory; consensus validation MUST compare each witness with the commitment at the transaction's block position.

---

# Specification

## Types and Limits

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

/// Domain prefix for multisig account CREATE2 salt derivation.
pub const MULTISIG_ACCOUNT_DOMAIN: &[u8] = b"tempo:multisig:account";

/// Safe Singleton Factory used only to deploy the dedicated recovery factory.
pub const MULTISIG_RECOVERY_SINGLETON_FACTORY: Address =
    address!("914d7Fec6aaC8cd542e72Bca78B30650d45643d7");

/// Expected runtime-code hash of the Safe Singleton Factory.
pub const MULTISIG_RECOVERY_SINGLETON_FACTORY_RUNTIME_HASH: B256 =
    b256!("2fa86add0aed31f33a762c9d88e807c475bd51d0f52bd0955754b2608f7e4989");

/// Permissionless salt used to deploy the dedicated factory through the bootstrap factory.
pub const MULTISIG_RECOVERY_FACTORY_DEPLOYMENT_SALT: B256 = B256::ZERO;

/// Dedicated CREATE2 factory for recovery wallets on non-Tempo EVM chains.
pub const MULTISIG_RECOVERY_FACTORY: Address =
    address!("5B572547f14c002Aac05f2deb19a36fbf23F2f33");

/// Keccak-256 of the dedicated recovery factory creation code.
pub const MULTISIG_RECOVERY_FACTORY_INIT_CODE_HASH: B256 =
    b256!("32c2f9b9926477ca22da4308d7efa1b92a2c2db3f434a0afa77fc32bdecbe48b");

/// Keccak-256 of the dedicated recovery factory runtime code.
pub const MULTISIG_RECOVERY_FACTORY_RUNTIME_HASH: B256 =
    b256!("e74489073cf29a2000b643988575b615febec333a826512de4e1e8db10228a60");

/// Keccak-256 of the canonical recovery wallet creation code.
pub const MULTISIG_RECOVERY_WALLET_INIT_CODE_HASH: B256 =
    b256!("583cc63a2e37f645b43eac911b1a6d6de08b83abdc308c61364edda8cfc3bd37");

/// Domain prefix for multisig account owner signatures.
pub const MULTISIG_SIGNATURE_DOMAIN: &[u8] = b"tempo:multisig:signature";

/// Domain prefix for persisted multisig configuration commitments.
pub const MULTISIG_CONFIG_DOMAIN: &[u8] = b"tempo:multisig:config";

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

/// Maximum threshold for one account configuration.
pub const MAX_MULTISIG_THRESHOLD: u8 = u8::MAX;

/// Maximum number of owner signatures in one multisig account signature.
pub const MAX_MULTISIG_SIGNATURES: usize = 8;

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

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

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

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

/// Owner configuration carried by a multisig account signature.
pub struct MultisigConfig {
    /// Caller-chosen value that permits distinct accounts with otherwise identical configurations.
    pub salt: B256,

    /// Configuration version. Zero identifies the initial configuration.
    pub version: u64,

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

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

/// Signature payload for a multisig account.
pub struct MultisigSignature {
    /// Account authorized by the owner signatures.
    pub account: Address,

    /// Complete applicable configuration.
    pub config: MultisigConfig,

    /// Ordered primitive or nested multisig account owner signatures.
    pub signatures: Vec<TempoSignature>,

    /// Optional owner update applied after successful transaction execution.
    pub config_update: Option<MultisigConfigUpdate>,
}

/// A replacement owner policy. The salt is preserved and the version is incremented implicitly.
pub struct MultisigConfigUpdate {
    /// Minimum total owner weight required after the update.
    pub threshold: u8,

    /// Strictly ascending replacement owners.
    pub owners: Vec<MultisigOwner>,
}
```

An account may have up to 48 owners so a worst-case initial authorization, including complete validation of eight depth-2 nested-owner configuration witnesses and eight maximum-size WebAuthn signatures in each, fits within the transaction gas cap while leaving room for fresh nonce creation and transaction overhead. Each multisig account signature may contain at most 8 owner signatures, and primitive owner signatures have a byte limit. Nested multisig 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 48 unique, nonzero, address-sorted owners. Weights MUST be nonzero and total at most 255.
- A configuration MUST NOT include the multisig account itself as an owner.
- Threshold MUST be nonzero and reachable by at most `MAX_MULTISIG_SIGNATURES` owners. Equivalently, it MUST NOT exceed the sum of the eight highest owner weights.
- Each owner signature MUST be a `TempoSignature::Primitive` or `TempoSignature::Multisig`.
- Primitive owner signatures MUST NOT exceed `MAX_MULTISIG_OWNER_SIGNATURE_BYTES`; nested owner signatures are bounded recursively by the signature-count and nesting-depth limits.

## Signature Encoding

A type `0x05` multisig signature MUST be encoded as:

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

config = rlp([salt, version, threshold, owners])
owner  = rlp([owner, weight])
config_update = rlp([]) | rlp([threshold, owners])
```

Rules:

- Exactly 65 signature bytes MUST decode as secp256k1 regardless of the first byte. Type `0x05` MUST be considered only at other lengths.
- Transactions carrying type `0x05` MUST be rejected before T12. Generic wire-format decoders MAY parse the type without hardfork context, but consensus validation MUST NOT accept it before T12.
- `signatures` MUST contain 1 to `MAX_MULTISIG_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.
- An empty `config_update` list means no update. A nonempty list MUST contain exactly `threshold` and `owners`.
- When `config.version == 0`, `account` MUST be derived from `config.salt`, `config.threshold`, and `config.owners`, and its persisted configuration commitment MUST be zero.
- When `config.version > 0`, the stored commitment for `account` MUST be nonzero and equal the commitment of `config`.
- Initial and current signatures MAY appear as nested owner or key authorization signatures when their respective commitment-state rules hold.

## Account Identity

The initial owner configuration and salt establish an account identity that remains stable across later owner changes. First, hash that identity into a CREATE2 salt:

```text
account_salt = keccak256(
  "tempo:multisig:account" ||
  salt ||
  uint8(threshold) ||
  uint8(owners.len()) ||
  owners[0].owner || uint8(owners[0].weight) ||
  ...
)
```

Then derive the account with the canonical recovery factory and wallet creation code:

```text
multisig_address = address(keccak256(
  0xff ||
  MULTISIG_RECOVERY_FACTORY ||
  account_salt ||
  MULTISIG_RECOVERY_WALLET_INIT_CODE_HASH
)[12:32])
```

Owner updates commit to the complete new configuration:

```text
config_commitment = keccak256(
  "tempo:multisig:config" ||
  salt ||
  uint64(version) ||
  uint8(threshold) ||
  uint8(owners.len()) ||
  owners[0].owner || uint8(owners[0].weight) ||
  ...
)
```

Rules:

- `account_salt` MUST use the formula exactly: ASCII domain, raw 32-byte salt, one-byte integers, raw concatenation, and no chain ID, RLP, or ABI encoding.
- Account derivation MUST use the EIP-1014 CREATE2 formula exactly, with the canonical recovery factory, `account_salt`, and recovery wallet init-code hash above.
- The same version-0 configuration MUST derive the same account on every Tempo chain and every EVM chain with the canonical factory and recovery wallet.
- Configuration commitments MUST use the formula exactly: ASCII domain, raw 32-byte salt, eight-byte big-endian version, one-byte integers, raw concatenation, and no chain ID, RLP, or ABI encoding.
- The derived address MUST be nonzero and outside virtual, active-precompile, TIP-20, and ZonePortal namespaces.
- Configuration updates MUST preserve the salt and replace the current threshold and owners without changing the account address.
- Version `0` MUST identify only the initial derived configuration and MUST NOT be persisted. Updated configurations MUST use monotonically increasing nonzero versions.
- A configuration update whose commitment equals zero MUST be rejected; zero is reserved to mean that the account still uses its initial derived configuration.

## Authorization

Owners approve:

```text
multisig_digest = keccak256(
  "tempo:multisig:signature" ||
  inner_digest ||
  account ||
  uint64(config_version) ||
  next_config_hash
)
```

`next_config_hash` is zero when `config_update` is absent. Otherwise it is the configuration hash of a configuration that preserves the current salt, increments the current version by one, and uses the update's threshold and owners.

The account binding prevents an owner signature from being reused for another multisig account or as an ordinary primitive signature. The configuration version prevents an owner signature from becoming valid again after later owner updates.

Rules:

- The digest MUST encode `config_version` as an eight-byte, big-endian integer and `next_config_hash` as 32 raw bytes. Initial authorization MUST use version `0`; current authorization MUST use the version in the committed witness.
- `inner_digest` MUST be `tx.signature_hash()` when direct or the parent's multisig account digest when nested.
- Owner signatures MUST be owner-ordered, belong to the applicable configuration, and reach threshold on the final item. Every owner signature at a node signs the same digest, including that node's `next_config_hash`.
- 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, including version-0 account derivation, and return `account` without proving commitment equality, membership, or quorum.
- Stateful validation MUST compare the witness with the account's block-position commitment and complete quorum verification before the account is treated as authorized.

## Transaction Execution

Initial transactions authorize counterfactual accounts without establishing multisig state. Current transactions supply the committed owner configuration and otherwise behave as ordinary Tempo transactions from that account.

Rules:

- `tx.from`, `tx.origin`, and top-level `msg.sender` MUST equal the account; one authorization MUST cover the call batch.
- An initial transaction MAY repeat while the account leaf's `config_hash` remains zero and MUST NOT require a zero protocol nonce or write multisig configuration state unless it carries `config_update`. Ordinary nonce validation and consumption apply; balance and storage MUST NOT prevent authorization.
- Authorization lists MUST reject `TempoSignature::Multisig` entries statelessly.
- Primitive authorization-list signatures authenticate their authority directly and MUST NOT require a separate configuration lookup. Address collisions with derived multisig accounts are outside this TIP's security scope.
- Keychain-signed authorization-list entries MUST retain the existing T0 behavior: they are skipped, MUST NOT install delegation, and MUST NOT require a separate configuration lookup.
- Subblock transactions MUST NOT use multisig outer or key authorization signatures.
- Sponsorship MUST use existing sponsored signing hashes.
- Pools MUST revalidate transactions carrying an initial or current signature for an account, or naming it as `key_authorization.key_id`, whenever that account's commitment changes, including after reorgs.
- A successful transaction carrying `config_update` MUST replace the account leaf's `config_hash` after its calls complete.

## Account Leaf Storage

Tempo extends the state-trie account value with one optional `config_hash` field. Implementations MUST encode the account leaf as follows:

```text
unconfigured_account = rlp([nonce, balance, storage_root, code_hash])
configured_account   = rlp([nonce, balance, storage_root, code_hash, config_hash])
```

Rules:

- `config_hash == 0` MUST use the four-field legacy encoding. This preserves existing state roots and account-leaf encodings for accounts that do not use configurable accounts.
- A nonzero `config_hash` MUST be encoded as the fifth field and MUST equal the current configuration commitment.
- The field is part of the account leaf and therefore of the state root, account proofs, execution witnesses, snapshots, and block access lists.
- Implementations MUST retrieve `config_hash` with the account record. Validation MUST NOT perform a separate contract-storage or precompile-storage lookup.
- This TIP MUST NOT persist owner arrays or direct owner-weight rows.
- Zero means no configuration update has occurred. It does not prove that the address is not a counterfactual multisig account.
- Once nonzero, `config_hash` MUST never return to zero.

## Configuration Updates

A direct multisig signature MAY carry `config_update`. No precompile, contract call, or additional authorization signature is used. The current quorum binds the update through `next_config_hash` in `multisig_digest`.

Rules:

- Only the outer transaction signature MAY carry `config_update`. Nested owner signatures, key authorization signatures, keychain signatures, and fee-payer signatures MUST NOT update configuration.
- A transaction carrying `config_update` MUST use a direct `TempoSignature::Multisig` outer signature and a zero keychain transaction key.
- When `config.version == 0`, `config` MUST derive `account` and the account leaf's `config_hash` MUST be zero. Otherwise, the hash of `config` MUST match the nonzero account-leaf field at the transaction's block position.
- The next configuration MUST preserve `config.salt`, use `config.version + 1`, and use the threshold and owners carried by `config_update`.
- The next configuration MUST satisfy the same owner, weight, ordering, and threshold rules as any other configuration, and its hash MUST be nonzero.
- Configuration validation MUST report the first applicable error in this order: empty owners, too many owners, zero threshold, invalid owner, invalid weight, duplicate owner, invalid owner order, weight overflow, then unreachable threshold.
- The update MUST be applied only after all transaction calls complete successfully. A rejected or reverted transaction MUST leave `config_hash` unchanged.
- The update is atomic with the transaction's other state changes. Owners explicitly authorize both the transaction calls through `inner_digest` and the replacement configuration through `next_config_hash`.

## Cross-chain Recovery

The CREATE2 derivation lets the initial owners materialize a recovery wallet at the multisig account address on another EVM chain. Tempo does not deploy EVM bytecode at the account and does not depend on the recovery contracts for transaction validation.

The canonical recovery artifacts are `TempoMultisigRecoveryFactory` and `TempoMultisigRecoveryWallet` in `tips/verify/src/TempoMultisigRecovery.sol`. Their build MUST use Solidity 0.8.34, optimizer enabled with 200 runs, via-IR enabled, Paris EVM version, and no compiler metadata (`bytecode_hash = "none"`, `cbor_metadata = false`). Paris bytecode avoids requiring PUSH0, MCOPY, or another post-Paris opcode on a recovery chain. The build and hash check MUST use Foundry commit `423ac0d4080830fd2ec6ea52175b323a095973e9`. The resulting hashes MUST equal the factory and wallet constants above.

Rules:

- On a supported non-Tempo EVM chain, tooling MUST first verify that `MULTISIG_RECOVERY_SINGLETON_FACTORY` contains code with hash `MULTISIG_RECOVERY_SINGLETON_FACTORY_RUNTIME_HASH`. This is Safe's Singleton Factory; it is the deterministic deployer, not the recovery factory.
- The dedicated recovery factory MUST be deployed by sending `MULTISIG_RECOVERY_FACTORY_DEPLOYMENT_SALT || TempoMultisigRecoveryFactory.creationCode` as raw calldata to the Safe Singleton Factory. Its EIP-1014 address MUST equal `MULTISIG_RECOVERY_FACTORY`, and its runtime-code hash MUST equal `MULTISIG_RECOVERY_FACTORY_RUNTIME_HASH`. A missing singleton or dedicated factory, or any hash mismatch, means this TIP provides no recovery guarantee on that chain.
- On every Tempo chain, the T12 state transition MUST reserve `MULTISIG_RECOVERY_FACTORY` with the one-byte `0xEF` marker and a nonce of at least one before native multisig authorization is accepted. The transition MUST replace any code already present at that address while preserving its balance, storage, and any higher nonce. The canonical recovery-factory runtime MUST NOT be installed on Tempo. The Safe Singleton Factory MAY remain available for unrelated deployments.
- The factory MUST deploy the canonical wallet with `CREATE2`, using `account_salt`, and bind that salt to the wallet before the deployment transaction returns. The deployed wallet MUST equal `multisig_address`.
- The wallet MUST accept initialization only from the factory that created it and exactly once.
- Recovery MUST carry the initial configuration fields `(salt, threshold, owners)`; version is implicitly zero. The wallet MUST recompute `account_salt` and reject a configuration that does not match the salt bound at deployment.
- Recovery signatures MUST commit to the recovery domain, destination chain ID, wallet address, `account_salt`, wallet nonce, and the complete requested transfer set.
- The baseline wallet MUST accept only canonical secp256k1 signatures from direct initial owners. Recovery signatures MUST use the 65-byte `r || s || v` encoding with `v` equal to `27` or `28`; values `0` and `1` MUST be rejected rather than normalized. P256, WebAuthn, and nested multisig owners contribute no baseline recovery weight; configurations that cannot reach threshold without them intentionally have no baseline cross-chain recovery. Tooling MUST calculate and disclose whether direct secp256k1 owners can reach the threshold and MUST NOT present baseline recovery as available otherwise.
- Recovery MUST enforce the initial configuration's owner ordering, weights, threshold, 48-owner limit, eight-signature limit, low-`s` secp256k1 form, and no-signatures-after-quorum rule.
- Recovery execution MUST be limited to nonzero native-value transfers with empty calldata and calls carrying the standard ERC-20 `transfer`, ERC-721 `transferFrom` / `safeTransferFrom`, or ERC-1155 `safeTransferFrom` / `safeBatchTransferFrom` selectors. Token calls MUST target deployed code. Approvals and other selectors MUST be rejected.
- Every NFT call MUST be valid canonical ABI for its selector, MUST name the recovery wallet as `from`, and MUST reject mismatched ERC-1155 ID and amount array lengths. This prevents an initial quorum from using a third party's operator approval through the recovery wallet.
- ERC-20 `transfer` and the selector shared by ERC-20/ERC-721 `transferFrom` MAY return no data or exactly ABI-encoded `true`; a revert, malformed return, or `false` return MUST revert recovery.
- The deployed wallet MUST implement ERC-165 and the ERC-721 and ERC-1155 receiver callbacks so safe transfers and mints to the materialized recovery address succeed.
- Selector restriction bounds the wallet interface but cannot prove a target contract's semantics. In particular, a native-value transfer to a contract may invoke its `receive` or `fallback` logic. Cross-chain applications MUST NOT treat a call from the recovery wallet as current Tempo authorization, governance authority, or a synchronized cross-chain identity.
- Owner updates on Tempo MUST NOT change `account_salt`, `multisig_address`, or baseline recovery authority. Removed initial owners retain recovery authority, while replacement owners do not gain it; loss of the initial quorum makes baseline recovery unavailable.
- A stronger recovery system MAY prove the current Tempo commitment through a checkpoint, oracle, or light client, but that system is outside this TIP and MUST use a distinct authorization domain.

## Account Keychain

### Access Keys on Multisig Accounts

A multisig account can delegate ordinary or admin authority through the existing account keychain. At T12, the owner quorum registers access keys through transaction-level key authorization, including with an initial configuration witness; TIP-1099 disables the legacy direct authorization selectors before T12.

The owner quorum authorizes a key authorization with this digest:

```text
multisig_digest(key_authorization.signature_hash(), account, config_version, ZERO_HASH)
```

#### Signed Key Authorization Encoding

At T12, `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 T12, the field MUST decode as `PrimitiveSignature`; multisig and keychain signatures MUST be rejected.
- At and after T12, the field MUST decode as `TempoSignature::Primitive` or `TempoSignature::Multisig`; `TempoSignature::Keychain` MUST be rejected.
- Multisig signatures MUST follow the initial and current witness rules above.
- Decoders MUST reject a list-valued signature field, malformed signature bytes, and trailing fields.

An active access key for a multisig 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 via `key_authorization`, including during initial authorization; existing key rules MUST apply.
- An account with a nonzero multisig commitment MUST NOT be newly registered as an access key for another account.
- A multisig key authorization signature MUST set its account to `key_authorization.account`, carry the complete applicable configuration, and use the digest above.
- A multisig key authorization MUST complete stateful configuration and quorum validation and satisfy root, rather than delegated-admin, authorization.
- A version-0 key authorization MAY register the key without persisting multisig configuration state.
- When both the outer signature and `key_authorization.signature` are multisig signatures, each MUST independently carry and validate the applicable configuration witness.
- Authorize-and-use MAY occur in one transaction, including during initial authorization, 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.
- Access key transactions MUST reject a parent with a nonzero multisig commitment and EVM code or EIP-7702 delegation.
- Stateless TIP-1020 `recover` and `verify` MUST reject bare multisig account signatures.
- Access-key, nested-owner, and key-authorization signatures MUST encode `config_update` as absent.
- Only a direct multisig account signature with a zero keychain transaction key MAY replace the parent's owner set.

## Gas

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

| Component | Gas |
|---|---:|
| Nested-node account code/delegation check | 2,600 |
| `full_primitive_cost(secp256k1)` | 3,000 |
| `full_primitive_cost(P256)` | 8,000 |
| `full_primitive_cost(WebAuthn)` | 8,000 + `calldata_gas(webauthn_data)` |

Hashing uses the exact preimages defined in [Account Identity](#account-identity) and [Authorization](#authorization), charged with the standard EVM Keccak schedule. The preimage names below refer to those byte strings and do not introduce additional encoding.

```text
keccak_cost(byte_len) =
  30 + 6 * ceil(byte_len / 32)

account_derivation_hash_cost(config) =
  keccak_cost(account_salt_preimage(config).len())
  + keccak_cost(85)

config_commitment_hash_cost(config) =
  keccak_cost(config_commitment_preimage(config).len())

multisig_digest_hash_cost =
  keccak_cost(multisig_digest_preimage.len())

config_proof_hash_cost(config) =
  account_derivation_hash_cost(config)  if config.version == 0
  config_commitment_hash_cost(config)   otherwise
```

The recursive authorization charge is:

```text
witness_bytes(signature) = rlp(account) || rlp(config) || rlp(config_update)

node(signature) =
    calldata_gas(witness_bytes(signature))
  + config_proof_hash_cost(signature.config)
  + multisig_digest_hash_cost
  + sum(full_primitive_cost(owner) or 2,600 + node(nested_owner))
```

Role-specific surcharges are:

```text
direct_multisig_surcharge =
  node(outer_signature)
  - 3,000

key_authorization_multisig_surcharge =
  node(key_authorization.signature)

config_update_write_cost =
  20,000  if the prior config_hash is zero
   5,000  otherwise
```

The account's configuration hash is obtained from the account leaf already loaded for sender or owner validation, so there is no separate configuration-read charge. The witness calldata charge scales with complete configuration parsing and validation. There are no owner-row or owner-weight storage reads.

Each nested node adds one cold account access for checking that the nested multisig owner has no bytecode or EIP-7702 delegation. Implementations only need the account's code hash and MUST NOT load its bytecode for this check.

The direct subtraction accounts for the secp256k1 transaction signature already included in ordinary intrinsic gas.

When a signed key authorization uses a multisig signature, its existing signature-verification term uses `key_authorization_multisig_surcharge`. It is charged independently of the outer signature and receives no 3,000 gas subtraction.

Initial use writes no multisig state. Configuration updates charge the zero-to-nonzero or nonzero-to-nonzero account-metadata write cost above and never receive a clearing refund.

Rules:

- Intrinsic gas MUST use the formulas above.
- Non-subblock transactions MUST validate fee affordability before verifying multisig owner approvals.
- Recursive intrinsic gas MUST be fully computed and checked against `gas_limit` before cryptographic owner-signature verification, including for nested nodes and RPC simulation.
- A multisig key authorization MUST add `key_authorization_multisig_surcharge` independently of any outer multisig surcharge.
- The 3,000 gas subtraction MUST apply once at the outer account node, never to nested nodes or key authorization sidecars.
- Direct multisig authorization MUST NOT add the account keychain's 900 gas processing buffer.
- Every initial and current node MUST charge its exact witness bytes once. WebAuthn data gas MUST continue to be charged only by `full_primitive_cost`.
- Initial authorization without `config_update` MUST NOT charge any configuration write.
- A signature carrying `config_update` MUST add `config_commitment_hash_cost(next)` and `config_update_write_cost`.
- Account derivation performed by RPC or tooling MUST use the exact formula, but does not contribute execution gas unless validation requires it.

## Tooling

Wallets, SDKs, RPCs, and indexers need to expose multisig accounts across address derivation, signing, transaction decoding, and configuration history.

Rules:

- Tooling MUST support the T12 configuration witness and update encoding, signed key authorization shape, and current account-leaf configuration hash and version; address derivation MUST use the formula in [Account Identity](#account-identity).
- Recovery tooling MUST verify the Safe Singleton Factory runtime, dedicated factory runtime, and wallet init-code hash before presenting a chain as supported, and MUST report whether the initial configuration has a direct secp256k1 recovery quorum.

## Observability

Successful multisig transactions carrying configuration updates expose enough data to reconstruct an account's configuration history.

Rules:

- The initial configuration MUST be reconstructed from a version-0 multisig signature; the account address and zero commitment do not reveal it.
- Indexers MUST treat an update as final only when the carrying transaction succeeds and the resulting account leaf contains the expected `config_hash`.
- The initial configuration uses version `0`; the first successful update uses version `1`, and each later update increments it by one.
- 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.

`ZERO_HASH` below denotes 32 zero bytes and therefore no configuration update.

### Initial Configuration

An initial transaction authorizes the account without storing its initial owner configuration. Its signature carries the version-0 config because no configuration exists in state yet.

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

// Ref: Account Identity formula
account = multisig_address

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

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

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

execute(tx)
```

### Current Configuration

A current transaction uses the configuration commitment stored for the account. Its signature carries the account address and complete configuration so validation can check the current owners and threshold.

```text
// Build a transaction after an owner update.
tx = transaction(
  from = account,
  calls = [...]
)

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

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

execute(tx)
```

### Nested Ownership

Each multisig account wraps the digest approved by its parent. This example uses a current multisig account as an owner of another current multisig account.

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

// Bind the transaction to the parent multisig account.
// Ref: Authorization formula
parent_digest = multisig_digest(
  tx.signature_hash(),
  parent_account,
  parent_config.version,
  ZERO_HASH
)

// Bind the parent digest to the child multisig account.
child_digest = multisig_digest(
  parent_digest,
  child_account,
  child_config.version,
  ZERO_HASH
)

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

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

// Attach the parent's multisig account signature.
tx.signature =
  0x05 || rlp([
    parent_account,
    parent_config,
    [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 = multisig_digest(
  tx.signature_hash(),
  account,
  config.version,
  ZERO_HASH
)

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

tx.signature =
  0x05 || rlp([
    account,
    config,
    [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 multisig 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 = multisig_digest(
  tx.signature_hash(),
  account,
  config.version,
  ZERO_HASH
)

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

tx.signature =
  0x05 || rlp([
    account,
    config,
    [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 = multisig_digest(tx.signature_hash(), account, config.version, ZERO_HASH)

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]
```

### Initial Configuration and Immediate Access Key Use

This transaction uses the counterfactual 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 multisig account.
// Ref: Types and Limits
config = multisig_config(
  salt = salt,
  version = 0,
  threshold = 2,
  owners = [
    [alice_address, 1],
    [bob_address, 1]
  ]
)

// Ref: Account Identity formula
account = multisig_address

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

// Ref: Authorization formula
key_authorization_digest = multisig_digest(
  key_authorization.signature_hash(),
  account,
  0,
  ZERO_HASH
)

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

// Include the signed key authorization in the initial 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 derives the account from `config`, registers the access key, and executes the same transaction as `account` under the key's restrictions without persisting a multisig configuration commitment.

### Initial Configuration and Subsequent Access Key Use

The owner quorum can use the initial configuration to register an access key without using that key for the same transaction. The access key can then authorize a subsequent transaction.

```text
// Define and derive the new multisig account.
// Ref: Types and Limits
config = multisig_config(
  salt = salt,
  version = 0,
  threshold = 2,
  owners = [
    [alice_address, 1],
    [bob_address, 1]
  ]
)

// Ref: Account Identity formula
account = multisig_address

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

// Ref: Authorization formula
key_authorization_digest = multisig_digest(
  key_authorization.signature_hash(),
  account,
  0,
  ZERO_HASH
)

// The key authorization independently carries the initial witness.
key_authorization_signature =
  0x05 || rlp([
    account,
    config,
    [
      sign(alice_key, key_authorization_digest),
      sign(bob_key, key_authorization_digest)
    ],
    []
  ])

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

// The owner quorum independently carries the same initial witness.
// Ref: Authorization formula
initial_digest = multisig_digest(
  initial_tx.signature_hash(),
  account,
  0,
  ZERO_HASH
)

initial_tx.signature =
  0x05 || rlp([
    account,
    config,
    [
      sign(alice_key, initial_digest),
      sign(bob_key, initial_digest)
    ],
    []
  ])

execute(initial_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 initial transaction registers the access key without writing a multisig commitment. The later transaction executes as `account` under the key's stored restrictions without another owner quorum.

### Configuration Rotation

The applicable initial or current quorum authorizes an owner update, which preserves the account address and applies to later transactions. This example performs the first update with the initial version-0 witness.

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

```text
// Build the transaction and replacement owner policy.
rotate_tx = transaction(
  from = account,
  calls = [...]
)
config_update = multisig_config_update(
  threshold = 1,
  owners = [[carol_address, 1]]
)

next_config = multisig_config(
  salt = config.salt,
  version = 1,
  threshold = config_update.threshold,
  owners = config_update.owners
)
next_config_hash = config_commitment(next_config)

// The initial owners authorize both the calls and the first update.
// Ref: Authorization formula
rotate_digest = multisig_digest(
  rotate_tx.signature_hash(),
  account,
  0,
  next_config_hash
)

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

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 = multisig_digest(
  next_tx.signature_hash(),
  account,
  next_config.version,
  ZERO_HASH
)

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

## Backwards Compatibility

Primitive-signed fee payers, access keys, and authorization-list authorities remain stateless under the address-collision assumption, while keychain-signed authorization-list entries retain their existing skipped behavior. Generic `SignedKeyAuthorization` decoders also accept the new multisig signature form, while consensus rejects it before T12 and primitive encodings remain unchanged.

Rules:

- This TIP MUST NOT add or reorder transaction fields.
- Transactions that do not use multisig accounts MUST remain byte-identical.

## Invariants

Rules:

- **Stable identity.** Initial authorization MUST satisfy `tx.from == multisig_address`, where `multisig_address` is the canonical CREATE2 recovery address. A configuration update MUST NOT change the address.
- **Witness selection.** A zero commitment MUST accept only the derived initial witness; a nonzero commitment MUST accept only a matching current witness.
- **Configuration validity.** Configurations MUST satisfy all owner, weight, total-weight, and threshold limits above, and MUST NOT include their own account as an owner.
- **Single-field state.** The account leaf MUST contain at most one nonzero configuration hash and MUST NOT persist owner rows.
- **No account code.** A multisig 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`. Every nested multisig signature MUST carry and validate its own initial or current witness. 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.** Applicable owner weights MUST reach threshold on the final owner signature, not before.
- **Current-state authorization.** Current authorization MUST match the configuration commitment at its block position.
- **Multisig account sender.** Direct quorum transactions MUST use `TempoSignature::Multisig` as the outer signature.
- **Signature contexts.** Multisig signatures MUST be outer, nested owner, or key authorization signatures.
- **Key authorization.** Transactions MAY carry `key_authorization`. A multisig key authorization MUST independently carry an initial or current witness and match `key_authorization.account`.
- **Account keychain access.** Multisig accounts MAY own access keys. Multisig signatures MUST NOT authorize keychain access. Access keys MUST NOT replace parent owner sets.
- **Fee payer.** When `fee_payer_signature` is present, it MUST be secp256k1. Its recovered address is treated as a primitive identity and MUST NOT require a separate configuration lookup.
- **Config update identity.** A configuration update MUST require direct outer-quorum authorization for the account and a zero keychain transaction key.
- **Config update binding.** Every owner approval at the outer node MUST bind the same nonzero `next_config_hash`, and the update MUST be applied only after successful execution.
- **Recovery binding.** A canonical recovery wallet MUST deploy at `multisig_address` and accept only an initial configuration that re-derives its bound `account_salt`.
- **Recovery authority.** Baseline cross-chain recovery MUST remain bound to the initial owner configuration and MUST NOT be represented as current Tempo authorization after an owner update.
