Multisig
Description
Section titled “Description”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:
Prerequisites
Section titled “Prerequisites”- LocalNet running (via
algokit localnet start)
Run This Example
Section titled “Run This Example”From the repository’s examples directory:
cd examplesuv run python transact/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()