# Multisig Transactions

:::info
This guide is intended to be low-level. If you are looking for a high-level abstraction, check out
[Viem's Multisig Transactions guide](https://viem.sh/tempo/guides/multisig-transactions).
:::

## Overview

Ox models a native Tempo multisig with a weighted
[`MultisigConfig`](/tempo/reference/MultisigConfig) and a top-level
[`SignatureEnvelope`](/tempo/reference/SignatureEnvelope) of type `multisig`. The initial,
version-0 config derives a stable account address. Owners approve a multisig-specific digest, and
their combined weight must meet the configured threshold.

Every multisig signature carries the complete applicable config. A version-0 config authorizes the
counterfactual account while its onchain config commitment is zero. After an owner update, the
signature carries the current nonzero version whose commitment is stored by the network.

:::warning
Native multisig support is experimental. These examples describe Ox's current API. TIP-1061 is a
draft and its protocol shape may change, so confirm that your Ox and Tempo node versions agree
before producing signatures.
:::

[See the TIP-1061 draft](https://tips.sh/1061)

## Recipes

### Derive a Weighted Multisig Account

Normalize the initial config with
[`MultisigConfig.from`](/tempo/reference/MultisigConfig/from), then derive its permanent account
with [`MultisigConfig.getAddress`](/tempo/reference/MultisigConfig/getAddress).

```ts twoslash
import { Address, Hex, Secp256k1 } from 'ox'
import { MultisigConfig } from 'ox/tempo'

const ownerPrivateKeys = [
  Secp256k1.randomPrivateKey(),
  Secp256k1.randomPrivateKey(),
  Secp256k1.randomPrivateKey(),
]
const owners = ownerPrivateKeys.map((privateKey) => ({
  owner: Address.fromPublicKey(Secp256k1.getPublicKey({ privateKey })),
  weight: 1,
}))

// [!code focus:start]
const initialConfig = MultisigConfig.from({
  owners,
  salt: Hex.random(32),
  threshold: 2, // [!code hl]
})
const account = MultisigConfig.getAddress(initialConfig)
// @log: '0x...'
// [!code focus:end]
```

`MultisigConfig.from` sorts owners by ascending address, applies the zero salt and version when
omitted, and rejects invalid thresholds, weights, versions, salts, or owner lists.

### Sign an Initial Transaction

Build a nonempty transaction, derive its owner-approval digest with
[`MultisigConfig.getSignPayload`](/tempo/reference/MultisigConfig/getSignPayload), and collect
enough owner signatures to meet the threshold. Use
[`SignatureEnvelope.sortMultisigApprovals`](/tempo/reference/SignatureEnvelope/sortMultisigApprovals)
to put approvals in the order required by Tempo.

Pass the complete initial config to
[`SignatureEnvelope.from`](/tempo/reference/SignatureEnvelope/from). Ox derives `account` from a
version-0 config when you omit the account.

```ts twoslash
import { Address, Hex, Secp256k1 } from 'ox'
import { MultisigConfig, SignatureEnvelope, TxEnvelopeTempo } from 'ox/tempo'

// 1. Set up the owner keys and initial config.
const ownerPrivateKeys = [
  Secp256k1.randomPrivateKey(),
  Secp256k1.randomPrivateKey(),
  Secp256k1.randomPrivateKey(),
]
const initialConfig = MultisigConfig.from({
  owners: ownerPrivateKeys.map((privateKey) => ({
    owner: Address.fromPublicKey(Secp256k1.getPublicKey({ privateKey })),
    weight: 1,
  })),
  salt: Hex.random(32),
  threshold: 2,
})
const account = MultisigConfig.getAddress(initialConfig)

// 2. Build the initial transaction.
const transaction = TxEnvelopeTempo.from({
  calls: [
    {
      to: '0x0000000000000000000000000000000000000000',
    },
  ],
  chainId: 4217,
  nonce: 0n,
})
// [!code focus:start]
// 3. Derive the multisig approval payload.
const payload = TxEnvelopeTempo.getSignPayload(transaction)
const approvalPayload = MultisigConfig.getSignPayload({
  account,
  config: initialConfig,
  payload,
})

// 4. Collect sufficient approval weight and sort the approvals.
const approvals = ownerPrivateKeys.slice(0, 2).map((privateKey) =>
  SignatureEnvelope.from(
    Secp256k1.sign({
      payload: approvalPayload,
      privateKey,
    }),
  ),
)
const orderedApprovals = SignatureEnvelope.sortMultisigApprovals({
  account,
  config: initialConfig,
  payload,
  signatures: approvals,
})

// 5. Attach the complete config and serialize the transaction.
const bootstrapSignature = SignatureEnvelope.from({
  config: initialConfig, // [!code hl]
  signatures: orderedApprovals,
})
const serialized = TxEnvelopeTempo.serialize(transaction, {
  signature: bootstrapSignature,
})
// @log: '0x76...'
// [!code focus:end]
```

The version-0 config lives in the signature, not in `transaction.calls`. An initial transaction does
not persist multisig state, so the same config remains valid until an owner update stores a nonzero
config commitment.

### Sign a Later Transaction

Reuse the original `initialConfig` to derive the permanent account, even if the owners have changed.
Track the complete current config offchain because the network stores only its commitment. Use that
config for the approval digest and signature witness.

```ts twoslash
import { Address, Hex, Secp256k1 } from 'ox'
import { MultisigConfig, SignatureEnvelope, TxEnvelopeTempo } from 'ox/tempo'

// 1. Set up the owner keys and initial config.
const ownerPrivateKeys = [
  Secp256k1.randomPrivateKey(),
  Secp256k1.randomPrivateKey(),
  Secp256k1.randomPrivateKey(),
]
const initialConfig = MultisigConfig.from({
  owners: ownerPrivateKeys.map((privateKey) => ({
    owner: Address.fromPublicKey(Secp256k1.getPublicKey({ privateKey })),
    weight: 1,
  })),
  salt: Hex.random(32),
  threshold: 2,
})
const account = MultisigConfig.getAddress(initialConfig)

// 2. Load the complete config committed by the latest owner update.
const currentConfig = MultisigConfig.from({
  ...initialConfig,
  version: 1,
})

// 3. Build a later transaction.
const transaction = TxEnvelopeTempo.from({
  calls: [
    {
      to: '0xcafebabecafebabecafebabecafebabecafebabe',
      value: 1n,
    },
  ],
  chainId: 4217,
  nonce: 1n,
})
// [!code focus:start]
// 4. Derive the multisig approval payload with the current config version.
const payload = TxEnvelopeTempo.getSignPayload(transaction)
const approvalPayload = MultisigConfig.getSignPayload({
  account,
  config: currentConfig,
  payload,
})

// 5. Collect sufficient approval weight and sort the approvals.
const approvals = ownerPrivateKeys.slice(1, 3).map((privateKey) =>
  SignatureEnvelope.from(
    Secp256k1.sign({
      payload: approvalPayload,
      privateKey,
    }),
  ),
)
const orderedApprovals = SignatureEnvelope.sortMultisigApprovals({
  account,
  config: currentConfig,
  payload,
  signatures: approvals,
})

// 6. Attach the current config and approvals.
const signature = SignatureEnvelope.from({
  account,
  config: currentConfig, // [!code hl]
  signatures: orderedApprovals, // [!code hl]
})
const serialized = TxEnvelopeTempo.serialize(transaction, {
  signature,
})
// @log: '0x76...'
// [!code focus:end]
```

The network hashes `currentConfig` and checks that its commitment matches the value stored for
`account` before validating the approvals.

## Best Practices

### Persist Config Witnesses

Store the normalized initial config with the account because it remains the source for deriving the
permanent address. After each owner update, store or index the complete new config. The precompile
stores only the config commitment, not the owner list.

### Sort Both Config Owners and Approvals

Construct configs with `MultisigConfig.from` and order every approval set with
`sortMultisigApprovals`. These are separate ordering requirements.

### Check Approval Weight

Ox validates the config, but the network decides whether the supplied approvals meet the active
threshold. Count weights from the current config witness before collecting and broadcasting a
signature.

### Authorize Access Keys with Owner Approvals

A native multisig can provision an account-bound non-admin access key. Set the authorization's
`account` to the multisig address and `isAdmin` to `false`, then sign
`KeyAuthorization.getSignPayload` with a multisig envelope whose owner approvals use the current
config. Attach the signed authorization as `keyAuthorization` when provisioning the key; later
transactions omit it and use the account-bound keychain signature flow.

When both the outer transaction and the key authorization use multisig signatures, each signature
carries and validates its own complete applicable config witness.

### Use the Applicable Config

Use the version-0 config only while the account's stored commitment is zero. After an owner update,
use the complete config whose commitment is currently stored for the account.

## See More

<Cards>
  <Card icon="lucide:mail-open" title="Transaction Envelopes" description="Construct and sign Tempo transaction envelopes." to="/tempo/guides/transaction-envelopes" />

  <Card icon="lucide:signature" title="Signature Envelopes" description="Work with Tempo's primitive and stateful signature formats." to="/tempo/guides/signature-envelopes" />

  <Card icon="lucide:square-function" title="MultisigConfig" description="Review the complete experimental multisig API." to="/tempo/reference/MultisigConfig" />
</Cards>
