A multisignature account, also called multisig, cannot initiate transactions on its own.
Instead, it relies on cosignatory accounts to create transactions and sign them on its behalf.
This tutorial shows how to convert a regular account into a multisig account that requires approval from one of two
cosignatories.
If the account is already multisig, the tutorial instead demonstrates how to remove the cosignatories and revert
the account to a regular account.
The multisignature structure used in this tutorial is shown below:
Multilevel multisignature accounts
More complex configurations, where a cosignatory is itself a multisig account, are also supported,
up to three levels deep.
Multisig accounts can be configured in any order.
However, once an account is converted into a multisig, it can no longer sign its own transactions and must rely
exclusively on the cosignatories configured at that point.
importjsonimportosimporttimeimporturllib.requestfromsymbolchain.CryptoTypesimportPrivateKeyfromsymbolchain.facade.SymbolFacadeimportSymbolFacadefromsymbolchain.scimportAmountfromsymbolchain.symbol.NetworkimportNetworkTimestampNODE_URL=os.environ.get('NODE_URL','https://reference.symboltest.net:3001')print(f'Using node {NODE_URL}')# Helper function to announce a transactiondefannounce_transaction(payload,label):print(f'Announcing {label} to /transactions')request=urllib.request.Request(f'{NODE_URL}/transactions',data=payload.encode(),headers={'Content-Type':'application/json'},method='PUT')withurllib.request.urlopen(request)asresponse:print(f' Response: {response.read().decode()}')# Helper function to wait for transaction confirmationdefwait_for_confirmation(transaction_hash,label):print(f'Waiting for {label} confirmation...')forattemptinrange(60):time.sleep(1)try:url=f'{NODE_URL}/transactionStatus/{transaction_hash}'withurllib.request.urlopen(url)asresponse:status=json.loads(response.read().decode())print(f' Transaction status: {status["group"]}')ifstatus['group']=='confirmed':print(f'{label} confirmed in {attempt} seconds')returnifstatus['group']=='failed':raiseException(f'{label} failed: {status["code"]}')excepturllib.error.HTTPError:print(' Transaction status: unknown')raiseException(f'{label} not confirmed after 60 seconds')# Returns the cosignatory addresses of the provided multisig account,# or an empty list if the account is not multisig or has never been useddefget_multisig_cosignatories(address):multisig_path=f'/account/{address}/multisig'print(f'Getting cosignatories from {multisig_path}')try:url=f'{NODE_URL}{multisig_path}'withurllib.request.urlopen(url)asresponse:status=json.loads(response.read().decode())cosignatories=status['multisig']['cosignatoryAddresses']print(f' Response: {cosignatories}')returncosignatoriesexcepturllib.error.HTTPError:# The address has never been usedprint(' Response: No cosignatories')return[]# Returns a transaction that turns a regular account into a multisigdefmultisig_enable_transaction():# Create an embedded multisig account modification transaction# that adds two cosignatoriesembedded_transaction=facade.transaction_factory.create_embedded({'type':'multisig_account_modification_transaction_v1',# This is the account that will be turned into a multisig'signer_public_key':multisig_key_pair.public_key,# Increment of the number of signatures required for approvals'min_approval_delta':1,# Increment of the number of signatures required for removals'min_removal_delta':1,'address_additions':cosignatory_addresses})# Build the aggregate transactionembedded_transactions=[embedded_transaction]transaction=facade.transaction_factory.create({'type':'aggregate_complete_transaction_v3',# This is the account that will pay for this transaction'signer_public_key':multisig_key_pair.public_key,'deadline':timestamp.add_hours(2).timestamp,'transactions_hash':facade.hash_embedded_transactions(embedded_transactions),'transactions':embedded_transactions})# Reserve space for two cosignatures (each is 104 bytes)# and calculate fee for the final transaction sizetransaction.fee=Amount(fee_mult*(transaction.size+104*len(cosignatory_key_pairs)))print('Enabling the multisig with the aggregate transaction:')print(json.dumps(transaction.to_json(),indent=2))# Sign the aggregate transaction with the multisig's signaturefacade.transaction_factory.attach_signature(transaction,facade.sign_transaction(multisig_key_pair,transaction))# Append signatures from all cosignatoriesforcosignatory_key_pairincosignatory_key_pairs:transaction.cosignatures.append(facade.cosign_transaction(cosignatory_key_pair,transaction))returntransaction# Returns a transaction that turns a multisig into a regular accountdefmultisig_disable_transaction():# Create two embedded multisig account modification transactions# because cosignatories must be removed one by oneembedded_transaction_1=facade.transaction_factory.create_embedded({'type':'multisig_account_modification_transaction_v1',# This is the multisig account that will be modified'signer_public_key':multisig_key_pair.public_key,# Keep required signatures unchanged for this step'min_approval_delta':0,'min_removal_delta':0,'address_deletions':[cosignatory_addresses[1]]})embedded_transaction_2=facade.transaction_factory.create_embedded({'type':'multisig_account_modification_transaction_v1',# This is the multisig account that will be modified'signer_public_key':multisig_key_pair.public_key,# Decrease required signatures after final removal'min_approval_delta':-1,'min_removal_delta':-1,'address_deletions':[cosignatory_addresses[0]]})# Build the aggregate transactionembedded_transactions=[embedded_transaction_1,embedded_transaction_2]transaction=facade.transaction_factory.create({'type':'aggregate_complete_transaction_v3',# This is the account that will pay for all transactions'signer_public_key':cosignatory_key_pairs[0].public_key,'deadline':timestamp.add_hours(2).timestamp,'transactions_hash':facade.hash_embedded_transactions(embedded_transactions),'transactions':embedded_transactions})# Calculate fee for the final transaction size# (No need to reserve space for cosignatures, as there are none)transaction.fee=Amount(fee_mult*transaction.size)print('Disabling the multisig with the aggregate transaction:')print(json.dumps(transaction.to_json(),indent=2))# Sign the aggregate transaction using the first cosigner's signaturefacade.transaction_factory.attach_signature(transaction,facade.sign_transaction(cosignatory_key_pairs[0],transaction))returntransactionfacade=SymbolFacade('testnet')KEY_TEMPLATE='0'*63+'{}'# Setup the keys for the multisig account and its two cosignatoriesMULTISIG_PRIVATE_KEY=os.getenv('MULTISIG_PRIVATE_KEY',KEY_TEMPLATE.format(1))multisig_key_pair=SymbolFacade.KeyPair(PrivateKey(MULTISIG_PRIVATE_KEY))multisig_address=facade.network.public_key_to_address(multisig_key_pair.public_key)print(f'Multisig address: {multisig_address} 'f'(public key {multisig_key_pair.public_key})')cosignatory_key_pairs=[]cosignatory_addresses=[]foriinrange(2):COSIGNATORY_PRIVATE_KEY=os.getenv(f'COSIGNATORY{i}_PRIVATE_KEY',KEY_TEMPLATE.format(i+2))kp=SymbolFacade.KeyPair(PrivateKey(COSIGNATORY_PRIVATE_KEY))cosignatory_key_pairs.append(kp)addr=facade.network.public_key_to_address(kp.public_key)cosignatory_addresses.append(addr)print(f'Cosignatory {i} address: {addr} (public key {kp.public_key})')try:# Fetch current network timetime_path='/node/time'print(f'Fetching current network time from {time_path}')withurllib.request.urlopen(f'{NODE_URL}{time_path}')asresponse:response_json=json.loads(response.read().decode())receive_timestamp=(response_json['communicationTimestamps']['receiveTimestamp'])timestamp=NetworkTimestamp(int(receive_timestamp))print(f' Network time: {timestamp.timestamp} ms since nemesis')# Fetch recommended feesfee_path='/network/fees/transaction'print(f'Fetching recommended fees from {fee_path}')withurllib.request.urlopen(f'{NODE_URL}{fee_path}')asresponse:response_json=json.loads(response.read().decode())median_mult=response_json['medianFeeMultiplier']minimum_mult=response_json['minFeeMultiplier']fee_mult=max(median_mult,minimum_mult)print(f' Fee multiplier: {fee_mult}')# Get current state of the multisig account and decide which# operation to performcosignatories=get_multisig_cosignatories(multisig_address)iflen(cosignatories)==0:# Enable the multisigtransaction=multisig_enable_transaction()# This operation must be signed by the multisig accountsigner_key_pair=multisig_key_pairelse:# Disable the multisigtransaction=multisig_disable_transaction()# This operation must be signed by one of the cosignerssigner_key_pair=cosignatory_key_pairs[0]json_payload=facade.transaction_factory.attach_signature(transaction,facade.sign_transaction(signer_key_pair,transaction))# Announce and wait for confirmationtransaction_hash=facade.hash_transaction(transaction)print(f'Built aggregate transaction with hash: {transaction_hash}')announce_transaction(json_payload,'aggregate transaction')wait_for_confirmation(transaction_hash,'aggregate transaction')exceptExceptionase:print(e)
import{PrivateKey}from'symbol-sdk';import{KeyPair,SymbolTransactionFactory,models,NetworkTimestamp,SymbolFacade}from'symbol-sdk/symbol';constNODE_URL=process.env.NODE_URL||'https://reference.symboltest.net:3001';console.log('Using node',NODE_URL);// Helper function to announce a transactionasyncfunctionannounceTransaction(payload,label){console.log(`Announcing ${label} to /transactions`);constresponse=awaitfetch(`${NODE_URL}/transactions`,{method:'PUT',headers:{'Content-Type':'application/json'},body:payload});console.log(' Response:',awaitresponse.text());}// Helper function to wait for transaction confirmationasyncfunctionwaitForConfirmation(transactionHash,label){console.log(`Waiting for ${label} confirmation...`);for(letattempt=0;attempt<60;attempt++){awaitnewPromise(resolve=>setTimeout(resolve,1000));try{constresponse=awaitfetch(`${NODE_URL}/transactionStatus/${transactionHash}`);conststatus=awaitresponse.json();console.log(' Transaction status:',status.group);if(status.group==='confirmed'){console.log(`${label} confirmed in`,attempt,'seconds');return;}if(status.group==='failed'){thrownewError(`${label} failed: ${status.code}`);}}catch(e){if(e.message.includes('failed'))throwe;console.log(' Transaction status: unknown');}}thrownewError(`${label} not confirmed after 60 seconds`);}// Returns the cosignatory addresses of the provided multisig account,// or an empty list if the account is not multisig or has never been usedasyncfunctiongetMultisigCosignatories(address){constmultisigPath=`/account/${address}/multisig`;console.log(`Getting cosignatories from ${multisigPath}`);try{constresponse=awaitfetch(`${NODE_URL}${multisigPath}`);constjson=awaitresponse.json();constcosignatories=json.multisig.cosignatoryAddresses;console.log(' Response:',JSON.stringify(cosignatories));returncosignatories;}catch{console.log(' Response: No cosignatories');return[];}}// Returns a transaction that turns a regular account into a multisigfunctionmultisigEnableTransaction(timestamp,feeMult){// Create an embedded multisig account modification transaction// that adds two cosignatoriesconstembeddedTransaction=facade.transactionFactory.createEmbedded({type:'multisig_account_modification_transaction_v1',// This is the account that will be turned into a multisigsignerPublicKey:multisigKeyPair.publicKey,// Delta of the number of signatures required for approvalsminApprovalDelta:1,// Delta of the number of signatures required for removalsminRemovalDelta:1,addressAdditions:cosignatoryAddresses});// Build the aggregate transactionconstembeddedTransactions=[embeddedTransaction];consttransaction=facade.transactionFactory.create({type:'aggregate_complete_transaction_v3',// This is the account that will pay for this transactionsignerPublicKey:multisigKeyPair.publicKey,deadline:timestamp.addHours(2).timestamp,transactionsHash:facade.static.hashEmbeddedTransactions(embeddedTransactions),transactions:embeddedTransactions});// Reserve space for two cosignatures (each is 104 bytes)// and calculate fee for the final transaction sizetransaction.fee=newmodels.Amount(feeMult*(transaction.size+104*cosignatoryKeyPairs.length));console.log('Enabling the multisig with the aggregate transaction:');console.log(JSON.stringify(transaction.toJson(),null,2));// Sign the aggregate transaction with the multisig's signatureSymbolTransactionFactory.attachSignature(transaction,facade.signTransaction(multisigKeyPair,transaction));// Append signatures from all cosignatoriesfor(constcosignatoryKeyPairofcosignatoryKeyPairs){transaction.cosignatures.push(facade.cosignTransaction(cosignatoryKeyPair,transaction));}returntransaction;}// Returns a transaction that turns a multisig into a regular accountfunctionmultisigDisableTransaction(timestamp,feeMult){// Create two embedded multisig account modification transactions// because cosignatories must be removed one by oneconstembeddedTransaction1=facade.transactionFactory.createEmbedded({type:'multisig_account_modification_transaction_v1',// This is the multisig account that will be modifiedsignerPublicKey:multisigKeyPair.publicKey,// Keep required signatures unchanged for this stepminApprovalDelta:0,minRemovalDelta:0,addressDeletions:[cosignatoryAddresses[1]]});constembeddedTransaction2=facade.transactionFactory.createEmbedded({type:'multisig_account_modification_transaction_v1',// This is the multisig account that will be modifiedsignerPublicKey:multisigKeyPair.publicKey,// Decrease required signatures after final removalminApprovalDelta:-1,minRemovalDelta:-1,addressDeletions:[cosignatoryAddresses[0]]});// Build the aggregate transactionconstembeddedTransactions=[embeddedTransaction1,embeddedTransaction2];consttransaction=facade.transactionFactory.create({type:'aggregate_complete_transaction_v3',// This is the account that will pay for this transactionsignerPublicKey:cosignatoryKeyPairs[0].publicKey,deadline:timestamp.addHours(2).timestamp,transactionsHash:facade.static.hashEmbeddedTransactions(embeddedTransactions),transactions:embeddedTransactions});// Calculate fee for the final transaction size// (No need to reserve space for cosignatures, as there are none)transaction.fee=newmodels.Amount(feeMult*transaction.size);console.log('Disabling the multisig with the aggregate transaction:');console.log(JSON.stringify(transaction.toJson(),null,2));// Sign the aggregate transaction using the first cosigner's signatureSymbolTransactionFactory.attachSignature(transaction,facade.signTransaction(cosignatoryKeyPairs[0],transaction));returntransaction;}constfacade=newSymbolFacade('testnet');constKEY_PREFIX='0'.repeat(63);// Setup the keys for the multisig account and its two cosignatoriesconstMULTISIG_PRIVATE_KEY=process.env.MULTISIG_PRIVATE_KEY||(KEY_PREFIX+'1');constmultisigKeyPair=newKeyPair(newPrivateKey(MULTISIG_PRIVATE_KEY));constmultisigAddress=facade.network.publicKeyToAddress(multisigKeyPair.publicKey);console.log(`Multisig address: ${multisigAddress}`,`(public key ${multisigKeyPair.publicKey})`);constcosignatoryKeyPairs=[];constcosignatoryAddresses=[];for(leti=0;i<2;i++){constCOSIGNATORY_PRIVATE_KEY=process.env[`COSIGNATORY${i}_PRIVATE_KEY`]||(KEY_PREFIX+String(i+2));constkp=newKeyPair(newPrivateKey(COSIGNATORY_PRIVATE_KEY));cosignatoryKeyPairs.push(kp);constaddr=facade.network.publicKeyToAddress(kp.publicKey);cosignatoryAddresses.push(addr);console.log(`Cosignatory ${i} address: ${addr}`,`(public key ${kp.publicKey})`);}try{// Fetch current network timeconsttimePath='/node/time';console.log('Fetching current network time from',timePath);consttimeResponse=awaitfetch(`${NODE_URL}${timePath}`);consttimeJSON=awaittimeResponse.json();consttimestamp=newNetworkTimestamp(timeJSON.communicationTimestamps.receiveTimestamp);console.log(' Network time:',timestamp.timestamp,'ms since nemesis');// Fetch recommended feesconstfeePath='/network/fees/transaction';console.log('Fetching recommended fees from',feePath);constfeeResponse=awaitfetch(`${NODE_URL}${feePath}`);constfeeJSON=awaitfeeResponse.json();constmedianMult=feeJSON.medianFeeMultiplier;constminimumMult=feeJSON.minFeeMultiplier;constfeeMult=Math.max(medianMult,minimumMult);console.log(' Fee multiplier:',feeMult);// Get current state of the multisig account and decide which// operation to performconstcosignatories=awaitgetMultisigCosignatories(multisigAddress);lettransaction;letsignerKeyPair;if(cosignatories.length===0){// Enable the multisigtransaction=multisigEnableTransaction(timestamp,feeMult);// This operation must be signed by the multisig accountsignerKeyPair=multisigKeyPair;}else{// Disable the multisigtransaction=multisigDisableTransaction(timestamp,feeMult);// This operation must be signed by one of the cosignerssignerKeyPair=cosignatoryKeyPairs[0];}constpayload=SymbolTransactionFactory.attachSignature(transaction,facade.signTransaction(signerKeyPair,transaction));// Announce and wait for confirmationconsttransactionHash=facade.hashTransaction(transaction).toString();console.log('Built aggregate transaction with hash:',transactionHash);awaitannounceTransaction(payload,'aggregate transaction');awaitwaitForConfirmation(transactionHash,'aggregate transaction');}catch(e){console.error(e.message,'| Cause:',e.cause?.code??'unknown');}
The code begins by defining two helper functions.
For details on how transactions are announced and how their confirmation is tracked, see the
Transfer transaction tutorial.
The remaining helper functions are described in the sections below.
Depending on whether the account is already configured as a multisig,
a transaction is created to enable or disable it as appropriate.
Finally, the transaction is announced and confirmed.
KEY_TEMPLATE='0'*63+'{}'# Setup the keys for the multisig account and its two cosignatoriesMULTISIG_PRIVATE_KEY=os.getenv('MULTISIG_PRIVATE_KEY',KEY_TEMPLATE.format(1))multisig_key_pair=SymbolFacade.KeyPair(PrivateKey(MULTISIG_PRIVATE_KEY))multisig_address=facade.network.public_key_to_address(multisig_key_pair.public_key)print(f'Multisig address: {multisig_address} 'f'(public key {multisig_key_pair.public_key})')cosignatory_key_pairs=[]cosignatory_addresses=[]foriinrange(2):COSIGNATORY_PRIVATE_KEY=os.getenv(f'COSIGNATORY{i}_PRIVATE_KEY',KEY_TEMPLATE.format(i+2))kp=SymbolFacade.KeyPair(PrivateKey(COSIGNATORY_PRIVATE_KEY))cosignatory_key_pairs.append(kp)addr=facade.network.public_key_to_address(kp.public_key)cosignatory_addresses.append(addr)print(f'Cosignatory {i} address: {addr} (public key {kp.public_key})')
constKEY_PREFIX='0'.repeat(63);// Setup the keys for the multisig account and its two cosignatoriesconstMULTISIG_PRIVATE_KEY=process.env.MULTISIG_PRIVATE_KEY||(KEY_PREFIX+'1');constmultisigKeyPair=newKeyPair(newPrivateKey(MULTISIG_PRIVATE_KEY));constmultisigAddress=facade.network.publicKeyToAddress(multisigKeyPair.publicKey);console.log(`Multisig address: ${multisigAddress}`,`(public key ${multisigKeyPair.publicKey})`);constcosignatoryKeyPairs=[];constcosignatoryAddresses=[];for(leti=0;i<2;i++){constCOSIGNATORY_PRIVATE_KEY=process.env[`COSIGNATORY${i}_PRIVATE_KEY`]||(KEY_PREFIX+String(i+2));constkp=newKeyPair(newPrivateKey(COSIGNATORY_PRIVATE_KEY));cosignatoryKeyPairs.push(kp);constaddr=facade.network.publicKeyToAddress(kp.publicKey);cosignatoryAddresses.push(addr);console.log(`Cosignatory ${i} address: ${addr}`,`(public key ${kp.publicKey})`);}
The tutorial requires three separate accounts.
Their private keys can be provided through environment variables.
If not set, default values are used:
Environment Variable
Default value
Purpose
MULTISIG_PRIVATE_KEY
0000..0001
Multisig account
COSIGNATORY0_PRIVATE_KEY
0000..0002
First cosignatory account
COSIGNATORY1_PRIVATE_KEY
0000..0003
Second cosignatory account
Each private key is a 64-character hexadecimal string.
The multisig account and its first cosignatory must hold enough funds to announce transactions.
If the default values are used, these accounts may already be funded.
The snippet above derives and stores the key pair and address of each account for later use.
# Fetch current network timetime_path='/node/time'print(f'Fetching current network time from {time_path}')withurllib.request.urlopen(f'{NODE_URL}{time_path}')asresponse:response_json=json.loads(response.read().decode())receive_timestamp=(response_json['communicationTimestamps']['receiveTimestamp'])timestamp=NetworkTimestamp(int(receive_timestamp))print(f' Network time: {timestamp.timestamp} ms since nemesis')# Fetch recommended feesfee_path='/network/fees/transaction'print(f'Fetching recommended fees from {fee_path}')withurllib.request.urlopen(f'{NODE_URL}{fee_path}')asresponse:response_json=json.loads(response.read().decode())median_mult=response_json['medianFeeMultiplier']minimum_mult=response_json['minFeeMultiplier']fee_mult=max(median_mult,minimum_mult)print(f' Fee multiplier: {fee_mult}')
The following function retrieves the list of current cosignatories for a given address using the
/account/{address}/multisigGET endpoint.
If the account is not configured as a multisig, or has never been used, the function returns an empty list.
# Returns the cosignatory addresses of the provided multisig account,# or an empty list if the account is not multisig or has never been useddefget_multisig_cosignatories(address):multisig_path=f'/account/{address}/multisig'print(f'Getting cosignatories from {multisig_path}')try:url=f'{NODE_URL}{multisig_path}'withurllib.request.urlopen(url)asresponse:status=json.loads(response.read().decode())cosignatories=status['multisig']['cosignatoryAddresses']print(f' Response: {cosignatories}')returncosignatoriesexcepturllib.error.HTTPError:# The address has never been usedprint(' Response: No cosignatories')return[]
// Returns the cosignatory addresses of the provided multisig account,// or an empty list if the account is not multisig or has never been usedasyncfunctiongetMultisigCosignatories(address){constmultisigPath=`/account/${address}/multisig`;console.log(`Getting cosignatories from ${multisigPath}`);try{constresponse=awaitfetch(`${NODE_URL}${multisigPath}`);constjson=awaitresponse.json();constcosignatories=json.multisig.cosignatoryAddresses;console.log(' Response:',JSON.stringify(cosignatories));returncosignatories;}catch{console.log(' Response: No cosignatories');return[];}}
This list is then used to determine the tutorial's mode of operation,
build the appropriate configuration transaction, and sign it.
# Get current state of the multisig account and decide which# operation to performcosignatories=get_multisig_cosignatories(multisig_address)iflen(cosignatories)==0:# Enable the multisigtransaction=multisig_enable_transaction()# This operation must be signed by the multisig accountsigner_key_pair=multisig_key_pairelse:# Disable the multisigtransaction=multisig_disable_transaction()# This operation must be signed by one of the cosignerssigner_key_pair=cosignatory_key_pairs[0]json_payload=facade.transaction_factory.attach_signature(transaction,facade.sign_transaction(signer_key_pair,transaction))
// Get current state of the multisig account and decide which// operation to performconstcosignatories=awaitgetMultisigCosignatories(multisigAddress);lettransaction;letsignerKeyPair;if(cosignatories.length===0){// Enable the multisigtransaction=multisigEnableTransaction(timestamp,feeMult);// This operation must be signed by the multisig accountsignerKeyPair=multisigKeyPair;}else{// Disable the multisigtransaction=multisigDisableTransaction(timestamp,feeMult);// This operation must be signed by one of the cosignerssignerKeyPair=cosignatoryKeyPairs[0];}constpayload=SymbolTransactionFactory.attachSignature(transaction,facade.signTransaction(signerKeyPair,transaction));
The only differences between enabling and disabling the multisig are the transaction that is created and
the account that signs it, as shown in the next two sections.
All changes to the multisig configuration of an account, including adding or removing cosignatories,
are performed using a MultisigAccountModificationTransaction, which must be embedded in an
aggregate transaction:
# Create an embedded multisig account modification transaction# that adds two cosignatoriesembedded_transaction=facade.transaction_factory.create_embedded({'type':'multisig_account_modification_transaction_v1',# This is the account that will be turned into a multisig'signer_public_key':multisig_key_pair.public_key,# Increment of the number of signatures required for approvals'min_approval_delta':1,# Increment of the number of signatures required for removals'min_removal_delta':1,'address_additions':cosignatory_addresses})
// Create an embedded multisig account modification transaction// that adds two cosignatoriesconstembeddedTransaction=facade.transactionFactory.createEmbedded({type:'multisig_account_modification_transaction_v1',// This is the account that will be turned into a multisigsignerPublicKey:multisigKeyPair.publicKey,// Delta of the number of signatures required for approvalsminApprovalDelta:1,// Delta of the number of signatures required for removalsminRemovalDelta:1,addressAdditions:cosignatoryAddresses});
The embedded MultisigAccountModificationTransaction includes the following fields:
signer_public_key: public key of the account whose multisig configuration will be modified.
min_approval_delta: difference between the desired value and the current value of the number of
signatures that will be required to approve a transaction from the multisig account.
In this case the account is initially a regular account, so the current required number of signatures is 0.
To convert it into a multisig account that requires one signature from one of its cosignatories,
the delta is set to 1.
The delta value can be negative to reduce the current value, as shown in the next section.
min_removal_delta: Difference in the number of signatures required to remove cosignatories from the account
configuration.
This allows, for example, requiring more signatures to remove a cosignatory than to approve a regular transaction,
which is often a more sensitive governance operation.
address_additions: list of addresses of the cosignatories that will be added to the account.
The cosignatory_addresses variable was prepared during the setup phase.
Safety measures
The protocol includes safety mechanisms that help prevent locking an account into an invalid state.
Transactions that would result in an invalid multisig configuration are rejected with an error.
For example, when:
The number of cosignatories is lower than the minimum number of required signatures
An address that is not a consignatory is removed
Required signatures are missing
Unnecessary signatures are included
The embedded transaction is then wrapped in an aggregate transaction, even though it is the only inner transaction:
# Build the aggregate transactionembedded_transactions=[embedded_transaction]transaction=facade.transaction_factory.create({'type':'aggregate_complete_transaction_v3',# This is the account that will pay for this transaction'signer_public_key':multisig_key_pair.public_key,'deadline':timestamp.add_hours(2).timestamp,'transactions_hash':facade.hash_embedded_transactions(embedded_transactions),'transactions':embedded_transactions})# Reserve space for two cosignatures (each is 104 bytes)# and calculate fee for the final transaction sizetransaction.fee=Amount(fee_mult*(transaction.size+104*len(cosignatory_key_pairs)))print('Enabling the multisig with the aggregate transaction:')print(json.dumps(transaction.to_json(),indent=2))
// Build the aggregate transactionconstembeddedTransactions=[embeddedTransaction];consttransaction=facade.transactionFactory.create({type:'aggregate_complete_transaction_v3',// This is the account that will pay for this transactionsignerPublicKey:multisigKeyPair.publicKey,deadline:timestamp.addHours(2).timestamp,transactionsHash:facade.static.hashEmbeddedTransactions(embeddedTransactions),transactions:embeddedTransactions});// Reserve space for two cosignatures (each is 104 bytes)// and calculate fee for the final transaction sizetransaction.fee=newmodels.Amount(feeMult*(transaction.size+104*cosignatoryKeyPairs.length));console.log('Enabling the multisig with the aggregate transaction:');console.log(JSON.stringify(transaction.toJson(),null,2));
# Sign the aggregate transaction with the multisig's signaturefacade.transaction_factory.attach_signature(transaction,facade.sign_transaction(multisig_key_pair,transaction))# Append signatures from all cosignatoriesforcosignatory_key_pairincosignatory_key_pairs:transaction.cosignatures.append(facade.cosign_transaction(cosignatory_key_pair,transaction))
// Sign the aggregate transaction with the multisig's signatureSymbolTransactionFactory.attachSignature(transaction,facade.signTransaction(multisigKeyPair,transaction));// Append signatures from all cosignatoriesfor(constcosignatoryKeyPairofcosignatoryKeyPairs){transaction.cosignatures.push(facade.cosignTransaction(cosignatoryKeyPair,transaction));}
In this case, the signature of the account being converted into a multisig is required,
along with the signatures of the cosignatories, which explicitly acknowledge their new responsibility.
One of the signatures is the main signer of the transaction and is added using .
The remaining signatures are cosignatures and are added using .
The choice of the main signer only affects which account pays the transaction fee.
Once an account has multisig enabled, its own signature is no longer required.
Any transaction involving that account instead requires signatures from its cosignatories.
Disabling a multisig configuration requires removing all cosignatories.
The process is similar to enabling it, with ttwo key differences:
cosignatories must be removed one by one, and the multisig account itself cannot sign the transaction.
For this reason, two MultisigAccountModificationTransactions are created:
# Create two embedded multisig account modification transactions# because cosignatories must be removed one by oneembedded_transaction_1=facade.transaction_factory.create_embedded({'type':'multisig_account_modification_transaction_v1',# This is the multisig account that will be modified'signer_public_key':multisig_key_pair.public_key,# Keep required signatures unchanged for this step'min_approval_delta':0,'min_removal_delta':0,'address_deletions':[cosignatory_addresses[1]]})embedded_transaction_2=facade.transaction_factory.create_embedded({'type':'multisig_account_modification_transaction_v1',# This is the multisig account that will be modified'signer_public_key':multisig_key_pair.public_key,# Decrease required signatures after final removal'min_approval_delta':-1,'min_removal_delta':-1,'address_deletions':[cosignatory_addresses[0]]})
// Create two embedded multisig account modification transactions// because cosignatories must be removed one by oneconstembeddedTransaction1=facade.transactionFactory.createEmbedded({type:'multisig_account_modification_transaction_v1',// This is the multisig account that will be modifiedsignerPublicKey:multisigKeyPair.publicKey,// Keep required signatures unchanged for this stepminApprovalDelta:0,minRemovalDelta:0,addressDeletions:[cosignatoryAddresses[1]]});constembeddedTransaction2=facade.transactionFactory.createEmbedded({type:'multisig_account_modification_transaction_v1',// This is the multisig account that will be modifiedsignerPublicKey:multisigKeyPair.publicKey,// Decrease required signatures after final removalminApprovalDelta:-1,minRemovalDelta:-1,addressDeletions:[cosignatoryAddresses[0]]});
In both transactions, signer_public_key is set to the multisig account's public key.
The first transaction removes cosignatory_addresses[1] without modifying the approval or removal deltas,
because one cosignatory still remains and signatures are still required.
The second transaction removes the last remaining cosignatory and sets both min_approval_delta and
min_removal_delta to -1.
At this point, the current value of both fields is 1, as configured during the enable step,
and the desired value is 0, so the delta is -1.
Both embedded transactions are then wrapped in an aggregate transaction and signed:
# Build the aggregate transactionembedded_transactions=[embedded_transaction_1,embedded_transaction_2]transaction=facade.transaction_factory.create({'type':'aggregate_complete_transaction_v3',# This is the account that will pay for all transactions'signer_public_key':cosignatory_key_pairs[0].public_key,'deadline':timestamp.add_hours(2).timestamp,'transactions_hash':facade.hash_embedded_transactions(embedded_transactions),'transactions':embedded_transactions})# Calculate fee for the final transaction size# (No need to reserve space for cosignatures, as there are none)transaction.fee=Amount(fee_mult*transaction.size)print('Disabling the multisig with the aggregate transaction:')print(json.dumps(transaction.to_json(),indent=2))# Sign the aggregate transaction using the first cosigner's signaturefacade.transaction_factory.attach_signature(transaction,facade.sign_transaction(cosignatory_key_pairs[0],transaction))
// Build the aggregate transactionconstembeddedTransactions=[embeddedTransaction1,embeddedTransaction2];consttransaction=facade.transactionFactory.create({type:'aggregate_complete_transaction_v3',// This is the account that will pay for this transactionsignerPublicKey:cosignatoryKeyPairs[0].publicKey,deadline:timestamp.addHours(2).timestamp,transactionsHash:facade.static.hashEmbeddedTransactions(embeddedTransactions),transactions:embeddedTransactions});// Calculate fee for the final transaction size// (No need to reserve space for cosignatures, as there are none)transaction.fee=newmodels.Amount(feeMult*transaction.size);console.log('Disabling the multisig with the aggregate transaction:');console.log(JSON.stringify(transaction.toJson(),null,2));// Sign the aggregate transaction using the first cosigner's signatureSymbolTransactionFactory.attachSignature(transaction,facade.signTransaction(cosignatoryKeyPairs[0],transaction));
The aggregate transaction is signed by cosignatory_addresses[0].
This is the only valid option: once an account has cosignatories, it can no longer sign transactions on its own,
and cosignatory_addresses[1] is removed from the multisig after the first embedded transaction is executed.
As a result, no cosignatures are required.
Only the main signature is needed.
The entire operation can be initiated and approved by a single cosignatory because the multisig was configured with a
minimum removal requirement of one signature.
The cosignatories could also have been removed in the opposite order, as both have equal authority.
The only difference would be which account signs the transaction and pays the transaction fee.
# Announce and wait for confirmationtransaction_hash=facade.hash_transaction(transaction)print(f'Built aggregate transaction with hash: {transaction_hash}')announce_transaction(json_payload,'aggregate transaction')wait_for_confirmation(transaction_hash,'aggregate transaction')
// Announce and wait for confirmationconsttransactionHash=facade.hashTransaction(transaction).toString();console.log('Built aggregate transaction with hash:',transactionHash);awaitannounceTransaction(payload,'aggregate transaction');awaitwaitForConfirmation(transactionHash,'aggregate transaction');
Using node https://reference.symboltest.net:3001
Multisig address: TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA (public key 4CB5ABF6AD79FBF5ABBCCAFCC269D85CD2651ED4B885B5869F241AEDF0A5BA29)
Cosignatory 0 address: TBBHGE77IHHOIYA46B3XSORRNR2L5MLW54YO75Y (public key 7422B9887598068E32C4448A949ADB290D0F4E35B9E01B0EE5F1A1E600FE2674)
Cosignatory 1 address: TBBWZ2X4EXGQ65XPUNWGSJX4LHW5NWMPDNGUERY (public key F381626E41E7027EA431BFE3009E94BDD25A746BEEC468948D6C3C7C5DC9A54B)
Fetching current network time from /node/time
Network time: 102516815591 ms since nemesis
Fetching recommended fees from /network/fees/transaction
Fee multiplier: 100
Getting cosignatories from /account/TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA/multisig
Response: ['98427313FF41CEE4601CF077793A316C74BEB176EF30EFF7', '98436CEAFC25CD0F76EFA36C6926FC59EDD6D98F1B4D4247']
Disabling the multisig with the aggregate transaction:
{
"signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"signer_public_key": "7422B9887598068E32C4448A949ADB290D0F4E35B9E01B0EE5F1A1E600FE2674",
"version": 3,
"network": 152,
"type": 16705,
"fee": "32800",
"deadline": "102524015591",
"transactions_hash": "F1985D5D2CC1DB30CE3B0AE8532874E14AFA9F6F2237FF0D95DA819F6483F34F",
"transactions": [
{
"signer_public_key": "4CB5ABF6AD79FBF5ABBCCAFCC269D85CD2651ED4B885B5869F241AEDF0A5BA29",
"version": 1,
"network": 152,
"type": 16725,
"min_removal_delta": 0,
"min_approval_delta": 0,
"address_additions": [],
"address_deletions": [
"98436CEAFC25CD0F76EFA36C6926FC59EDD6D98F1B4D4247"
]
},
{
"signer_public_key": "4CB5ABF6AD79FBF5ABBCCAFCC269D85CD2651ED4B885B5869F241AEDF0A5BA29",
"version": 1,
"network": 152,
"type": 16725,
"min_removal_delta": -1,
"min_approval_delta": -1,
"address_additions": [],
"address_deletions": [
"98427313FF41CEE4601CF077793A316C74BEB176EF30EFF7"
]
}
],
"cosignatures": []
}
Built aggregate transaction with hash: 2481B252AB15881368AD74BC21373205AFB0A23E4181DFB01A7D116FC9DDFFF7
Announcing aggregate transaction to /transactions
Response: {"message":"packet 9 was pushed to the network via /transactions"}
Waiting for aggregate transaction confirmation...
Transaction status: unconfirmed
Transaction status: unconfirmed
...
Transaction status: confirmed
aggregate transaction confirmed in 9 seconds
Key points in the output:
Lines 2-4: Addresses and public keys of all involved accounts.
Line 10 (Response: [ ... ]): Existing cosignatories have been detected.
Line 27-32 (First embedded transaction): The minimum number of required signatures will remain unchanged,
no new cosignatories will be added, and one existing cosignatory will be removed.
Line 39-44 (Second embedded transaction): The minimum number of required signatures will be decreased by one,
no new cosignatories will be added, and the last remaining cosignatory will be removed.
The transaction hashes shown in the output can be used to look up the transactions in the
Symbol Testnet Explorer.