# 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 config
derives a stable account address. Owners approve a multisig-specific digest, and their combined
weight must meet the configured threshold.

The first transaction bootstraps the account by carrying the initial config in the signature's
`init` field. Every later transaction omits `init` and relies on the config 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 when none is
provided, and rejects invalid thresholds, weights, salts, or owner lists.

### Sign the Bootstrap 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.

Set `init: true` when calling
[`SignatureEnvelope.from`](/tempo/reference/SignatureEnvelope/from). Ox derives `account` from
`initialConfig` and places the normalized config in `init`.

```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,
})

// 2. Build the bootstrap 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({
  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({
  initialConfig,
  payload,
  signatures: approvals,
})

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

The bootstrap config lives in the signature, not in `transaction.calls`. Submit this as the first
accepted transaction from the derived account.

### Sign a Later Transaction

Reuse the original `initialConfig` to derive the permanent account, even if the onchain owner
config has since been updated. Read the current config version before deriving the approval digest,
and omit `init` from every post-bootstrap signature.

```ts twoslash
import { AbiFunction, Address, Hex, RpcTransport, 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. Read the current config version from the native multisig precompile.
const getConfig = AbiFunction.from(
  'function getConfig(address account) view returns ((uint64 version, uint8 threshold, (address owner, uint8 weight)[] owners) config)',
)
const transport = RpcTransport.fromHttp('https://rpc.example.com')
const result = await transport.request({
  method: 'eth_call',
  params: [
    {
      data: AbiFunction.encodeData(getConfig, [account]),
      to: '0xaacc000000000000000000000000000000000000',
    },
  ],
})
const { version } = AbiFunction.decodeResult(getConfig, result)

// 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({
  initialConfig,
  payload,
  version, // [!code hl]
})

// 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({
  initialConfig,
  payload,
  signatures: approvals,
  version, // [!code hl]
})

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

Without `init`, the signature contains the derived account and ordered approvals only.

## Best Practices

### Persist the Initial Config

Store the normalized initial config with the account. It remains the source for deriving the
permanent account address after later config updates. Before signing, read the current version and
owner weights from the onchain config.

### 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 onchain config 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 version. Attach the signed authorization as `keyAuthorization` when provisioning the key;
later transactions omit it and use the account-bound keychain signature flow.

When provisioning the key during bootstrap, include `init` on either the transaction's multisig
signature or the authorization's multisig signature. Put `init` on the authorization signature when
the new access key signs and submits that first transaction.

### Include `init` Exactly Once

Use `init: true` only on the bootstrap transaction. A later transaction with `init`, or a first
transaction without it, cannot follow the intended initialization flow.

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