Skip to content

Multisig

← Back to Transactions

This example demonstrates how to create and use a 2-of-3 multisig account. Key concepts:

  • Creating a MultisigAccount with version, threshold, and addresses
  • Deriving the multisig address from the participant addresses
  • Signing transactions with a subset of participants (2 of 3)
  • Demonstrating that insufficient signatures (1 of 3) will fail Prerequisites:
  • LocalNet running (via algokit localnet start)

From the repository’s examples directory:

Terminal window
cd examples
uv run python transact/10_multisig.py

View source on GitHub

10_multisig.py
# ruff: noqa: N999, C901, PLR0912, PLR0915, PLR2004
"""
Example: Multisig
This example demonstrates how to create and use a 2-of-3 multisig account.
Key concepts:
- Creating a MultisigAccount with version, threshold, and addresses
- Deriving the multisig address from the participant addresses
- Signing transactions with a subset of participants (2 of 3)
- Demonstrating that insufficient signatures (1 of 3) will fail
Prerequisites:
- LocalNet running (via `algokit localnet start`)
"""
from shared import (
create_algod_client,
format_algo,
get_account_balance,
print_error,
print_header,
print_info,
print_step,
print_success,
shorten_address,
wait_for_confirmation,
)
from algokit_transact import (
MultisigAccount,
MultisigMetadata,
PaymentTransactionFields,
Transaction,
TransactionType,
assign_fee,
)
from algokit_utils import AlgorandClient
def main() -> None:
print_header("Multisig Example (2-of-3)")
# Step 1: Initialize clients
print_step(1, "Initialize Algod Client")
algod = create_algod_client()
algorand = AlgorandClient.default_localnet()
try:
algod.status()
print_info("Connected to LocalNet Algod")
except Exception as e:
print_error(f"Failed to connect to LocalNet: {e}")
print_info("Make sure LocalNet is running (e.g., algokit localnet start)")
return
# Step 2: Create 3 individual accounts
print_step(2, "Create 3 Individual Accounts")
account1 = algorand.account.random()
account2 = algorand.account.random()
account3 = algorand.account.random()
print_info(f"Account 1: {shorten_address(account1.addr)}")
print_info(f"Account 2: {shorten_address(account2.addr)}")
print_info(f"Account 3: {shorten_address(account3.addr)}")
# Step 3: Create MultisigAccount with version=1, threshold=2, and all 3 addresses
print_step(3, "Create MultisigAccount (2-of-3)")
# The multisig parameters:
# - version: 1 (standard multisig version)
# - threshold: 2 (minimum signatures required)
# - addrs: list of participant addresses (order matters!)
# - signers: list of AddressWithSigners objects that can sign
multisig_addrs = [account1.addr, account2.addr, account3.addr]
multisig_params = MultisigMetadata(
version=1,
threshold=2,
addrs=multisig_addrs,
)
# Create the MultisigAccount with 2 sub-signers (accounts 1 and 2)
# These are the accounts that will provide signatures
multisig_with_2_signers = MultisigAccount(
params=multisig_params,
sub_signers=[account1, account2],
)
print_info("Multisig version: 1")
print_info("Multisig threshold: 2")
print_info(f"Number of participants: {len(multisig_addrs)}")
# Step 4: Show the derived multisig address
print_step(4, "Show Derived Multisig Address")
# The multisig address is deterministically derived from:
# Hash("MultisigAddr" || version || threshold || pk1 || pk2 || pk3)
multisig_address = multisig_with_2_signers.address
print_info(f"Multisig address: {multisig_address}")
print_info("")
print_info("The multisig address is derived by hashing:")
print_info(' "MultisigAddr" prefix + version + threshold + all public keys')
print_info(" Order of public keys matters - different order = different address!")
# Step 5: Fund the multisig address
print_step(5, "Fund the Multisig Address")
dispenser = algorand.account.localnet_dispenser()
funding_amount = 5_000_000 # 5 ALGO
suggested_params = algod.suggested_params()
fund_tx_without_fee = Transaction(
transaction_type=TransactionType.Payment,
sender=dispenser.addr,
first_valid=suggested_params.first_valid,
last_valid=suggested_params.last_valid,
genesis_hash=suggested_params.genesis_hash,
genesis_id=suggested_params.genesis_id,
payment=PaymentTransactionFields(
receiver=multisig_address,
amount=funding_amount,
),
)
fund_tx = assign_fee(
fund_tx_without_fee,
fee_per_byte=suggested_params.fee,
min_fee=suggested_params.min_fee,
)
signed_fund_tx = dispenser.signer([fund_tx], [0])
algod.send_raw_transaction(signed_fund_tx[0])
wait_for_confirmation(algod, fund_tx.tx_id())
multisig_balance = get_account_balance(algorand, multisig_address)
print_info(f"Funded multisig with {format_algo(funding_amount)}")
print_info(f"Multisig balance: {format_algo(multisig_balance)}")
# Step 6: Create a payment transaction from the multisig
print_step(6, "Create Payment Transaction from Multisig")
receiver = algorand.account.random()
payment_amount = 1_000_000 # 1 ALGO
pay_params = algod.suggested_params()
payment_tx_without_fee = Transaction(
transaction_type=TransactionType.Payment,
sender=multisig_address, # The sender is the multisig address
first_valid=pay_params.first_valid,
last_valid=pay_params.last_valid,
genesis_hash=pay_params.genesis_hash,
genesis_id=pay_params.genesis_id,
payment=PaymentTransactionFields(
receiver=receiver.addr,
amount=payment_amount,
),
)
payment_tx = assign_fee(
payment_tx_without_fee,
fee_per_byte=pay_params.fee,
min_fee=pay_params.min_fee,
)
print_info(f"Payment amount: {format_algo(payment_amount)}")
print_info(f"Sender (multisig): {shorten_address(multisig_address)}")
print_info(f"Receiver: {shorten_address(receiver.addr)}")
print_info(f"Transaction ID: {payment_tx.tx_id()}")
# Step 7: Sign with 2 of the 3 accounts using MultisigAccount.signer
print_step(7, "Sign with 2 of 3 Accounts")
print_info("Signing with accounts 1 and 2 (meeting 2-of-3 threshold)...")
print_info("")
print_info("How multisig signing works:")
print_info(" 1. Each sub-signer signs the transaction individually")
print_info(" 2. Signatures are collected into a MultisigSignature structure")
print_info(" 3. The structure includes version, threshold, and all subsigs")
print_info(" 4. Subsigs contain public key + signature (or undefined if not signed)")
print_info("")
# The MultisigAccount.signer automatically collects signatures from all sub-signers
signed_txns = multisig_with_2_signers.signer([payment_tx], [0])
print_info(f"Signed transaction size: {len(signed_txns[0])} bytes")
print_success("Transaction signed by accounts 1 and 2!")
# Step 8: Submit and verify the transaction succeeds
print_step(8, "Submit and Verify Transaction")
algod.send_raw_transaction(signed_txns[0])
print_info("Transaction submitted to network...")
pending_info = wait_for_confirmation(algod, payment_tx.tx_id())
print_info(f"Transaction confirmed in round: {pending_info.confirmed_round}")
# Verify balances
multisig_balance_after = get_account_balance(algorand, multisig_address)
try:
receiver_info = algorand.account.get_information(receiver.addr)
receiver_balance = receiver_info.amount.micro_algo
except Exception:
receiver_balance = 0
print_info(f"Multisig balance after: {format_algo(multisig_balance_after)}")
print_info(f"Receiver balance: {format_algo(receiver_balance)}")
if receiver_balance == payment_amount:
print_success("Receiver received the payment!")
# Step 9: Demonstrate that 1 signature is insufficient
print_step(9, "Demonstrate Insufficient Signatures (1 of 3)")
print_info("Creating a MultisigAccount with only 1 sub-signer (account 3)...")
print_info("")
# Create a MultisigAccount with only 1 signer - below the threshold
multisig_with_1_signer = MultisigAccount(
params=multisig_params,
sub_signers=[account3],
)
# Create another payment transaction
insufficient_params = algod.suggested_params()
insufficient_tx_without_fee = Transaction(
transaction_type=TransactionType.Payment,
sender=multisig_address,
first_valid=insufficient_params.first_valid,
last_valid=insufficient_params.last_valid,
genesis_hash=insufficient_params.genesis_hash,
genesis_id=insufficient_params.genesis_id,
payment=PaymentTransactionFields(
receiver=receiver.addr,
amount=500_000, # 0.5 ALGO
),
)
insufficient_tx = assign_fee(
insufficient_tx_without_fee,
fee_per_byte=insufficient_params.fee,
min_fee=insufficient_params.min_fee,
)
print_info("Signing with only account 3 (not meeting 2-of-3 threshold)...")
# Sign with only 1 account
insufficient_signed_txns = multisig_with_1_signer.signer([insufficient_tx], [0])
# Try to submit - this should fail
try:
algod.send_raw_transaction(insufficient_signed_txns[0])
print_info("ERROR: Transaction should have been rejected!")
except Exception as error:
error_message = str(error)
print_info("Transaction rejected as expected!")
if "multisig" in error_message.lower():
print_info("Reason: Insufficient signatures for multisig")
else:
print_info(f"Reason: {error_message[:100]}...")
print_success("Demonstrated that 1 signature is insufficient for 2-of-3 multisig!")
# Summary
print_info("")
print_info("Summary - Multisig Key Points:")
print_info(" - MultisigAccount wraps multiple signers with a threshold")
print_info(" - version=1 is the standard multisig version")
print_info(" - threshold specifies minimum signatures required")
print_info(" - The multisig address is deterministically derived from params")
print_info(" - Order of addresses matters for address derivation")
print_info(" - Transactions require at least threshold signatures to succeed")
threshold = 2
num_addrs = len(multisig_addrs)
print_info(f" - This example used {threshold}-of-{num_addrs} multisig")
print_success("Multisig example completed!")
if __name__ == "__main__":
main()