Your First Multisig
This tutorial introduces assorted K-of-N multi-signer authentication operations and supplements content from the following tutorials:
Try out the above tutorials (which include dependency installations) before moving on to multisig operations.
Step 1: Pick an SDK
This tutorial, a community contribution, was created for the Python SDK.
Other developers are invited to add support for the TypeScript SDK, Rust SDK, and Unity SDK!
Step 2: Start the example
Navigate to the Python SDK directory:
cd <aptos-core-parent-directory>/aptos-core/ecosystem/python/sdk/
Run the multisig.py
example:
poetry run python -m examples.multisig
This example uses the Aptos devnet, which has historically been reset each Thursday. Make sure devnet is live when you try running the example!
Step 3: Generate single signer accounts
First, we will generate single signer accounts for Alice, Bob, and Chad:
alice = Account.generate()
bob = Account.generate()
chad = Account.generate()
print("\n=== Account addresses ===")
print(f"Alice: {alice.address()}")
print(f"Bob: {bob.address()}")
print(f"Chad: {chad.address()}")
print("\n=== Authentication keys ===")
print(f"Alice: {alice.auth_key()}")
print(f"Bob: {bob.auth_key()}")
print(f"Chad: {chad.auth_key()}")
print("\n=== Public keys ===")
print(f"Alice: {alice.public_key()}")
print(f"Bob: {bob.public_key()}")
print(f"Chad: {chad.public_key()}")
Fresh accounts are generated for each example run, but the output should resemble:
=== Account addresses ===
Alice: 0x93c1b7298d53dd0d517f503f2d3188fc62f6812ab94a412a955720c976fecf96
Bob: 0x85eb913e58d0885f6a966d98c76e4d00714cf6627f8db5903e1cd38cc86d1ce0
Chad: 0x14cf8dc376878ac268f2efc7ba65a2ee0ac13ceb43338b6106dd88d8d23e087a
=== Authentication keys ===
Alice: 0x93c1b7298d53dd0d517f503f2d3188fc62f6812ab94a412a955720c976fecf96
Bob: 0x85eb913e58d0885f6a966d98c76e4d00714cf6627f8db5903e1cd38cc86d1ce0
Chad: 0x14cf8dc376878ac268f2efc7ba65a2ee0ac13ceb43338b6106dd88d8d23e087a
=== Public keys ===
Alice: 0x3f23f869632aaa4378f3d68560e08d18b1fc2e80f91d6f9d1b39d720b0ef7a3f
Bob: 0xcf21e85337a313bdac33d068960a3e52d22ce0e6190e9acc03a1c9930e1eaf3e
Chad: 0xa1a2aef8525eb20655387d3ed50b9a3ea1531ef6117f579d0da4bcf5a2e1f76d
For each user, note the account address and authentication key are identical, but the public key is different.
Step 4: Generate a multisig account
Next generate a K-of-N multi-signer public key and account address for a multisig account requiring two of the three signatures:
threshold = 2
multisig_public_key = MultiPublicKey(
[alice.public_key(), bob.public_key(), chad.public_key()], threshold
)
multisig_address = AccountAddress.from_multi_ed25519(multisig_public_key)
print("\n=== 2-of-3 Multisig account ===")
print(f"Account public key: {multisig_public_key}")
print(f"Account address: {multisig_address}")
The multisig account address depends on the public keys of the single signers. (Hence, it will be different for each example.) But the output should resemble:
=== 2-of-3 Multisig account ===
Account public key: 2-of-3 Multi-Ed25519 public key
Account address: 0x08cac3b7b7ce4fbc5b18bc039279d7854e4c898cbf82518ac2650b565ad4d364
Step 5: Fund all accounts
Next fund all accounts:
print("\n=== Funding accounts ===")
alice_start = 10_000_000
bob_start = 20_000_000
chad_start = 30_000_000
multisig_start = 40_000_000
alice_fund = faucet_client.fund_account(alice.address(), alice_start)
bob_fund = faucet_client.fund_account(bob.address(), bob_start)
chad_fund = faucet_client.fund_account(chad.address(), chad_start)
multisig_fund = faucet_client.fund_account(multisig_address, multisig_start)
await asyncio.gather(*[alice_fund, bob_fund, chad_fund, multisig_fund])
alice_balance = rest_client.account_balance(alice.address())
bob_balance = rest_client.account_balance(bob.address())
chad_balance = rest_client.account_balance(chad.address())
multisig_balance = rest_client.account_balance(multisig_address)
[alice_balance, bob_balance, chad_balance, multisig_balance] = await asyncio.gather(
*[alice_balance, bob_balance, chad_balance, multisig_balance]
)
print(f"Alice's balance: {alice_balance}")
print(f"Bob's balance: {bob_balance}")
print(f"Chad's balance: {chad_balance}")
print(f"Multisig balance: {multisig_balance}")
=== Funding accounts ===
Alice's balance: 10000000
Bob's balance: 20000000
Chad's balance: 30000000
Multisig balance: 40000000
Step 6: Send coins from the multisig
This transaction will send 100 octas from the multisig account to Chad's account. Since it is a two-of-three multisig account, signatures are required from only two individual signers.
Step 6.1: Gather individual signatures
First generate a raw transaction, signed by Alice and Bob, but not by Chad.
entry_function = EntryFunction.natural(
module="0x1::coin",
function="transfer",
ty_args=[TypeTag(StructTag.from_str("0x1::aptos_coin::AptosCoin"))],
args=[
TransactionArgument(chad.address(), Serializer.struct),
TransactionArgument(100, Serializer.u64),
],
)
chain_id = await rest_client.chain_id()
raw_transaction = RawTransaction(
sender=multisig_address,
sequence_number=0,
payload=TransactionPayload(entry_function),
max_gas_amount=rest_client.client_config.max_gas_amount,
gas_unit_price=rest_client.client_config.gas_unit_price,
expiration_timestamps_secs=(
int(time.time()) + rest_client.client_config.expiration_ttl
),
chain_id=chain_id,
)
alice_signature = alice.sign(raw_transaction.keyed())
bob_signature = bob.sign(raw_transaction.keyed())
assert raw_transaction.verify(alice.public_key(), alice_signature)
assert raw_transaction.verify(bob.public_key(), bob_signature)
print("\n=== Individual signatures ===")
print(f"Alice: {alice_signature}")
print(f"Bob: {bob_signature}")
Again, signatures vary for each example run:
=== Individual signatures ===
Alice: 0x41b9dd65857df2d8d8fba251336357456cc9f17974de93292c13226f560102eac1e70ddc7809a98cd54ddee9b79853e8bf7d18cfef23458f23e3a335c3189e0d
Bob: 0x6305101f8f3ad5a75254a8fa74b0d9866756abbf359f9e4f2b54247917caf8c52798a36c5a81c77505ebc1dc9b80f2643e8fcc056bcc4f795e80b229fa41e509
Step 6.2: Submit the multisig transaction
Next generate a multisig authenticator and submit the transaction:
sig_map = [ # Map from signatory public key to signature.
(alice.public_key(), alice_signature),
(bob.public_key(), bob_signature),
]
multisig_signature = MultiSignature(multisig_public_key, sig_map)
authenticator = Authenticator(
MultiEd25519Authenticator(multisig_public_key, multisig_signature)
)
signed_transaction = SignedTransaction(raw_transaction, authenticator)
print("\n=== Submitting transfer transaction ===")
tx_hash = await rest_client.submit_bcs_transaction(signed_transaction)
await rest_client.wait_for_transaction(tx_hash)
print(f"Transaction hash: {tx_hash}")
=== Submitting transfer transaction ===
Transaction hash: 0x3ff2a848bf6145e6df3abb3ccb8b94fefd07ac16b4acb0c694fa7fa30b771f8c
Step 6.3: Check balances
Check the new account balances:
print("\n=== New account balances===")
alice_balance = rest_client.account_balance(alice.address())
bob_balance = rest_client.account_balance(bob.address())
chad_balance = rest_client.account_balance(chad.address())
multisig_balance = rest_client.account_balance(multisig_address)
[alice_balance, bob_balance, chad_balance, multisig_balance] = await asyncio.gather(
*[alice_balance, bob_balance, chad_balance, multisig_balance]
)
print(f"Alice's balance: {alice_balance}")
print(f"Bob's balance: {bob_balance}")
print(f"Chad's balance: {chad_balance}")
print(f"Multisig balance: {multisig_balance}")
=== New account balances===
Alice's balance: 10000000
Bob's balance: 20000000
Chad's balance: 30000100
Multisig balance: 39999300